Módulo 4 · Lección 10 de 45
Migraciones
El historial de cómo ha cambiado tu base de datos, escrito en ficheros que se guardan con el código.
Cambiar un modelo es fácil: se edita una línea. Cambiar una tabla que ya tiene datos dentro, y que además existe en tu máquina, en la de dos compañeros y en el servidor, no lo es. Hay que aplicar el mismo cambio en los cuatro sitios, en el mismo orden, sin perder nada y sabiendo en todo momento cuáles van por dónde.
Las migraciones son la respuesta de Django a eso: cada cambio del modelo se convierte en un fichero de Python que describe la modificación, esos ficheros se guardan en el repositorio junto al código, y cada base de datos lleva la cuenta de cuáles ha aplicado. Es control de versiones para la forma de tus tablas.
Dos órdenes y por qué son dos#
python manage.py makemigrationsCompara tus modelos con el estado que describen las migraciones que ya hay, y escribe un fichero con las diferencias. No toca la base de datos. Ni siquiera necesita que haya una.
python manage.py migrateMira qué migraciones hay escritas, qué migraciones se han aplicado ya en esta base de datos, y ejecuta las que falten.
La separación es lo que hace que el sistema sirva para algo. El fichero que genera la primera es un artefacto que se guarda en el repositorio, se revisa como cualquier otro código y viaja con él. Cuando alguien se baja tu rama, no ejecuta makemigrations: ejecuta migrate y su base de datos acaba exactamente igual que la tuya, con los mismos pasos y en el mismo orden.
Cómo es un fichero de migración#
El de la agenda, generado al escribir el modelo:
# Generated by Django 6.1.1 on 2026-09-22 16:16
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Contacto',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True,
serialize=False, verbose_name='ID')),
('nombre', models.CharField(max_length=100, verbose_name='nombre')),
('correo', models.EmailField(max_length=254, unique=True, verbose_name='correo')),
('telefono', models.CharField(blank=True, max_length=20, verbose_name='teléfono')),
('grupo', models.CharField(default='sin', max_length=15, verbose_name='grupo')),
('notas', models.TextField(blank=True, verbose_name='notas')),
('creado', models.DateTimeField(auto_now_add=True, verbose_name='creado')),
],
),
]Es código Python normal, y se puede leer y editar. Tres partes:
dependencies dice qué migraciones tienen que estar aplicadas antes que esta. Aquí está vacía porque es la primera. Eso es lo que forma el grafo: las migraciones no son una lista ordenada por su número, son un grafo de dependencias, y el número del nombre es solo una comodidad para leerlas.
operations es lo que hay que hacer. Hay unas quince operaciones posibles —CreateModel, AddField, AlterField, RemoveField, RenameField, RunPython, RunSQL— y cada makemigrations produce las que correspondan.
initial = True marca la primera de la aplicación.
Fíjate en que el choices del campo grupo no aparece. Eso no es un olvido: choices no afecta a la base de datos, como se vio en la lección anterior, y Django lo omite para no generar migraciones inútiles cada vez que cambies una etiqueta.
Cómo sabe Django lo que falta#
En una tabla. Está en tu base de datos desde el primer migrate:
>>> import sqlite3
>>> c = sqlite3.connect('db.sqlite3')
>>> list(c.execute("SELECT app, name, applied FROM django_migrations WHERE app='contactos'"))
[('contactos', '0001_initial', '2026-09-22 16:22:43.017800')]Una fila por migración aplicada, con la fecha. Eso es todo el mecanismo: migrate compara los ficheros que hay en disco con las filas de esa tabla y ejecuta la diferencia.
La forma cómoda de verlo:
python manage.py showmigrations contactoscontactos
[X] 0001_initial
[X] 0002_contacto_empresa
[ ] 0003_normalizar_gruposLa cruz significa aplicada. Es lo primero que hay que mirar cuando una base de datos no tiene la forma que debería.
Añadir un campo#
Añade empresa al modelo y ejecuta makemigrations:
It is impossible to add a non-nullable field 'empresa' to contacto without specifying
a default. This is because the database needs something to populate existing rows.
Please select a fix:
1) Provide a one-off default now
2) Quit and manually define a default value in models.py.
Select an option:Esta pregunta desconcierta la primera vez y es completamente razonable. La tabla ya tiene dos filas. El campo nuevo es obligatorio. ¿Qué se pone en las filas que ya existen? Nadie más que tú puede contestarlo.
Hay tres salidas:
- Un valor de una vez, la opción 1. Django lo pone en las filas existentes y no lo deja en el modelo.
- Un
defaulten el modelo, la opción 2. Es lo mismo pero permanente, y conviene si el valor tiene sentido también para las filas futuras. blank=True, que es lo que suele querer decirse en un campo de texto. Entonces la cadena vacía es un valor válido y Django deja de preguntar.
Con blank=True, el fichero que sale:
class Migration(migrations.Migration):
dependencies = [
('contactos', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='contacto',
name='empresa',
field=models.CharField(blank=True, max_length=100, verbose_name='empresa'),
),
]Deshacer#
Se migra hacia atrás indicando hasta dónde:
python manage.py migrate contactos 0001Unapplying contactos.0002_contacto_empresa... OKDjango ejecuta las operaciones a la inversa: donde había un AddField, quita la columna. showmigrations pasa a enseñar la casilla vacía, y para volver adelante basta con migrate contactos.
Esto funciona bien y conviene saber lo que no recupera: los datos de una columna eliminada no vuelven. Deshacer un AddField borra la columna con lo que tuviera dentro; volver a aplicarla la crea vacía. La reversibilidad es de la estructura, no del contenido.
Y hay operaciones que no se pueden deshacer en absoluto, señaladas con IrreversibleError al intentarlo. Un RunSQL sin SQL inverso es una de ellas.
Migraciones de datos#
Todo lo anterior cambia la forma de las tablas. A veces hay que cambiar el contenido: pasar todos los grupos a minúsculas, partir un campo de nombre completo en dos, rellenar una columna nueva a partir de otra.
Eso también es una migración, y se crea vacía:
python manage.py makemigrations contactos --empty --name normalizar_gruposY se rellena con RunPython:
from django.db import migrations
def poner_en_minusculas(apps, schema_editor):
Contacto = apps.get_model('contactos', 'Contacto')
for contacto in Contacto.objects.all():
contacto.grupo = contacto.grupo.lower()
contacto.save(update_fields=['grupo'])
def volver_atras(apps, schema_editor):
Contacto = apps.get_model('contactos', 'Contacto')
for contacto in Contacto.objects.all():
contacto.grupo = contacto.grupo.capitalize()
contacto.save(update_fields=['grupo'])
class Migration(migrations.Migration):
dependencies = [
('contactos', '0002_contacto_empresa'),
]
operations = [
migrations.RunPython(poner_en_minusculas, volver_atras),
]Funciona en los dos sentidos:
antes: [('Ana Ruiz', 'TRABAJO'), ('Luis Pardo', 'familia')]
despues: [('Ana Ruiz', 'trabajo'), ('Luis Pardo', 'familia')]
tras deshacer:[('Ana Ruiz', 'Trabajo'), ('Luis Pardo', 'Familia')]La segunda función es opcional y conviene escribirla siempre que se pueda: es lo que hace la migración reversible. Cuando de verdad no hay vuelta atrás, se pone migrations.RunPython.noop, que deja claro que se ha pensado.
Los dos problemas de trabajar en equipo#
Dos migraciones con el mismo número. Tu compañera y tú añadís un campo cada uno en vuestra rama, las dos migraciones se llaman 0004_algo y dependen de la 0003. Al juntarlas:
CommandError: Conflicting migrations detected; multiple leaf nodes in the migration
graph: (0004_a, 0004_b in contactos).
To fix them run 'python manage.py makemigrations --merge'El mensaje trae la solución. --merge crea una migración vacía que depende de las dos y vuelve a dejar un solo final en el grafo. No hay que renumerar nada ni elegir cuál sobra: las dos se quedan.
Alguien ha cambiado un modelo y no ha generado la migración. Eso se detecta antes de que llegue a nadie, con una orden pensada para la integración continua:
python manage.py makemigrations --check --dry-runNo escribe nada y termina con código de error si faltan migraciones por generar. Es una línea en el guion de pruebas, y evita la situación de bajarse una rama y encontrarse la base de datos sin la columna que el código ya usa.