Learn / Frameworks / Django / Models and Migrations

Beginner 17 min

Models and Migrations

Describe tables in Python, migrate them and query with the ORM.

What you will learn

  • Define models
  • Run migrations
  • Query with filter/get/order_by

A Django model is a Python class describing one database table. Each attribute is a column. You never write SQL for the common cases; the ORM (object-relational mapper) does it for you.

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

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

    def __str__(self):
        return self.name

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

    class Meta:
        ordering = ["-created"]

    def __str__(self):
        return self.title

Common fields: CharField, TextField, IntegerField, BooleanField, DateTimeField, EmailField, ForeignKey (one-to-many) and ManyToManyField. Django adds an id primary key automatically.

Migrations

Migrations are versioned scripts that carry model changes into the database. Change a model, generate a migration, then apply it.

python manage.py makemigrations
python manage.py migrate

Commit migration files to git so teammates and servers apply the same changes.

Querying with the ORM

Open the shell with python manage.py shell and try:

from blog.models import Author, Post

ada = Author.objects.create(name="Ada")
Post.objects.create(author=ada, title="Hello", body="First post", published=True)

Post.objects.all()                              # everything
Post.objects.filter(published=True)             # WHERE published
Post.objects.filter(title__contains="Hel")      # LIKE
Post.objects.exclude(author=ada)
Post.objects.order_by("-created")[:5]           # ORDER BY / LIMIT
Post.objects.get(id=1)                          # exactly one, or raises
ada.posts.count()                               # via related_name

Querysets are lazy: nothing hits the database until you iterate, slice or evaluate them, so you can chain filters freely. Lookups use double underscores: __gt, __in, __icontains, __startswith, and even across relations, author__name="Ada".

post = Post.objects.get(id=1)
post.title = "Updated"
post.save()

Post.objects.filter(published=False).update(published=True)
post.delete()

Avoiding the N+1 problem

Looping over posts and printing post.author.name runs one extra query per post. Fetch related rows up front:

posts = Post.objects.select_related("author")        # JOIN for foreign keys
posts = Author.objects.prefetch_related("posts")    # 2 queries for reverse/many-to-many
Tip

Add print(qs.query) or use the Django Debug Toolbar to see the SQL generated behind your code.

Try it yourself

Add a Comment model with a foreign key to Post, a text field and a timestamp. Make and apply the migration, then create two comments in the shell.

Show solution
class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
    text = models.TextField()
    created = models.DateTimeField(auto_now_add=True)

# shell:
# post = Post.objects.first()
# Comment.objects.create(post=post, text="Nice")
# post.comments.create(text="Thanks")