Django Migrations: A Comprehensive Guide

4 interview angles 10 min read source

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

  1. Detects Changes: Compares your current models with the database schema
  2. Generates Migration Files: Creates Python files in migrations/ directory
  3. Handles Dependencies: Determines the order of migrations
  4. 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

  1. Checks Migration History: Determines which migrations need to be applied
  2. Executes SQL: Runs the necessary SQL commands to update the database
  3. Updates Migration Table: Records which migrations have been applied
  4. 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 changes
  • migrate: Applies migrations to the database
  • showmigrations: Shows migration status
  • sqlmigrate: Shows SQL that would be executed

Best Practices:

  1. Test migrations before applying to production
  2. Use meaningful names for custom migrations
  3. Keep migrations small and focused
  4. Handle data migrations carefully
  5. 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 makemigrations miss?” - 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?” - RunPython with a reverse function, using the historical model via apps.get_model rather 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.