Mueva modelos entre aplicaciones Django (1.8) con las referencias de ForeignKey requeridas

Esta es una extensión de esta pregunta: cómo mover un modelo entre dos aplicaciones Django (Django 1.7)

Necesito mover un montón de modelos de old_app a new_app . La mejor respuesta parece ser la de Ozan , pero con las referencias de clave externa requeridas, las cosas son un poco más complicadas. @halfnibble presenta una solución en los comentarios a la respuesta de Ozan, pero todavía tengo problemas con el orden preciso de los pasos (por ejemplo, ¿cuándo copio los modelos a new_app , cuándo borro los modelos de old_app , qué migraciones se llevarán a cabo? en old_app.migrations vs. new_app.migrations , etc.)

¡Cualquier ayuda es muy apreciada!

Migración de un modelo entre aplicaciones.

La respuesta corta es, ¡no lo hagas!

Pero esa respuesta rara vez funciona en el mundo real de proyectos vivos y bases de datos de producción. Por lo tanto, he creado un ejemplo de repository de GitHub para demostrar este proceso bastante complicado.

Estoy usando MySQL. (No, esas no son mis verdaderas credenciales).

El problema

El ejemplo que estoy usando es un proyecto de fábrica con una aplicación de autos que inicialmente tiene un modelo de Car y un modelo de Tires .

 factory |_ cars |_ Car |_ Tires 

El modelo Car tiene una relación ForeignKey con Tires . (Como en, especifica los neumáticos a través del modelo de coche).

Sin embargo, pronto nos damos cuenta de que Tires va a ser un modelo grande con sus propios puntos de vista, etc., y por eso lo queremos en su propia aplicación. La estructura deseada es por lo tanto:

 factory |_ cars |_ Car |_ tires |_ Tires 

Y debemos mantener la relación de ForeignKey entre el Car y los Tires ya que demasiado depende de la preservación de los datos.

La solución

Paso 1. Configurar la aplicación inicial con mal diseño.

Navega por el código del paso 1.

Paso 2. Cree una interfaz de administración y agregue un montón de datos que contengan relaciones ForeignKey.

Ver el paso 2.

Paso 3. Decide mover el modelo de Tires a su propia aplicación. Cortar y pegar meticulosamente el código en la nueva aplicación de neumáticos. Asegúrese de actualizar el modelo de Car para que apunte a los nuevos tires.Tires . Modelo de tires.Tires .

Luego ejecute ./manage.py makemigrations y ./manage.py makemigrations copia de seguridad de la base de datos en algún lugar (en caso de que esto falle horriblemente).

Finalmente, ejecute ./manage.py migrate y vea el mensaje de error de Doom,

django.db.utils.IntegrityError: (1217, ‘No se puede eliminar o actualizar una fila principal: una restricción de clave externa falla’)

Ver código y migraciones hasta el momento en el paso 3.

Paso 4. La parte difícil. La migración generada automáticamente no puede ver que simplemente ha copiado un modelo a una aplicación diferente. Entonces, tenemos que hacer algunas cosas para remediar esto.

Puede seguir adelante y ver las migraciones finales con comentarios en el paso 4. Hice una prueba para verificar que funciona.

Primero, vamos a trabajar en cars . Tienes que hacer una nueva migración vacía. Esta migración realmente debe ejecutarse antes de la migración creada más recientemente (la que no se ejecutó). Por lo tanto, renumeré la migración que creé y cambié las dependencias para ejecutar primero mi migración personalizada y luego la última migración autogenerada para la aplicación de cars .

Puedes crear una migración vacía con:

 ./manage.py makemigrations --empty cars 

Paso 4.a. Realiza la migración old_app personalizada.

En esta primera migración personalizada, solo realizaré una migración de “base de datos”. Django le da la opción de dividir las operaciones de “estado” y “base de datos”. Puedes ver cómo se hace esto viendo el código aquí .

Mi objective en este primer paso es cambiar el nombre de las tablas de la base de datos de oldapp_model a newapp_model sin newapp_model el estado de Django. Tienes que descubrir cómo Django habría llamado tu tabla de base de datos según el nombre de la aplicación y el nombre del modelo.

Ahora está listo para modificar la migración inicial de tires .

Paso 4.b. Modificar la migración inicial de new_app.

Las operaciones están bien, pero solo queremos modificar el “estado” y no la base de datos. ¿Por qué? Porque estamos guardando las tablas de la base de datos de la aplicación cars . Además, debe asegurarse de que la migración personalizada realizada anteriormente sea una dependencia de esta migración. Ver el archivo de migración de neumáticos.

Entonces, ahora hemos cambiado el nombre de cars.Tires to tires.Tires en la base de datos, y cambiamos el estado de Django para reconocer la tabla de tires.Tires .

Paso 4.c. Modificar old_app última migración autogenerada.

