# Introduction to Django ORM

[Django](https://www.djangoproject.com/) 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.

[ad-logs]

## 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](https://docs.djangoproject.com/en/stable/intro/tutorial01/) 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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/80f703ee-950a-467f-af78-53b336011000/md2x =1948x728)

Let's start by creating a directory for your project and moving into it:

```command
mkdir django_library
```
```command
cd django_library
```

Now, create a virtual environment. This keeps your project's dependencies separate from other projects:

```command
python3 -m venv env
```

Activate the virtual environment:

```command
source env/bin/activate
```

Next, install Django inside the virtual environment:

```command
pip install django
```

Now create a new Django project:

```command
django-admin startproject library_project .
```

Then create a new app called `books`:

```command
python manage.py startapp books
```

Now, edit the `books/models.py` file to define your first models:

```python
[label 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`:

```python
[label library_project/settings.py]
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
[highlight]
    'books',  # Add your app here
[/highlight]
]
```

First, generate the migration files for the `books` app:

```command
python manage.py makemigrations books
```

```text
[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:

```command
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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/3924f137-0bff-4a23-d9a5-957a09ce8200/lg2x =2040x1258)

### Creating objects

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

```python
[label 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:

```command
python add_books.py
```

```text
[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:

```python
# 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:

```python
# 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:

```python
[label 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:

```command
python query_books.py
```

```text
[output]
==== All Books ====
Pride and Prejudice by Jane Austen
```

Django provides many ways to query your data. To get a specific object, use `get()`:

```python
# Get a specific book by primary key
book = Book.objects.get(pk=1)
```

To filter records based on criteria, use `filter()`:

```python
# 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:

```python
# Books published after 1900
modern_books = Book.objects.filter(publication_date__gt="1900-01-01")
```

To control the ordering of results, use `order_by()`:

```python
# 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()`:

```python
# 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:

```python
[label 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:

```command
python update_books.py
```

```text
[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:

```python
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:

```python
# 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:

```python
[label 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:

```command
python delete_books.py
```

```text
[output]
Deleting: Pride and Prejudice
Book deleted successfully
Remaining books: 0
```

To delete a single object, call its `delete()` method:

```python
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:

```python
# 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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
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.

```python
# 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`.

```python
# 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.

```python
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.

```python
# 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`.

```python
# 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
```

```python
# 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:

```python
# 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:

```python
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:

```python
# Only load specific fields from the database
authors = Author.objects.only('name', 'birth_date')
```

```python
# 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:

```python
# Slow: creates 1000 authors with 1000 separate INSERTs
for i in range(1000):
    Author.objects.create(name=f"Author {i}")
```

```python
# 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:

```python
# Bulk update: set the same biography for multiple authors
Author.objects.filter(name__startswith="Author").update(biography="A generated author biography.")
```

```python
# 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](https://docs.djangoproject.com/) and stay connected with the Django community.

Happy coding!