Django ORM Patterns

The Django ORM is powerful but easy to misuse. These patterns keep queries efficient and code readable.

QuerySet Chaining

QuerySets are lazy — they don't hit the database until evaluated:

# No query yet — just building the filter
posts = Post.objects.filter(status=Post.Status.PUBLISHED)
posts = posts.filter(categories__slug="python")
posts = posts.order_by("-published_date")

# NOW the query runs (iteration triggers evaluation)
for post in posts:
    print(post.title)

Avoiding N+1 Queries

The most common performance problem. Use select_related for ForeignKey and prefetch_related for reverse/M2M:

# BAD: N+1 — one query per doc to get its subject
docs = Doc.objects.all()
for doc in docs:
    print(doc.subject.name)  # Extra query each time

# GOOD: single JOIN
docs = Doc.objects.select_related("subject").all()
for doc in docs:
    print(doc.subject.name)  # No extra query

# For reverse relations or M2M
subjects = Subject.objects.prefetch_related("docs").all()
for subject in subjects:
    print(subject.docs.count())  # Already cached

update_or_create for Idempotent Seeding

Perfect for seed scripts that might run multiple times:

subject, created = Subject.objects.update_or_create(
    slug="ai-agents",  # Lookup key
    defaults={          # Fields to set/update
        "name": "AI Agents",
        "description": "...",
        "updated_at": int(time.time()),
    },
)
action = "Created" if created else "Updated"

Aggregation

Use the database for math, not Python:

from django.db.models import Count, Avg, Q

# Count published docs per subject
subjects = Subject.objects.annotate(
    doc_count=Count("docs", filter=Q(docs__is_published=True))
)

for s in subjects:
    print(f"{s.name}: {s.doc_count} docs")

Manager Methods

Encapsulate common queries in custom managers:

class PostManager(models.Manager):
    def published(self):
        return self.filter(status=Post.Status.PUBLISHED)

    def by_category(self, slug):
        return self.published().filter(categories__slug=slug)

class Post(models.Model):
    objects = PostManager()

# Usage
posts = Post.objects.published()
python_posts = Post.objects.by_category("python")