Django Models and the ORM

Defining models, migrations, the ORM query API, and ForeignKey/ManyToMany relationships.

Defining a model

A Django model is a Python class in models.py that maps to a database table — each class attribute is a models.Field describing a column:

Python
# blog/models.py
from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField(unique=True)

    def __str__(self):
        return self.name

class Post(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField()
    published = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="posts")

    def __str__(self):
        return self.title

Every model automatically gets an auto-incrementing id primary key unless you define your own. __str__ controls how an instance displays as text — in the Django admin, in the shell, in error messages — so it's worth defining on every model.

Migrations

Django doesn't touch your database schema until you tell it to. Migrations are generated files that describe schema changes, which you create from your model changes and then apply:

Bash
python3 manage.py makemigrations
Plaintext
Migrations for 'blog':
  blog/migrations/0001_initial.py
    - Create model Author
    - Create model Post
Bash
python3 manage.py migrate

makemigrations inspects the difference between your models and the last known migration state, and writes a new migration file describing exactly what changed. migrate actually applies pending migrations — for this app's own tables and for Django's built-in apps (auth, sessions, admin) — against the configured database. Every time you add a field, add a model, or change a field's type, the cycle is the same: edit models.py, run makemigrations, run migrate.

The ORM query API

Django's ORM lets you query the database using Python method chains instead of writing raw SQL. All of these run from the Django shell (python3 manage.py shell) or from anywhere in your app code:

Python
from blog.models import Author, Post

# Create
author = Author.objects.create(name="Ada Lovelace", email="ada@example.com")
Post.objects.create(title="Hello, Django", body="...", author=author, published=True)

# Read — all rows
all_posts = Post.objects.all()

# Read — filtered
published_posts = Post.objects.filter(published=True)
recent = Post.objects.filter(published=True).order_by("-created_at")[:5]

# Read — a single row (raises DoesNotExist if none match, or MultipleObjectsReturned if more than one does)
post = Post.objects.get(id=1)

# Update
post.title = "Hello, Django (Updated)"
post.save()

# Delete
post.delete()

Every Model.objects.... call returns a QuerySet, which is lazy — building up Post.objects.filter(...).order_by(...) doesn't hit the database at all until you actually iterate it, slice it, or call something like .first() or list() on it. This lets you compose filters across multiple lines without paying for multiple queries.

Model relationships

Django models three kinds of relationships directly as field types:

Python
class Author(models.Model):
    name = models.CharField(max_length=100)

class Category(models.Model):
    name = models.CharField(max_length=50)

class Post(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="posts")
    categories = models.ManyToManyField(Category, related_name="posts")
  • ForeignKey — many-to-one. Many Posts belong to one Author. on_delete is required and controls what happens to a Post when its Author is deleted (CASCADE deletes the posts too; PROTECT blocks the deletion; SET_NULL requires the field to be nullable).
  • ManyToManyField — many-to-many, backed by an automatically created join table. A Post can belong to several Category objects, and a Category can hold many Posts.
  • OneToOneField — a one-to-one link, commonly used to extend Django's built-in User model with a separate Profile model.

related_name names the reverse relationship — with related_name="posts" on Author.posts, you can go from an author to their posts without a separate query builder:

Python
ada = Author.objects.get(name="Ada Lovelace")
ada.posts.all()          # every Post where author=ada

post = Post.objects.get(id=1)
post.categories.all()    # every Category attached to this post
post.categories.add(some_category)

Common mistakes

  • Editing models.py and forgetting to run makemigrations and migrate — the database schema silently falls out of sync with the model definitions.
  • Using .get() when a query might return zero or multiple rows — it raises an exception in either case; .filter().first() is the safer choice when "zero or one" is a valid outcome.
  • Forgetting on_delete on a ForeignKey — Django requires an explicit choice (there's no silent default), specifically to force a deliberate decision about what happens to dependent rows.

Interview questions

Q: Walk through the migrations workflow for adding a new field to an existing model. Add the field to the model class in models.py, run python manage.py makemigrations to generate a migration file describing that schema change, review the generated file (Django sometimes needs to ask for a default value for existing rows if the field isn't nullable), then run python manage.py migrate to actually apply it against the database. The migration file itself is committed to version control, so every environment (a teammate's machine, staging, production) applies the exact same sequence of schema changes.

Q: What's the practical difference between ForeignKey and ManyToManyField? ForeignKey models a many-to-one relationship — many rows in one table each point to a single row in another (many Posts, one Author each) — and is stored as a column holding the related row's ID. ManyToManyField models a genuine many-to-many relationship (a Post can have several Categorys, and a Category can apply to many Posts), which Django implements under the hood with an automatically managed join table, not a single foreign key column.