Volviendo a los coches, necesitamos modificar esa última migración autogenerada. Debe requerir nuestra primera migración de automóviles personalizados y la migración inicial de neumáticos (que acabamos de modificar).

Aquí deberíamos dejar las operaciones de AlterField porque el modelo de Car apunta a un modelo diferente (aunque tenga los mismos datos). Sin embargo, debemos eliminar las líneas de migración relacionadas con DeleteModel porque el modelo de cars.Tires ya no existe. Se ha convertido completamente en tires.Tires . tires.Tires . Ver esta migración .

Paso 4.d. Limpia el modelo antiguo en old_app .

Por último, pero no menos importante, debe realizar una migración personalizada final en la aplicación de coches. Aquí, haremos una operación de “estado” solo para eliminar el modelo de cars.Tires . cars.Tires . Es solo para el estado porque la tabla de la base de datos para cars.Tires ya han sido renombrados. Esta última migración limpia el estado restante de Django.

Acabamos de mover dos modelos de old_app a new_app , pero las referencias FK estaban en algunos modelos de app_x y app_y , en lugar de modelos de old_app .

En este caso, siga los pasos proporcionados por Nostalg.io de la siguiente manera:

  • Mueva los modelos de old_app a new_app , luego actualice las declaraciones de import través de la base del código.
  • makemigrations
  • Siga el paso 4.a. Pero use AlterModelTable para todos los modelos movidos. Dos para mi
  • Siga el paso 4.b. como es.
  • Siga el paso 4.c. Pero también, para cada aplicación que tenga un archivo de migración recién generado, edítelos manualmente, de manera que migre las state_operations en state_operations lugar.
  • Siga el paso 4.d, pero use DeleteModel para todos los modelos movidos.

Notas:

  • Todos los archivos de migración generados automáticamente y editados de otras aplicaciones dependen del archivo de migración personalizado de old_app donde se usa AlterModelTable para cambiar el nombre de la tabla (s). (creado en el paso 4.a.)
  • En mi caso, tuve que eliminar el archivo de migración generado automáticamente de old_app porque no tenía ninguna operación de AlterField , solo las operaciones DeleteModel y RemoveField . O manténgalo con operations = [] vacías operations = []
  • Para evitar excepciones de migración al crear la base de datos de prueba desde cero, asegúrese de que la migración personalizada de old_app creó en el Paso 4.a. tiene todas las dependencias de migración anteriores de otras aplicaciones.

     old_app 0020_auto_others 0021_custom_rename_models.py dependencies: ('old_app', '0020_auto_others'), ('app_x', '0002_auto_20170608_1452'), ('app_y', '0005_auto_20170608_1452'), ('new_app', '0001_initial'), 0022_auto_maybe_empty_operations.py dependencies: ('old_app', '0021_custom_rename_models'), 0023_custom_clean_models.py dependencies: ('old_app', '0022_auto_maybe_empty_operations'), app_x 0001_initial.py 0002_auto_20170608_1452.py 0003_update_fk_state_operations.py dependencies ('app_x', '0002_auto_20170608_1452'), ('old_app', '0021_custom_rename_models'), app_y 0004_auto_others_that_could_use_old_refs.py 0005_auto_20170608_1452.py 0006_update_fk_state_operations.py dependencies ('app_y', '0005_auto_20170608_1452'), ('old_app', '0021_custom_rename_models'), 

Por cierto: hay un ticket abierto sobre esto: https://code.djangoproject.com/ticket/24686

En caso de que necesite mover el modelo y ya no tenga acceso a la aplicación (o no quiera acceder), puede crear una nueva Operación y considerar la posibilidad de crear un nuevo modelo solo si el modelo migrado no lo hace. existe.

En este ejemplo, estoy pasando ‘MyModel’ de old_app a myapp.

 class MigrateOrCreateTable(migrations.CreateModel): def __init__(self, source_table, dst_table, *args, **kwargs): super(MigrateOrCreateTable, self).__init__(*args, **kwargs) self.source_table = source_table self.dst_table = dst_table def database_forwards(self, app_label, schema_editor, from_state, to_state): table_exists = self.source_table in schema_editor.connection.introspection.table_names() if table_exists: with schema_editor.connection.cursor() as cursor: cursor.execute("RENAME TABLE {} TO {};".format(self.source_table, self.dst_table)) else: return super(MigrateOrCreateTable, self).database_forwards(app_label, schema_editor, from_state, to_state) class Migration(migrations.Migration): dependencies = [ ('myapp', '0002_some_migration'), ] operations = [ MigrateOrCreateTable( source_table='old_app_mymodel', dst_table='myapp_mymodel', name='MyModel', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=18)) ], ), ] 

Después de terminar el trabajo traté de hacer una nueva migración. Pero me encuentro con el siguiente error: ValueError: Unhandled pending operations for models: oldapp.modelname (referred to by fields: oldapp.HistoricalProductModelName.model_ref_obj)

