Master Django models, field types, relationships, querysets, and the ORM. Learn to design efficient database schemas for your Django projects.
The Django ORM (Object-Relational Mapper) is one of the framework's most powerful features. It lets you interact with your database using Python code instead of writing raw SQL. In this guide, you'll master models, querysets, relationships, and advanced ORM techniques.
The ORM translates Python classes into database tables and Python method calls into SQL queries. This means you can switch databases (SQLite, PostgreSQL, MySQL) without changing your application code.
# Instead of writing SQL like this:
SELECT * FROM products WHERE price > 50 ORDER BY name;
# You write Python like this:
Product.objects.filter(price__gt=50).order_by('name')
Every model is a Python class that inherits from models.Model. Each attribute represents a database column:
from django.db import models
from django.contrib.auth.models import User
class Category(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
description = models.TextField(blank=True)
class Meta:
verbose_name_plural = "categories"
ordering = ['name']
def __str__(self):
return self.name
class Product(models.Model):
STATUS_CHOICES = [
('draft', 'Draft'),
('published', 'Published'),
('archived', 'Archived'),
]
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='products')
author = models.ForeignKey(User, on_delete=models.CASCADE)
description = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2)
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
is_featured = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
| Field | Python Type | Database Type | Use Case |
|---|---|---|---|
CharField | str | VARCHAR | Short text (name, title) |
TextField | str | TEXT | Long text (description, content) |
IntegerField | int | INTEGER | Whole numbers |
DecimalField | Decimal | NUMERIC | Money, precise numbers |
BooleanField | bool | BOOLEAN | True/False flags |
DateTimeField | datetime | TIMESTAMP | Date and time |
SlugField | str | VARCHAR | URL-friendly strings |
EmailField | str | VARCHAR | Email addresses |
URLField | str | VARCHAR | URLs |
FileField | FieldFile | VARCHAR | File uploads |
ImageField | ImageFieldFile | VARCHAR | Image uploads |
JSONField | dict/list | JSON | Structured JSON data |
UUIDField | UUID | UUID/CHAR | Unique identifiers |
Django supports three types of database relationships:
Many products belong to one category:
class Product(models.Model):
category = models.ForeignKey(
Category,
on_delete=models.CASCADE, # Delete products when category is deleted
related_name='products' # Access from category: category.products.all()
)
# Usage:
category = Category.objects.get(slug='electronics')
category.products.all() # All products in this category
category.products.count() # Number of products
CASCADE — Delete related objectsPROTECT — Prevent deletion if related objects existSET_NULL — Set to NULL (requires null=True)SET_DEFAULT — Set to default valueDO_NOTHING — Do nothing (can cause integrity errors)
Products can have multiple tags, and tags can belong to multiple products:
class Tag(models.Model):
name = models.CharField(max_length=50)
class Product(models.Model):
tags = models.ManyToManyField(Tag, blank=True)
# Usage:
product.tags.add(tag1, tag2) # Add tags
product.tags.remove(tag1) # Remove a tag
product.tags.all() # All tags for product
tag.product_set.all() # All products with tag
Each user has exactly one profile:
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to='avatars/', blank=True)
# Usage:
user.userprofile.bio # Access profile from user
The QuerySet API is how you read data from the database. QuerySets are lazy — they don't hit the database until you actually need the data.
# Get all objects
Product.objects.all()
# Filter (WHERE clause)
Product.objects.filter(status='published')
Product.objects.filter(price__gte=10, price__lte=100) # AND condition
Product.objects.filter(title__icontains='django') # Case-insensitive search
# Exclude
Product.objects.exclude(status='archived')
# Get single object (raises DoesNotExist if not found)
Product.objects.get(pk=1)
Product.objects.get(slug='my-product')
# Order by
Product.objects.order_by('price') # Ascending
Product.objects.order_by('-created_at') # Descending
# Slicing (LIMIT/OFFSET)
Product.objects.all()[:5] # First 5
Product.objects.all()[5:10] # Items 6-10
# Chaining (all querysets are chainable)
Product.objects.filter(status='published').order_by('-price')[:10]
# Values (get dictionaries instead of objects)
Product.objects.values('title', 'price')
# Count, exists
Product.objects.count()
Product.objects.filter(is_featured=True).exists()
Django provides powerful field lookups using double-underscore syntax:
| Lookup | SQL Equivalent | Example |
|---|---|---|
exact | = | name__exact='Django' |
iexact | ILIKE | name__iexact='django' |
contains | LIKE '%x%' | title__contains='API' |
icontains | ILIKE '%x%' | title__icontains='api' |
gt / gte | > / >= | price__gte=10 |
lt / lte | < / <= | price__lt=100 |
in | IN | status__in=['draft','published'] |
startswith | LIKE 'x%' | title__startswith='Django' |
isnull | IS NULL | due_date__isnull=True |
range | BETWEEN | price__range=(10, 50) |
year/month/day | EXTRACT | created_at__year=2025 |
# Create
product = Product.objects.create(
title='My Product',
price=29.99,
category=category
)
# Or create in two steps
product = Product(title='My Product', price=29.99)
product.save()
# Update single object
product.price = 39.99
product.save()
# Bulk update (efficient — single SQL query)
Product.objects.filter(status='draft').update(status='published')
# Delete
product.delete()
Product.objects.filter(status='archived').delete()
Avoid the N+1 query problem by preloading related data:
# BAD: N+1 queries (1 query + 1 per product for category)
products = Product.objects.all()
for p in products:
print(p.category.name) # Hits database each time!
# GOOD: select_related for ForeignKey (SQL JOIN)
products = Product.objects.select_related('category', 'author').all()
# GOOD: prefetch_related for ManyToMany (separate query)
products = Product.objects.prefetch_related('tags').all()
select_related() for ForeignKey/OneToOne fields and prefetch_related() for ManyToMany fields when you know you'll access the related objects. This can reduce hundreds of queries to just 1-2.
from django.db.models import Avg, Count, Sum, Max, Min
# Aggregate (returns a dictionary)
Product.objects.aggregate(
avg_price=Avg('price'),
total=Count('id'),
max_price=Max('price')
)
# {'avg_price': Decimal('45.50'), 'total': 120, 'max_price': Decimal('299.99')}
# Annotate (adds computed field to each object)
categories = Category.objects.annotate(
product_count=Count('products'),
avg_price=Avg('products__price')
)
for cat in categories:
print(cat.name, cat.product_count, cat.avg_price)
Add business logic directly to your models:
class Product(models.Model):
# ... fields ...
@property
def is_on_sale(self):
return self.sale_price is not None
def get_absolute_url(self):
from django.urls import reverse
return reverse('product_detail', kwargs={'slug': self.slug})
def apply_discount(self, percent):
self.price = self.price * (1 - percent / 100)
self.save(update_fields=['price'])
save(update_fields=['field_name']) when updating specific fields. It generates a more efficient SQL query and avoids race conditions.
# After changing models:
python manage.py makemigrations # Generate migration files
python manage.py migrate # Apply to database
# See generated SQL without running it:
python manage.py sqlmigrate app_name 0001
# Check for issues:
python manage.py check
The Django ORM is a powerful abstraction that lets you work with databases using Pythonic code. Key takeaways:
filter(), exclude(), and field lookups to query preciselyselect_related/prefetch_related to avoid N+1 queries