Demystifying the Django ORM

This video features Simon Charette at Djangonaut Space 2024 in Online.

Demystifying the Django ORM
0:44:26
Published November 18, 2024
664 views

Simon Charette presents his talk, "Demystifying the Django ORM" to the Djangonaut Space 2024 Session 3 team.

The slides can be found at: http://charettes.name/djangonauts2024/

To learn more about Djangonaut Space and how to launch your own mission to contribute to the Django ecosystem, visit us at https://djangonaut.space

Summary

Simon Charette explains Django’s ORM as a mapping between Python model objects and relational database tables, then breaks query processing into four phases: resolving, compilation, execution, and construction. Resolving builds and validates a lazy, database-agnostic expression tree; compilation turns it into backend-specific SQL and parameters; the database driver executes it; and construction converts returned tuples into properly typed model instances, dictionaries, or value lists. He also explains how expressions such as `F`, lookups, and database functions compose, why SQL parameters help prevent injection, and how backend differences are hidden through field conversion. In the questions, he describes `get_or_create()` as a safe composition of lookup and insertion that handles race conditions, clarifies lazy querysets and `F` expressions, and notes that reducing hidden database I/O during resolution is important for a future asynchronous ORM.

Key takeaways

  • Django maps model classes and fields to database tables and columns, while model operations become SQL statements.
  • Querysets remain lazy during the resolving phase, where expressions are accumulated and validated without contacting the database.
  • Compilation recursively converts resolved expressions into backend-specific SQL and separate parameters, with specialized compilers for different operations.
  • Execution delegates SQL handling to Django’s database backend and Python driver, while construction converts returned tuples into model instances or other result types.
  • Field conversion hides database-specific representations, such as SQLite strings for datetimes or MySQL integer booleans.
  • `get_or_create()` combines a lookup and an insert and is safer than implementing the sequence manually because of possible race conditions.

Summarised automatically from the transcript.

Transcript

7,122 words · auto-generated Show

Automatically transcribed, so expect mistakes in names and technical terms.

0:00

Speaker 1: Thank you, Priya, and uh thank you Django Not for adding me. Uh I'm happy to be sharing a bit more uh details on the URM and trying to demystify uh a bit of how it works. So before we start, my name is Simon Charrette. I'm based in Montreal, Canada, and I've been contributing for to Django for about 10 years now. I mostly to the URM and migration nowadays because it's the field that interests me. uh the most and uh but otherwise I started with as Priya said with uh translating strings and making changes to forms and uh content types and so on. And I slowly got into the the RM and migrations because um

0:45

Speaker 1: where I was working at we were using uh a lot of Postgres specificities that required me to tweak the RM a bit and make changes and I just got booked to fixing bugs and uh making reviews around it I'm currently employed at Zapier. It's an automation company as a principal, engineered there. So, um before we start into looking into the Django ORM specificities, it's I think it's worth taking a bit of time talking about what an ORM actually is. The acronym ORM stands for object relational mapping or mapper. And it's a paradigm that is often used in a programming language that

1:31

Speaker 1: support the object-oriented uh design. So if you're able to define classes, um your will be uh We'd be more easy conceptually to map classes to database tables and maps map instances of these classes to rows of the table. If that's a bit confusing to you, we're going to go over a few examples, but that's one of the reasons why this paradigm is often used in object-oriented uh programming. The ORM allows you to interact with the database to retrieve and update data. So that is Selecting it, aggregating it,

2:16

Speaker 1: inserting new data, updating it, and deleting it as well. And in the case of the Django RM, and I guess other ones that are Written in Python, it allows you to map table rows to Python objects. So what does it look like concretely in the case of Django? If you define a model that model subclass, in our case, we have the author class here, and you define a field on it. You will uh when you run migrations for this particular model. Django will create a um table for you. If you're not familiar with what an SQL table is, it's a way to represent data in a tabular form.

3:02

Speaker 1: So just like you would define an HTML table where you have some columns that define what is exactly in each row or if you use um tabular software like uh excel or google sheet It's relatively the same concept where obviously the SQL level allows you to define more constraint and relationship between some of your tables, but It's tabular data. So you have columns that define what exactly you will see in each column in each row for this particular column, and you have an association with it. So in this case you can see that there are some repetition repetition of field so in this case author model is mapped to the uh table name author and uh the name field is mapped to the name column in the SQL definition

