python - initial - django migrations
Carga de datos iniciales con Django 1.7 y migraciones de datos (7)
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),
]
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?
- Escriba el código de Python con múltiples llamadas a
mymodel.create(...)
, - 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.
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.
Inspirado por algunos de los comentarios (principalmente n__o''s) y el hecho de que tengo muchos archivos initial_data.*
en varias aplicaciones, decidí crear una aplicación Django que facilitaría la creación de estas migraciones de datos.
Usando django-migration-fixture puede simplemente ejecutar el siguiente comando de administración y buscará a través de todos sus archivos INSTALLED_APPS
para initial_data.*
Y los convertirá 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.
Ver django-migration-fixture para instrucciones de instalación / uso.
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í:
Cree un archivo de dispositivo en
<yourapp>/fixtures/initial_data.json
Crea tu migración vacía:
python manage.py makemigrations --empty <yourapp>
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.
Para dar 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 este método está en desuso.
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 este (003) y solo ejecutará las siguientes migraciones.
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
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
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 conapp_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).Ejecutarlo
python manage.py migrate <yourapp>