web-dev-qa-db-fra.com

Déplacement de modèles entre des applications Django (1.8) avec les références ForeignKey requises

Voici une extension de cette question: Comment déplacer un modèle entre deux applications Django (Django 1.7)

Je dois déplacer plusieurs modèles de old_app à new_app. La meilleure réponse semble être celle de de Ozan , mais avec les références de clé étrangère requises, les choses sont un peu plus compliquées. @halfnibble présente une solution dans les commentaires à la réponse d'Ozan, mais je continue à avoir des problèmes avec l'ordre précis des étapes (par exemple, quand est-ce que je copie les modèles dans new_app, quand est-ce que je supprime-t-il les modèles de old_app? dans old_app.migrations vs. new_app.migrations, etc.) 

Toute aide est très appréciée! 

27
Shreyas

Migration d'un modèle entre les applications.

La réponse courte est: ne le fais pas !!

Mais cette réponse fonctionne rarement dans le monde réel des projets vivants et des bases de données de production. Par conséquent, j'ai créé un exemple GitHub repo pour illustrer ce processus plutôt compliqué.

J'utilise MySQL. (Non, ce ne sont pas mes vraies références).

Le problème

L'exemple que j'utilise est un projet d'usine avec un cars app qui a initialement un modèle Car et un modèle Tires

factory
  |_ cars
    |_ Car
    |_ Tires

Le modèle Car a une relation dans ForeignKey avec Tires. (Comme dans, vous spécifiez les pneus via le modèle de voiture). 

Cependant, nous réalisons rapidement que Tires sera un grand modèle avec ses propres vues, etc., et nous le voulons donc dans sa propre application. La structure souhaitée est donc:

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

Et nous devons conserver la relation de ForeignKey entre Car et Tires car trop dépend de la préservation des données.

La solution

Étape 1. Installez l'application initiale avec un mauvais design.

Parcourez le code de étape 1.

Étape 2. Créez une interface d'administration et ajoutez un tas de données contenant des relations ForeignKey. 

Voir étape 2.

Étape 3. Décide de déplacer le modèle Tires vers sa propre application. Couper et coller méticuleusement le code dans la nouvelle application pour pneus. Assurez-vous de mettre à jour le modèle Car pour qu'il pointe vers le nouveau modèle tires.Tires.

Ensuite, lancez ./manage.py makemigrations et sauvegardez la base de données quelque part (juste au cas où cela échouerait horriblement).

Enfin, lancez ./manage.py migrate et voyez le message d'erreur de Doom,

Django.db.utils.IntegrityError: (1217, 'Impossible de supprimer ou de mettre à jour une ligne parente: une contrainte de clé étrangère échoue')

Affichez le code et les migrations jusqu'à présent à la étape 3.

Étape 4. La partie la plus délicate. La migration générée automatiquement ne montre pas que vous avez simplement copié un modèle dans une autre application. Nous devons donc faire certaines choses pour remédier à cela.

Vous pouvez suivre et afficher les dernières migrations avec des commentaires dans étape 4. J'ai testé cela pour vérifier qu'il fonctionnait bien. 

