CodeOath
← All posts
Python70 min total · 18 parts

Django Fundamentals: MVT Architecture, the ORM, and Middleware

Contents — Part 5 of 18: Migrations: Version Control for Your Schema
Part 5 of 18 · ~2 min

Migrations: Version Control for Your Schema

python manage.py makemigrations   # generate a migration file from model changes
python manage.py migrate          # apply pending migrations to the database
python manage.py sqlmigrate orders 0003  # preview the actual SQL a migration will run
python manage.py showmigrations   # see which migrations are applied vs. pending

A migration is a versioned, reviewable Python file describing exactly how the schema changed — the same discipline Git gives your code, applied to your database structure, and critically, applied in the same order on every environment (dev, staging, production).

What actually happens when migrations conflict

Two developers each add a migration on separate branches, both numbered against the same prior migration (e.g., both 0005_x depending on 0004). Django detects this as two migrations with the same dependency, forming a diverging history:

python manage.py makemigrations --merge   # generates a merge migration, like a git merge commit

This is a genuinely common source of confusion on teams — it isn't a bug, it's Django correctly detecting that the migration graph branched, exactly like a Git history that needs a merge commit.

Data migrations vs. schema migrations

Most migrations only change schema (add a column, add an index). A data migration additionally transforms existing rows, using RunPython:

from django.db import migrations

def populate_full_name(apps, schema_editor):
    # NOTE: use the historical model from apps.get_model, not the "live"
    # imported model — the live model may have fields this migration doesn't know about yet
    Order = apps.get_model("orders", "Order")
    for order in Order.objects.all():
        order.full_name = f"{order.first_name} {order.last_name}"
        order.save()

class Migration(migrations.Migration):
    dependencies = [("orders", "0004_order_full_name")]
    operations = [migrations.RunPython(populate_full_name)]

Using apps.get_model instead of importing the real model class matters: migrations must remain correct even years later, replayed against a database that's mid-way through its history — importing the current Order class directly could reference a field that doesn't exist yet at that point in the migration sequence.