| name | tg-migrations |
| description | Django migration best practices for the World of Darkness application. Use when creating data migrations, handling schema changes with existing data, squashing old migrations, testing migrations in CI/CD, or planning multi-step migrations for breaking changes. Triggers on migration work, database schema changes, or data transformation tasks. |
Migration Best Practices
Core Principles
- Never edit applied migrations - Once in production, migrations are immutable
- Test before deployment - Test both forward and reverse migrations
- Keep migrations focused - One logical change per migration
- Document complex migrations - Explain non-obvious data transformations
- Plan multi-step migrations - For breaking changes, maintain backward compatibility
Data Migrations for Schema Changes
When to Use Data Migrations
Create data migrations when you need to:
- Transform existing data during schema changes
- Populate new fields with calculated values
- Migrate data between models (e.g., JSONField to related models)
- Clean up or normalize existing data
Three-Step Pattern for New Required Fields
Step 1: Add field as nullable
operations = [
migrations.AddField(
model_name='character',
name='generation',
field=models.IntegerField(null=True, blank=True),
),
]
Step 2: Populate with data
def populate_generation(apps, schema_editor):
Character = apps.get_model('characters', 'Character')
for char in Character.objects.filter(character_type='vampire', generation__isnull=True):
char.generation = 13
char.save(update_fields=['generation'])
def reverse_populate(apps, schema_editor):
Character = apps.get_model('characters', 'Character')
Character.objects.update(generation=None)
operations = [
migrations.RunPython(populate_generation, reverse_populate),
]
Step 3: Make non-nullable
operations = [
migrations.AlterField(
model_name='character',
name='generation',
field=models.IntegerField(default=13),
),
]
Data Migration Best Practices
Use Historical Models
def migrate_data(apps, schema_editor):
Character = apps.get_model('characters', 'Character')
from characters.models import Character
Handle Large Datasets
def migrate_large_dataset(apps, schema_editor):
Character = apps.get_model('characters', 'Character')
for char in Character.objects.iterator(chunk_size=1000):
char.new_field = calculate_value(char)
char.save(update_fields=['new_field'])
updates = []
for char in Character.objects.iterator(chunk_size=1000):
char.new_field = calculate_value(char)
updates.append(char)
if len(updates) >= 1000:
Character.objects.bulk_update(updates, ['new_field'])
updates = []
if updates:
Character.objects.bulk_update(updates, ['new_field'])
Always Provide Reverse Migrations
operations = [
migrations.RunPython(
forward_migration,
reverse_migration,
),
]
Make Migrations Idempotent
def migrate_data(apps, schema_editor):
"""Migration that can safely run multiple times."""
Model = apps.get_model('app', 'Model')
if not Model.objects.filter(special_flag=True).exists():
pass
Migration Squashing
When to Squash
- App has 50+ migrations
- Migrations are all applied to production
- Old migrations contain outdated data transformations
Squashing Process
python manage.py showmigrations characters
python manage.py squashmigrations characters 0001 0050
python manage.py migrate characters
python manage.py check
python manage.py test characters
After Squashing
After squashed migration is in production for several weeks:
rm characters/migrations/0001_initial.py
Common Migration Patterns
Renaming a Field (Safe)
operations = [migrations.AddField('Model', 'new_name', field=...)]
operations = [
migrations.RunPython(
lambda apps, se: apps.get_model('app', 'Model').objects.update(new_name=F('old_name'))
),
]
operations = [migrations.RemoveField('Model', 'old_name')]
Changing Field Type
operations = [migrations.AddField('Model', 'field_new', new_field_type)]
def transform_data(apps, schema_editor):
Model = apps.get_model('app', 'Model')
for obj in Model.objects.iterator():
obj.field_new = transform(obj.field_old)
obj.save(update_fields=['field_new'])
operations = [migrations.RemoveField('Model', 'field_old')]
operations = [migrations.RenameField('Model', 'field_new', 'field_old')]
Testing Migrations
CI/CD Migration Tests
python manage.py makemigrations --check --dry-run
python manage.py migrate
python manage.py migrate characters 0000
python manage.py migrate characters
Migration Test Class
from django.test import TransactionTestCase
from django.core.management import call_command
class MigrationTestCase(TransactionTestCase):
migrate_from = None
migrate_to = None
app_name = 'characters'
def setUp(self):
if self.migrate_from:
call_command('migrate', self.app_name, self.migrate_from)
super().setUp()
def migrate(self, target):
call_command('migrate', self.app_name, target)
def test_migration_forward_backward(self):
if not self.migrate_to or not self.migrate_from:
self.skipTest("Migration range not specified")
self.migrate(self.migrate_to)
self.migrate(self.migrate_from)
Pre-Deployment Checklist
Before Creating Migration
Before Committing
Before Deploying
Troubleshooting
Migration Conflicts
python manage.py makemigrations
Fake Migrations (Emergency)
python manage.py migrate app_name 0042 --fake
Rolling Back
python manage.py migrate app_name 0041_previous
python manage.py makemigrations
python manage.py migrate