python - migrations - fixture dirs django




Carga de datos iniciales con Django 1.7 y migraciones de datos (5)

Recientemente cambié de Django 1.6 a 1.7 y comencé a usar migraciones (nunca usé el sur).

Antes de 1.7, solía cargar datos iniciales con un archivo fixture/initial_data.json , que se cargó con el comando python manage.py syncdb (al crear la base de datos).

Ahora, comencé a usar migraciones, y este comportamiento está en desuso:

Si una aplicación utiliza migraciones, no hay carga automática de accesorios. Dado que las migraciones serán necesarias para las aplicaciones en Django 2.0, este comportamiento se considera obsoleto. Si desea cargar datos iniciales para una aplicación, considere hacerlo en una migración de datos. ( https://docs.djangoproject.com/en/1.7/howto/initial-data/#automatically-loading-initial-data-fixtures )

La documentación oficial no tiene un claro ejemplo de cómo hacerlo, por lo que mi pregunta es:

¿Cuál es la mejor manera de importar dichos datos iniciales utilizando migraciones de datos?

  1. Escriba el código de Python con múltiples llamadas a mymodel.create(...) ,
  2. Use o escriba una función Django ( como llamar datos de carga) para cargar datos desde un archivo de dispositivo JSON.

Yo prefiero la segunda opción.

No quiero usar South, ya que Django parece ser capaz de hacerlo de forma nativa ahora.


Version corta

NO debe usar el loaddata administración de loaddata datos directamente en una migración de datos.

# Bad example for a data migration
from django.db import migrations
from django.core.management import call_command


def load_fixture(apps, schema_editor):
    # No, it's wrong. DON'T DO THIS!
    call_command('loaddata', 'your_data.json', app_label='yourapp')


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

    operations = [
        migrations.RunPython(load_fixture),
    ]

Versión larga

loaddata utiliza django.core.serializers.python.Deserializer que utiliza los modelos más actualizados para deserializar datos históricos en una migración. Ese es un comportamiento incorrecto.

Por ejemplo, se supone que hay una migración de datos que utiliza el loaddata administración de datos de carga para cargar datos desde un dispositivo, y ya se aplica en su entorno de desarrollo.

Más tarde, decide agregar un nuevo campo requerido al modelo correspondiente, por lo que lo hace y realiza una nueva migración contra su modelo actualizado (y posiblemente proporcione un valor único para el nuevo campo cuando ./manage.py makemigrations solicite )

Ejecutas la siguiente migración, y todo está bien.

Finalmente, ha terminado de desarrollar su aplicación Django y la implementa en el servidor de producción. Ahora es el momento de ejecutar todas las migraciones desde cero en el entorno de producción.

Sin embargo, la migración de datos falla . Esto se debe a que el modelo deserializado del comando loaddata , que representa el código actual, no se puede guardar con datos vacíos para el nuevo campo requerido que ha agregado. ¡El accesorio original carece de los datos necesarios para ello!

Pero incluso si actualiza el dispositivo con los datos requeridos para el nuevo campo, la migración de datos aún falla . Cuando se está ejecutando la migración de datos, la siguiente migración que agrega la columna correspondiente a la base de datos, aún no se aplica. ¡No puede guardar datos en una columna que no existe!

Conclusión: en una migración de datos, el comando loaddata introduce una posible incoherencia entre el modelo y la base de datos. Definitivamente NO deberías usarlo directamente en una migración de datos.

La solución

loaddata comando loaddata basa en la función django.core.serializers.python._get_model para obtener el modelo correspondiente de un dispositivo, que devolverá la versión más actualizada de un modelo. Necesitamos aplicar un parche de mono para obtener el modelo histórico.

(El siguiente código funciona para Django 1.8.x)

# Good example for a data migration
from django.db import migrations
from django.core.serializers import base, python
from django.core.management import call_command


def load_fixture(apps, schema_editor):
    # Save the old _get_model() function
    old_get_model = python._get_model

    # Define new _get_model() function here, which utilizes the apps argument to
    # get the historical version of a model. This piece of code is directly stolen
    # from django.core.serializers.python._get_model, unchanged.
    def _get_model(model_identifier):
        try:
            return apps.get_model(model_identifier)
        except (LookupError, TypeError):
            raise base.DeserializationError("Invalid model identifier: '%s'" % model_identifier)

    # Replace the _get_model() function on the module, so loaddata can utilize it.
    python._get_model = _get_model

    try:
        # Call loaddata command
        call_command('loaddata', 'your_data.json', app_label='yourapp')
    finally:
        # Restore old _get_model() function
        python._get_model = old_get_model


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

    operations = [
        migrations.RunPython(load_fixture),
    ]

En mi opinión, los accesorios son un poco malos. Si su base de datos cambia con frecuencia, mantenerlos actualizados pronto será una pesadilla. En realidad, no es solo mi opinión, en el libro "Two Scoops of Django" se explica mucho mejor.

En cambio, escribiré un archivo de Python para proporcionar la configuración inicial. Si necesitas algo más, te sugiero que mires Factory Boy .

Si necesita migrar algunos datos, debe usar las migraciones de datos .

También hay "Grabar sus accesorios, utilizar fábricas de modelos" sobre el uso de accesorios.


La mejor forma de cargar datos iniciales en aplicaciones migradas es a través de migraciones de datos (como también se recomienda en los documentos). La ventaja es que el dispositivo se carga tanto en las pruebas como en la producción.

@n__o sugirió volver a loaddata comando loaddata en la migración. En mis pruebas, sin embargo, llamar directamente al comando loaddata también funciona bien. Todo el proceso es así:

  1. Cree un archivo de dispositivo en <yourapp>/fixtures/initial_data.json

  2. Crea tu migración vacía:

    python manage.py makemigrations --empty <yourapp>
    
  3. Edite su archivo de migración /migrations/0002_auto_xxx.py

    from django.db import migrations
    from django.core.management import call_command
    
    
    def loadfixture(apps, schema_editor):
        call_command('loaddata', 'initial_data.json')
    
    
    class Migration(migrations.Migration):
    
        dependencies = [
            ('<yourapp>', '0001_initial'),
        ]
    
        operations = [
            migrations.RunPython(loadfixture),
        ]
    

Las soluciones presentadas anteriormente no me funcionaron desafortunadamente. Descubrí que cada vez que cambio mis modelos tengo que actualizar mis accesorios. Idealmente, en cambio, escribiría migraciones de datos para modificar los datos creados y los datos cargados en el dispositivo de manera similar.

Para facilitar esto , escribí una función rápida que se verá en el directorio de fixtures de la aplicación actual y cargará un dispositivo. Ponga esta función en una migración en el punto del historial del modelo que coincida con los campos de la migración.


Actualización : consulte el comentario de @GwynBleidD a continuación para conocer los problemas que puede causar esta solución, y vea la respuesta de @ Rockallite a continuación para obtener un enfoque que sea más duradero para los futuros cambios en el modelo.

Suponiendo que tiene un archivo de dispositivo en <yourapp>/fixtures/initial_data.json

  1. Crea tu migración vacía:

    En Django 1.7:

    python manage.py makemigrations --empty <yourapp>
    

    En Django 1.8+, puede proporcionar un nombre:

    python manage.py makemigrations --empty <yourapp> --name load_intial_data
    
  2. Edite su archivo de migración <yourapp>/migrations/0002_auto_xxx.py

    2.1. Implementación personalizada, inspirada en los datos de loaddata Django (respuesta inicial):

    import os
    from sys import path
    from django.core import serializers
    
    fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fixtures'))
    fixture_filename = 'initial_data.json'
    
    def load_fixture(apps, schema_editor):
        fixture_file = os.path.join(fixture_dir, fixture_filename)
    
        fixture = open(fixture_file, 'rb')
        objects = serializers.deserialize('json', fixture, ignorenonexistent=True)
        for obj in objects:
            obj.save()
        fixture.close()
    
    def unload_fixture(apps, schema_editor):
        "Brutally deleting all entries for this model..."
    
        MyModel = apps.get_model("yourapp", "ModelName")
        MyModel.objects.all().delete()
    
    class Migration(migrations.Migration):  
    
        dependencies = [
            ('yourapp', '0001_initial'),
        ]
    
        operations = [
            migrations.RunPython(load_fixture, reverse_code=unload_fixture),
        ]
    

    2.2. Una solución más simple para load_fixture (por sugerencia de @ juliocesar):

    from django.core.management import call_command
    
    fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fixtures'))
    fixture_filename = 'initial_data.json'
    
    def load_fixture(apps, schema_editor):
        fixture_file = os.path.join(fixture_dir, fixture_filename)
        call_command('loaddata', fixture_file) 
    

    Útil si quieres usar un directorio personalizado.

    2.3. Lo más simple: llamar a app_label carga con app_label cargará accesorios del <yourapp> de dispositivos <yourapp> automáticamente:

    from django.core.management import call_command
    
    fixture = 'initial_data'
    
    def load_fixture(apps, schema_editor):
        call_command('loaddata', fixture, app_label='yourapp') 
    

    Si no especifica app_label , loaddata intentará cargar el nombre del archivo de todos los directorios de accesorios de aplicaciones (que probablemente no desee).

  3. Ejecutarlo

    python manage.py migrate <yourapp>
    




data-migration