Cómo crear migraciones de base de datos

Este documento explica cómo estructurar y escribir migraciones de base de datos para diferentes escenarios que podrías encontrar. Para material introductorio sobre migraciones, consulta la guía de temas.

Migraciones de datos y bases de datos múltiples

Cuando se utilizan varias bases de datos, es posible que debas determinar si ejecutar una migración contra una base de datos en particular. Por ejemplo, podrías querer solo ejecutar una migración sobre una base de datos específica.

Para hacerlo puedes verificar el alias de la conexión a la base de datos dentro de una operación RunPython mirando la propiedad schema_editor.connection.alias:

from django.db import migrations


def forwards(apps, schema_editor):
    if schema_editor.connection.alias != "default":
        return
    # Your migration code goes here


class Migration(migrations.Migration):
    dependencies = [
        # Dependencies to other migrations
    ]

    operations = [
        migrations.RunPython(forwards),
    ]

También puedes proporcionar pistas que se pasarán al método allow_migrate() de los routers de bases de datos como **hints:

myapp/dbrouters.py
class MyRouter:
    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if "target_db" in hints:
            return db == hints["target_db"]
        return True

Then, para aprovechar esto en tus migraciones, haz lo siguiente:

from django.db import migrations


def forwards(apps, schema_editor):
    # Your migration code goes here
    ...


class Migration(migrations.Migration):
    dependencies = [
        # Dependencies to other migrations
    ]

    operations = [
        migrations.RunPython(forwards, hints={"target_db": "default"}),
    ]

Si tu operación RunPython o RunSQL solo afecta a un modelo, es buena práctica pasar model_name como una pista para hacerlo lo más transparente posible al router. Esto es especialmente importante para aplicaciones reutilizables y de terceros.

Migraciones que agregan campos únicos

Aplicar una migración «simple» que agrega un campo no nulo único a una tabla con filas existentes lanzará un error porque el valor utilizado para poblar las filas existentes se genera solo, lo que rompe la restricción de unicidad.

Por tanto, se deben seguir los siguientes pasos. En este ejemplo, agregaremos un campo no nulo UUIDField con un valor por defecto. Modifica el campo correspondiente según tus necesidades.

  • Agrega el campo en tu modelo con los argumentos default=uuid.uuid4 y unique=True (elige un valor por defecto adecuado para el tipo de campo que estás agregando).

  • Ejecuta la orden makemigrations. Esto debería generar una migración con una operación AddField.

  • Genera dos archivos de migración vacíos para la misma aplicación ejecutando makemigrations myapp --empty dos veces. Hemos renombrado los archivos de migración para darles nombres significativos en los ejemplos a continuación.

  • Copia la operación AddField de la migración auto-generada (el primer de los tres nuevos archivos) y pégala en la última migración, cambia AddField por AlterField, y agrega importaciones de uuid y models. Por ejemplo:

    0006_remove_uuid_null.py
    # Generated by Django A.B on YYYY-MM-DD HH:MM
    from django.db import migrations, models
    import uuid
    
    
    class Migration(migrations.Migration):
        dependencies = [
            ("myapp", "0005_populate_uuid_values"),
        ]
    
        operations = [
            migrations.AlterField(
                model_name="mymodel",
                name="uuid",
                field=models.UUIDField(default=uuid.uuid4, unique=True),
            ),
        ]
    
  • Edita el primer archivo de migración. La clase de la migración generada debería parecerse a esto:

    0004_add_uuid_field.py
    class Migration(migrations.Migration):
        dependencies = [
            ("myapp", "0003_auto_20150129_1705"),
        ]
    
        operations = [
            migrations.AddField(
                model_name="mymodel",
                name="uuid",
                field=models.UUIDField(default=uuid.uuid4, unique=True),
            ),
        ]
    

    Cambia unique=True a null=True – esto creará el campo nulo intermedio y diferirá la creación del constraint único hasta que hayamos poblado valores únicos en todas las filas.

  • En el primer archivo de migración vacío, agrega una operación RunPython o RunSQL para generar un valor único (UUID en el ejemplo) para cada fila existente. También agrega una importación de uuid. Por ejemplo:

    0005_populate_uuid_values.py
    # Generated by Django A.B on YYYY-MM-DD HH:MM
    from django.db import migrations
    import uuid
    
    
    def gen_uuid(apps, schema_editor):
        MyModel = apps.get_model("myapp", "MyModel")
        for row in MyModel.objects.all():
            row.uuid = uuid.uuid4()
            row.save(update_fields=["uuid"])
    
    
    class Migration(migrations.Migration):
        dependencies = [
            ("myapp", "0004_add_uuid_field"),
        ]
    
        operations = [
            # omit reverse_code=... if you don't want the migration to be reversible.
            migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop),
        ]
    
  • Ahora puedes aplicar las migraciones de manera habitual con el comando migrate.

    Ten en cuenta que hay un condición de carrera si permites crear objetos mientras esta migración está ejecutándose. Los objetos creados después del AddField y antes de RunPython tendrán sus UUID originales sobrescritas.

