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.
Django migrations are Python files that contain instructions for modifying your database schema. They enable you to:
# 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;
# 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')),
],
),
]
# 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),
),
]
# 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
# 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()
# 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',
),
]
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.
Signals
Django signals provide a decoupled way to allow certain senders to notify a set of receivers when some actions have taken place. They're particularly useful for performing actions when models are saved, deleted, or when other events occur in your Django application.
How Migrations Work
Understanding the internal mechanics of Django migrations helps you write better migrations, debug issues, and optimize database schema changes. This deep dive explores how Django tracks, generates, and applies migrations.