3:49

Speaker 1: How is data stored in these tables? So if we use the author. objects. create, if you use the ORM a bit, you might be familiar with this method. Allows you to create objects. So you pass the exact fields that you define in your model, and this will translate to this insert into SQL query. That gets then stored on the database server a bit like you see on the ID name table there. So in this case the ID is It's implicit um because of it's the default one that Django provides, but that's that's the way um the query gets translated. Again, you can see that there's um a bit of like similarity here right you you see that the model name is mapped to the table name

4:36

Speaker 1: and the literal uh the name column is is is wrapped in the column that's going to be inserted and the literal value, Carlton Gibson, as well, a well-known Jenko contributor, is passed as a literal value in this QL query. Data is retrieved using an SQL statement. So that is the select one in this case. If you do something like author. objects. filter by name again, Carlton here. uh you're good that's going to result in this parched RSGL and you're going to get back instances of um alter through that matching your actual query So um that's kind of like a small primer on what while the ORM maps it to SQL, but

5:23

Speaker 1: the intimidating part often lies or is perceived to be in how does it actually turn into SQL, right? It it seems kind of like simple there. You could see there's mapping, but the RM allows you to do so many things. I think that over the years the the one conceptual model that has helped me uh reason about the ORM is to see it uh in um a sequence of phase so uh four phases in this case so in order to understand it better we'll go over each phase individually The first phase is the resolving phase, followed by the compilation phase, what is actually compiled. uh it then gets started to um gets passed and executed against the server and then it's constructed back into

6:12

Speaker 1: model instances So let's start looking at the uh by looking at the resolving phase. The resolving phase is maybe one of the phases you're the most um familiar with it's the phase where uh coursets are kept lazy so you can chain operation against them you could do multiple filter call you could do you could use the only method annotate um name it. Any form of uh query set method that keeps returning a query set it can be considered to be in the resolving phase. So what exactly happens during the resolving phase? So The resolving phase is the phase where when you pass keyword arguments to filter or you passed specific field reference to annotate

6:57

Speaker 1: and so on, the ORM will validate that the value that you're passing as your filter. is valid. So if you're trying to match against an integer field, for example, you're passing a string, the RM will tell you during the resolving phase, that doesn't make sense. You cannot filter against an integer with a string value. And all of this um logic um happens in uh through a method called resolve expression. So each components or abstraction that can be passed to the RM and is able to resolve column and resolve literal value. has a method on it called resolve expression. So if you search for this particular name in the um

7:44

Speaker 1: in the code base, you will find a few classes that implement it. The idea behind it is that you've got an object, it's completely standalone, it has a resolve expression method, and it accepts a query. And what the Object should return when the function is called is a an object that is resolved within the context of the provided query. So let's take an example here. If we look at author. objects. filter name, Colton Gibson, what we saw previously, what the RM does under the hood is that it creates a Fname object based on the core argument name that you've passed. And it calls the resolve expression method on it, and it's going to resolve the F

8:30

Speaker 1: name instance in the context of the author query. So The field name by itself doesn't mean much, but if you resolve it within the context of an author , the ORM is able to map it to the actual column from the author table. So what what will get returned is a call instance. If you try to pass something that doesn't exist or doesn't make much sense in the context of author, so for example this case missing the RM will tell you raise a field error telling you well I don't know what you mean by that um there's no missing field on the alter model or on or on any of its relationship The resolving phase is uh accumulative, so you can chain methods over and over again and more and more and more resolving will happen.

9:20

Speaker 1: So this is kind of like a simplified example of what would happen here. So in this case we we pass the filter and we pass the order by. We use a lookup on name. So we do name that start with. And this is going to be turned into an accumulation of where criteria over over again, the more filter call we make. And the RM will build a structure of Python objects. from resolvable to um another kind of object that we'll we will talk about called compilable. So in this case You can see that we've got the name and starts with that's being turned into a class. Um starts with, in this case it's a it's a lookup, uh, and it has two components, the column and the literal value that you pass

