Database Migrations
Introduction
Django migrations are version-controlled Python files that describe schema changes—new tables, columns, indexes, and data transforms. They replace ad-hoc SQL scripts in team workflows. This chapter covers the full makemigrations / migrate cycle, inspecting migration history, fixtures, data migrations, and collaboration rules after you defined models in Models and ORM Basics and QuerySets and Relationships.
Prerequisites
- Models and ORM Basics —
Postmodel - QuerySets and Relationships —
author,tagsfields - Virtual environment activated in
hello-django/
How Migrations Fit In
models.py change
→ makemigrations (generates Python migration file)
→ migrate (applies to database)
→ django_migrations table records applied stateDjango tracks applied migrations in the django_migrations table. Each app has a migrations/ folder with numbered files like 0001_initial.py, 0002_post_author.py.
Core Commands
| Command | Purpose |
|---|---|
python manage.py makemigrations | Create migration files from model changes |
python manage.py makemigrations blog | Limit to one app |
python manage.py migrate | Apply all pending migrations |
python manage.py migrate blog 0001 | Migrate app to a specific state |
python manage.py showmigrations | List applied [X] vs pending [ ] |
python manage.py sqlmigrate blog 0001 | Print SQL without running it |
python manage.py migrate --plan | Preview migration order |
Typical workflow
# Edit blog/models.py — add a field
python manage.py makemigrations blog
python manage.py migrate
python manage.py showmigrations blogExample output after adding summary to Post:
Migrations for 'blog':
blog/migrations/0003_post_summary.py
+ Add field summary to postReading a Migration File
blog/migrations/0003_post_summary.py (generated):
Code explanation:
dependencies— must run after listed migrationsoperations— ordered list of schema changes- File name includes sequence number—never reuse numbers in Git
Inspect SQL:
python manage.py sqlmigrate blog 0003Schema Change Examples
Add nullable field (safe on live DB)
summary = models.CharField(max_length=300, blank=True, default="")Run makemigrations / migrate. Existing rows get default="".
Rename field
Prefer explicit migration:
# models.py: rename body → content (example)Django may ask:
Did you rename post.body to post.content? [y/N]Answer y to preserve data instead of drop+add.
Remove field
Delete from models.py, makemigrations, migrate. Data in that column is lost—back up production first.
Add non-null field to populated table
Provide default= or use a multi-step migration:
- Add field
null=True - Data migration to fill values
- Alter field
null=False
Squashing and Conflicts (Teams)
When two branches add 0003_*.py, Git merge conflicts occur. Resolution:
- Keep both logical changes in one new migration, or
- One developer rebases and regenerates
makemigrations
Rules:
- Commit migration files to Git with model changes
- Do not edit applied migrations on shared branches
- Do not delete migration files that others already applied
For long histories, python manage.py squashmigrations blog 0001 0010 combines migrations (optional maintenance).
Roll Back
python manage.py migrate blog 0002Reverts blog to migration 0002 state (runs reverse operations when defined). Not all operations are fully reversible—test on staging.
Warning
Production Rollbacks
Prefer forward-fix migrations (new migration correcting data) over downgrade on live databases with traffic.
Initial Data: Fixtures
Fixtures serialize model rows to JSON/YAML for repeatable seeds.
blog/fixtures/posts.json:
Load:
python manage.py loaddata postsDump existing data:
python manage.py dumpdata blog.Post --indent 2 > blog/fixtures/posts.jsonUse fixtures for demos and tests; large production seeds often use management commands instead.
Data Migrations (RunPython)
Transform rows without changing model fields:
blog/migrations/0004_populate_slugs.py:
Code explanation:
- Use
apps.get_model()— not directfrom blog.models import Post(historical model state) backwardsoptional for rollback- Keep data migrations small and idempotent when possible
Fake Migrations (Advanced)
When DB already matches models (legacy import):
python manage.py migrate blog 0001 --fakeMarks migration applied without running SQL—dangerous if schema actually differs. Use only when you know the DB state.
Zero-Downtime Awareness (Preview)
Production deploy order often:
- Deploy code compatible with old and new schema
- Run migrations
- Deploy code that requires new schema
Additive changes (new nullable column) are safer than destructive ones (drop column) under load. Full patterns appear in Production Deployment.
Compare with Flask-Migrate
| Django | Flask-Migrate (Alembic) |
|---|---|
Built-in makemigrations | flask db migrate |
migrate | flask db upgrade |
Per-app migrations/ | Single versions/ folder |
See Flask migrations.
Common Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| Model change, no migration | OperationalError: no such column | makemigrations + migrate |
| Migration not in Git | Works locally, fails on CI | Commit migrations/*.py |
| Edit old migration after deploy | Drift, integrity errors | New forward migration only |
loaddata duplicate PK | IntegrityError | Flush test DB or use fresh PKs |
| Wrong DB in production | Migrated SQLite, not MySQL | Check DJANGO_SETTINGS_MODULE |
Post-Chapter Checklist
- You run
makemigrationsafter every model change - You use
showmigrationsandsqlmigratefor debugging - Migration files are committed to version control
- You understand fixtures and
RunPythondata migrations - You know team rules: do not rewrite applied migrations
FAQ## FAQ
Delete all migrations and start over?
Only on disposable dev DBs: delete migrations/ except __init__.py, delete db.sqlite3, makemigrations, migrate. Never on shared/production history.
migrate without makemigrations?
migrate only applies existing files—it does not detect model diffs.
Multiple databases?
migrate --database=replica — requires DATABASE_ROUTERS in settings.
Migration asks for one-off default?
Pick a sensible default for existing rows or cancel and add default= in model first.
Empty migration?
python manage.py makemigrations --empty blog for custom RunPython / RunSQL.