Django Migrations: A Comprehensive Guide
Introduction
Django migrations are Django’s way of propagating changes you make to your models (adding a field, deleting a model, etc.) into your database schema. They’re designed to be mostly automatic, but you’ll need to know when to make them, when to run them, and how to handle common problems.
What are Migrations?
Migrations are Django’s way of evolving your database schema over time in a consistent and organized way. They’re like version control for your database schema.
Key Concepts
- Migration Files: Python files that describe database schema changes
- Migration History: Django tracks which migrations have been applied
- Dependencies: Migrations can depend on other migrations
- Reversibility: Most migrations can be rolled back
Migration Workflow
1. Change your models (models.py)
2. Create migration files (makemigrations)
3. Apply migrations to database (migrate)
4. Verify changes
The makemigrations Command
Basic Usage
# Create migrations for all apps
python manage.py makemigrations
# Create migrations for specific app
python manage.py makemigrations myapp
# Create migrations with custom name
python manage.py makemigrations myapp --name add_user_profile
# Create empty migration
python manage.py makemigrations myapp --empty
# Create migration with custom message
python manage.py makemigrations myapp --name add_custom_field --empty
What makemigrations Does
- Detects Changes: Compares your current models with the database schema
- Generates Migration Files: Creates Python files in
migrations/directory - Handles Dependencies: Determines the order of migrations
- Validates Changes: Checks for potential issues
Example Migration Generation
# models.py
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.CharField(max_length=100)
publication_date = models.DateField()
price = models.DecimalField(max_digits=10, decimal_places=2)
def __str__(self):
return self.title
Running python manage.py makemigrations creates:
# migrations/0001_initial.py
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name='Book',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('author', models.CharField(max_length=100)),
('publication_date', models.DateField()),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
],
),
]
Adding a New Field
# models.py - Add a new field
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.CharField(max_length=100)
publication_date = models.DateField()
price = models.DecimalField(max_digits=10, decimal_places=2)
isbn = models.CharField(max_length=13, unique=True) # New field
def __str__(self):
return self.title
Running python manage.py makemigrations creates:
# migrations/0002_book_isbn.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('myapp', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='book',
name='isbn',
field=models.CharField(max_length=13, unique=True),
),
]
Modifying Existing Fields
# models.py - Modify existing field
class Book(models.Model):
title = models.CharField(max_length=300) # Changed from 200 to 300
author = models.CharField(max_length=100)
publication_date = models.DateField()
price = models.DecimalField(max_digits=10, decimal_places=2)
isbn = models.CharField(max_length=13, unique=True)
def __str__(self):
return self.title
Running python manage.py makemigrations creates:
# migrations/0003_alter_book_title.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('myapp', '0002_book_isbn'),
]
operations = [
migrations.AlterField(
model_name='book',
name='title',
field=models.CharField(max_length=300),
),
]
The migrate Command
Basic Usage
# Apply all pending migrations
python manage.py migrate
# Apply migrations for specific app
python manage.py migrate myapp
# Apply specific migration
python manage.py migrate myapp 0002
# Show migration status
python manage.py showmigrations
# Show SQL that would be executed
python manage.py sqlmigrate myapp 0001
# Fake a migration (mark as applied without running)
python manage.py migrate myapp 0001 --fake
What migrate Does
- Checks Migration History: Determines which migrations need to be applied
- Executes SQL: Runs the necessary SQL commands to update the database
- Updates Migration Table: Records which migrations have been applied
- Handles Dependencies: Ensures migrations are applied in the correct order
Migration Status
# Check migration status
python manage.py showmigrations
# Output example:
myapp
[X] 0001_initial
[X] 0002_book_isbn
[ ] 0003_alter_book_title # Not applied yet
Applying Migrations
# Apply all pending migrations
python manage.py migrate
# Output example:
Operations to perform:
Apply all migrations: myapp
Running migrations:
Applying myapp.0003_alter_book_title... OK
Advanced Migration Scenarios
Adding Fields with Default Values
# models.py
class Book(models.Model):
title = models.CharField(max_length=300)
author = models.CharField(max_length=100)
publication_date = models.DateField()
price = models.DecimalField(max_digits=10, decimal_places=2)
isbn = models.CharField(max_length=13, unique=True)
genre = models.CharField(max_length=50, default='Fiction') # New field with default
def __str__(self):
return self.title
Generated migration:
# migrations/0004_book_genre.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('myapp', '0003_alter_book_title'),
]
operations = [
migrations.AddField(
model_name='book',
name='genre',
field=models.CharField(default='Fiction', max_length=50),
),
]
Adding Nullable Fields
# models.py
class Book(models.Model):
title = models.CharField(max_length=300)
author = models.CharField(max_length=100)
publication_date = models.DateField()
price = models.DecimalField(max_digits=10, decimal_places=2)
isbn = models.CharField(max_length=13, unique=True)
genre = models.CharField(max_length=50, default='Fiction')
description = models.TextField(null=True, blank=True) # Nullable field
def __str__(self):
return self.title
Generated migration:
# migrations/0005_book_description.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('myapp', '0004_book_genre'),
]
operations = [
migrations.AddField(
model_name='book',
name='description',
field=models.TextField(blank=True, null=True),
),
]
Adding Non-Nullable Fields
When adding a non-nullable field to an existing table with data, Django will prompt for a default value:
# models.py
class Book(models.Model):
title = models.CharField(max_length=300)
author = models.CharField(max_length=100)
publication_date = models.DateField()
price = models.DecimalField(max_digits=10, decimal_places=2)
isbn = models.CharField(max_length=13, unique=True)
genre = models.CharField(max_length=50, default='Fiction')
description = models.TextField(null=True, blank=True)
publisher = models.CharField(max_length=100) # Non-nullable field
def __str__(self):
return self.title
Running python manage.py makemigrations will prompt:
You are trying to add a non-nullable field 'publisher' to book without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
2) Quit, and let me add a default in models.py
Select an option: 1
Please enter the default value now, as valid Python.
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
>>> 'Unknown Publisher'
Adding Foreign Key Relationships
# models.py
class Author(models.Model):
name = models.CharField(max_length=100)
email = models.EmailField(unique=True)
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=300)
author = models.ForeignKey(Author, on_delete=models.CASCADE) # Foreign key
publication_date = models.DateField()
price = models.DecimalField(max_digits=10, decimal_places=2)
isbn = models.CharField(max_length=13, unique=True)
genre = models.CharField(max_length=50, default='Fiction')
description = models.TextField(null=True, blank=True)
publisher = models.CharField(max_length=100)
def __str__(self):
return self.title
Generated migration:
# migrations/0006_auto_20231201_1234.py
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('myapp', '0005_book_description'),
]
operations = [
migrations.CreateModel(
name='Author',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('email', models.EmailField(max_length=254, unique=True)),
],
),
migrations.AddField(
model_name='book',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='myapp.author'),
),
]
Custom Migrations
Creating Empty Migrations
python manage.py makemigrations myapp --empty --name custom_migration
Custom Migration Example
# migrations/0007_custom_migration.py
from django.db import migrations
def set_default_genre(apps, schema_editor):
Book = apps.get_model('myapp', 'Book')
for book in Book.objects.all():
if not book.genre:
book.genre = 'Unknown'
book.save()
def reverse_default_genre(apps, schema_editor):
Book = apps.get_model('myapp', 'Book')
for book in Book.objects.filter(genre='Unknown'):
book.genre = ''
book.save()
class Migration(migrations.Migration):
dependencies = [
('myapp', '0006_auto_20231201_1234'),
]
operations = [
migrations.RunPython(set_default_genre, reverse_default_genre),
]
Data Migrations
# migrations/0008_data_migration.py
from django.db import migrations
def migrate_author_data(apps, schema_editor):
Book = apps.get_model('myapp', 'Book')
Author = apps.get_model('myapp', 'Author')
# Create authors from existing book author strings
for book in Book.objects.all():
if hasattr(book, 'author') and isinstance(book.author, str):
author, created = Author.objects.get_or_create(
name=book.author,
defaults={'email': f'{book.author.lower().replace(" ", ".")}@example.com'}
)
book.author = author
book.save()
def reverse_author_data(apps, schema_editor):
Book = apps.get_model('myapp', 'Book')
Author = apps.get_model('myapp', 'Author')
# Convert back to string (if needed)
for book in Book.objects.all():
if hasattr(book, 'author') and hasattr(book.author, 'name'):
book.author = book.author.name
book.save()
class Migration(migrations.Migration):
dependencies = [
('myapp', '0007_custom_migration'),
]
operations = [
migrations.RunPython(migrate_author_data, reverse_author_data),
]
Migration Commands Reference
makemigrations Options
# Basic options
python manage.py makemigrations [app_label] [app_label ...]
# Create empty migration
python manage.py makemigrations --empty [app_label]
# Custom migration name
python manage.py makemigrations --name migration_name [app_label]
# Dry run (show what would be created)
python manage.py makemigrations --dry-run [app_label]
# Merge migrations
python manage.py makemigrations --merge
# Check for conflicts
python manage.py makemigrations --check
migrate Options
# Basic options
python manage.py migrate [app_label] [migration_name]
# Apply specific migration
python manage.py migrate myapp 0001
# Fake migration (mark as applied without running)
python manage.py migrate --fake [app_label]
# Show plan without executing
python manage.py migrate --plan
# Show SQL that would be executed
python manage.py sqlmigrate [app_label] [migration_name]
# Show migration status
python manage.py showmigrations [app_label]
# List migration dependencies
python manage.py migrate --list
Common Migration Scenarios
Renaming a Field
# models.py - Rename field
class Book(models.Model):
title = models.CharField(max_length=300)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
publication_date = models.DateField()
price = models.DecimalField(max_digits=10, decimal_places=2)
isbn = models.CharField(max_length=13, unique=True)
genre = models.CharField(max_length=50, default='Fiction')
description = models.TextField(null=True, blank=True)
publisher = models.CharField(max_length=100)
pub_date = models.DateField() # Renamed from publication_date
def __str__(self):
return self.title
Generated migration:
# migrations/0009_rename_publication_date_book_pub_date.py
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('myapp', '0008_data_migration'),
]
operations = [
migrations.RenameField(
model_name='book',
old_name='publication_date',
new_name='pub_date',
),
]
Deleting a Field
# models.py - Remove field
class Book(models.Model):
title = models.CharField(max_length=300)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
pub_date = models.DateField()
price = models.DecimalField(max_digits=10, decimal_places=2)
isbn = models.CharField(max_length=13, unique=True)
genre = models.CharField(max_length=50, default='Fiction')
description = models.TextField(null=True, blank=True)
# publisher field removed
def __str__(self):
return self.title
Generated migration:
# migrations/0010_remove_book_publisher.py
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('myapp', '0009_rename_publication_date_book_pub_date'),
]
operations = [
migrations.RemoveField(
model_name='book',
name='publisher',
),
]
Changing Field Type
# models.py - Change field type
class Book(models.Model):
title = models.CharField(max_length=300)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
pub_date = models.DateField()
price = models.DecimalField(max_digits=10, decimal_places=2)
isbn = models.CharField(max_length=13, unique=True)
genre = models.CharField(max_length=50, default='Fiction')
description = models.TextField(null=True, blank=True)
pages = models.IntegerField() # Changed from PositiveIntegerField
def __str__(self):
return self.title
Generated migration:
# migrations/0011_alter_book_pages.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('myapp', '0010_remove_book_publisher'),
]
operations = [
migrations.AlterField(
model_name='book',
name='pages',
field=models.IntegerField(),
),
]
Migration Best Practices
1. Always Test Migrations
# Test migrations on a copy of production data
python manage.py migrate --plan
python manage.py sqlmigrate myapp 0001
2. Use Meaningful Migration Names
# Good
python manage.py makemigrations --name add_user_profile
# Bad
python manage.py makemigrations --name migration_001
3. Handle Data Migrations Carefully
# Always provide reverse functions
def forward_func(apps, schema_editor):
# Forward migration logic
pass
def reverse_func(apps, schema_editor):
# Reverse migration logic
pass
migrations.RunPython(forward_func, reverse_func)
4. Use --dry-run for Testing
# See what would be created without actually creating it
python manage.py makemigrations --dry-run
5. Keep Migrations Small and Focused
# Good: One change per migration
class Migration(migrations.Migration):
operations = [
migrations.AddField(
model_name='book',
name='isbn',
field=models.CharField(max_length=13, unique=True),
),
]
# Bad: Multiple unrelated changes
class Migration(migrations.Migration):
operations = [
migrations.AddField(...),
migrations.CreateModel(...),
migrations.AlterField(...),
migrations.DeleteModel(...),
]
Troubleshooting Common Issues
1. Migration Conflicts
# Check for conflicts
python manage.py makemigrations --check
# Merge conflicting migrations
python manage.py makemigrations --merge
2. Circular Dependencies
# Use run_before or run_after in migration dependencies
class Migration(migrations.Migration):
dependencies = [
('myapp', '0001_initial'),
]
run_before = [
('otherapp', '0001_initial'),
]
3. Database State Mismatch
# Fake migrations to sync state
python manage.py migrate --fake myapp 0001
# Reset migration state (dangerous!)
python manage.py migrate myapp zero --fake
4. Large Tables and Performance
# Use database-specific operations for large tables
from django.db import migrations
class Migration(migrations.Migration):
operations = [
migrations.RunSQL(
"ALTER TABLE myapp_book ADD COLUMN new_field VARCHAR(100) DEFAULT ''",
"ALTER TABLE myapp_book DROP COLUMN new_field"
),
]
Summary
Django migrations provide a powerful and flexible way to manage database schema changes:
Key Commands:
makemigrations: Creates migration files from model changesmigrate: Applies migrations to the databaseshowmigrations: Shows migration statussqlmigrate: Shows SQL that would be executed
Best Practices:
- Test migrations before applying to production
- Use meaningful names for custom migrations
- Keep migrations small and focused
- Handle data migrations carefully
- Always provide reverse functions for custom migrations
Common Scenarios:
- Adding/removing fields
- Changing field types
- Adding relationships
- Data migrations
- Handling conflicts
Django migrations make database schema management safe, version-controlled, and reversible, making them essential for any Django project.
Interview angle
- “How do you add a non-null column to a large table safely?” - in stages: add it nullable, backfill in batches outside the migration, then set NOT NULL. A single-step add can lock and rewrite the table.
- “What does
makemigrationsmiss?” - renames (it emits drop plus add, losing data), data transformations, and anything outside the model layer. Always read the generated migration rather than trusting it. - “How do you do a data migration?” -
RunPythonwith a reverse function, using the historical model viaapps.get_modelrather than importing the current one - the current model may have fields that didn’t exist at that migration point. - “Who runs migrations on deploy?” - one process, not every replica. Use a dedicated job or an advisory lock, and keep them out of application startup.