1. Introduction

Django ORM makes it easy to work with relational databases by offering a high-level abstraction over SQL. This blog will guide you through setting up a Django model and using it to illustrate ORM operations—from basic CRUD to advanced querying and optimizations.

1.1. Why Django ORM?

  • Simple syntax: Enables Python-based queries, eliminating the need for raw SQL.
  • Database agnosticism: Allows easy switching between databases.
  • Migrations and schema management: Simplifies database migrations.

2. Creating the Django Model

To start, let’s create a sample model we’ll use throughout this blog—a Book model with attributes for title, author, price, publication date, and relationships to other models.

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

class Publisher(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
    price = models.DecimalField(max_digits=6, decimal_places=2)
    publication_date = models.DateField()

    def __str__(self):
        return self.title

Note: Click here to learn more about Django Models.

2.1. Model Explanation

  • The author and Publisher have a one-to-many relationship with the Book.
  • price and publication_date fields are used for additional data handling and examples.

Once we’ve created these models, we can migrate them to create the corresponding database tables.

python manage.py makemigrations
python manage.py migrate

Note: Click here to learn more about migrations in Django.

3. Basic CRUD Operations

With our Book model set up, we can perform basic CRUD operations.

3.1. Create

To add a new book, instantiate a Book object and save it.

from myapp.models import Book, Author, Publisher
from datetime import datetime

author = Author.objects.create(name="William S. Vincent")
publisher = Publisher.objects.create(name="Tech Books Publishing")

# Create and save a book
book = Book(title="Django for Beginners", author=author, publisher=publisher, price=29.99, publication_date=datetime.now())
book.save()

3.2. Read

Retrieve all books or filter based on specific fields.

# Fetch all books
books = Book.objects.all()

# Filter books by author
filtered_books = Book.objects.filter(author=author)

3.3. Update

Retrieve an instance, modify it, and save the changes.

# Update a book's title
book = Book.objects.get(id=1)
book.title = "Advanced Django"
book.save()

3.4. Delete

Remove an instance from the database.

# Delete a book
book = Book.objects.get(id=1)
book.delete()

4. Advanced Querying with Django ORM

Django ORM offers a range of options for performing more complex queries.

4.1. Using Q Objects for Complex Lookups

Q objects allow the use of logical operators in queries, like OR conditions.

from django.db.models import Q

# Books by either "Author A" or "Author B"
books = Book.objects.filter(Q(author__name="Author A") | Q(author__name="Author B"))

Note: Click here to learn more about Q objects in Python.

4.2. Aggregation and Annotation

aggregate() and annotate() methods allow calculations on query results.

from django.db.models import Avg

# Calculate the average price of all books
average_price = Book.objects.aggregate(Avg('price'))

Note: Click here to learn more about aggregation in Django.

4.3. Using values() and values_list()

Retrieve specific fields, which can be more efficient when working with large datasets.

# Fetch only book titles
titles = Book.objects.values_list('title', flat=True)

5. Custom Queries in Django ORM

Sometimes, the ORM alone may not suffice, and custom queries might be needed.

5.1. Executing Raw SQL Queries

With .raw(), you can execute SQL directly.

# Fetch books using raw SQL
books = Book.objects.raw("SELECT * FROM myapp_book WHERE price > %s", [20.00])

5.2. Custom Managers and QuerySets

You can add custom querying functionality using custom managers.

from django.db import models

class BookManager(models.Manager):
    def expensive_books(self):
        return self.filter(price__gt=50)

class Book(models.Model):
    # model fields
    objects = BookManager()

# Using the custom manager method
expensive_books = Book.objects.expensive_books()

6. Performance Optimization Techniques

For applications with large datasets, performance optimization becomes crucial.

6.1. Use select_related and prefetch_related

select_related and prefetch_related reduce the number of database queries in related lookups.

# Use select_related for foreign key relationships
books = Book.objects.select_related('publisher').all()

# Use prefetch_related for many-to-many relationships
books = Book.objects.prefetch_related('author').all()

Note: Click here to learn more about select_related and prefetch_related.

6.2. Limit QuerySets for Large Datasets

Limit fields with .only() and restrict the number of rows returned.

# Fetch only specific fields
books = Book.objects.only("title", "author")[:10]

6.3. Caching Strategies

Cache frequently accessed data to reduce load on the database.

Step 1: Configure Caching in settings.py

Add a caching backend in your settings.py. For example, using in-memory caching:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
        'LOCATION': 'unique-snowflake',
    }
}

Step 2: Use Caching in Queries

Here’s how to cache the result of a query:

from django.core.cache import cache
from myapp.models import Book

# Check if the data is already in cache
cached_books = cache.get('all_books')

if not cached_books:
    # Query the database if not in cache
    cached_books = list(Book.objects.all())
    # Store the result in cache for 5 minutes (300 seconds)
    cache.set('all_books', cached_books, 300)

# Use the cached_books data
for book in cached_books:
    print(book.title)

7. ORM Best Practices

  • Use select_related and prefetch_related to optimize database queries for related objects.
  • Limit fields in queries using .only() or .defer() to fetch only the necessary data.
  • Combine multiple filters into a single query to avoid unnecessary database hits.
  • Use values() or values_list() when you only need specific fields for better performance.
  • Leverage database indexing on frequently queried fields for faster lookups.
  • Cache expensive queries using Django's caching framework to reduce database load.
  • Avoid unnecessary QuerySet evaluations by being mindful of lazy evaluation.
  • Profile and debug queries with tools like Django Debug Toolbar or SQL query logs.
  • Use custom managers for complex, reusable query logic.

8. Django ORM Gotchas

8.1. Lazy Evaluation

Querysets are lazily evaluated—they only hit the database when you access data.

# No query until accessed
books = Book.objects.all()
print(books)  # Query executed here

8.2. Understanding Auto-Generated SQL

Django ORM generates SQL under the hood, which you can inspect using the query attribute.

# Print SQL query
print(str(Book.objects.filter(author=author).query))

9. Conclusion

Django ORM is a versatile, powerful tool that simplifies database operations in Django applications. By understanding its advanced querying capabilities, using custom managers, and implementing optimization techniques, you can build scalable, efficient applications.

Also Read:

How to build web applications with Django

Django QuerySets explained