10:09

Speaker 1: for it. and a similar picture for order by. So if you you specify order by the core set will get more and more expression. In this case it will create the F field again, a F um ID inst f instance with the ID parameter, resolve it, and as we've seen when we do so, we get back a um call instance when we do that with an F instance. So, a small summary of the resolving phase. A resolvable is a Python object with a resolve expression method. It accepts a query object to resolve itself within the context of the query. And it returns a compilable. We will look into what a compilable means in a moment.

10:55

Speaker 1: This phase is accumulative, so um during that phase the query set remains lazy and more resolving can occur, but more importantly Nothing gets to the database yet. It's more kind of like a phase where you you build a query set and you define it. If you look at the code base, most resolvable are subclasses of DjangoDB modeled expression. So this is kind of like a protocol of some form, the old uh resolvable idea, but it's um you can only you can subclass expression and implement the right method and you will get what you need um out of the box by doing that. And most of the resolvable are composed of other resolvable in a nested build fashion. So what does that mean exactly? the uh

11:40

Speaker 1: the way um things are are structured uh in the rm the only re the only thing that really needs to be resolved are field reference so everything else are kind of like container of field reference or or or of literal value and they can be nested on top of each other. So for example, we saw that the lookup was a resolvable itself but it was um composed of other resolvable that was that were nested into it so you've got you kind of got this uh nested doll structure that allows you to represent very complex uh queries. And most importantly This phase is back in agnostic. So what I mean by that is that at this point, there's no logic that is

12:26

Speaker 1: SQLite or Postgres or MySQL specific. You're building a representation of a query that is an intermediate DRV representation that is tied to the ORM, but it's not something that is specific to SQLite or Postgres or MySQL. So we looked at the resolving phase. What comes next is the compilation phase. Compilation phase, if you want to summarize it, you take a resolvable and you get back a string representing SQL and parameters. That's what it does. So you you start from a query object and you turn back into a string SQL and parameters. This phase is triggered by any non-lazy query set

13:11

Speaker 1: operation. So any operation that would not return the query set. So for example, get aggregate, update, first, like all of these methods that actually get to the database triggers the compilation because the query needs to be turned into actual SQL to get executed When it's triggered, the proper settings of database entry are uh is determined from routers, so depending on which model you're writing to. the right database backend is going to be picked so it could be Postgres SQLite whatever you have to find there and depending on the type of operation you're uh asking for so an aggregation and update an insert a different uh compiler will be used because uh different kind of query needs to be generated.

13:59

Speaker 1: Could be a select statement, an insert statement, and so on. And just like resolving, uh, the compilation phase is um recursively compiles its constituents. So You could have like a compilable that contains other compilable and uh it stitches all this tree up together in a kind of like nested fashion. So uh we've seen that a uh a resolvable is a Python object with a resolve expression method on it. Well A compilable is a Python object with a as SQL method on it. And what this asql take as parameter is the connection object, so a connection to Postgres and so on. and the compiler object, which determines which kind of operation you want to compile this expression for.

14:46

Speaker 1: So in the case of call, which is result from F instances You get pass a connection in a compiler and you get returned a reference, so actual SQL that can be used that reference the column. In this case, the table name is wrapped in quotes And the column as well. In the case of a column, there's no parameters. So there's no literal value that we want to pass the back end. So the params with this dual is an empty tuple. If we look at the Starts with lookup, being past the column and the literal value. When it compiles, it's going to be recursively compiling its constituent. So in this case, as we've seen, when we pass a call It's as SQL

15:31

Speaker 1: gets compiled to the left hand side. So the part on the left-hand side of the like in the SQL, and the literal value carton in this case gets uh compiled to a parameter SLDR, so the percentage sign S and the name itself is a parameter because it's a literal value that we want to pass down to the ORM. So we touch a bit about it, but how do resolvable and compilables relate to each other? So a resolvable resolve expression method must return a compilable. A resolvable object can also be a compilable. So it's possible to define a class that implements both protocol Um

16:17

