Cargando datos iniciales con Django 1.7 y migraciones de datos

95

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

Antes de 1.7, solía cargar los datos iniciales con un fixture/initial_data.jsonarchivo, que se cargaba con el python manage.py syncdbcomando (al crear la base de datos).

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

Si una aplicación usa migraciones, no hay carga automática de accesorios. Dado que se requerirán migraciones 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 ejemplo claro de cómo hacerlo, por eso mi pregunta es:

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

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

Prefiero la segunda opción.

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

Mickaël
fuente
3
Además, quiero agregar otra pregunta a la pregunta original del OP: ¿Cómo deberíamos hacer migraciones de datos para datos que no pertenecen a nuestras aplicaciones? Por ejemplo, si alguien está utilizando el marco de trabajo de los sitios, necesita tener una conexión con los datos de los sitios. Dado que el marco de los sitios no está relacionado con nuestras aplicaciones, ¿dónde deberíamos poner esa migración de datos? Gracias !
Serafeim
Un punto importante que todavía no ha sido abordado por nadie aquí es qué sucede cuando necesita agregar datos definidos en una migración de datos a una base de datos en la que ha falsificado migraciones. Dado que las migraciones fueron falsas, su migración de datos no se ejecutará y debe hacerlo a mano. En este punto, también puede llamar a loaddata en un archivo de accesorio.
hekevintran
Otro escenario interesante es lo que sucede si tiene una migración de datos para crear instancias de auth.Group, por ejemplo, y más adelante tiene un nuevo grupo que desea crear como datos semilla. Deberá crear una nueva migración de datos. Esto puede resultar molesto porque los datos de inicialización de su grupo estarán en varios archivos. Además, en el caso de que desee restablecer las migraciones, tendrá que revisar para encontrar las migraciones de datos que configuran los datos semilla y también los transfieren.
hekevintran
@Serafeim La pregunta "Dónde poner los datos iniciales para una aplicación de terceros" no cambia si usa una migración de datos en lugar de accesorios, ya que solo cambia la forma en que se cargan los datos. Utilizo una pequeña aplicación personalizada para cosas como esta. Si la aplicación de terceros se llama "foo", llamo a mi aplicación simple que contiene la migración de datos / dispositivo "foo_integration".
guettli
@guettli sí, probablemente usar una aplicación adicional sea la mejor manera de hacerlo.
Serafeim

Respuestas:

81

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


Suponiendo que tiene un archivo de accesorio 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 Django ' loaddata(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(según la 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 desea utilizar un directorio personalizado.

    2.3. Más simple: llamar loaddatacon app_labelcargará los dispositivos desde el directorio del <yourapp>' fixturesautomá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 lo especifica app_label, loaddata intentará cargar el fixturenombre de archivo de todos los directorios de accesorios de las aplicaciones (lo que probablemente no desee).

  3. Ejecutarlo

    python manage.py migrate <yourapp>
No
fuente
1
ok, tienes razón ... También las llamadas loaddata('loaddata', fixture_filename, app_label='<yourapp>')también irán directamente al directorio de dispositivos de la aplicación (por lo tanto, no es necesario crear la ruta completa del dispositivo)
n__o
15
Con ese método, el serializador funcionará en el estado de los modelos de los models.pyarchivos actuales , que pueden tener algunos campos adicionales o algunos otros cambios. Si se realizaron algunos cambios después de crear la migración, fallará (por lo que ni siquiera podemos crear migraciones de esquemas después de esa migración). Para solucionarlo, podemos cambiar temporalmente el registro de aplicaciones en el que está trabajando el serializador en el registro proporcionado a la función de migración en el primer parámetro. El registro a la ruta se encuentra en django.core.serializers.python.apps.
GwynBleidD
3
¿Por qué estamos haciendo esto? ¿Por qué Django se vuelve cada vez más difícil de ejecutar y mantener? No quiero pasar por esto, quiero una interfaz de línea de comandos simple que me resuelva este problema, es decir, como solía ser con los dispositivos. Se supone que Django hace estas cosas más fáciles, no más difíciles :(
CpILL
1
@GwynBleidD Este es un punto muy importante que está haciendo, y creo que debería aparecer en esta respuesta aceptada. Es el mismo comentario que aparece como comentario en el ejemplo de código de migración de datos de la documentación . ¿Conoce otra forma de usar serializadores con lo provisto app registry, sin cambiar una variable global (que podría causar problemas en un futuro hipotético con migraciones de bases de datos paralelas)?
Anuncio N
3
Esta respuesta a kazoo junto con la aceptación es exactamente la razón por la que recomiendo a la gente no usar stackoverflow. Incluso ahora con los comentarios y anécdotas, todavía tengo gente en #django refiriéndose a esto.
shangxiao
50

Version corta

NO debe usar el loaddatacomando de administración 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

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

Por ejemplo, suponga que hay una migración de datos que utiliza loaddataun comando de administración para cargar datos desde un dispositivo y ya está aplicado en su entorno de desarrollo.

Más tarde, decide agregar un nuevo campo obligatorio al modelo correspondiente, así que lo hace y realiza una nueva migración contra su modelo actualizado (y posiblemente proporcione un valor único al nuevo campo cuando se le ./manage.py makemigrationssolicite).

Ejecuta 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 que ejecute todas las migraciones desde cero en el entorno de producción.

Sin embargo, la migración de datos falla . Eso es porque el modelo deserializado de loaddatacomando, que representa el código actual, no se puede guardar con datos vacíos para el nuevo campo obligatorio que agregó. ¡El aparato original carece de los datos necesarios para ello!

Pero incluso si actualiza el dispositivo con los datos necesarios 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, elloaddatacomando introduce una posible inconsistencia entre el modelo y la base de datos. Definitivamente NO debeusarlo directamente en una migración de datos.

La solución

loaddataEl comando se basa en la django.core.serializers.python._get_modelfunción para obtener el modelo correspondiente de un dispositivo, que devolverá la versión más actualizada de un modelo. Necesitamos parchearlo para que obtenga 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. However, here it
    # has a different context, specifically, the apps variable.
    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),
    ]
