Django ORM Patterns
Master Django ORM for building efficient, scalable database-driven applications with complex queries and relationships.
Model Definition
Define models with proper field types, constraints, and metadata.
from django.db import models from django.core.validators import MinValueValidator, MaxValueValidator
class User(models.Model): email = models.EmailField(unique=True, db_index=True) name = models.CharField(max_length=100) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['email']),
models.Index(fields=['created_at', 'is_active']),
]
verbose_name = 'User'
verbose_name_plural = 'Users'
def __str__(self):
return self.email
class Post(models.Model): title = models.CharField(max_length=200) content = models.TextField() author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts') published = models.BooleanField(default=False) views = models.PositiveIntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['author', 'published']),
]
QuerySet API Basics
Use Django's QuerySet API for efficient database queries.
All records
users = User.objects.all()
Filtering
active_users = User.objects.filter(is_active=True) inactive_users = User.objects.exclude(is_active=True)
Get single record (raises exception if not found or multiple found)
user = User.objects.get(email='user@example.com')
Get or create
user, created = User.objects.get_or_create( email='user@example.com', defaults={'name': 'John Doe'} )
Update or create
user, created = User.objects.update_or_create( email='user@example.com', defaults={'name': 'Jane Doe', 'is_active': True} )
Chaining filters
posts = Post.objects.filter(published=True).filter(author__is_active=True)
Order by
users = User.objects.order_by('-created_at', 'name')
Limit results
recent_users = User.objects.all()[:10]
Count
user_count = User.objects.filter(is_active=True).count()
Exists
has_active_users = User.objects.filter(is_active=True).exists()
Q Objects for Complex Queries
Build complex queries with Q objects for OR and NOT operations.
from django.db.models import Q
OR queries
users = User.objects.filter( Q(name__icontains='john') | Q(email__icontains='john') )
AND with OR
users = User.objects.filter( Q(is_active=True) & (Q(name__icontains='john') | Q(email__icontains='john')) )
NOT queries
users = User.objects.filter(~Q(is_active=True))
Complex combinations
posts = Post.objects.filter( Q(published=True) & (Q(author__name__icontains='john') | Q(title__icontains='important')) & ~Q(views__lt=100) )
Dynamic query building
def search_users(name=None, email=None, is_active=None): query = Q() if name: query &= Q(name__icontains=name) if email: query &= Q(email__icontains=email) if is_active is not None: query &= Q(is_active=is_active) return User.objects.filter(query)
F Objects for Field References
Use F objects to reference model fields in queries and updates.
from django.db.models import F
Compare fields
posts = Post.objects.filter(views__gt=F('author__posts__count'))
Update based on current value
Post.objects.filter(published=True).update(views=F('views') + 1)
Avoid race conditions
post = Post.objects.get(id=1) post.views = F('views') + 1 post.save() post.refresh_from_db() # Get updated value
Complex expressions
from django.db.models import ExpressionWrapper, IntegerField
Post.objects.annotate( adjusted_views=ExpressionWrapper( F('views') * 2 + 10, output_field=IntegerField() ) )
Aggregation and Annotation
Perform database-level calculations and add computed fields.
from django.db.models import Count, Sum, Avg, Max, Min
Simple aggregation
from django.db.models import Avg avg_views = Post.objects.aggregate(Avg('views'))
Returns: {'views__avg': 42.5}
Multiple aggregations
stats = Post.objects.aggregate( total_posts=Count('id'), avg_views=Avg('views'), max_views=Max('views'), min_views=Min('views') )
Annotation (adds field to each object)
users = User.objects.annotate( post_count=Count('posts'), total_views=Sum('posts__views') )
for user in users: print(f"{user.name}: {user.post_count} posts, {user.total_views} views")
Filter by annotation
popular_users = User.objects.annotate( post_count=Count('posts') ).filter(post_count__gt=10)
Complex annotations
from django.db.models import Case, When, Value, CharField
User.objects.annotate( user_type=Case( When(post_count__gt=10, then=Value('prolific')), When(post_count__gt=5, then=Value('active')), default=Value('casual'), output_field=CharField() ) )
Prefetch and Select Related (N+1 Prevention)
Optimize queries by reducing database hits with eager loading.
Select related (for ForeignKey and OneToOne)
posts = Post.objects.select_related('author').all() for post in posts: print(post.author.name) # No additional query
Prefetch related (for ManyToMany and reverse ForeignKey)
from django.db.models import Prefetch
users = User.objects.prefetch_related('posts').all() for user in users: for post in user.posts.all(): # No additional query print(post.title)
Custom prefetch
users = User.objects.prefetch_related( Prefetch( 'posts', queryset=Post.objects.filter(published=True).order_by('-created_at') ) )
Multiple levels
posts = Post.objects.select_related( 'author' ).prefetch_related( 'author__posts' # Prefetch all posts by the same author )
Combining both
Post.objects.select_related('author').prefetch_related('tags')
Custom Managers and QuerySets
Create reusable query logic with custom managers and querysets.
from django.db import models
class PublishedQuerySet(models.QuerySet): def published(self): return self.filter(published=True)
def recent(self):
return self.order_by('-created_at')[:10]
def by_author(self, author):
return self.filter(author=author)
class PublishedManager(models.Manager): def get_queryset(self): return PublishedQuerySet(self.model, using=self._db)
def published(self):
return self.get_queryset().published()
def recent(self):
return self.get_queryset().recent()
class Post(models.Model): # fields... objects = models.Manager() # Default manager published_posts = PublishedManager() # Custom manager
class Meta:
base_manager_name = 'objects'
Usage
Post.published_posts.published().recent() Post.published_posts.published().by_author(user)
Chaining custom methods
class UserQuerySet(models.QuerySet): def active(self): return self.filter(is_active=True)
def with_posts(self):
return self.annotate(post_count=Count('posts')).filter(post_count__gt=0)
User.objects.active().with_posts()
Transactions and Atomic Blocks
Ensure data consistency with database transactions.
from django.db import transaction
Atomic decorator
@transaction.atomic def create_user_with_post(email, name, post_title): user = User.objects.create(email=email, name=name) Post.objects.create(title=post_title, author=user) return user
Context manager
def update_user_posts(user_id): try: with transaction.atomic(): user = User.objects.select_for_update().get(id=user_id) user.posts.update(published=True) user.is_active = True user.save() except Exception as e: # Transaction is rolled back raise
Savepoints
from django.db import transaction
with transaction.atomic(): user = User.objects.create(email='user@example.com')
sid = transaction.savepoint()
try:
Post.objects.create(title='Test', author=user)
except:
transaction.savepoint_rollback(sid)
else:
transaction.savepoint_commit(sid)
Select for update (locking)
with transaction.atomic(): user = User.objects.select_for_update().get(id=1) user.is_active = False user.save()
Advanced Select and Prefetch Patterns
Master complex query optimization with advanced eager loading techniques.
from django.db.models import Prefetch, Count, Q
Basic select_related (ForeignKey, OneToOne)
posts = Post.objects.select_related('author', 'category')
Multi-level select_related
comments = Comment.objects.select_related('post__author__profile')
Prefetch with custom queryset
users = User.objects.prefetch_related( Prefetch( 'posts', queryset=Post.objects.filter(published=True).select_related('category'), to_attr='published_posts' ) )
Multiple prefetch with different filters
authors = User.objects.prefetch_related( Prefetch( 'posts', queryset=Post.objects.filter(published=True), to_attr='published_posts' ), Prefetch( 'posts', queryset=Post.objects.filter(published=False), to_attr='draft_posts' ) )
Nested prefetch
posts = Post.objects.prefetch_related( Prefetch( 'comments', queryset=Comment.objects.select_related('author').prefetch_related( Prefetch( 'replies', queryset=Comment.objects.select_related('author') ) ) ) )
Prefetch with annotations
users = User.objects.prefetch_related( Prefetch( 'posts', queryset=Post.objects.annotate( comment_count=Count('comments') ).filter(comment_count__gt=0) ) )
Database Functions and Expressions
Leverage database functions for complex operations.
from django.db.models import F, Value, CharField, Case, When, Q from django.db.models.functions import Concat, Lower, Upper, Length, Substr, Coalesce
String operations
users = User.objects.annotate( full_name=Concat('first_name', Value(' '), 'last_name') )
users = User.objects.annotate( email_lower=Lower('email'), name_upper=Upper('name') )
String functions
posts = Post.objects.annotate( title_length=Length('title') ).filter(title_length__gt=50)
Substring
posts = Post.objects.annotate( title_preview=Substr('title', 1, 50) )
Coalesce (return first non-null value)
posts = Post.objects.annotate( display_name=Coalesce('custom_title', 'title', Value('Untitled')) )
Date functions
from django.db.models.functions import TruncDate, TruncMonth, ExtractYear, Now
posts = Post.objects.annotate( created_date=TruncDate('created_at'), created_month=TruncMonth('created_at'), created_year=ExtractYear('created_at') )
Date arithmetic
from datetime import timedelta from django.utils import timezone
recent_posts = Post.objects.filter( created_at__gte=timezone.now() - timedelta(days=7) )
Mathematical functions
from django.db.models.functions import Abs, Ceil, Floor, Round
products = Product.objects.annotate( price_rounded=Round('price'), discount_abs=Abs('discount') )
Conditional expressions
User.objects.annotate( user_type=Case( When(posts__count__gt=100, then=Value('power_user')), When(posts__count__gt=10, then=Value('active')), When(posts__count__gt=0, then=Value('casual')), default=Value('lurker'), output_field=CharField() ) )
Complex conditional updates
Post.objects.update( status=Case( When(Q(published=True) & Q(views__gt=1000), then=Value('viral')), When(Q(published=True) & Q(views__gt=100), then=Value('popular')), When(published=True, then=Value('published')), default=Value('draft'), output_field=CharField() ) )
Advanced Aggregation Patterns
Perform complex database-level calculations.
from django.db.models import ( Count, Sum, Avg, Max, Min, StdDev, Variance, Q, F, Value, CharField, When, Case ) from django.db.models.functions import Coalesce
Multiple aggregations with filters
stats = Post.objects.aggregate( total_posts=Count('id'), published_posts=Count('id', filter=Q(published=True)), draft_posts=Count('id', filter=Q(published=False)), avg_views=Avg('views'), max_views=Max('views'), total_views=Sum('views'), std_dev_views=StdDev('views') )
Conditional aggregation
User.objects.aggregate( active_users=Count('id', filter=Q(is_active=True)), inactive_users=Count('id', filter=Q(is_active=False)), avg_posts_active=Avg('posts__count', filter=Q(is_active=True)) )
Annotation with conditional aggregation
users = User.objects.annotate( published_post_count=Count('posts', filter=Q(posts__published=True)), draft_post_count=Count('posts', filter=Q(posts__published=False)), total_views=Sum('posts__views'), avg_post_views=Avg('posts__views') ).filter(published_post_count__gt=0)
Group by with annotation
from django.db.models.functions import TruncDate
daily_stats = Post.objects.annotate( date=TruncDate('created_at') ).values('date').annotate( post_count=Count('id'), total_views=Sum('views'), avg_views=Avg('views') ).order_by('-date')
Subquery aggregation
from django.db.models import OuterRef, Subquery
Get latest comment for each post
latest_comment = Comment.objects.filter( post=OuterRef('pk') ).order_by('-created_at')
posts = Post.objects.annotate( latest_comment_date=Subquery(latest_comment.values('created_at')[:1]), latest_comment_author=Subquery(latest_comment.values('author__name')[:1]) )
Complex nested aggregation
User.objects.annotate( total_post_views=Sum('posts__views'), total_comment_count=Count('posts__comments'), avg_comments_per_post=Case( When(posts__count=0, then=Value(0)), default=Count('posts__comments') / Count('posts', distinct=True) ) )
Database Indexes and Optimization
Optimize query performance with proper indexing.
class Post(models.Model): title = models.CharField(max_length=200, db_index=True) author = models.ForeignKey(User, on_delete=models.CASCADE) published = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
# Single field
models.Index(fields=['created_at']),
# Composite index
models.Index(fields=['author', 'published']),
# Descending index
models.Index(fields=['-created_at']),
# Named index
models.Index(fields=['title'], name='post_title_idx'),
# Partial index (PostgreSQL)
models.Index(
fields=['author'],
name='published_posts_idx',
condition=models.Q(published=True)
),
# Expression index (PostgreSQL)
models.Index(
Lower('title'),
name='post_title_lower_idx'
),
# Multi-column with includes (PostgreSQL)
models.Index(
fields=['author'],
name='author_includes_idx',
include=['title', 'created_at']
),
]
# Unique together
unique_together = [['author', 'title']]
# Constraints (Django 2.2+)
constraints = [
models.UniqueConstraint(
fields=['author', 'slug'],
name='unique_author_slug'
),
models.CheckConstraint(
check=Q(views__gte=0),
name='views_non_negative'
),
]
Query optimization techniques
Only load needed fields
posts = Post.objects.only('id', 'title', 'author_id')
Defer heavy fields
posts = Post.objects.defer('content', 'metadata')
Values and values_list for dictionaries/tuples
post_data = Post.objects.values('id', 'title', 'author__name') post_ids = Post.objects.values_list('id', flat=True)
Combine optimizations
posts = Post.objects.select_related('author').only( 'title', 'author__name' ).filter( published=True )
Use iterator() for large querysets
for post in Post.objects.iterator(chunk_size=1000): process_post(post)
Use explain() to analyze queries
print(Post.objects.filter(published=True).explain(analyze=True))
Model Inheritance Patterns
Implement proper model inheritance strategies.
from django.db import models
Abstract base classes (no database table)
class TimeStampedModel(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class Post(TimeStampedModel): title = models.CharField(max_length=200) content = models.TextField() # Inherits created_at and updated_at
Multi-table inheritance (separate tables, joins required)
class BaseContent(models.Model): title = models.CharField(max_length=200) created_at = models.DateTimeField(auto_now_add=True)
class Article(BaseContent): # Has implicit OneToOne to BaseContent body = models.TextField() published = models.BooleanField(default=False)
class Video(BaseContent): duration = models.IntegerField() video_url = models.URLField()
Proxy models (same table, different behavior)
class PublishedPostManager(models.Manager): def get_queryset(self): return super().get_queryset().filter(published=True)
class Post(models.Model): title = models.CharField(max_length=200) published = models.BooleanField(default=False)
class Meta:
ordering = ['-created_at']
class PublishedPost(Post): objects = PublishedPostManager()
class Meta:
proxy = True
ordering = ['-created_at']
def publish(self):
self.published = True
self.save()
When to use each:
- Abstract: Share fields/methods, no polymorphic queries
- Multi-table: Need polymorphic queries, different fields
- Proxy: Same fields, different managers/methods
Advanced QuerySet Methods
Master advanced QuerySet operations.
Bulk operations for performance
posts = [ Post(title=f'Post {i}', author=user) for i in range(1000) ] Post.objects.bulk_create(posts, batch_size=100)
Bulk update (Django 2.2+)
posts = Post.objects.filter(author=user) for post in posts: post.views += 1 Post.objects.bulk_update(posts, ['views'], batch_size=100)
Bulk create with returning IDs (PostgreSQL)
posts = Post.objects.bulk_create(posts, batch_size=100, ignore_conflicts=True)
Update with F expressions (atomic, no race conditions)
Post.objects.filter(id=1).update(views=F('views') + 1)
Get or create with complex lookups
user, created = User.objects.get_or_create( email='user@example.com', defaults={ 'name': 'John Doe', 'is_active': True } )
Update or create
post, created = Post.objects.update_or_create( author=user, slug='my-post', defaults={ 'title': 'My Post', 'content': 'Updated content' } )
In bulk (Django 4.1+)
Post.objects.bulk_create( posts, update_conflicts=True, update_fields=['title', 'content'], unique_fields=['author', 'slug'] )
Union, intersection, difference
published = Post.objects.filter(published=True) featured = Post.objects.filter(featured=True)
all_posts = published.union(featured) # Posts that are published OR featured both = published.intersection(featured) # Posts that are both only_published = published.difference(featured) # Published but not featured
Distinct
authors = Post.objects.values('author').distinct()
With PostgreSQL distinct on
posts = Post.objects.order_by('author', '-created_at').distinct('author')
Reverse queryset
recent_first = Post.objects.order_by('-created_at') oldest_first = recent_first.reverse()
None queryset
empty = Post.objects.none() # Returns empty queryset
Raw SQL When Needed
Use raw SQL for complex queries that ORM cannot handle efficiently.
Raw queries
users = User.objects.raw('SELECT * FROM app_user WHERE is_active = %s', [True]) for user in users: print(user.name)
Execute custom SQL
from django.db import connection
def get_user_stats(): with connection.cursor() as cursor: cursor.execute(""" SELECT u.id, u.name, COUNT(p.id) as post_count FROM app_user u LEFT JOIN app_post p ON p.author_id = u.id GROUP BY u.id, u.name HAVING COUNT(p.id) > 5 """) columns = [col[0] for col in cursor.description] return [dict(zip(columns, row)) for row in cursor.fetchall()]
Combining ORM with raw SQL
User.objects.raw(""" SELECT * FROM app_user WHERE id IN ( SELECT DISTINCT author_id FROM app_post WHERE published = TRUE ) """)
Migrations Best Practices
Manage database schema changes safely and efficiently.
Create migration
python manage.py makemigrations
Custom migration
from django.db import migrations
def forwards_func(apps, schema_editor): User = apps.get_model('app', 'User') for user in User.objects.all(): user.is_active = True user.save()
def reverse_func(apps, schema_editor): pass
class Migration(migrations.Migration): dependencies = [ ('app', '0001_initial'), ]
operations = [
migrations.RunPython(forwards_func, reverse_func),
]
Add field with default
class Migration(migrations.Migration): operations = [ migrations.AddField( model_name='user', name='status', field=models.CharField(max_length=20, default='active'), ), ]
Rename field
operations = [ migrations.RenameField( model_name='user', old_name='name', new_name='full_name', ), ]
Add index
operations = [ migrations.AddIndex( model_name='post', index=models.Index(fields=['author', 'created_at']), ), ]
When to Use This Skill
Use django-orm-patterns when building modern, production-ready applications that require advanced patterns, best practices, and optimal performance.
Signal Patterns and Best Practices
Use Django signals carefully for decoupled event handling.
from django.db.models.signals import post_save, pre_save, post_delete, m2m_changed from django.dispatch import receiver from django.db import transaction
Basic signal receiver
@receiver(post_save, sender=Post) def post_created_handler(sender, instance, created, **kwargs): if created: # Send notification notify_followers(instance.author, instance)
Pre-save validation
@receiver(pre_save, sender=User) def normalize_email(sender, instance, **kwargs): if instance.email: instance.email = instance.email.lower()
Conditional signal execution
@receiver(post_save, sender=Post) def update_stats(sender, instance, created, update_fields, **kwargs): # Skip if only certain fields updated if update_fields and 'views' in update_fields: return
# Update statistics
instance.author.update_post_count()
M2M changed signal
@receiver(m2m_changed, sender=Post.tags.through) def tags_changed(sender, instance, action, **kwargs): if action == 'post_add': # Tags were added pass elif action == 'post_remove': # Tags were removed pass
Avoid signals in transactions
@receiver(post_save, sender=Order) def send_confirmation_email(sender, instance, created, **kwargs): if created: # Wait for transaction to commit transaction.on_commit(lambda: send_email(instance))
Disconnect signals when needed
from django.test import TestCase
class PostTestCase(TestCase): def setUp(self): # Disconnect signal for testing post_save.disconnect(post_created_handler, sender=Post)
def tearDown(self):
# Reconnect signal
post_save.connect(post_created_handler, sender=Post)
Custom Field Types
Create reusable custom field types for complex data.
from django.db import models import json
JSON field (before Django 3.1)
class JSONField(models.TextField): def from_db_value(self, value, expression, connection): if value is None: return value return json.loads(value)
def to_python(self, value):
if isinstance(value, dict):
return value
if value is None:
return value
return json.loads(value)
def get_prep_value(self, value):
return json.dumps(value)
Encrypted field
from cryptography.fernet import Fernet
class EncryptedField(models.TextField): def init(self, *args, **kwargs): self.cipher_suite = Fernet(settings.FIELD_ENCRYPTION_KEY) super().init(*args, **kwargs)
def from_db_value(self, value, expression, connection):
if value is None:
return value
return self.cipher_suite.decrypt(value.encode()).decode()
def get_prep_value(self, value):
if value is None:
return value
return self.cipher_suite.encrypt(value.encode()).decode()
Usage
class User(models.Model): metadata = JSONField(default=dict) ssn = EncryptedField()
Array field (PostgreSQL)
from django.contrib.postgres.fields import ArrayField
class Post(models.Model): tags = ArrayField(models.CharField(max_length=50), default=list) ratings = ArrayField(models.IntegerField(), default=list)
class Meta:
indexes = [
models.Index(fields=['tags']),
]
Query array fields
posts = Post.objects.filter(tags__contains=['django']) posts = Post.objects.filter(tags__overlap=['python', 'django'])
Query Debugging and Profiling
Debug and optimize database queries effectively.
from django.db import connection, reset_queries from django.test.utils import override_settings import time
Log all queries
@override_settings(DEBUG=True) def analyze_queries(func): def wrapper(*args, **kwargs): reset_queries() start = time.time() result = func(*args, **kwargs) end = time.time()
print(f"Function: {func.__name__}")
print(f"Number of queries: {len(connection.queries)}")
print(f"Time taken: {end - start:.2f}s")
for query in connection.queries:
print(f"SQL: {query['sql']}")
print(f"Time: {query['time']}s\n")
return result
return wrapper
Usage
@analyze_queries def get_user_posts(user_id): user = User.objects.get(id=user_id) posts = user.posts.all() return list(posts)
Django Debug Toolbar integration
Add to INSTALLED_APPS
INSTALLED_APPS = [ 'debug_toolbar', ]
Middleware
MIDDLEWARE = [ 'debug_toolbar.middleware.DebugToolbarMiddleware', ]
Explain queries
queryset = Post.objects.filter(published=True) print(queryset.explain()) # Basic explain print(queryset.explain(verbose=True)) # Verbose print(queryset.explain(analyze=True)) # Actually run query
Query count assertion in tests
from django.test import TestCase from django.test.utils import override_settings
class PostTestCase(TestCase): def test_query_count(self): with self.assertNumQueries(3): # Should execute exactly 3 queries user = User.objects.get(id=1) posts = list(user.posts.all()) comments = list(Comment.objects.filter(post__in=posts))
Find duplicate queries
def find_duplicate_queries(): from collections import Counter
queries = [q['sql'] for q in connection.queries]
duplicates = [q for q, count in Counter(queries).items() if count > 1]
for sql in duplicates:
print(f"Duplicate query: {sql}")
Advanced Manager Patterns
Build sophisticated custom managers for complex business logic.
from django.db import models from django.db.models import Q, Count, Avg
class PostQuerySet(models.QuerySet): def published(self): return self.filter(published=True)
def draft(self):
return self.filter(published=False)
def by_author(self, author):
return self.filter(author=author)
def popular(self, min_views=100):
return self.filter(views__gte=min_views)
def recent(self, days=7):
from django.utils import timezone
from datetime import timedelta
cutoff = timezone.now() - timedelta(days=days)
return self.filter(created_at__gte=cutoff)
def with_stats(self):
return self.annotate(
comment_count=Count('comments'),
avg_rating=Avg('ratings__score')
)
def optimized(self):
return self.select_related('author').prefetch_related('comments')
class PostManager(models.Manager.from_queryset(PostQuerySet)): def get_queryset(self): return super().get_queryset().filter(deleted_at__isnull=True)
def with_deleted(self):
return super().get_queryset()
class Post(models.Model): title = models.CharField(max_length=200) author = models.ForeignKey(User, on_delete=models.CASCADE) published = models.BooleanField(default=False) views = models.IntegerField(default=0) deleted_at = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True)
objects = PostManager()
def soft_delete(self):
from django.utils import timezone
self.deleted_at = timezone.now()
self.save()
Usage - methods chain naturally
recent_popular = Post.objects.published().recent().popular().with_stats() author_drafts = Post.objects.by_author(user).draft().optimized()
Multiple manager pattern
class AllPostsManager(models.Manager): def get_queryset(self): return super().get_queryset()
class Post(models.Model): # ... fields ... objects = PostManager() # Default, excludes deleted all_objects = AllPostsManager() # Includes deleted
Database-Specific Features
Leverage PostgreSQL-specific features when available.
from django.contrib.postgres.fields import ArrayField, JSONField, HStoreField from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank from django.contrib.postgres.aggregates import ArrayAgg, StringAgg
Full-text search
class Post(models.Model): title = models.CharField(max_length=200) content = models.TextField() search_vector = SearchVectorField(null=True)
class Meta:
indexes = [
GinIndex(fields=['search_vector']),
]
Update search vector
from django.contrib.postgres.search import SearchVector
Post.objects.update( search_vector=SearchVector('title', weight='A') + SearchVector('content', weight='B') )
Search
query = SearchQuery('django') posts = Post.objects.annotate( rank=SearchRank('search_vector', query) ).filter(search_vector=query).order_by('-rank')
Array aggregation
authors = User.objects.annotate( post_titles=ArrayAgg('posts__title', distinct=True), tags_list=StringAgg('posts__tags', delimiter=', ', distinct=True) )
JSON operations
from django.contrib.postgres.fields.jsonb import KeyTextTransform
users = User.objects.annotate( city=KeyTextTransform('city', 'metadata') ).filter(city='New York')
Range fields
from django.contrib.postgres.fields import IntegerRangeField, DateRangeField
class Event(models.Model): name = models.CharField(max_length=200) date_range = DateRangeField() capacity = IntegerRangeField()
from django.db.models import Q from datetime import date
Find events happening on a specific date
events = Event.objects.filter(date_range__contains=date(2024, 1, 15))
Find overlapping events
events = Event.objects.filter( date_range__overlap=(date(2024, 1, 1), date(2024, 1, 31)) )
Django ORM Best Practices
-
Use select_related and prefetch_related - Always optimize queries to prevent N+1 problems
-
Index frequently queried fields - Add database indexes for fields used in filters and joins
-
Use get_or_create carefully - Wrap in transactions when dealing with race conditions
-
Avoid queries in loops - Batch operations and use bulk methods when possible
-
Use only() and defer() wisely - Load only necessary fields for large models
-
Leverage F() expressions - Use database-level operations to avoid race conditions
-
Use transactions for data integrity - Wrap related operations in atomic blocks
-
Create custom managers - Encapsulate common query patterns in reusable managers
-
Use exists() for checks - More efficient than count() when only checking existence
-
Monitor query performance - Use django-debug-toolbar to identify slow queries
-
Implement soft deletes with managers - Use custom managers to hide deleted records
-
Use database functions - Leverage Django's database functions for complex operations
-
Batch database operations - Use bulk_create and bulk_update for large datasets
-
Use iterator() for large datasets - Avoid loading entire querysets into memory
-
Apply database constraints - Use CheckConstraint and UniqueConstraint for data integrity
Django ORM Common Pitfalls
-
N+1 query problem - Forgetting to use select_related or prefetch_related causes excessive queries
-
Loading too much data - Using .all() without pagination can cause memory issues
-
Inefficient updates - Using save() in loops instead of bulk_update or update()
-
Missing database indexes - Slow queries on unindexed fields in large tables
-
Incorrect use of get() - Not handling DoesNotExist or MultipleObjectsReturned exceptions
-
Lazy evaluation confusion - Querysets are lazy; understand when they actually execute
-
Transaction isolation issues - Not using select_for_update when needed for locking
-
Mixing F() with save() - Must call refresh_from_db() after saving F() expressions
-
Inefficient aggregations - Running Python calculations instead of database aggregations
-
Migration conflicts - Not coordinating migrations in team environments
-
Signal performance issues - Signals in tight loops can cause performance problems
-
Overusing signals - Prefer explicit calls over implicit signal-based logic
-
Not using transactions with signals - Signals fire before transaction commit by default
-
Incorrect distinct() usage - Using distinct() without understanding its implications
-
Ignoring database-specific features - Missing out on PostgreSQL full-text search, arrays, etc.
Resources
-
Django ORM Documentation
-
Django QuerySet API Reference
-
Django Database Optimization
-
Django Migration Guide
-
Django Database Transactions