Si su modelo de Django usando HistoricalRecords campo de registros HistoricalRecords no olvide agregar modelos / tablas adicionales mientras sigue la respuesta de @Nostalg.io.

Agregue el siguiente elemento a las operaciones de database_operations de database_operations en el primer paso (4.a):

  migrations.AlterModelTable('historicalmodelname', 'newapp_historicalmodelname'), 

y agregue Eliminar adicional en state_operations en el último paso (4.d):

  migrations.DeleteModel(name='HistoricalModleName'), 

Esto me funcionó, pero estoy seguro de que sabré por qué es una idea terrible. Agregue esta función y una operación que lo llame a su migración old_app:

 def migrate_model(apps, schema_editor): old_model = apps.get_model('old_app', 'MovingModel') new_model = apps.get_model('new_app', 'MovingModel') for mod in old_model.objects.all(): mod.__class__ = new_model mod.save() class Migration(migrations.Migration): dependencies = [ ('new_app', '0006_auto_20171027_0213'), ] operations = [ migrations.RunPython(migrate_model), migrations.DeleteModel( name='MovingModel', ), ] 

Paso 1: copia de seguridad de su base de datos!
Asegúrese de que su migración new_app se ejecute primero y / o un requisito de la migración old_app. Rechace la eliminación del tipo de contenido obsoleto hasta que haya completado la migración old_app.

después de Django 1.9 es posible que desee pasar un poco más con cuidado:
Migration1: Crear nueva tabla
Migration2: tabla de poblar
Migration3: Modificar los campos en otras tablas
Migration4: Borrar tabla antigua

La forma de Nostalg.io trabajó en los reenvíos (la generación automática de todas las demás aplicaciones FK que hacen referencia a ella). Pero necesitaba también al revés. Para esto, la AlterTable al revés tiene que suceder antes de que cualquier FK esté atrasada (en original sucedería después de eso). Así que para esto, divido AlterTable en 2 AlterTableF y AlterTableR separados, cada uno de ellos funciona solo en una dirección, luego uso uno delantero en lugar del original en la primera migración personalizada y uno inverso en la última migración de autos (ambos suceden en la aplicación de autos ). Algo como esto:

 #cars/migrations/0002...py : class AlterModelTableF( migrations.AlterModelTable): def database_backwards(self, app_label, schema_editor, from_state, to_state): print( 'nothing back on', app_label, self.name, self.table) class Migration(migrations.Migration): dependencies = [ ('cars', '0001_initial'), ] database_operations= [ AlterModelTableF( 'tires', 'tires_tires' ), ] operations = [ migrations.SeparateDatabaseAndState( database_operations= database_operations) ] #cars/migrations/0004...py : class AlterModelTableR( migrations.AlterModelTable): def database_forwards(self, app_label, schema_editor, from_state, to_state): print( 'nothing forw on', app_label, self.name, self.table) def database_backwards(self, app_label, schema_editor, from_state, to_state): super().database_forwards( app_label, schema_editor, from_state, to_state) class Migration(migrations.Migration): dependencies = [ ('cars', '0003_auto_20150603_0630'), ] # This needs to be a state-only operation because the database model was renamed, and no longer exists according to Django. state_operations = [ migrations.DeleteModel( name='Tires', ), ] database_operations= [ AlterModelTableR( 'tires', 'tires_tires' ), ] operations = [ # After this state operation, the Django DB state should match the actual database structure. migrations.SeparateDatabaseAndState( state_operations=state_operations, database_operations=database_operations) ] 

He creado un comando de administración para hacer precisamente eso: mover un modelo de una aplicación Django a otra – basado en las sugerencias de nostalgic.io en https://stackoverflow.com/a/30613732/1639699

Lo puedes encontrar en GitHub en alexei / django-move-model

Volviendo a esto después de un par de meses (después de implementar con éxito el enfoque de Lucianovici), me parece que se vuelve mucho más sencillo si cuida apuntar db_table a la tabla anterior (si solo le importa la organización del código y no mente nombres obsoletos en la base de datos).

  • No necesitará migraciones de AlterModelTable, por lo que no es necesario realizar el primer paso personalizado.
  • Aún necesita cambiar los modelos y las relaciones sin tocar la base de datos.

Entonces, lo que hice fue tomar las migraciones automáticas de Django y envolverlas en migraciones.SeparateDatabaseAndState.

Tenga en cuenta (nuevamente) que esto solo podría funcionar si se cuidó de apuntar db_table a la tabla anterior para cada modelo.

No estoy seguro de si hay algún problema con esto que no haya visto todavía, pero parece haber funcionado en mi sistema de desarrollo (que, por supuesto, me preocupé de hacer una copia de seguridad). Todos los datos se ven intactos. Voy a echar un vistazo más de cerca para comprobar si surge algún problema …

Tal vez también sea posible luego cambiar el nombre de las tablas de la base de datos en un paso separado, lo que hace que todo este proceso sea menos complicado.