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:
mkdir django_library
cd django_library
Now, create a virtual environment. This keeps your project's dependencies separate from other projects:
python3 -m venv env
Activate the virtual environment:
source env/bin/activate
Next, install Django inside the virtual environment:
pip install django
Now create a new Django project:
django-admin startproject library_project .
Then create a new app called books
:
python manage.py startapp books
Now, edit the books/models.py
file to define your first models:
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
birth_date = models.DateField(null=True, blank=True)
biography = models.TextField(blank=True)
def __str__(self):
return self.name
class Meta:
ordering = ['name']
class Book(models.Model):
GENRE_CHOICES = [
('FIC', 'Fiction'),
('NON', 'Non-Fiction'),
('SCI', 'Science'),
('HIS', 'History'),
]
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='books')
publication_date = models.DateField()
isbn = models.CharField(max_length=13, unique=True)
genre = models.CharField(max_length=3, choices=GENRE_CHOICES, default='FIC')
pages = models.PositiveIntegerField(default=0)
def __str__(self):
return self.title
class Meta:
ordering = ['-publication_date']
indexes = [
models.Index(fields=['title']),
models.Index(fields=['isbn']),
]
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
:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'books', # Add your app here
]
First, generate the migration files for the books
app:
python manage.py makemigrations books
Migrations for 'books':
books/migrations/0001_initial.py
+ Create model Author
+ Create model Book
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:
python manage.py migrate
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:
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'library_project.settings')
django.setup()
from books.models import Author, Book
# Create an author
austen = Author.objects.create(
name="Jane Austen",
birth_date="1775-12-16",
biography="English novelist known for her six major novels."
)
# Create a book for this author
Book.objects.create(
title="Pride and Prejudice",
author=austen,
publication_date="1813-01-28",
isbn="9780141439518",
genre="FIC",
pages=432
)
print("Created author and book successfully!")
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:
python add_books.py
Created author and book successfully!
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:
# Create with a single method call
orwell = Author.objects.create(
name="George Orwell",
birth_date="1903-06-25"
)
Alternatively, you can instantiate a model and then save it separately:
# Create with instantiation + save
tolkien = Author(
name="J.R.R. Tolkien",
birth_date="1892-01-03"
)
tolkien.save()
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:
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'library_project.settings')
django.setup()
from books.models import Author, Book
# Get all books
print("==== All Books ====")
all_books = Book.objects.all()
for book in all_books:
print(f"{book.title} by {book.author.name}")
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:
python query_books.py
==== All Books ====
Pride and Prejudice by Jane Austen
Django provides many ways to query your data. To get a specific object, use get()
:
# Get a specific book by primary key
book = Book.objects.get(pk=1)
To filter records based on criteria, use filter()
:
# Find books by a specific author
austen_books = Book.objects.filter(author__name="Jane Austen")
You can use field lookups with double underscores for more complex conditions:
# Books published after 1900
modern_books = Book.objects.filter(publication_date__gt="1900-01-01")
To control the ordering of results, use order_by()
:
# Order books by publication date (oldest first)
ordered_books = Book.objects.order_by('publication_date')
# Order by price in descending order (newest first)
newest_first = Book.objects.order_by('-publication_date')
If you only need a single record, use first()
or last()
:
# Get the oldest book
oldest_book = Book.objects.order_by('publication_date').first()
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:
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'library_project.settings')
django.setup()
from books.models import Book
# Update a single book
try:
book = Book.objects.get(title="Pride and Prejudice")
print(f"Before: {book.title}, Pages: {book.pages}")
book.pages = 424 # Update the page count
book.save()
# Refresh from database
book.refresh_from_db()
print(f"After: {book.title}, Pages: {book.pages}")
except Book.DoesNotExist:
print("Book not found")
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:
python update_books.py
Before: Pride and Prejudice, Pages: 432
After: Pride and Prejudice, Pages: 424
To update an existing object, you retrieve it, modify its attributes, and then save it:
book = Book.objects.get(title="1984")
book.genre = "DYS" # Change genre to Dystopian
book.save()
For updating multiple records at once, use the update()
method on a QuerySet:
# Update all fiction books to classic fiction
Book.objects.filter(genre="FIC").update(genre="CLS")
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:
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'library_project.settings')
django.setup()
from books.models import Book
# Delete a specific book
try:
book = Book.objects.get(title="Pride and Prejudice")
print(f"Deleting: {book.title}")
book.delete()
print("Book deleted successfully")
# Verify deletion
remaining = Book.objects.count()
print(f"Remaining books: {remaining}")
except Book.DoesNotExist:
print("Book not found")
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:
python delete_books.py
Deleting: Pride and Prejudice
Book deleted successfully
Remaining books: 0
To delete a single object, call its delete()
method:
author = Author.objects.get(name="George Orwell")
author.delete() # This also deletes all associated books because of CASCADE
For bulk deletions, call delete()
on a QuerySet:
# Delete all books published before 1800
Book.objects.filter(publication_date__lt="1800-01-01").delete()
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:
from django.db.models import Q
# Books that are either fiction OR have more than 400 pages
complex_query = Book.objects.filter(Q(genre="FIC") | Q(pages__gt=400))
# Books that are NOT fiction AND published after 1950
complex_query = Book.objects.filter(~Q(genre="FIC"), publication_date__gt="1950-01-01")
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:
from django.db.models import Avg, Count, Sum, Min, Max
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:
author_book_counts = Author.objects.annotate(num_books=Count('books'))
for author in author_book_counts:
print(f"{author.name} has written {author.num_books} book(s).")
To get the average number of pages across all books, use the aggregate()
method:
avg_pages = Book.objects.aggregate(Avg('pages'))
print(f"Average pages per book: {avg_pages['pages__avg']}")
To find the author with the most books, annotate with a count and order the results in descending order:
most_prolific_author = Author.objects.annotate(num_books=Count('books')).order_by('-num_books').first()
if most_prolific_author:
print(f"{most_prolific_author.name} has the most books: {most_prolific_author.num_books}")
To get the total number of pages written by each author, use Sum
on the related books’ pages
field:
author_page_counts = Author.objects.annotate(total_pages=Sum('books__pages'))
for author in author_page_counts:
print(f"{author.name} has written {author.total_pages} total pages.")
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.
# Forward access (from author to books)
author = Author.objects.get(name="Jane Austen")
jane_austen_books = author.books.all() # Uses the related_name defined in the model
In this example, author.books.all()
retrieves all books written by Jane Austen by following the reverse relationship from Author
to Book
.
# Backward access (from book to author)
book = Book.objects.get(title="Pride and Prejudice")
author = book.author # Direct access to the related author
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.
from django.db.models import F
# Increase the page count of all books by 10%
Book.objects.update(pages=F('pages') * 1.1)
This updates the pages
field for every book by multiplying it by 1.1—without pulling the data into Python first.
# Find books where the title is the same as the author's name
Book.objects.filter(title=F('author__name'))
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
.
# Without select_related:
# Makes one query for books, then one query per book to get the author
books = Book.objects.all()
for book in books:
print(f"{book.title} by {book.author.name}") # Triggers extra queries
# With select_related:
# Fetches books and their authors in a single query using a JOIN
books = Book.objects.select_related('author').all()
for book in books:
print(f"{book.title} by {book.author.name}") # No extra queries
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:
# Prefetch all books for each author
authors = Author.objects.prefetch_related('books').all()
for author in authors:
print(f"{author.name} has written {author.books.count()} books")
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:
class Meta:
indexes = [
models.Index(fields=['title']),
models.Index(fields=['isbn']),
]
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:
# Only load specific fields from the database
authors = Author.objects.only('name', 'birth_date')
# Load everything except the 'biography' field
authors = Author.objects.defer('biography')
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:
# Slow: creates 1000 authors with 1000 separate INSERTs
for i in range(1000):
Author.objects.create(name=f"Author {i}")
# Fast: creates all authors in a single query
authors = [Author(name=f"Author {i}") for i in range(1000)]
Author.objects.bulk_create(authors)
You can also perform bulk updates and deletes:
# Bulk update: set the same biography for multiple authors
Author.objects.filter(name__startswith="Author").update(biography="A generated author biography.")
# Bulk delete: remove all test authors
Author.objects.filter(name__startswith="Author").delete()
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!
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github