Master Django models and the ORM. Learn to define database models, field types, relationships (ForeignKey, ManyToMany, OneToOne), migrations, QuerySets, and advanced database queries in Django.
Django models are Python classes that define the structure of your database tables. The Django ORM (Object-Relational Mapper) lets you interact with databases using Python instead of SQL.
Defining Models
# blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils.text import slugify
from django.urls import reverse
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def __str__(self):
return self.name
class Category(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
description = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
class Meta:
verbose_name_plural = 'categories'
ordering = ['name']
class Post(models.Model):
STATUS_DRAFT = 'draft'
STATUS_PUBLISHED = 'published'
STATUS_CHOICES = [
(STATUS_DRAFT, 'Draft'),
(STATUS_PUBLISHED, 'Published'),
]
title = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)
tags = models.ManyToManyField(Tag, blank=True)
content = models.TextField()
excerpt = models.TextField(max_length=500, blank=True)
featured_image = models.ImageField(upload_to='posts/%Y/%m/', blank=True, null=True)
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default=STATUS_DRAFT)
views_count = models.PositiveIntegerField(default=0)
is_featured = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
published_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['-created_at']),
models.Index(fields=['status', '-published_at']),
]
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('blog:post-detail', kwargs={'slug': self.slug})
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super().save(*args, **kwargs)
@property
def is_published(self):
return self.status == self.STATUS_PUBLISHED
def increment_views(self):
self.views_count += 1
self.save(update_fields=['views_count'])
Field Types
Django provides many built-in field types:
from django.db import models
class ProductExample(models.Model):
# Text Fields
name = models.CharField(max_length=200) # Short text (VARCHAR)
description = models.TextField() # Long text
slug = models.SlugField(max_length=200) # URL-friendly text
email = models.EmailField() # Email validation
url = models.URLField() # URL validation
uuid = models.UUIDField() # UUID field
# Numeric Fields
price = models.DecimalField(max_digits=10, decimal_places=2)
quantity = models.IntegerField(default=0)
stock = models.PositiveIntegerField(default=0)
rating = models.FloatField(default=0.0)
# Boolean Fields
is_active = models.BooleanField(default=True)
is_featured = models.BooleanField(default=False)
# Date/Time Fields
created_at = models.DateTimeField(auto_now_add=True) # Set on create
updated_at = models.DateTimeField(auto_now=True) # Set on every save
publish_date = models.DateField(null=True, blank=True)
sale_start = models.TimeField(null=True, blank=True)
duration = models.DurationField(null=True, blank=True)
# File Fields
image = models.ImageField(upload_to='products/')
document = models.FileField(upload_to='docs/')
# Choice Field
CATEGORY_CHOICES = [
('electronics', 'Electronics'),
('clothing', 'Clothing'),
('books', 'Books'),
('food', 'Food'),
]
category = models.CharField(max_length=20, choices=CATEGORY_CHOICES)
# JSON Field (Django 3.1+)
metadata = models.JSONField(default=dict, blank=True)
# IP Address
created_from_ip = models.GenericIPAddressField(null=True, blank=True)
Relationships
ForeignKey (Many-to-One)
class Comment(models.Model):
post = models.ForeignKey(
Post,
on_delete=models.CASCADE, # Delete comments when post is deleted
related_name='comments' # Access via post.comments.all()
)
author = models.ForeignKey(
User,
on_delete=models.SET_NULL, # Set null when user is deleted
null=True,
related_name='comments'
)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f'Comment by {self.author} on {self.post}'
on_delete options:
CASCADE — Delete related objectsSET_NULL — Set to null (requires null=True)SET_DEFAULT — Set to default valuePROTECT — Prevent deletion if related objects existDO_NOTHING — Do nothing (can cause integrity errors)
ManyToManyField
class Article(models.Model):
title = models.CharField(max_length=200)
tags = models.ManyToManyField('Tag', blank=True)
# With intermediary table (through model)
liked_by = models.ManyToManyField(
User,
through='ArticleLike',
related_name='liked_articles',
blank=True
)
class ArticleLike(models.Model):
"""Intermediary table for likes with extra data."""
user = models.ForeignKey(User, on_delete=models.CASCADE)
article = models.ForeignKey(Article, on_delete=models.CASCADE)
liked_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ['user', 'article']
OneToOneField
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
website = models.URLField(blank=True)
location = models.CharField(max_length=100, blank=True)
birth_date = models.DateField(null=True, blank=True)
def __str__(self):
return f'Profile of {self.user.username}'
def get_avatar_url(self):
if self.avatar:
return self.avatar.url
return '/static/images/default-avatar.png'
Migrations
# Create migration files based on model changes
python manage.py makemigrations
# Make migrations for a specific app
python manage.py makemigrations blog
# Preview the SQL that will be run
python manage.py sqlmigrate blog 0001
# Apply migrations
python manage.py migrate
# Check migration status
python manage.py showmigrations
# Undo a migration (rollback)
python manage.py migrate blog 0001 # Revert to migration 0001
# Squash migrations (combine multiple into one)
python manage.py squashmigrations blog 0001 0005
QuerySets — Retrieving Data
from blog.models import Post, Category, Tag
# --- Basic Retrieval ---
# Get all objects
all_posts = Post.objects.all()
# Get with filter
published = Post.objects.filter(status='published')
featured = Post.objects.filter(is_featured=True, status='published')
# Get single object (raises DoesNotExist if not found)
post = Post.objects.get(id=1)
post = Post.objects.get(slug='my-first-post')
# Safe get with get_object_or_404 (in views)
from django.shortcuts import get_object_or_404
post = get_object_or_404(Post, slug='my-post')
# Get first / last
first_post = Post.objects.first()
latest_post = Post.objects.last()
# Count
total = Post.objects.count()
published_count = Post.objects.filter(status='published').count()
# Check existence
exists = Post.objects.filter(slug='test').exists()
# --- Filtering ---
# Exact match
Post.objects.filter(status='published')
# Case-insensitive contains
Post.objects.filter(title__icontains='python')
# Starts with / ends with
Post.objects.filter(title__startswith='How')
Post.objects.filter(title__endswith='?')
# Greater/Less than
Post.objects.filter(views_count__gt=100)
Post.objects.filter(views_count__gte=100)
Post.objects.filter(views_count__lt=10)
Post.objects.filter(views_count__lte=10)
# In a list
Post.objects.filter(status__in=['draft', 'published'])
# Not in
Post.objects.exclude(status='draft')
# Null checks
Post.objects.filter(published_at__isnull=True)
Post.objects.filter(featured_image__isnull=False)
# Date filtering
from django.utils import timezone
today = timezone.now().date()
Post.objects.filter(created_at__date=today)
Post.objects.filter(created_at__year=2026)
Post.objects.filter(created_at__month=6)
# Relationship filtering
Post.objects.filter(author__username='alice')
Post.objects.filter(category__name='Technology')
Post.objects.filter(tags__name='python') # Many-to-many
# --- Complex Queries with Q objects ---
from django.db.models import Q
# OR condition
Post.objects.filter(Q(status='published') | Q(is_featured=True))
# AND condition (same as chaining .filter())
Post.objects.filter(Q(status='published') & Q(author__username='alice'))
# NOT condition
Post.objects.filter(~Q(status='draft'))
# Complex combined
Post.objects.filter(
Q(status='published') | Q(is_featured=True),
~Q(author__username='banned_user')
)
QuerySets — Ordering and Slicing
# Order by field
Post.objects.order_by('created_at') # Ascending
Post.objects.order_by('-created_at') # Descending
Post.objects.order_by('-created_at', 'title') # Multiple fields
# Random order
Post.objects.order_by('?')
# Slicing (like Python list slicing — returns QuerySet)
first_five = Post.objects.all()[:5]
posts_6_to_10 = Post.objects.all()[5:10]
latest_ten = Post.objects.order_by('-created_at')[:10]
# Specific field values only (flat list)
titles = Post.objects.values_list('title', flat=True) # ['Post 1', 'Post 2', ...]
ids = Post.objects.values_list('id', flat=True)
# Dictionary of fields
posts_data = Post.objects.values('id', 'title', 'author__username')
Aggregation and Annotation
from django.db.models import Count, Sum, Avg, Max, Min, F, Value
from django.db.models.functions import Concat
# Aggregation — single result
from blog.models import Post
stats = Post.objects.aggregate(
total=Count('id'),
total_views=Sum('views_count'),
avg_views=Avg('views_count'),
max_views=Max('views_count'),
)
print(stats)
# {'total': 50, 'total_views': 12345, 'avg_views': 246.9, 'max_views': 3210}
# Annotation — adds computed field to each object
posts = Post.objects.annotate(
comment_count=Count('comments'),
total_likes=Count('liked_by')
)
for post in posts:
print(f'{post.title}: {post.comment_count} comments, {post.total_likes} likes')
# Group by (category post counts)
category_counts = Category.objects.annotate(
post_count=Count('post')
).order_by('-post_count')
# Using F expressions (reference another field)
Post.objects.filter(views_count__gt=F('comments') * 10)
Optimizing Queries
# select_related — for ForeignKey/OneToOne (SQL JOIN)
# Avoids N+1 query problem for single related objects
posts = Post.objects.select_related('author', 'category').filter(status='published')
# prefetch_related — for ManyToMany/reverse FK
# Separate query, cached in Python
posts = Post.objects.prefetch_related('tags', 'comments').all()
# Combine both
posts = Post.objects.select_related('author', 'category').prefetch_related('tags')
# Only fetch specific fields (faster for large models)
posts = Post.objects.only('id', 'title', 'slug', 'created_at')
# Defer specific fields (fetch everything except these)
posts = Post.objects.defer('content', 'metadata')
# Checking query count (debugging)
from django.test.utils import CaptureQueriesContext
from django.db import connection
with CaptureQueriesContext(connection) as queries:
list(Post.objects.select_related('author').all())
print(f'Query count: {len(queries)}')
Creating, Updating, and Deleting
# --- Create ---
# Method 1: create()
post = Post.objects.create(
title='My New Post',
content='Post content here...',
author=user,
status='published'
)
# Method 2: save()
post = Post(title='Another Post', content='...', author=user)
post.save()
# Method 3: get_or_create()
tag, created = Tag.objects.get_or_create(
name='Python',
defaults={'slug': 'python'}
)
if created:
print('New tag created')
else:
print('Tag already existed')
# Method 4: update_or_create()
obj, created = Post.objects.update_or_create(
slug='my-post',
defaults={'title': 'Updated Title', 'status': 'published'}
)
# --- Update ---
# Update single object
post = Post.objects.get(id=1)
post.title = 'Updated Title'
post.save()
# Update only specific fields (more efficient)
post.save(update_fields=['title', 'status'])
# Bulk update (single SQL query)
Post.objects.filter(status='draft').update(status='published')
# Using F() for atomic updates
from django.db.models import F
Post.objects.filter(id=1).update(views_count=F('views_count') + 1)
# --- Delete ---
post = Post.objects.get(id=1)
post.delete()
# Bulk delete
Post.objects.filter(status='draft', created_at__year=2024).delete()
# Delete all
Post.objects.all().delete() # ⚠️ Careful!
Custom Model Managers
from django.db import models
class PublishedManager(models.Manager):
"""Custom manager to return only published posts."""
def get_queryset(self):
return super().get_queryset().filter(status='published')
def featured(self):
return self.get_queryset().filter(is_featured=True)
def by_author(self, author):
return self.get_queryset().filter(author=author)
def recent(self, count=5):
return self.get_queryset().order_by('-published_at')[:count]
class Post(models.Model):
# ... fields ...
status = models.CharField(max_length=10, default='draft')
# Default manager
objects = models.Manager()
# Custom manager
published = PublishedManager()
# Usage:
Post.published.all() # All published posts
Post.published.featured() # Featured published posts
Post.published.by_author(user) # Published posts by user
Post.published.recent(10) # Latest 10 published posts
Model Signals
from django.db.models.signals import post_save, pre_save, post_delete
from django.dispatch import receiver
from django.contrib.auth.models import User
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
"""Create a profile whenever a new User is created."""
if created:
UserProfile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
"""Save user profile whenever user is saved."""
instance.profile.save()
@receiver(pre_save, sender=Post)
def set_published_date(sender, instance, **kwargs):
"""Automatically set published_at when status changes to published."""
from django.utils import timezone
if instance.status == 'published' and not instance.published_at:
instance.published_at = timezone.now()
Summary
In this lesson, you learned:
- ✅ Defining Django models with various field types
- ✅ Configuring relationships (ForeignKey, ManyToMany, OneToOne)
- ✅ Creating and applying migrations
- ✅ Querying the database with the ORM (filter, get, exclude)
- ✅ Complex queries with Q objects
- ✅ Aggregation and annotation
- ✅ Optimizing queries (select_related, prefetch_related)
- ✅ Creating, updating, and deleting records
- ✅ Custom model managers
- ✅ Model signals for automation
*Next Lesson: Django Views →*