Módulo 2 · Bases de datos

Lección 11 — Migraciones versionadas

Cambios de esquema sin caída: cómo evolucionar TicketFlow con datos reales dentro.

Publicada
En esta lección
  1. Objetivos
  2. 1. La historia como contrato
  3. 2. Expand-contract: cambiar sin caer
  4. 3. Migraciones de datos: en Python, no en SQL suelto
  5. 4. Los tres peligros (y su lint)
  6. 5. Aplicación: las mejoras de la 08
  7. Autoevaluación

Stack: Django 5 + PostgreSQL · Proyecto: TicketFlow Estado: Publicada Prerrequisito: Lección 10 — Transacciones


Objetivos

  1. Tratar las migraciones como historia versionada (nunca editar lo desplegado).
  2. Aplicar el patrón expand-contract para cambios sin caída.
  3. Detectar migraciones peligrosas antes de que tumben producción.
  4. Aplicar las 3 mejoras de la 08 a TicketFlow con migraciones seguras.

1. La historia como contrato

makemigrations genera ficheros versionados en el repo. Reglas del oficio:

  • Lo desplegado no se edita: si una migración ya corrió en producción, su corrección es una migración NUEVA (nunca retocar el fichero viejo).
  • Una migración, una intención: "añade field X" — no "remaquillado del sábado" (irreversible e inrevisable).
  • migrations.RunPython.noop en el reverse: toda migración de datos define su marcha atrás (aunque sea noop); sin reverse, no hay rollback del deploy.
  • Squash con criterio: al acumular cientos, squashmigrations condensa el histórico de una app — solo cuando las viejas ya no corren en ningún entorno vivo.

2. Expand-contract: cambiar sin caer

El deploy sin downtime exige que la versión vieja y la nueva convivan unos minutos (N pods desplegando en rollo). El patrón:

1. EXPAND    añade lo nuevo (columna nullable, tabla, índice) — compatible con el código viejo
2. DUAL WRITE / migrar datos  el código nuevo escribe viejo+nuevo; un job rellena el histórico
3. SWITCH    el código lee/escribe solo lo nuevo
4. CONTRACT  elimina lo viejo (columna/constraint) — cuando ningún pod corre el código viejo

Ejemplo real: renombrar events_event.name → title. Mal camino: rename_field directo (la versión vieja falla al no ver la columna). Camino expand-contract: añadir title, doble escritura, switch, luego RemoveField(name).

El detalle que tumba despliegues: añadir una columna NOT NULL sin default o crear un índice bloqueante. En PostgreSQL moderno: ADD COLUMN... DEFAULT... NOT NULL es metadato (rápido), pero ALTER COLUMN TYPE o una FK sin índice previo bloquean. Regla: CREATE INDEX CONCURRENTLY (no bloquea escrituras; no puede ir dentro de transacción → atomic = False en la migración).

3. Migraciones de datos: en Python, no en SQL suelto

python
def set_default_state(apps, schema_editor):
    Event = apps.get_model("events", "Event")
    Event.objects.filter(state__isnull=True).update(state=EventState.DRAFT)

class Migration(migrations.Migration):
    dependencies = [("events", "0004_previous")]
    operations = [
        migrations.RunPython(set_default_state, migrations.RunPython.noop),
    ]

apps.get_model (no el import directo): la versión histórica del modelo, no la actual. Lotes con .iterator() + paginación en tablas grandes (no cargar 2M de filas en memoria). Las de datos peligrosas (regex masivas, llamadas externas) → jamás: la migración es determinista y local.

4. Los tres peligros (y su lint)

CambioRiesgoCamino seguro
ADD COLUMN NOT NULL sin defaultbloqueo o fallo en filas viejasnullable → backfill → NOT NULL
DROP COLUMNcódigo viejo en vuelo la usaexpand-contract completo primero
ALTER COLUMN TYPErewrite de tabla, bloqueocolumna nueva + dual write + switch
CREATE INDEX (grande)bloquea escriturasCONCURRENTLY

Herramienta: django-migration-linter marca operaciones peligrosas en CI. Tu checklist de despliegue: lint en pipeline → migrate con --noinput tras backup → smoke test del health check (Lección 02) → rollback = revert de código + migraciones por diseño compatibles hacia atrás un paso.

5. Aplicación: las mejoras de la 08

Las tres mejoras que anotaste (NOT NULLs, CHECK de coherencia, slug único) se aplican expand-contract: cada una con su migración, backfill de datos, y lint verde. Los ejercicios te guían paso a paso.


Autoevaluación

  1. ¿Por qué editar una migración ya desplegada es sabotaje aunque "todavía nadie la corra en local"?
  2. Explica expand-contract con el renombrado name→title, paso a paso, y di qué rompe cada paso si te lo saltas.
  3. ¿Por qué CREATE INDEX CONCURRENTLY exige atomic = False y qué pierdes al salir de la transacción?
  4. ¿Por qué apps.get_model y no from events.models import Event en una RunPython?
  5. Tu linter marca RemoveField como peligroso. ¿Qué tiene de peligroso borrar una columna si el código nuevo ya no la usa?

Continúa con los ejercicios. Las soluciones solo tras intentarlo.