Django is a Python web framework that helps you build websites quickly and with clean, practical code. One of its most powerful features is the Object-Relational Mapping (ORM) system. It lets you work with databases using Python code instead of writing SQL queries by hand.
Django’s ORM is a big reason why many developers choose the framework. It gives you a great mix of simplicity and control. With features like smart querying, built-in database migrations, and automatic table creation, it’s one of the most advanced ORMs in the Python world.
In this article, we’ll walk through the basics of Django’s ORM—from defining models to running more complex queries.
Prerequisites
Before you start, make sure you know some basic Python and have a general idea of how Django works. You should also understand simple database concepts like tables, relationships, and queries.
If you're new to Django, check out the official Django tutorial to get up to speed.
Understanding Django models
Models are the foundation of Django’s ORM. They define what your database tables look like and how they relate to each other. Each model is a Python class that inherits from django.db.models.Model, and each class attribute becomes a field in your database.
Let's start by creating a directory for your project and moving into it:
Now, create a virtual environment. This keeps your project's dependencies separate from other projects:
Activate the virtual environment:
Next, install Django inside the virtual environment:
Now create a new Django project:
Then create a new app called books:
Now, edit the books/models.py file to define your first models:
Let's break down what we've created:
1. The Author model represents the authors of books with fields for name, birth date, and biography.
2. The Book model represents individual books with fields for title, publication date, ISBN, genre, and page count.
3. A relationship is established between books and authors using a ForeignKey field, which means each book belongs to one author, but an author can have multiple books.
Before Django can use these models, we need to add our app to the project's INSTALLED_APPS setting in settings.py:
First, generate the migration files for the books app:
This tells Django to create a migration file that records the changes to your models.
Next, apply the migrations to create the actual tables in the database:
Django will generate the necessary SQL to create the database tables based on your model definitions. The makemigrations command creates migration files that describe the changes needed to update the database schema, and the migrate command applies those changes to the database.
CRUD operations with Django ORM
The primary purpose of any ORM is to provide an intuitive way to perform CRUD (Create, Read, Update, Delete) operations on your data. Let's explore how to perform these operations using Django's ORM.
Creating objects
Let's create a script called add_books.py to populate our database with some initial data:
This script sets up Django, imports your models, and uses .create() to add an author and a related book to the database.
Run this script to add data to your database:
You can create objects in Django using two main approaches. The first is using the create() method as shown above, which creates and saves the object in one step:
Alternatively, you can instantiate a model and then save it separately:
Both approaches are equivalent - the choice depends on your preference and specific needs.
Reading objects
Once you’ve added data to your database, the next step is learning how to retrieve it. Django’s ORM makes it easy to query your data using simple Python code—no need to write SQL manually.
Now let's create query_books.py to retrieve data from our database:
The Book.objects.all() method fetches all records from the books table and returns them as a QuerySet of Book instances. Each instance represents a row in the table, and you can access properties like title and author directly.
Run this script to see your books:
Django provides many ways to query your data. To get a specific object, use get():
To filter records based on criteria, use filter():
You can use field lookups with double underscores for more complex conditions:
To control the ordering of results, use order_by():
If you only need a single record, use first() or last():
Updating objects
At some point, you'll need to update existing data in your database—whether you're fixing a typo, updating a field, or changing a relationship. Django makes this easy using the ORM.
Let's create update_books.py to modify existing data:
This script fetches a specific book by title, updates its pages field, and saves the change back to the database.
The call to refresh_from_db() reloads the object so you can confirm the update.
Run this script to see the update in action:
To update an existing object, you retrieve it, modify its attributes, and then save it:
For updating multiple records at once, use the update() method on a QuerySet:
This is more efficient than retrieving and saving each object individually.
Deleting objects
Eventually, you’ll need to remove data from your database—whether you’re cleaning up test records or deleting old entries. Django’s ORM makes deleting objects simple and safe.
Finally, let's create delete_books.py to remove records:
Here’s what the script does:
- Finds a book by title using
get(). - Deletes it with
book.delete(). - Prints confirmation and shows how many books are left in the database.
- Handles the case where the book doesn’t exist to avoid a crash.
Run this script to delete a book:
To delete a single object, call its delete() method:
For bulk deletions, call delete() on a QuerySet:
Remember that when you delete an object with relationships, the behavior depends on the on_delete option you specified in the model definition. In our case with on_delete=models.CASCADE, deleting an author will also delete all of their books.
Advanced querying techniques
While basic CRUD operations might suffice for simple applications, Django's ORM truly shines with its advanced querying capabilities. Let's explore some of these features.
Complex lookups using Q objects
Django's Q objects allow you to build complex queries with OR, AND, and NOT conditions:
Aggregations and annotations
Django’s ORM includes built-in support for performing calculations like totals, averages, counts, and more—directly at the database level. These are useful when you need summary statistics or want to add calculated values to your query results.
Start by importing the aggregation functions:
To count the number of books for each author, use the annotate() method. This adds a new field to each Author object with the number of related Book entries:
To get the average number of pages across all books, use the aggregate() method:
To find the author with the most books, annotate with a count and order the results in descending order:
To get the total number of pages written by each author, use Sum on the related books’ pages field:
These aggregation and annotation tools let you pull meaningful insights from your data with just a few lines of code—efficiently and directly from the database.
Working with related objects
Django makes it easy to navigate relationships between models using simple attribute access.
In this example, author.books.all() retrieves all books written by Jane Austen by following the reverse relationship from Author to Book.
Here, you can access the related Author instance directly from a Book object using the author field.
Using F expressions for database-level operations
Django’s F expressions let you reference model fields directly in queries. This is useful for performing operations that should happen at the database level—especially when you want updates to be atomic and efficient.
This updates the pages field for every book by multiplying it by 1.1—without pulling the data into Python first.
In this case, F('author__name') compares the title field of a book to its related author’s name. F expressions are especially useful when you want to compare or modify fields relative to each other within a single query.
Optimizing performance with Django ORM
Django’s ORM is powerful and convenient, but you need to understand how to make your queries efficient to get the most out of it. Here are some best practices that can help you reduce database load and improve overall performance.
Reducing database queries with select_related and prefetch_related
One of the most common performance pitfalls is the N+1 query problem—this happens when accessing related objects, causing a new query for each item in a queryset. Django provides two tools to help with this: select_related and prefetch_related.
Use select_related for foreign key and one-to-one relationships—it works by joining related tables in the same query.
For many-to-many or reverse foreign key relationships, use prefetch_related, which performs separate queries but combines the results efficiently:
Using database indexes wisely
Indexes speed up queries by allowing the database to look up rows faster. They’re especially helpful on fields used for filtering, ordering, or joining.
If you're using fields like title or isbn frequently in queries, indexing them is a smart move:
But keep in mind: too many indexes can slow down writes (inserts and updates) and increase storage size. Use them thoughtfully.
Deferring fields with defer() and only()
If your model has large fields (like long text or binary data) that you don’t always need, you can speed things up by telling Django to skip them initially:
This helps reduce the amount of data transferred from the database, especially in list views or bulk operations.
Bulk operations for efficiency
Creating, updating, or deleting many objects one by one can be slow. Instead, use Django’s built-in bulk operations to handle large batches more efficiently:
You can also perform bulk updates and deletes:
Applying these techniques makes your Django app faster, more efficient, and better prepared to scale. Let me know if you want to dive into query inspection or debugging tools like django-debug-toolbar next!
Final thoughts
Django’s ORM makes it easy to work with databases using clean, Pythonic code. It handles everything from basic CRUD to advanced queries and performance tuning.
As you continue learning, explore topics like Django REST Framework, async ORM support, and advanced query expressions.
For more, check out the official Django docs and stay connected with the Django community.
Happy coding!