Migrations

Migrations

Django migrations provide a version control system for your database schema, allowing you to evolve your models over time while maintaining data integrity. Understanding migrations is essential for managing database changes in development, testing, and production environments.

Migrations

Django migrations provide a version control system for your database schema, allowing you to evolve your models over time while maintaining data integrity. Understanding migrations is essential for managing database changes in development, testing, and production environments.

What Are Django Migrations?

Django migrations are Python files that contain instructions for modifying your database schema. They enable you to:

  • Track Schema Changes: Version control your database structure alongside your code
  • Apply Changes Safely: Automatically generate SQL commands for schema modifications
  • Maintain Data Integrity: Preserve existing data during schema changes
  • Collaborate Effectively: Share database changes across development teams
  • Deploy Consistently: Ensure production databases match your development schema

Migration Lifecycle

# The migration process follows this lifecycle:

# 1. Model Changes
class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    # Adding a new field
    author = models.ForeignKey(User, on_delete=models.CASCADE)

# 2. Generate Migration
# python manage.py makemigrations

# 3. Review Generated Migration
# migrations/0002_post_author.py

# 4. Apply Migration
# python manage.py migrate

# 5. Database Schema Updated
# ALTER TABLE blog_post ADD COLUMN author_id INTEGER NOT NULL;

Basic Migration Operations

Creating Your First Migration

# models.py - Initial model
from django.db import models

class Category(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        return self.name

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        return self.title

# Generate initial migration
# python manage.py makemigrations blog

# Generated migration file: blog/migrations/0001_initial.py
from django.db import migrations, models
import django.db.models.deletion

class Migration(migrations.Migration):
    initial = True
    
    dependencies = []
    
    operations = [
        migrations.CreateModel(
            name='Category',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=100)),
                ('description', models.TextField(blank=True)),
                ('created_at', models.DateTimeField(auto_now_add=True)),
            ],
        ),
        migrations.CreateModel(
            name='Post',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('title', models.CharField(max_length=200)),
                ('content', models.TextField()),
                ('created_at', models.DateTimeField(auto_now_add=True)),
                ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category')),
            ],
        ),
    ]

Common Migration Scenarios

# Adding a field
class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    # New field added
    author = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
    created_at = models.DateTimeField(auto_now_add=True)

# Generated migration: 0002_post_author.py
class Migration(migrations.Migration):
    dependencies = [
        ('auth', '0012_alter_user_first_name_max_length'),
        ('blog', '0001_initial'),
    ]
    
    operations = [
        migrations.AddField(
            model_name='post',
            name='author',
            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='auth.user'),
        ),
    ]

# Removing a field
class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    # removed: created_at field
    updated_at = models.DateTimeField(auto_now=True)

# Generated migration: 0003_remove_post_created_at_post_updated_at.py
class Migration(migrations.Migration):
    dependencies = [
        ('blog', '0002_post_author'),
    ]
    
    operations = [
        migrations.RemoveField(
            model_name='post',
            name='created_at',
        ),
        migrations.AddField(
            model_name='post',
            name='updated_at',
            field=models.DateTimeField(auto_now=True, default=django.utils.timezone.now),
            preserve_default=False,
        ),
    ]

# Changing field properties
class Post(models.Model):
    # Changed max_length from 200 to 300
    title = models.CharField(max_length=300)
    content = models.TextField()
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    updated_at = models.DateTimeField(auto_now=True)

# Generated migration: 0004_alter_post_title.py
class Migration(migrations.Migration):
    dependencies = [
        ('blog', '0003_remove_post_created_at_post_updated_at'),
    ]
    
    operations = [
        migrations.AlterField(
            model_name='post',
            name='title',
            field=models.CharField(max_length=300),
        ),
    ]

Migration Commands

Essential Migration Commands

# Create migrations for model changes
python manage.py makemigrations

# Create migrations for specific app
python manage.py makemigrations blog

# Create empty migration for custom operations
python manage.py makemigrations --empty blog

# Apply migrations
python manage.py migrate

# Apply migrations for specific app
python manage.py migrate blog

# Apply migrations up to specific migration
python manage.py migrate blog 0003

# Show migration status
python manage.py showmigrations

