Back to Scaling Python Applications guides

Introduction to Django ORM

Stanley Ulili
Updated on April 10, 2025

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.

Screenshot of the Django ORM workflow

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:

books/models.py
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:

library_project/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
Output
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.

Screenshot of the CRUD diagram

Creating objects

Let's create a script called add_books.py to populate our database with some initial data:

add_books.py
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
Output
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:

query_books.py
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
Output
==== 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:

update_books.py
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
Output
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:

delete_books.py
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
Output
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.

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.

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!

Author's avatar
Article by
Stanley Ulili
Stanley Ulili is a technical educator at Better Stack based in Malawi. He specializes in backend development and has freelanced for platforms like DigitalOcean, LogRocket, and AppSignal. Stanley is passionate about making complex topics accessible to developers.
Got an article suggestion? Let us know
Next article
Jinja Templating in Python: A Practical Guide
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

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
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
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.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github