Skip to main content
Advertisement

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

CommandDescription
makemigrationsGenerate migration file
migrateApply to DB
sqlmigrateSQL preview
showmigrationsCheck applied status
makemigrations --mergeMerge conflicts
RunPythonData migration
SeparateDatabaseAndStateSeparate DB/state handling

Safe migrations are best done small, incrementally, with a rollback plan.

Advertisement