# Show migration status for specific app
python manage.py showmigrations blog

# Show SQL for a migration without applying
python manage.py sqlmigrate blog 0001

# Check for migration issues
python manage.py check

# Reverse migrations (go back to previous state)
python manage.py migrate blog 0002

# Completely unapply all migrations for an app
python manage.py migrate blog zero

Migration Planning and Review

# Always review generated migrations before applying
# Example migration review checklist:

class MigrationReviewChecklist:
    """
    Migration Review Checklist:
    
    1. Data Safety
       - Will this migration cause data loss?
       - Are there adequate default values for new NOT NULL fields?
       - Is there a rollback plan?
    
    2. Performance Impact
       - Will this migration lock tables for a long time?
       - Are there indexes being created that might slow down the migration?
       - Should this be done during maintenance windows?
    
    3. Dependencies
       - Are all required dependencies listed?
       - Will this migration conflict with other pending migrations?
       - Are there circular dependencies?
    
    4. Backwards Compatibility
       - Can this migration be safely reversed?
       - Will reversing this migration cause data loss?
       - Are there any irreversible operations?
    
    5. Production Considerations
       - Is this migration safe to run on production data?
       - Should this be split into multiple smaller migrations?
       - Are there any manual steps required before or after?
    """
    pass

# Example of a complex migration that needs careful review
class Migration(migrations.Migration):
    dependencies = [
        ('blog', '0004_alter_post_title'),
    ]
    
    operations = [
        # This operation might be slow on large tables
        migrations.AddField(
            model_name='post',
            name='slug',
            field=models.SlugField(max_length=300, unique=True, default=''),
            preserve_default=False,
        ),
        
        # This will create an index, which might take time
        migrations.AlterField(
            model_name='post',
            name='title',
            field=models.CharField(max_length=300, db_index=True),
        ),
        
        # Custom operation to populate slug field
        migrations.RunPython(
            code=populate_post_slugs,
            reverse_code=migrations.RunPython.noop,
        ),
    ]

def populate_post_slugs(apps, schema_editor):
    """Populate slug field for existing posts"""
    Post = apps.get_model('blog', 'Post')
    from django.utils.text import slugify
    
    for post in Post.objects.all():
        post.slug = slugify(post.title)
        post.save()

Migration Best Practices

Safe Migration Patterns

# 1. Adding nullable fields (safe)
class SafeMigration1(migrations.Migration):
    operations = [
        migrations.AddField(
            model_name='post',
            name='excerpt',
            field=models.TextField(blank=True, null=True),
        ),
    ]

# 2. Adding fields with defaults (safe)
class SafeMigration2(migrations.Migration):
    operations = [
        migrations.AddField(
            model_name='post',
            name='view_count',
            field=models.PositiveIntegerField(default=0),
        ),
    ]

# 3. Two-step approach for adding NOT NULL fields
# Step 1: Add nullable field
class Migration1(migrations.Migration):
    operations = [
        migrations.AddField(
            model_name='post',
            name='author',
            field=models.ForeignKey(
                'auth.User',
                on_delete=models.CASCADE,
                null=True,
                blank=True
            ),
        ),
    ]

# Step 2: Populate field and make it NOT NULL
class Migration2(migrations.Migration):
    dependencies = [
        ('blog', '0001_add_author_nullable'),
    ]
    
    operations = [
        # First populate the field
        migrations.RunPython(
            code=populate_author_field,
            reverse_code=migrations.RunPython.noop,
        ),
        
        # Then make it NOT NULL
        migrations.AlterField(
            model_name='post',
            name='author',
            field=models.ForeignKey(
                'auth.User',
                on_delete=models.CASCADE,
            ),
        ),
    ]

def populate_author_field(apps, schema_editor):
    """Populate author field for existing posts"""
    Post = apps.get_model('blog', 'Post')
    User = apps.get_model('auth', 'User')
    
    # Get or create a default author
    default_author, created = User.objects.get_or_create(
        username='system',
        defaults={
            'email': 'system@example.com',
            'first_name': 'System',
            'last_name': 'User',
        }
    )
    
    # Update posts without authors
    Post.objects.filter(author__isnull=True).update(author=default_author)

