Stack: Django 5 + PostgreSQL · Proyecto: TicketFlow Estado: Publicada Prerrequisito: Lección 10 — Transacciones
Objetivos
- Tratar las migraciones como historia versionada (nunca editar lo desplegado).
- Aplicar el patrón expand-contract para cambios sin caída.
- Detectar migraciones peligrosas antes de que tumben producción.
- 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.noopen 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,
squashmigrationscondensa 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 viejoEjemplo 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
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)
| Cambio | Riesgo | Camino seguro |
|---|---|---|
| ADD COLUMN NOT NULL sin default | bloqueo o fallo en filas viejas | nullable → backfill → NOT NULL |
| DROP COLUMN | código viejo en vuelo la usa | expand-contract completo primero |
| ALTER COLUMN TYPE | rewrite de tabla, bloqueo | columna nueva + dual write + switch |
| CREATE INDEX (grande) | bloquea escrituras | CONCURRENTLY |
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
- ¿Por qué editar una migración ya desplegada es sabotaje aunque "todavía nadie la corra en local"?
- Explica expand-contract con el renombrado name→title, paso a paso, y di qué rompe cada paso si te lo saltas.
- ¿Por qué
CREATE INDEX CONCURRENTLYexigeatomic = Falsey qué pierdes al salir de la transacción? - ¿Por qué
apps.get_modely nofrom events.models import Eventen una RunPython? - Tu linter marca
RemoveFieldcomo 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.