Migration Strategies
Django migrations are a version control system that reflects model changes in the database. In production environments, they must be applied carefully.
Basic Migration Flow
# 1. Generate migration file after model changes
python manage.py makemigrations
# 2. Preview migration content (SQL preview)
python manage.py sqlmigrate products 0001
# 3. Apply to DB
python manage.py migrate
# 4. Check status
python manage.py showmigrations
python manage.py showmigrations products
Safe Schema Changes
# Step 1: Add new column — start with null=True
# products/models.py
class Product(models.Model):
# existing fields...
discount_rate = models.DecimalField(
max_digits=5, decimal_places=2,
null=True, blank=True, # ✅ Start with null allowed
default=0.0,
)
python manage.py makemigrations --name "add_discount_rate_to_product"
python manage.py migrate
# Step 2: Fill in existing data (data migration)
# python manage.py makemigrations --empty products --name "fill_discount_rate"
# products/migrations/0003_fill_discount_rate.py
from django.db import migrations
def fill_discount(apps, schema_editor):
Product = apps.get_model("products", "Product")
Product.objects.filter(discount_rate__isnull=True).update(discount_rate=0.0)
def unfill_discount(apps, schema_editor):
"""reverse function for rollback"""
pass
class Migration(migrations.Migration):
dependencies = [("products", "0002_add_discount_rate_to_product")]
operations = [
migrations.RunPython(fill_discount, unfill_discount),
]
# Step 3: Remove null (separate migration)
class Product(models.Model):
discount_rate = models.DecimalField(
max_digits=5, decimal_places=2,
default=0.0, # remove null=True, blank=True
)
Complex Schema Change Patterns
# ① Column rename — RenameField
# migrations/0004_rename_price.py
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [("products", "0003_fill_discount_rate")]
operations = [
migrations.RenameField(
model_name="product",
old_name="price",
new_name="selling_price",
),
]
# ② Table split (including data migration)
def split_address(apps, schema_editor):
User = apps.get_model("users", "User")
Address = apps.get_model("users", "Address")
for user in User.objects.all():
if user.address_text: # existing field
Address.objects.create(
user=user,
street=user.address_text,
city="",
)
class Migration(migrations.Migration):
operations = [
# 1. Create new table
migrations.CreateModel(
name="Address",
fields=[
("id", ...),
("user", ...),
("street", ...),
],
),
# 2. Migrate data
migrations.RunPython(split_address),
# 3. Remove old field (after sufficient verification)
# migrations.RemoveField(model_name="user", name="address_text"),
]
Resolving Migration Conflicts
# When two developers run makemigrations simultaneously
# Two files created: 0003_alice.py and 0003_bob.py → conflict
# Attempt automatic merge
python manage.py makemigrations --merge
# Review generated merge file then apply
python manage.py migrate
products/
├── 0001_initial.py
├── 0002_add_discount_rate.py
├── 0003_alice.py ← conflict
├── 0003_bob.py ← conflict
└── 0004_merge.py ← merge file
Large Table Migrations
# Tables with tens of millions of rows: index creation causes table lock
# Use django-pg-zero-downtime-migrations or handle manually
# ✅ Non-concurrent index creation (PostgreSQL)
from django.db import migrations, models
class Migration(migrations.Migration):
atomic = False # ← disable transaction (for index creation)
operations = [
migrations.SeparateDatabaseAndState(
database_operations=[
migrations.RunSQL(
"CREATE INDEX CONCURRENTLY idx_product_price ON products_product(price);",
"DROP INDEX IF EXISTS idx_product_price;",
)
],
state_operations=[
migrations.AddIndex(
model_name="product",
index=models.Index(fields=["price"], name="idx_product_price"),
)
],
)
]
Migration Best Practices
✅ Always commit migration files to git
✅ After makemigrations, verify SQL with sqlmigrate
✅ Test on staging before production deployment
✅ Apply large table migrations during off-peak hours
✅ Column deletion in stages (remove code → allow null → actual deletion)
❌ Overusing squashmigrations (loses history)
❌ Deploying after manually editing migration files without migrate
❌ Applying large migrations simultaneously with deployment
Summary
| Command | Description |
|---|---|
makemigrations | Generate migration file |
migrate | Apply to DB |
sqlmigrate | SQL preview |
showmigrations | Check applied status |
makemigrations --merge | Merge conflicts |
RunPython | Data migration |
SeparateDatabaseAndState | Separate DB/state handling |
Safe migrations are best done small, incrementally, with a rollback plan.