Speaker 1: that has both resolve expression on it and Azql on it. And in practice, most resolvable objects are also compilable because they don't need to have anything special regarding. uh their resolving. So they they can return themselves with their constituent or what is called their source expression. turn back into uh resolved a resolved um compilable themselves and uh from there uh you're able to implement both protocol on it and if you look at Django DD models expression again This class as is both a resolvable and a compilable. Well the compilable part is more of a stub, so you need to actually implement it, and that's what most of the subclasses of it do

17:09

Speaker 1: A kind of like overview of um how um the uh quarts uh or What is also uh often referenced as the d under d under uh syntax uh gets turned into under the hood. So We've talked a lot about expression compilable and resolvable, but the reality is that a few years ago that was not the case. It was not possible to extend the RM at all. um to do uh these these kind of things today that you can do by using uh creating your own lookup classes or creating um passing around um the funk instance and so on And uh previously the only way to actually do lookup was to use this syntax. So name underscore underscore

17:54

Speaker 1: start with and call them. And um but today what what this is equivalent to is if you look at how you you if you use the name that underscore start with syntax It's pretty much an equivalent of importing the starts with lookup and passing it directly to filter with Fname in VarloCalton But the reality is that it's way more uh practical and and it's it's it's kind of like nice syntactic sugar to be able to I'll keep using this quart syntax, right? You don't have to do any import, you don't have to do class creation, it's way more readable. But it's important to remember that it's it at this point it's more of like a short end uh over uh actu importing all of these objects yourself and doing it

18:41

Speaker 1: and it's quite more um uh it's easier to uh to read and reason about We talked a bit about um resolvable that um resolve to themselves. Well literal value are a good example of that. So If you pass an integer or a string or a daytime or anything that is not tied to a field reference or anything like that. In the case of like the value class, which is a subclass of uh expression, it's a resolvable and both and a compilable. If you Call if you resolve 42 in the context of a query, well, it doesn't change anything, right?

19:26

Speaker 1: Because the it's 42, right? It doesn't it's not tied to any table or anything like that. It's more it's just like a literal value If you compile it though, you get back this placeholder and the parameter as we've seen. If you view the func um instance, which allows you to call arbitrary SQL function You can see that things are a bit more different here. So if we pass func and we pass a fname instance and we resolve it, we can set we get returning another func instance. So in this case, func is both resolvable and compilable but you can set its source expression so its constituents were uh themselves resolved so in this case f was resolved to call uh author in name And if we were to compile what gets returned,

20:13

Speaker 1: you can see that now func is wrapping the SQL of its call within the function name that you've provided. The last piece of the puzzle with regards to query compilation are the compilers themselves So we've looked at how bits of SQL can get compiled, so things that you're being you're passing around and so on, but you need the whole um Skeleton around the query as well to be set up. So in the case of create, update and delete, they result in vastly different queries. Um insert syntax and treat and update syntax and delete syntax have very different um ways of

20:59

Speaker 1: structuring the table reference and and how fields must be passed and so on. So they we have a specialized compiler for them. They all inherit from SQL compiler so you you could could have a look at it But the the basic idea there is that what the SQL compiler is in charge of is stitching all the bits together. So we've looked at our columns, reference can get compiled. We look at our lookups, so things that result in the where. clause get compiled but if we want to look at like a simplified example of our uh of our of our a sql compiler uh it's going to be turning fill reference into SQL, you can kind of like think of it using this pseudocode

21:44

Speaker 1: where when the SQL compiler is asked to produce the exact SQL that eventually is going to get executed It's going to use um some form of like interpolation with it it's going to compile every column that it needs to select. by calling compile which translates into calling as SQL on all of the fields that have been resolved, so all the call and uh we'll collate it together and make sure to prevent SQL injection by not combining params and SQL itself. delegating that to the back end which is part of the execution funds. So we reach a point in our query where resolved it, we're sure that it's something that makes sense on the RM perspective.

22:31

Speaker 1: Now we're at a point after compilation where we've got the actual SQL string and its parameter and we want to execute the query. So the execution phase, it's um From the Django's perspective, a lot of it a lot happened on the database backend , and a lot of it happens in the actual driver that you've installed. Python came up with a interface for how these drivers should be defined. It's PEP249. You can have a look at it. It's pretty old at this time, but it's still very relevant. If you're using uh Postgres as your backend, you most likely using Psycho PG as your driver or um uh backend um python backend