Rockallita
fuente
1
Rockallite, haces un punto muy fuerte. Sin embargo, su respuesta me dejó preguntándome, ¿la solución 2.1 de la respuesta de @ n__o / @ mlissner que se basa en objects = serializers.deserialize('json', fixture, ignorenonexistent=True)sufriría el mismo problema que loaddata? ¿O ignorenonexistent=Truecubre todos los problemas posibles?
Dário
7
Si observa la fuente , encontrará que el ignorenonexistent=Trueargumento tiene dos efectos: 1) ignora los modelos de un dispositivo que no están en las definiciones de modelo más actuales, 2) ignora los campos de un modelo de un dispositivo que no están en la definición de modelo correspondiente más actual. Ninguno de ellos maneja la situación del nuevo campo requerido en el modelo . Entonces, sí, creo que sufre el mismo problema que el simple loaddata.
Rockallite
Esto funcionó muy bien una vez que descubrí que mi antiguo json tenía modelos referenciados a otros modelos usando a natural_key(), que este método no parece admitir; simplemente reemplacé el valor natural_key con la identificación real del modelo referenciado.
dsummersl
1
Probablemente esta respuesta como respuesta aceptada sería más útil, porque al ejecutar casos de prueba se crea una nueva base de datos y todas las migraciones se aplican desde cero. Esta solución soluciona los problemas que enfrentará un proyecto con unittest en caso de no reemplazar _get_model en la migración de datos. Tnx
Mohammad ali baghershemirani
Gracias por la actualización y las explicaciones, @Rockallite. Mi respuesta inicial se publicó unas semanas después de que se introdujeran las migraciones en Django 1.7, y la documentación sobre cómo proceder no estaba clara (y aún lo está, la última vez que verifiqué). Con suerte, Django actualizará su mecanismo de migración / datos de carga para tener en cuenta el historial del modelo algún día.
n__o
6

Inspirado por algunos de los comentarios (es decir, n__o) y el hecho de que tengo muchos initial_data.*archivos repartidos en varias aplicaciones, decidí crear una aplicación Django que facilitaría la creación de estas migraciones de datos.

Usando Django-migración-accesorio simplemente puede ejecutar el siguiente comando de gestión y se buscará a través de toda su INSTALLED_APPSde initial_data.*archivos y convertirlos en migraciones de datos.

./manage.py create_initial_data_fixtures
Migrations for 'eggs':
  0002_auto_20150107_0817.py:
Migrations for 'sausage':
  Ignoring 'initial_data.yaml' - migration already exists.
Migrations for 'foo':
  Ignoring 'initial_data.yaml' - not migrated.

Consulte django-migration-fixture para obtener instrucciones de instalación / uso.

alexhayes
fuente
2

Para darle a su base de datos algunos datos iniciales, escriba una migración de datos. En la migración de datos, use la función RunPython para cargar sus datos.

No escriba ningún comando loaddata, ya que esta forma está obsoleta.

Sus migraciones de datos se ejecutarán solo una vez. Las migraciones son una secuencia ordenada de migraciones. Cuando se ejecutan las migraciones 003_xxxx.py, django migrations escribe en la base de datos que esta aplicación se migra hasta esta (003), y solo ejecutará las siguientes migraciones.

FlogFR
fuente
Entonces, ¿me anima a repetir llamadas a myModel.create(...)(o usar un bucle) en la función RunPython?
Mickaël
casi sí. Las bases de datos transaccionales lo manejarán perfectamente :)
FlogFR
1