Migraciones no atómicas

En bases de datos que admiten transacciones DDL (SQLite y PostgreSQL), las migraciones se ejecutarán dentro de una transacción por defecto. Para casos de uso como realizar migraciones de datos en tablas grandes, es posible que desees evitar que una migración se ejecute dentro de una transacción estableciendo el atributo atomic a False:

from django.db import migrations


class Migration(migrations.Migration):
    atomic = False

Dentro de tal migración, todas las operaciones se ejecutarán sin una transacción. Es posible ejecutar partes de la migración dentro de una transacción utilizando atomic() o pasando atomic=True a ``RunPython””.

Los siguientes son ejemplos de migraciones no atómicas que actualizan una gran tabla en lotes más pequeños:

import uuid

from django.db import migrations, transaction


def gen_uuid(apps, schema_editor):
    MyModel = apps.get_model("myapp", "MyModel")
    while MyModel.objects.filter(uuid__isnull=True).exists():
        with transaction.atomic():
            for row in MyModel.objects.filter(uuid__isnull=True)[:1000]:
                row.uuid = uuid.uuid4()
                row.save()


class Migration(migrations.Migration):
    atomic = False

    operations = [
        migrations.RunPython(gen_uuid),
    ]

La atomic atributo no tiene efecto en bases de datos que no admiten transacciones DDL (por ejemplo, MySQL, Oracle). (El soporte a declaraciones DDL atónicas de MySQL se refiere a declaraciones individuales más que a múltiples declaraciones envueltas en una transacción que pueden ser revertidas.)

Controlar el orden de las migraciones

Django determina el orden en que deben aplicarse las migraciones no por el nombre del archivo de cada migración, sino construyendo un grafo utilizando dos propiedades de la clase Migration: dependencies y run_before.

Si has utilizado el comando makemigrations ya habrás visto en acción dependencies porque las migraciones auto-creadas tienen definido esto como parte de su proceso de creación.

La propiedad dependencies se declara de la siguiente manera:

from django.db import migrations


class Migration(migrations.Migration):
    dependencies = [
        ("myapp", "0123_the_previous_migration"),
    ]

Normalmente esto será suficiente, pero de vez en cuando podrías necesitar asegurarte de que tu migración se ejecute antes de otras migraciones. Esto es útil, por ejemplo, para hacer que las migraciones de aplicaciones terceras corran después de tu reemplazo AUTH_USER_MODEL.

Para lograr esto, coloca todas las migraciones que dependen de la tuya en el atributo run_before de tu clase Migration:

class Migration(migrations.Migration):
    ...

    run_before = [
        ("third_party_app", "0001_do_awesome"),
    ]

Preferir usar dependencies sobre run_before cuando sea posible. Solo debes utilizar run_before si es indeseable o impráctico especificar dependencies en la migración que deseas ejecutar después de la que estás escribiendo.

Migrar datos entre aplicaciones terceras

Puedes utilizar una migración de datos para mover los datos de una aplicación de terceros a otra.

Si planeas eliminar la antigua app más adelante, necesitarás establecer la propiedad dependencies según si la antigua app está instalada o no. De lo contrario, tendrás dependencias faltantes una vez que desinstales la antigua app. De manera similar, necesitarás capturar el error LookupError en la llamada a apps.get_model() que recupera modelos de la antigua app. Esta aproximación te permite desplegar tu proyecto en cualquier lugar sin tener que instalar y luego desinstalar la antigua app primero.