23:17

Speaker 1: sdk i'm not sure exactly how how to say it but uh In this case, what this layer does is it packs the SQL in params, and it's going to do two things when you do so, when you execute the SQL. It's going to be returning you a list of tuples. So the data that you selected, what the query actually returns you in the form of like a list of tuples, or it's going to raise an exception. Because a query that you've executed is invalid or the state in which the database received it is incoherent and so on. There's A lot of exceptions that could be raised, but we will focus on the happy path here. So what does it look like exactly?

24:03

Speaker 1: Now that you've compiled the result , you can create a cursor from the connection. So when you When the RM wants to execute a query, it's going to resolve it, execute it, and when it has uh compile it and when it has its compile SQL. it will um execute it against the cursor of the connection that um your query was mapped to. And what it will what the back end will return is again uh Either an exception or it will return a list of tuples. So a list of kind of like all the rows that you match your select statement or if you use something like insert and returning. uh you're going to get result if you use update it's going to be the number of of rows that were updated um

24:51

Speaker 1: and so on um but ultimately you're going to get a list of tuples A lot of stuff happened there. Obviously, you're kind of like defining what how you want things to do, but it all happens on the database server. So that's Not going to be covered in this talk, but um the SQL, you send it to the database backend, it does all of its magic. uh to actually get the tuple efficiently based on the index that you have and so on. And in the case of uh of Django, we only care about writing um well compiling the proper SQL for the back end and getting back result. Everything else that happens on the other side is none of our business. The uh

25:37

Speaker 1: and when you uh are done executing comes the last phase. So um what I like to call the construction phase. So you get back tuples, but in most cases you you don't want tuples, right? You want model instances. Um you want uh to be able to uh reflect about your data in a way that kind of like maps your the reality of your data that you define through your models. So how does that happen? When the tuples get returned from the back end, we're going to need to construct the actual return type and course as well the value that are returned. Depending on if you use alt, values, or values list , a different kind of collection will need to be returned as well. If you use values, for example, it's going to be a list of dict.

26:24

Speaker 1: If you use values list, it's going to be a list of doubles. If you didn't use anything, it's going to be a list of model instances. So two things happen during the construction phase. One that gets often overlooked is the coercion phase. So what does coercion mean exactly? It means to convert from one type to another type. Good example of that. SQLite doesn't have a native type for date times or dates for what it's worth. So all of that, all of them are stored in strings in the database. But when you interact with SQLite and you you store daytime instances on your model and so on, you don't want to get back strings, right? You want to get back daytime objects. So um

27:09

Speaker 1: since SQL Lite doesn't have this notion of Date times, we need to have a small layer that is able to take the strings returned from S3Lite and turn them back into datetime objects. The same thing happened for the JSON field. Um, even if it's stored as JSON and manipulate like able to be manipulated by the database backend. As JSON on the back end, when it gets turned back to Django, it's in a string form. So it needs to be turned back into actual JSON structure. And the way you do that JSON is transmitted as string over the wire and we do a JSON load and we use the decoder that you might have defined on your JSON field Similar picture for Boolean

27:55

Speaker 1: on MySQL. MySQL doesn't have a Boolean type. Under the audit uses TinyInt, which stores one bit, either one or zero. And uh in order to make sure that this detail doesn't leak through, uh, we want the one of the goals of the ORM is to abstract the kind of like backend that you're using the we turn back the integer into a Boolean. And this is done by using the from db value. So if you've defined your own class, field class in the past and you need it to overwrite from db value , that's the reason why it happens during the construction of model instances. From db value is going to be called. And lastly, now you've got all the tuples with all the types that are converted, uh

28:42

Speaker 1: course to the right value, the one that you're expected based on your model we're going to turn it into the right iterable type. So in the case of uh the um values, well you want an iterable of dipped. So in order to do that, you need to create digs that map the fields that you've passed to values to the actual tuples together. In the case of values list, well, it's pretty simple on the recent version of Django because we make sure that the order that you specified in your SELECT and the fill that you've passed to values list is going to be the exact order that we use when we do the select. So it's going to be very fast, no conversion, the exact tuples that are returned from the database are going to be returned. So if you need to deal with a lot of data, usually values

