Skip to main content

Managing Database Migrations

To propagate changes made to your models into your database schema, you must detect the differences between your current model definitions and the existing migration state, generate migration files, and then execute those migrations against your database.

Detecting Model Changes

You can programmatically detect changes in your models by comparing the current state of your installed apps against the state recorded in your existing migrations using the MigrationAutodetector.

from django.apps import apps
from django.db import connections
from django.db.migrations.autodetector import MigrationAutodetector
from django.db.migrations.loader import MigrationLoader
from django.db.migrations.state import ProjectState
from django.db.migrations.questioner import MigrationQuestioner

# 1. Load existing migrations from disk and database
connection = connections['default']
loader = MigrationLoader(connection, ignore_no_migrations=True)

# 2. Get the "from" state (current migration graph state)
# and the "to" state (current models in code)
from_state = loader.project_state()
to_state = ProjectState.from_apps(apps)

# 3. Detect changes
autodetector = MigrationAutodetector(
from_state,
to_state,
questioner=MigrationQuestioner()
)

# 4. Generate the migration plan
changes = autodetector.changes(
graph=loader.graph,
trim_to_apps=None,
convert_apps=None,
migration_name="manual_update"
)

The MigrationAutodetector uses _detect_changes to identify operations like CreateModel, AddField, or RenameModel. It relies on deep_deconstruct to recursively compare field arguments and determine if an AlterField operation is necessary.

Executing Migrations

Once migrations are generated and saved to disk, use the MigrationExecutor to apply them to the database.

from django.db import connections
from django.db.migrations.executor import MigrationExecutor

connection = connections['default']
executor = MigrationExecutor(connection)

# Define the target: (app_label, migration_name)
# Use None for the migration name to unapply all migrations for an app
targets = [('myapp', '0002_added_field')]

# Check for inconsistent history before proceeding
executor.loader.check_consistent_history(connection)

# Create the execution plan
plan = executor.migration_plan(targets)

# Run the migrations
executor.migrate(targets, plan=plan, fake=False)

The MigrationExecutor handles the heavy lifting of:

  1. Planning: migration_plan determines which migrations need to be applied (forwards) or unapplied (backwards) to reach the target state.
  2. State Management: It uses mutate_state on Migration instances to track how the project state evolves as each operation runs.
  3. Recording: It uses a MigrationRecorder to update the django_migrations table after successful execution.

Defining a Migration Class

Migrations are defined as subclasses of django.db.migrations.migration.Migration. While usually generated by makemigrations, you can manually define them for data migrations or complex schema changes.

from django.db import migrations, models

class Migration(migrations.Migration):
# The first migration in an app usually has no dependencies
# or depends on other apps' initial migrations.
dependencies = [
('myapp', '0001_initial'),
]

# Operations are executed in order.
operations = [
migrations.AddField(
model_name='profile',
name='bio',
field=models.TextField(blank=True),
),
]

# Set to False if you want to manage transactions manually
# (only on supported backends).
atomic = True

Key Attributes

  • operations: A list of Operation instances (e.g., CreateModel, DeleteModel, AlterField).
  • dependencies: Tuples of (app_label, migration_name) that must be applied before this migration.
  • initial: A boolean indicating if this is the "initial" migration for an app. If True, MigrationExecutor.detect_soft_applied can skip it if the tables already exist (useful for --fake-initial).

Common Migration Tasks

Rolling Back Changes

To roll back to a specific migration, provide that migration as the target. To unapply all migrations for an app, use None as the migration name.

# Roll back 'myapp' to migration 0001
targets = [('myapp', '0001_initial')]
plan = executor.migration_plan(targets)
executor.migrate(targets, plan=plan)

Faking Migrations

If you have manually modified the database schema and want to mark migrations as applied without running the SQL, use the fake parameter.

# Mark migrations as applied without executing SQL
executor.migrate(targets, plan=plan, fake=True)

Handling Custom Migration Locations

If your migrations are not in the default migrations package, configure MIGRATION_MODULES in your settings. MigrationLoader.migrations_module checks this setting to locate migration files.

# settings.py
MIGRATION_MODULES = {
'myapp': 'custom_location.migrations_myapp',
}

Troubleshooting

Inconsistent Migration History

The MigrationLoader.check_consistent_history method ensures that no applied migration has an unapplied dependency. If this occurs, it raises an InconsistentMigrationHistory error. This often happens when a team member merges a migration that depends on a migration you haven't applied yet.

Circular Dependencies

MigrationAutodetector attempts to resolve dependencies automatically, but circular dependencies can occur, especially with swappable models like AUTH_USER_MODEL. The autodetector uses _resolve_dependency and TopologicalSorter in _sort_migrations to try and find a valid execution order. If it fails, it raises a ValueError.

Partially Applied Squashed Migrations

If you are using squashed migrations (migrations with the replaces attribute), MigrationLoader will only use the squashed migration if all replaced migrations are either fully applied or fully unapplied. If they are partially applied, the loader will revert to using the original migrations to avoid database corruption.