Django Intermediate

Django Models & ORM: Complete Guide

Master Django models, field types, relationships, querysets, and the ORM. Learn to design efficient database schemas for your Django projects.

DjangoZen Team Mar 29, 2026 6 min read 26 views

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.

What is the Django ORM?

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')

Defining Models

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

Common Field Types

FieldPython TypeDatabase TypeUse Case
CharFieldstrVARCHARShort text (name, title)
TextFieldstrTEXTLong text (description, content)
IntegerFieldintINTEGERWhole numbers
DecimalFieldDecimalNUMERICMoney, precise numbers
BooleanFieldboolBOOLEANTrue/False flags
DateTimeFielddatetimeTIMESTAMPDate and time
SlugFieldstrVARCHARURL-friendly strings
EmailFieldstrVARCHAREmail addresses
URLFieldstrVARCHARURLs
FileFieldFieldFileVARCHARFile uploads
ImageFieldImageFieldFileVARCHARImage uploads
JSONFielddict/listJSONStructured JSON data
UUIDFieldUUIDUUID/CHARUnique identifiers

Relationships

Django supports three types of database relationships:

ForeignKey (Many-to-One)

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
on_delete options:
CASCADE — Delete related objects
PROTECT — Prevent deletion if related objects exist
SET_NULL — Set to NULL (requires null=True)
SET_DEFAULT — Set to default value
DO_NOTHING — Do nothing (can cause integrity errors)

ManyToManyField

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

OneToOneField

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

QuerySet API — Retrieving Data

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()

Field Lookups

Django provides powerful field lookups using double-underscore syntax:

LookupSQL EquivalentExample
exact=name__exact='Django'
iexactILIKEname__iexact='django'
containsLIKE '%x%'title__contains='API'
icontainsILIKE '%x%'title__icontains='api'
gt / gte> / >=price__gte=10
lt / lte< / <=price__lt=100
inINstatus__in=['draft','published']
startswithLIKE 'x%'title__startswith='Django'
isnullIS NULLdue_date__isnull=True
rangeBETWEENprice__range=(10, 50)
year/month/dayEXTRACTcreated_at__year=2025

Creating, Updating, and Deleting

# 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()

Performance: select_related & prefetch_related

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()
Performance Rule: Always use 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.

Aggregation & Annotation

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)

Model Methods & Properties

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'])
Best Practice: Use save(update_fields=['field_name']) when updating specific fields. It generates a more efficient SQL query and avoids race conditions.

Migrations Workflow

# 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

Summary

The Django ORM is a powerful abstraction that lets you work with databases using Pythonic code. Key takeaways:

  • Models define your schema; migrations keep it in sync
  • Use filter(), exclude(), and field lookups to query precisely
  • Understand relationship types: ForeignKey, ManyToMany, OneToOne
  • Always use select_related/prefetch_related to avoid N+1 queries
  • Use aggregation for statistics and annotation for computed fields