Premièrement, nous allons travailler sur cars. Vous devez effectuer une nouvelle migration vide. Cette migration doit en réalité être exécutée avant la dernière migration créée (celle dont l'exécution a échoué). Par conséquent, j'ai renuméroté la migration créée et modifié les dépendances afin d'exécuter d'abord la migration personnalisée, puis la dernière migration générée automatiquement pour l'application cars.

Vous pouvez créer une migration vide avec:

./manage.py makemigrations --empty cars

Étape 4.a. Créez une migration personnalisée old_app.

Dans cette première migration personnalisée, je vais seulement effectuer une migration "database_operations". Django vous offre la possibilité de scinder les opérations "d'état" et "de base de données". Vous pouvez voir comment cela se fait en consultant le code ici .

Mon objectif dans cette première étape est de renommer les tables de la base de données de oldapp_model en newapp_model sans modifier l'état de Django. Vous devez déterminer comment Django aurait nommé votre table de base de données en fonction du nom de l'application et du nom du modèle. 

Vous êtes maintenant prêt à modifier la migration tires initiale.

Étape 4.b. Modify new_app migration initiale

Les opérations sont correctes, mais nous souhaitons seulement modifier "l'état" et non la base de données. Pourquoi? Parce que nous conservons les tables de base de données de l'application cars. En outre, vous devez vous assurer que la migration personnalisée précédemment créée est une dépendance de cette migration. Voir le fichier pneus migration .

Nous avons donc renommé cars.Tires en tires.Tires dans la base de données et modifié l'état Django pour qu'il reconnaisse la table tires.Tires

Étape 4.c. Modify old_app dernière migration générée automatiquement.

Pour aller retour aux voitures, nous devons modifier cette dernière migration générée automatiquement. Cela devrait nécessiter notre première migration de voitures personnalisées, ainsi que la migration initiale des pneus (que nous venons de modifier). 

Ici, nous devrions laisser les opérations AlterField car le modèle Carpointe sur un modèle différent (même s'il a les mêmes données). Cependant, nous devons supprimer les lignes de migration concernant DeleteModel car le modèle cars.Tires n'existe plus. Il a été entièrement converti en tires.Tires. Voir cette migration .

Étape 4.d. Nettoyer le modèle obsolète dans old_app.

Dernier point mais non le moindre, vous devez créer une dernière migration personnalisée dans l'application Cars. Ici, nous ferons une opération "state" uniquement pour supprimer le modèle cars.Tires. Il s'agit d'un état uniquement car la table de base de données pour cars.Tires a déjà été renommée. Cette dernière migration nettoie le reste de l'état Django.

65
Nostalg.io

Nous venons tout juste de déplacer deux modèles de old_app à new_app, mais les références FK se trouvaient dans certains modèles à partir de app_x et app_y, au lieu de modèles à partir de old_app.

Dans ce cas, suivez les étapes fournies par Nostalg.io comme ceci:

  • Déplacez les modèles de old_app à new_app, puis mettez à jour les instructions import dans la base de code.
  • makemigrations.
  • Suivez l'étape 4.a. Mais utilisez AlterModelTable pour tous les modèles déplacés. Deux pour moi.
  • Suivez l'étape 4.b. comme si.
  • Suivez l'étape 4.c. Mais aussi, pour chaque application qui a un fichier de migration nouvellement généré, modifiez-les manuellement afin de migrer le state_operations à la place.
  • Suivez l'étape 4.d Mais utilisez DeleteModel pour tous les modèles déplacés.

Remarques:

  • Tous les fichiers de migration générés automatiquement modifiés à partir d'autres applications ont une dépendance sur le fichier de migration personnalisé à partir de old_appAlterModelTable est utilisé pour renommer la ou les tables. (créé à l'étape 4.a.)
  • Dans mon cas, j'ai dû supprimer le fichier de migration généré automatiquement de old_app car je n'avais aucune opération AlterField, seulement des opérations DeleteModel et RemoveField. Ou gardez-le avec operations = [] vide
  • Pour éviter les exceptions de migration lors de la création de la base de test, vous devez vous assurer que la migration personnalisée à partir de old_app a été créée à l'étape 4.a. a toutes les dépendances de migration précédentes à partir d'autres applications.

    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'),
    

BTW: Il y a un ticket ouvert à ce sujet: https://code.djangoproject.com/ticket/24686

2
Lucianovici

Une fois le travail terminé, j'ai essayé de faire une nouvelle migration. Mais je face à l'erreur suivante: ValueError: Unhandled pending operations for models: oldapp.modelname (referred to by fields: oldapp.HistoricalProductModelName.model_ref_obj)

Si votre modèle Django utilisant le champ HistoricalRecords, n'oubliez pas d'ajouter des modèles/tables supplémentaires tout en suivant la réponse de @ Nostalg.io.

Ajoutez l'élément suivant à database_operations à la première étape (4.a):

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

et ajoutez une suppression supplémentaire dans state_operations à la dernière étape (4.d):

    migrations.DeleteModel(name='HistoricalModleName'),
1
Daniil Mashkin

Si vous devez déplacer le modèle et que vous n'avez plus accès à l'application (ou si vous ne souhaitez pas accéder à celui-ci), vous pouvez créer une nouvelle opération et envisager de créer un nouveau modèle uniquement si le modèle migré ne le fait pas. exister.

Dans cet exemple, je passe «MyModel» de old_app à 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))
            ],
        ),
    ]
1
Gal Singer

La façon dont Nostalg.io a travaillé dans les avants (génération automatique de tous les autres FK d'applications le référençant). Mais j'avais aussi besoin de revenir en arrière. Pour cela, le retour AlterTable doit se faire avant que les FK ne soient rétrogradés (dans l'original, cela se produirait ensuite). Donc, pour cela, j’ai divisé AlterTable en deux AlterTableF et AlterTableR distincts, chacun ne travaillant que dans un sens, puis en utilisant un en avant au lieu de l’original dans la première migration personnalisée et en inversant l’un dans la dernière ). Quelque chose comme ça:

#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)
    ]   
0
svil

Cela a fonctionné pour moi mais je suis sûr que j'entendrai pourquoi c'est une si mauvaise idée. Ajoutez cette fonction et une opération qui l’appelle pour votre migration 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',
        ),
    ]     

Étape 1: sauvegardez votre base de données!
Assurez-vous que votre migration new_app est exécutée en premier et/ou que la migration old_app est requise. Refusez de supprimer le type de contenu obsolète jusqu'à ce que vous ayez terminé la migration old_app.

après Django 1.9, vous voudrez peut-être aborder un peu plus attentivement:
Migration1: Créer un nouveau tableau
Migration2: table de remplissage
Migration3: modifiez les champs d'autres tables
Migration4: Supprimer l'ancien tableau

0
Renoc

J'ai construit une commande de gestion pour faire exactement cela - déplacer un modèle d'une application Django à une autre - en fonction des suggestions de nostalgic.io à https://stackoverflow.com/a/30613732/1639699

Vous pouvez le trouver sur GitHub à alexei/Django-move-model

0
Alexei