# 4. Safe field renaming (three-step process)
# Step 1: Add new field
class RenameStep1(migrations.Migration):
    operations = [
        migrations.AddField(
            model_name='post',
            name='publication_date',
            field=models.DateTimeField(null=True, blank=True),
        ),
    ]

# Step 2: Copy data and update code
class RenameStep2(migrations.Migration):
    dependencies = [
        ('blog', 'rename_step1'),
    ]
    
    operations = [
        migrations.RunPython(
            code=copy_published_at_to_publication_date,
            reverse_code=copy_publication_date_to_published_at,
        ),
    ]

def copy_published_at_to_publication_date(apps, schema_editor):
    Post = apps.get_model('blog', 'Post')
    for post in Post.objects.all():
        post.publication_date = post.published_at
        post.save()

def copy_publication_date_to_published_at(apps, schema_editor):
    Post = apps.get_model('blog', 'Post')
    for post in Post.objects.all():
        post.published_at = post.publication_date
        post.save()

# Step 3: Remove old field (after code is updated)
class RenameStep3(migrations.Migration):
    dependencies = [
        ('blog', 'rename_step2'),
    ]
    
    operations = [
        migrations.RemoveField(
            model_name='post',
            name='published_at',
        ),
    ]

Performance Considerations

class PerformanceMigrationPatterns:
    """
    Performance considerations for migrations:
    
    1. Large Table Migrations
       - Consider maintenance windows for large tables
       - Use database-specific optimizations
       - Split large migrations into smaller chunks
    
    2. Index Creation
       - Create indexes CONCURRENTLY when possible (PostgreSQL)
       - Consider creating indexes during low-traffic periods
       - Monitor index creation progress
    
    3. Data Migrations
       - Process data in batches to avoid memory issues
       - Use bulk operations when possible
       - Provide progress feedback for long-running operations
    
    4. Backwards Compatibility
       - Ensure migrations can be safely reversed
       - Test rollback procedures
       - Document any manual steps required
    """
    
    @staticmethod
    def create_concurrent_index_migration():
        """Example of creating index concurrently (PostgreSQL)"""
        
        return migrations.RunSQL(
            sql="CREATE INDEX CONCURRENTLY idx_post_title ON blog_post(title);",
            reverse_sql="DROP INDEX idx_post_title;",
        )
    
    @staticmethod
    def batch_data_migration():
        """Example of processing data in batches"""
        
        def update_posts_in_batches(apps, schema_editor):
            Post = apps.get_model('blog', 'Post')
            
            batch_size = 1000
            total_posts = Post.objects.count()
            processed = 0
            
            while processed < total_posts:
                posts = Post.objects.all()[processed:processed + batch_size]
                
                for post in posts:
                    # Perform update operation
                    post.slug = slugify(post.title)
                    post.save()
                
                processed += batch_size
                print(f"Processed {processed}/{total_posts} posts")
        
        return migrations.RunPython(
            code=update_posts_in_batches,
            reverse_code=migrations.RunPython.noop,
        )

# Example of a production-ready migration
class ProductionMigration(migrations.Migration):
    """
    Production migration with safety checks and performance optimizations
    """
    
    dependencies = [
        ('blog', '0005_previous_migration'),
    ]
    
    operations = [
        # Add field as nullable first
        migrations.AddField(
            model_name='post',
            name='featured_image',
            field=models.ImageField(
                upload_to='posts/featured/',
                null=True,
                blank=True
            ),
        ),
        
        # Create index concurrently (PostgreSQL)
        migrations.RunSQL(
            sql="CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_post_featured "
                "ON blog_post(featured_image) WHERE featured_image IS NOT NULL;",
            reverse_sql="DROP INDEX IF EXISTS idx_post_featured;",
            state_operations=[
                migrations.AddIndex(
                    model_name='post',
                    index=models.Index(
                        fields=['featured_image'],
                        name='idx_post_featured',
                        condition=models.Q(featured_image__isnull=False)
                    ),
                ),
            ],
        ),
    ]
    
    # Add atomic = False for long-running migrations
    atomic = False

Django migrations provide a robust system for managing database schema evolution. Understanding how to create, review, and apply migrations safely ensures your application can evolve while maintaining data integrity and minimizing downtime in production environments.