How Django Migrations Generate and Apply Schema Changes
Django migrations serve as a version control system for your database schema, translating Python model declarations into database-specific SQL statements. This article explains the underlying mechanism of Django migrations, breaking down how the framework inspects model definitions, detects schema differences, writes migration files as pure Python code, and executes those instructions against a relational database while maintaining a reliable execution history.
1. Detecting
Schema Changes with makemigrations
When you run python manage.py makemigrations, Django
does not inspect your actual database. Instead, it inspects your
codebase. It performs this comparison through the following steps:
- State Construction: Django reconstructs the "historical state" of your models by reading all existing migration files in chronological order in memory.
- Current State Evaluation: Django loads the live
definitions currently present in your
models.pyfiles. - Autodetection: The
MigrationAutodetectorclass compares the historical state with the current state. It identifies differences, such as added models, removed fields, altered constraints, or modified indexes. - Operation Generation: The autodetector converts
these differences into a sequence of serialized Python objects known as
Operations (such as
migrations.CreateModel,migrations.AddField, ormigrations.AlterField).
2. The Structure of a Migration File
The output of makemigrations is a Python file placed in
the app's migrations/ directory. Each file defines a
subclass of django.db.migrations.Migration containing two
primary attributes:
dependencies: A list of tuples referencing previous migrations that must run before this one. Django uses these to build a Directed Acyclic Graph (DAG) to determine the exact execution order across all installed applications.operations: A list of operations that describe the changes to apply. Because these are defined as Python classes, migrations remain database-agnostic at this stage.
3.
Resolving Dependencies and Tracking State with migrate
Running python manage.py migrate triggers the process of
applying these Python-based instructions to the database:
- Reading Applied Migrations: Django queries a
special database table named
django_migrations. This table stores the app name, the migration name, and the timestamp of when each migration was executed. - Building the Execution Plan: Django compares the
migrations found in the filesystem against the records in
django_migrations. Any migration present on disk but absent from the database table is scheduled for execution according to the dependency graph.
4. Translating Python Operations to SQL
To execute an operation, Django uses a database-specific wrapper
called the SchemaEditor (e.g.,
DatabaseSchemaEditor for PostgreSQL, MySQL, or SQLite):
- The
SchemaEditortakes abstract operations (likeAddField) and translates them into raw, dialect-specific SQL DDL (Data Definition Language) queries, such asALTER TABLE "app_model" ADD COLUMN "field" varchar(255);. - For databases that support transactional DDL (like PostgreSQL), Django wraps the migration execution inside an atomic transaction. If any operation fails, the entire migration rolls back, preventing partial or corrupt schema states.
5. Finalizing the Database State
Once the SchemaEditor successfully executes the
generated SQL statements against the target database, Django inserts a
new row into the django_migrations table containing the app
label and migration name. This marks the migration as applied, ensuring
that subsequent runs of migrate ignore already-processed
files.