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:
- Planning:
migration_plandetermines which migrations need to be applied (forwards) or unapplied (backwards) to reach the target state. - State Management: It uses
mutate_stateonMigrationinstances to track how the project state evolves as each operation runs. - Recording: It uses a
MigrationRecorderto update thedjango_migrationstable 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 ofOperationinstances (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. IfTrue,MigrationExecutor.detect_soft_appliedcan 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.