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 15 min read 224 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

Models as the heart of a Django app

In Django, models are where your application's data and much of its logic live, and getting them right shapes everything else. A model defines a table, its fields define columns, and the relationships between models define how your data connects. Because so much of an application flows through its models — every query, every form, every view that touches data — thoughtful model design pays off throughout the codebase, while poor model design creates friction everywhere. Treating models as the foundational layer they are, and investing in designing them well, is one of the highest-leverage things you can do early in a Django project.

Relationships and how to choose them

Django models connect through three relationship types, and choosing correctly matters. A foreign key expresses a many-to-one relationship — many orders belong to one customer. A many-to-many expresses that records on both sides can relate to many of the other — students and courses. A one-to-one ties two records together exclusively. Picking the relationship that accurately models your domain keeps queries natural and data consistent, while a wrong choice forces awkward workarounds later. Understanding what each relationship means and selecting the one that matches reality is fundamental to a clean data model that serves the application well.

Migrations as version control for your schema

Migrations are how Django evolves your database schema over time, and they function as version control for your data structure. When you change a model, you generate a migration capturing that change, and applying it updates the database. This gives you a versioned, repeatable history of how your schema evolved, lets every environment stay in sync, and makes schema changes reviewable like code. Understanding migrations — generating them, applying them, and resolving the occasional conflict — is essential to working with Django models in any real project, because the schema is not static and migrations are the disciplined way it changes.

Querying efficiently through the ORM

The ORM lets you query the database in Python, but how you write those queries determines performance. Fetching related data efficiently, filtering and aggregating in the database rather than in Python, and avoiding the common trap of firing one query per object in a loop are what keep data access fast as your application grows. The ORM makes it easy to write queries that work but scale poorly, so understanding how your model queries translate to database work — and using the tools Django provides to make them efficient — is the difference between a data layer that stays fast and one that quietly degrades under real data volumes.

Adding behavior to models

Models are not just data definitions; they are a natural place to put behavior related to that data. Adding methods to a model lets you keep logic that operates on a record close to the record itself, so the behavior travels with the data and is reusable wherever the model is used. This keeps related logic organized and avoids scattering it across views. Understanding that models can hold methods expressing behavior — not only fields defining structure — encourages a clean design where data and the operations on it live together, which is a hallmark of well-organized Django code and makes your models a meaningful part of the application's logic rather than passive data containers.

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