29:30

Speaker 1: list is the way to use it, the way to make it use as little memory as possible. Lastly, if neither are used, and each chip, each tuple is turned into instances of the query model. So if you're querying for author, we're going to be turning them into author instances. A simplified example look a bit like that. So model classes have a class method on them called FromDB. And what this FromDB method act requires. . is the database against which the query was performed. This is important because each model instances is tied through the database that it was retrieved from for possible further interactions.

30:16

Speaker 1: with the model instances, the um map of fields that are going to be specified, and the uh the actual tuple um that was returned from the database. So in this case it would create an author only with the ID field and ID being one for the first instance, from db is the one that is in charge of calling from db value for uh all the the value fabric correctly And when this happens, well the model instances are going to be created and all the fields that are not specified through the second argument, this method, are going to be considered to be different. This is going to go through the altered

31:01

Speaker 1: init method and so on to initialize a field and send the appropriate signals. and turn uh return a query set with all of the authors accumulated in a a list that is tied to the query set uh called the result cache. So, summarize the four phases that we've seen. Resolving. Resolving is lazy. We accumulate, we validate that the input are right in a back-end agnostic way. There's no logic about Postgres or SQLite here. Campulation, we turn back this result query in a recursive manner in a SQL string end patterns that are meant to be passed in. to the backend driver to be executed. So we transmit the compile

31:47

Speaker 1: query and we delegate most of the logic to the driver, so Psycho PG or your MySQL driver of choice We delegate all that to the database. The database gets back to us, assuming we get the happy path. So the database was in a valid state and was happy to treat our query and return the data, which is the case most of the time. The tuples that are returned are going to be coursed to the proper expected type to avoid any abstraction leak from the database implementation. And the proper iterable type. And then you have your preset that is nicely built. And that is all. So yeah That was a lot, uh but I think that the important part there is that if you can

32:33

Speaker 1: think of Core set generated by the REM in these four phases, it might make it easier to reason about how things are structured. So there's a lot, but if you approach it by different layers, it makes it a bit easier to reason about the whole thing. So yeah, um thank you for having me and I'm happy to take uh questions if there are any.

32:58

Speaker 2: Amazing. This was such a deep dive into ORM. That was amazing. So yes, let's have the questions or how I'd say that the query sets for today. So please feel free to turn on your mic after raising the hands or maybe put it in the chat. Okay, the first one is Ramad. So yes, please.

33:26

Speaker 3: Okay. Thank you, Saiwan. It was a very interesting session. So I'm a bit curious about how the um get or create method works like So the old like can you like explain how the get or create method was using like these four steps that you've explained?

33:48

Speaker 1: Yep, uh that's a good question. So get or create is is uh is a mix of two things, right? Uh you want to retrieve that uh And assuming it doesn't exist, you want to try to insert it. So you want to make sure that whatever happens, the row will exist in the database and you will get something back out of it. So the way it's implemented, it's not a lazy method, right? It's not returning a query set, so it will perform a uh a compilation. So you pass two things to uh this method. There are a few fields that define the ones that you want to do the lookup against. So fields that you want to make sure that you have a unique constraint defined on it. and the defaults which are going to be used for insertion purpose assuming the row is not found.

34:36

Speaker 1: So the way getter create works, it's going to try to do a SELECT against the value and assuming it it cannot find it, it will um um create um the rule and uh So it's kind of like a mix of two things, a get and a create. And depending on which operation is used, the write that is going to be returned. There's a bit of like semantics around locking and how I guess the isolation level of your database is used there and how to deal with integrity. I don't think we need to go into that. But yeah, it's a composite of two operations, basically, a get and a create. It's just that if you write your own by doing

35:24

Speaker 1: if get If not, if object does not exist create, you're most likely not going to do it right with regards to this image because it's very hard to to do it right um because there's there could be race conditions so between the time that you do the actual get and you want to do the create There could be another request that does try to do the same thing and in between your get a request. Well, it's possible that it might have already been Inserted by another request. So yeah, it's a composite of get or create. And if you're you need to ensure that data exists. your mode you want to use this method instead of rolling out your own because it's it's very hard to to do right.