Aquí tienes un ejemplo de migración:

myapp/migrations/0124_move_old_app_to_new_app.py
from django.apps import apps as global_apps
from django.db import migrations


def forwards(apps, schema_editor):
    try:
        OldModel = apps.get_model("old_app", "OldModel")
    except LookupError:
        # The old app isn't installed.
        return

    NewModel = apps.get_model("new_app", "NewModel")
    NewModel.objects.bulk_create(
        NewModel(new_attribute=old_object.old_attribute)
        for old_object in OldModel.objects.all()
    )


class Migration(migrations.Migration):
    operations = [
        migrations.RunPython(forwards, migrations.RunPython.noop),
    ]
    dependencies = [
        ("myapp", "0123_the_previous_migration"),
        ("new_app", "0001_initial"),
    ]

    if global_apps.is_installed("old_app"):
        dependencies.append(("old_app", "0001_initial"))

Ten en cuenta también lo que quieres que suceda cuando se aplica nuevamente la migración. Podrías hacer nada (como en el ejemplo anterior) o eliminar algunos o todos los datos de la nueva aplicación. Ajusta el segundo argumento del operador RunPython según corresponda.

Cambiar un campo ManyToManyField para utilizar un modelo through

Si cambias un ManyToManyField para utilizar un modelo through, la migración predeterminada eliminará la tabla existente y creará una nueva, perdiendo las relaciones existentes. Para evitar esto, puedes utilizar SeparateDatabaseAndState para renombrar la tabla existente al nombre de la nueva tabla mientras le dices a la detección automática de migraciones que el nuevo modelo se ha creado. Puedes verificar el nombre de la tabla existente y el nombre del constraint mediante sqlmigrate o dbshell. Puedes verificar el nombre de la nueva tabla con la propiedad _meta.db_table del modelo through. Tu nuevo modelo through debe utilizar los mismos nombres para las claves foráneas que Django hizo. Además, si necesita campos adicionales, deben agregarse en operaciones después de SeparateDatabaseAndState.

Por ejemplo, si teníamos un modelo Book con un campo ManyToManyField que vincula a Author, podríamos agregar un modelo through AuthorBook con un nuevo campo is_primary, como se muestra a continuación:

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
    dependencies = [
        ("core", "0001_initial"),
    ]

    operations = [
        migrations.SeparateDatabaseAndState(
            database_operations=[
                # Old table name from checking with sqlmigrate, new table
                # name from AuthorBook._meta.db_table.
                migrations.RunSQL(
                    sql="ALTER TABLE core_book_authors RENAME TO core_authorbook",
                    reverse_sql="ALTER TABLE core_authorbook RENAME TO core_book_authors",
                ),
            ],
            state_operations=[
                migrations.CreateModel(
                    name="AuthorBook",
                    fields=[
                        (
                            "id",
                            models.AutoField(
                                auto_created=True,
                                primary_key=True,
                                serialize=False,
                                verbose_name="ID",
                            ),
                        ),
                        (
                            "author",
                            models.ForeignKey(
                                on_delete=django.db.models.deletion.DO_NOTHING,
                                to="core.Author",
                            ),
                        ),
                        (
                            "book",
                            models.ForeignKey(
                                on_delete=django.db.models.deletion.DO_NOTHING,
                                to="core.Book",
                            ),
                        ),
                    ],
                    options={
                        "constraints": [
                            models.UniqueConstraint(
                                fields=["author", "book"],
                                name="unique_author_book",
                            )
                        ],
                    },
                ),
                migrations.AlterField(
                    model_name="book",
                    name="authors",
                    field=models.ManyToManyField(
                        to="core.Author",
                        through="core.AuthorBook",
                    ),
                ),
            ],
        ),
        migrations.AddField(
            model_name="authorbook",
            name="is_primary",
            field=models.BooleanField(default=False),
        ),
    ]

Cambiar un modelo no administrado a administrado

Si quieres cambiar un modelo no administrado (managed=False) a administrado, debes eliminar managed=False y generar una migración antes de realizar otros cambios en la estructura de la base de datos relacionados con el modelo, ya que los cambios en la estructura que aparecen en la migración que contiene la operación para cambiar Meta.managed pueden no aplicarse.