Lamentablemente, las soluciones presentadas anteriormente no funcionaron para mí. Descubrí que cada vez que cambio mis modelos tengo que actualizar mis luminarias. Idealmente, escribiría migraciones de datos para modificar los datos creados y los datos cargados de dispositivos de manera similar.

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

Leifdenby
fuente
¡Gracias por esto! Escribí una versión que funciona con Python 3 (y pasa nuestro estricto Pylint). Puedes usarlo como fábrica con RunPython(load_fixture('badger', 'stoat')). gist.github.com/danni/1b2a0078e998ac080111
Danielle Madeley
1

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

En su lugar, escribiré un archivo Python para proporcionar la configuración inicial. Si necesitas algo más, te sugiero que mires al chico de la fábrica .

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

También hay "Grabe sus dispositivos, use fábricas de modelos" sobre el uso de dispositivos.

Griffosx
fuente
1
Estoy de acuerdo con su punto "difícil de mantener si hay cambios frecuentes", pero aquí el dispositivo solo tiene como objetivo proporcionar datos iniciales (y mínimos) al instalar el proyecto ...
Mickaël
1
Esto es para una carga de datos única, que si se hace dentro del contexto de las migraciones tiene sentido. Dado que si está dentro de una migración, no debería tener que realizar cambios en los datos json. Cualquier cambio de esquema que requiera cambios en los datos más adelante debe manejarse a través de otra migración (en ese punto, quizás otros datos en la base de datos también necesiten ser modificados).
mtnpaul
0

En Django 2.1, quería cargar algunos modelos (como nombres de países, por ejemplo) con datos iniciales.

Pero quería que esto sucediera automáticamente justo después de la ejecución de las migraciones iniciales.

Así que pensé que sería genial tener una sql/carpeta dentro de cada aplicación que requiriera que se carguen los datos iniciales.

Luego, dentro de esa sql/carpeta, tendría .sqlarchivos con los DML requeridos para cargar los datos iniciales en los modelos correspondientes, por ejemplo:

INSERT INTO appName_modelName(fieldName)
VALUES
    ("country 1"),
    ("country 2"),
    ("country 3"),
    ("country 4");

Para ser más descriptivo, así es como sql/se vería una aplicación que contiene una carpeta: ingrese la descripción de la imagen aquí

También encontré algunos casos en los que necesitaba que los sqlscripts se ejecutaran en un orden específico. Así que decidí prefijar los nombres de los archivos con un número consecutivo como se ve en la imagen de arriba.

Entonces necesitaba una forma de cargar cualquier SQLsdisponible dentro de cualquier carpeta de aplicación automáticamente al hacerlo python manage.py migrate.

Así que creé otra aplicación llamada initial_data_migrationsy luego añadí esta aplicación a la lista de INSTALLED_APPSen settings.pyarchivo. Luego creé una migrationscarpeta dentro y agregué un archivo llamado run_sql_scripts.py( que en realidad es una migración personalizada ). Como se ve en la siguiente imagen:

ingrese la descripción de la imagen aquí

Creé run_sql_scripts.pypara que se encargue de ejecutar todos los sqlscripts disponibles dentro de cada aplicación. Este luego es despedido cuando alguien corre python manage.py migrate. Esta costumbre migrationtambién agrega las aplicaciones involucradas como dependencias, de esa manera intenta ejecutar las sqldeclaraciones solo después de que las aplicaciones requeridas hayan ejecutado sus 0001_initial.pymigraciones (no queremos intentar ejecutar una declaración SQL contra una tabla que no existe).

Aquí está la fuente de ese script:

import os
import itertools

from django.db import migrations
from YourDjangoProjectName.settings import BASE_DIR, INSTALLED_APPS

SQL_FOLDER = "/sql/"

APP_SQL_FOLDERS = [
    (os.path.join(BASE_DIR, app + SQL_FOLDER), app) for app in INSTALLED_APPS
    if os.path.isdir(os.path.join(BASE_DIR, app + SQL_FOLDER))
]

SQL_FILES = [
    sorted([path + file for file in os.listdir(path) if file.lower().endswith('.sql')])
    for path, app in APP_SQL_FOLDERS
]


def load_file(path):
    with open(path, 'r') as f:
        return f.read()


class Migration(migrations.Migration):

    dependencies = [
        (app, '__first__') for path, app in APP_SQL_FOLDERS
    ]

    operations = [
        migrations.RunSQL(load_file(f)) for f in list(itertools.chain.from_iterable(SQL_FILES))
    ]

Espero que alguien lo encuentre útil, ¡funcionó bien para mí !. Si tiene alguna pregunta, por favor hágamelo saber.

NOTA: Puede que esta no sea la mejor solución ya que recién estoy comenzando con django, sin embargo, todavía quería compartir este "Cómo" con todos ustedes ya que no encontré mucha información mientras buscaba en Google sobre esto.

Antony Fuentes Artavia
fuente