36:07

Speaker 3: Thank you. Thank you Okay.

36:11

Speaker 2: The next question we have in the chat from Abdul Baseyo. The first question is: in the resolving phase on page 9. You mentioned F name. What is F? Could you please give an example of author query?

36:27

Speaker 1: Yep, uh so F name, if you use the ORM and you try to reference field, um F is a short end for field. So it's a field reference. So if you want to make a reference to a field, you're going to be using uh F object. In the case of what exactly is a SQL query, you can think of it as a lower level query set. So a query set as a query object and it's what it's used to accumulate all the result thing that you've add over your different filter calls. It's bound to the query set model. So yeah to summarize, F means field And um SQL query is a under-the-hood object of uh Core

37:14

Speaker 1: Set.

37:18

Speaker 2: Hope it answers your question. The another question is again from Abdul Baseo. Lazy resolution. What do you mean by lazy, please?

37:26

Speaker 1: Yeah, so if you use query set, uh you might have read from the position that they're lazy. Uh what it means is that um if you define them at the module level or um if you You defined uh particular fields or I guess Django REST framework serializers. You can define the query set by themselves doing filter call, but until you try to iterate over them so with a for loop or you try to do a get on on them, it it's just a declaration of the query. It's not going to be executed by itself. So by laziness I mean It's it's not going to reach out the data eagerly. It's going to be more declarative.

38:11

Speaker 2: Hope this answers. Next question is very interesting from Alex, and that is: what would you change in the ORM if you could break compatibility? Anything in it that you would do better if it could be changed or rewritten?

38:28

Speaker 1: It's a good question. I think there's a mix of things. I think that there's uh I guess I think that the the way um we still depend in a lot of areas on connection um to um determine to take some shortcuts. So I I've said that the resolving phase is completely database agnostic. And it's true in like 99. 9 % of the case, but there are still some cases there that uh predate the introduction of multiple database support and router and so on. And they use the default connection to make some decision about things, how things should be resolved.

39:15

Speaker 1: I think this is something we should we should fix. And I think this is something we will have to fix if we want to get fully into async. And the reason is that Some of this logic is tied to what we call features associated with the database. So when you define a database back in You can define which features it supports and which features it does not. And depending on that, different STL is going to be generated. Well, in this case, sometimes when you try to retrieve um some features, you need to reach out to the database to know which version it is. And that's I. O. You need to talk over the network to do so. But if you want the resolving face to be truly lazy and don't do anything that is I.

40:02

Speaker 1: O. because we want to move to async, for example, and we can't do that, block the event loop. During that time, we need to figure that out. And I think that's something that flew under the radar for a long time. But as we get closer and closer to the getting to an async ORM, we will have to figure out what we want to do in this case.

40:23

Speaker 2: Wow. Such a great answer. So yes, the next up we have Yash. Yes, please. Oh, I cannot

40:36

Speaker 1: hear you. I'm

40:38

Speaker 4: audible?

40:40

Speaker 1: Yeah, I can hear you

40:41

Speaker 4: now. Yeah, so like uh for the past week I've been trying to contribute and do like trying to understand Django ORM and the whole code base. I came to this thing like uh when I was working uh on this issue of atomic uh adding support of atomic upsurts I picked on that issue and I was working on it. And I got to know, I like I took a deep dive into how Postgres works internally and Django RM how it's working. I got to know about the role-level locking system, which you just also said now So I got no posters by default supports it right now. But I also got no Django, Django ORM, itself supports it. I was not getting one thing that if already Postgres R database supports it, the whole uh then why are we like uh getting things done through like the Django ORM Why not like

41:26

Speaker 4: let the database handle it? Because I think the work most of the times, from my point of view, for the past one week research, I feel like things are mostly done on being being done on the application layer. That's what I just wanted to know why the like importance of row-level locking in Django IM is being needed. Because I guess most of the other databases, MariaDB, MySQL, Postgres, I got I researched a little bit. They all support it by default. So that was my question. So

