Django's ORM lets you work with your database using Python code instead of raw SQL. Models define your data structure, and Django handles queries, migrations, and relationships.
Field Types
Python
from django.db import models
from django.contrib.postgres.fields import ArrayField, JSONField
class Article(models.Model):
# Text fields
title = models.CharField(max_length=200) # VARCHAR(200)
slug = models.SlugField(unique=True) # VARCHAR with URL-safe chars
summary = models.TextField() # TEXT
content = models.TextField(blank=True) # TEXT, optional
# Numbers
views = models.PositiveIntegerField(default=0) # INT >= 0
rating = models.DecimalField(max_digits=3, decimal_places=1) # DECIMAL(3,1)
is_published = models.BooleanField(default=False) # BOOLEAN
# Dates
created = models.DateTimeField(auto_now_add=True) # Set on creation
updated = models.DateTimeField(auto_now=True) # Set on every save
publish_date = models.DateField(null=True) # DATE only
# Files
image = models.ImageField(upload_to='articles/', blank=True)
attachment = models.FileField(upload_to='attachments/', blank=True)
# Relationships (defined below)
author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
category = models.ForeignKey('Category', on_delete=models.SET_NULL, null=True)
tags = models.ManyToManyField('Tag', blank=True)
class Meta:
ordering = ['-created']
indexes = [
models.Index(fields=['-created']),
models.Index(fields=['slug']),
]
verbose_name = 'article'
verbose_name_plural = 'articles'
def __str__(self):
return self.title
def get_absolute_url(self):
return f'/articles/{self.slug}/'
@property
def reading_time(self):
words = len(self.content.split())
return max(1, words // 200)Field Options Reference
| Option | Description | Example |
|---|---|---|
max_length |
Max string length | CharField(max_length=200) |
null |
Allow NULL in DB | CharField(null=True) |
blank |
Allow empty in forms | CharField(blank=True) |
default |
Default value | IntegerField(default=0) |
unique |
Enforce uniqueness | SlugField(unique=True) |
choices |
Limited choices | CharField(choices=STATUS) |
editable |
Show in admin/forms | DateTimeField(editable=False) |
help_text |
Help text in forms | CharField(help_text='...') |
verbose_name |
Human-readable name | CharField(verbose_name='Title') |
Relationships
Python
# One-to-Many (ForeignKey)
class Department(models.Model):
name = models.CharField(max_length=100)
class Employee(models.Model):
name = models.CharField(max_length=100)
department = models.ForeignKey(
Department,
on_delete=models.CASCADE, # Delete employees when dept deleted
related_name='employees' # dept.employees.all()
)
# Many-to-Many
class Article(models.Model):
tags = models.ManyToManyField('Tag', related_name='articles')
# One-to-One
class Profile(models.Model):
user = models.OneToOneField('auth.User', on_delete=models.CASCADE)
bio = models.TextField(blank=True)QuerySet API
Python
# Filtering
posts = Post.objects.filter(published=True, author__username='john')
posts = Post.objects.filter(created__date__year=2024)
posts = Post.objects.exclude(comments__isnull=True)
# Chaining (lazy evaluation — queries run when iterated)
recent_popular = (Post.objects
.filter(published=True)
.select_related('author')
.prefetch_related('tags', 'comments')
.annotate(num_comments=Count('comments'))
.order_by('-created')[:10])
# Aggregation
from django.db.models import Count, Avg, Sum, Max
stats = Post.objects.aggregate(
total_posts=Count('id'),
avg_comments=Avg('comments__id'),
max_views=Max('views'),
)
# F expressions (atomic database updates)
from django.db.models import F
Post.objects.filter(slug='my-post').update(views=F('views') + 1)
# Q objects (complex queries)
from django.db.models import Q
posts = Post.objects.filter(
Q(title__icontains='python') | Q(body__icontains='python'),
published=True
)
# Bulk operations
Post.objects.filter(published=False).update(published=True)
Post.objects.bulk_create([Post(title=f'Post {i}') for i in range(100)])Custom Model Managers
Python
class PublishedManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(published=True)
class Post(models.Model):
objects = models.Manager() # Default
published = PublishedManager() # Custom
# Usage: Post.published.all() — only published postsSignals
Python
# posts/signals.py
from django.db.models.signals import pre_save, post_save
from django.dispatch import receiver
from django.utils.text import slugify
from .models import Post
@receiver(pre_save, sender=Post)
def generate_slug(sender, instance, **kwargs):
if not instance.slug:
instance.slug = slugify(instance.title)
# Ensure uniqueness
original_slug = instance.slug
counter = 1
while Post.objects.filter(slug=instance.slug).exclude(pk=instance.pk).exists():
instance.slug = f'{original_slug}-{counter}'
counter += 1
@receiver(post_save, sender=Post)
def notify_followers(sender, instance, created, **kwargs):
if created and instance.published:
# Send notification to followers
print(f'New post: {instance.title}')Python
# posts/apps.py — register signals
class PostsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'posts'
def ready(self):
import posts.signals # noqaMigrations
Bash
# After changing models:
python manage.py makemigrations # Create migration file
python manage.py makemigrations --name fix_slug # Named migration
python manage.py migrate # Apply to database
python manage.py showmigrations # Show migration status
python manage.py sqlmigrate posts 0003 # Show SQL for a migration
python manage.py migrate posts 0002 # Rollback to specific migration💡 Tip: Use
select_related()for ForeignKey and OneToOne, andprefetch_related()for ManyToMany — they eliminate N+1 queries which are the #1 cause of slow Django apps.