41:51

Speaker 1: you're talking about things like the usage of like explicit usage of select for update, for example, for reliable logging. Yeah. So we use that in a particular area. I think it's One of the areas we use it is in update or create, if I remember correctly. And we use it there because if you are to make some alteration in memory within your application, you need to lock the row for that during that time. The reason is since the alterations are made in memory in order to prevent um racing calls, so um other calls that would try to update the row. Since you're not updating all the fields, you want to kind of like prevent that from happening. So if you want to look at a an actual um

42:37

Speaker 1: Reason why it's used, what I would suggest you do and this is something you can do uh for a lot of uh questions that you might have around uh code in the RM use the git blank feature in GitHub where you can see oh which change actually introduced this this uh why do we use that for update there right you look at the line in particular And Django does a very good job at linking back to tickets and linking back to pull request discussion. So in this particular case, If I remember correctly, it's because there's it could be a race condition and we want to prevent it from happening. But if you do a git blame, you'll be able to walk back to the pull request. and walk back to the ticket and see all of the discussion. It might even link to forum or developer mailing list discussions.

43:23

Speaker 1: So that might be easier to understand why it's there. A lot of the time there's There's things in there that seem like out of order, right? Why exactly we're doing that? But yeah, if you go through the historic background, it might uh explain it a bit. Does that answer your question, Yash?

43:43

Speaker 4: Yeah, kinda. I also had this condition with my mentor in the Jugnot program And we also like kind of he also kind of uh we came to this conclusion only that I think because over the time like previous databases might not have supported it. Like previously, that's why this was present. But now, like as all the databases are developing, they are being open source and all. I think they all have the speech feature now. So obviously ORM is very uncertain. That's what I got to know.

44:08

Speaker 1: Okay.

44:08

Speaker 4: Thank you so much.

44:14

Speaker 2: Okay. Uh I'll stop the recording now and then we can pick up more questions. Okay.

Questions this talk answers

What is a Django ORM, and what does it do?

An ORM maps object-oriented classes and instances to database tables and rows, letting you retrieve, aggregate, insert, update, and delete data through Python objects instead of writing SQL directly.

Discussed at 0:45

How does a Django model become a database table and store data?

When migrations run, Django creates a table for the model, mapping model fields to columns. Creating an object through the ORM is translated into an SQL INSERT with the corresponding table, columns, and values.

Discussed at 2:16

How does Django turn a QuerySet into SQL?

Django processes a QuerySet in stages: it resolves fields and expressions into an internal, database-agnostic representation, then recursively compiles that representation into SQL and parameters using the selected database backend.

Discussed at 5:23

What happens during the Django ORM resolving phase?

The resolving phase remains lazy while QuerySet methods are chained. Django validates filter values and resolves field references in the context of the query, accumulating a structure of expressions without contacting the database.

Discussed at 6:12

What happens during Django ORM query execution and result construction?

After compilation, Django sends the SQL and parameters through the database driver, which returns rows or an exception. Django then converts database values to the expected Python types and builds dictionaries, tuples, or model instances depending on whether `values()`, `values_list()`, or neither was used.

Discussed at 22:31

How does `get_or_create()` work in Django, and why is it safer than writing get-then-create yourself?

It first performs a lookup and, if no row is found, attempts to create one using the supplied defaults. Its implementation accounts for database constraints, locking, isolation, and race conditions that can occur between a separate get and create operation.

Discussed at 33:48

What does `F()` mean in a Django ORM query?

`F()` is a field reference: it lets a query refer to a model field rather than treating the value as a literal. Internally, Django resolves that reference to the appropriate column in the query.

Discussed at 36:27

What does lazy evaluation mean for Django QuerySets?

A QuerySet is declarative and does not execute when you build it with methods such as `filter()`. Database access occurs only when an operation such as iteration or `get()` requires results.

Discussed at 37:26

Presenters

Note: We understand that names change, people change, and bodies change. We respect each individual's journey and privacy. If you have any concerns about a video or need us to remove content, please don't hesitate to contact us. We will handle your request with care and promptly address any issues.

More videos by Simon Charette

More videos from Djangonaut Space