Stratégie de migration Django pour renommer un modèle et des champs de relation

154

Je prévois de renommer plusieurs modèles dans un projet Django existant où de nombreux autres modèles ont des relations de clé étrangère avec les modèles que je souhaite renommer. Je suis assez certain que cela nécessitera plusieurs migrations, mais je ne suis pas sûr de la procédure exacte.

Disons que je commence avec les modèles suivants dans une application Django appelée myapp:

class Foo(models.Model):
    name = models.CharField(unique=True, max_length=32)
    description = models.TextField(null=True, blank=True)


class AnotherModel(models.Model):
    foo = models.ForeignKey(Foo)
    is_awesome = models.BooleanField()


class YetAnotherModel(models.Model):
    foo = models.ForeignKey(Foo)
    is_ridonkulous = models.BooleanField()

Je veux renommer le Foomodèle parce que le nom n'a pas vraiment de sens et sème la confusion dans le code, ce Barqui rendrait un nom beaucoup plus clair.

D'après ce que j'ai lu dans la documentation de développement Django, je suppose la stratégie de migration suivante:

Étape 1

Modifier models.py:

class Bar(models.Model):  # <-- changed model name
    name = models.CharField(unique=True, max_length=32)
    description = models.TextField(null=True, blank=True)


class AnotherModel(models.Model):
    foo = models.ForeignKey(Bar)  # <-- changed relation, but not field name
    is_awesome = models.BooleanField()


class YetAnotherModel(models.Model):
    foo = models.ForeignKey(Bar)  # <-- changed relation, but not field name
    is_ridonkulous = models.BooleanField()

Notez que le AnotherModelnom du champ pour foone change pas, mais la relation est mise à jour avec le Barmodèle. Mon raisonnement est que je ne devrais pas trop changer à la fois et que si je change ce nom de champ enbar je risquerais de perdre les données de cette colonne.

Étape 2

Créez une migration vide:

python manage.py makemigrations --empty myapp

Étape 3

Modifiez la Migrationclasse dans le fichier de migration créé à l'étape 2 pour ajouter l' RenameModelopération à la liste des opérations:

class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '0001_initial'),
    ]

    operations = [
        migrations.RenameModel('Foo', 'Bar')
    ]

Étape 4

Appliquez la migration:

python manage.py migrate

Étape 5

Modifiez les noms de champs associés dans models.py:

class Bar(models.Model):
    name = models.CharField(unique=True, max_length=32)
    description = models.TextField(null=True, blank=True)


class AnotherModel(models.Model):
    bar = models.ForeignKey(Bar)  # <-- changed field name
    is_awesome = models.BooleanField()


class YetAnotherModel(models.Model):
    bar = models.ForeignKey(Bar)  # <-- changed field name
    is_ridonkulous = models.BooleanField()

Étape 6

Créez une autre migration vide:

python manage.py makemigrations --empty myapp

Étape 7

Modifiez la Migrationclasse dans le fichier de migration créé à l'étape 6 pour ajouter la ou les RenameFieldopérations pour tous les noms de champs associés à la liste des opérations:

class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '0002_rename_fields'),  # <-- is this okay?
    ]

    operations = [
        migrations.RenameField('AnotherModel', 'foo', 'bar'),
        migrations.RenameField('YetAnotherModel', 'foo', 'bar')
    ]

Étape 8

Appliquer la 2ème migration:

python manage.py migrate

Hormis la mise à jour du reste du code (vues, formulaires, etc.) pour refléter les nouveaux noms de variables, est-ce essentiellement ainsi que la nouvelle fonctionnalité de migration fonctionnerait?

En outre, cela semble être beaucoup d'étapes. Les opérations de migration peuvent-elles être condensées d'une manière ou d'une autre?

Merci!

Billet de cinq livres
la source

Réponses:

128

Donc, quand j'ai essayé cela, il semble que vous puissiez condenser les étapes 3-7:

class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '0001_initial'), 
    ]

    operations = [
        migrations.RenameModel('Foo', 'Bar'),
        migrations.RenameField('AnotherModel', 'foo', 'bar'),
        migrations.RenameField('YetAnotherModel', 'foo', 'bar')
    ]

Vous pouvez obtenir des erreurs si vous ne mettez pas à jour les noms où il est importé, par exemple admin.py et même des fichiers de migration plus anciens (!).

Mise à jour : comme le mentionne ceasaro , les nouvelles versions de Django sont généralement capables de détecter et de demander si un modèle est renommé. Alors essayez d' manage.py makemigrationsabord, puis vérifiez le fichier de migration.

Wasabigeek
la source
Merci d'avoir répondu. Depuis, j'ai migré en utilisant les étapes que j'ai décrites, mais je suis curieux de savoir si vous avez essayé cela avec des données existantes ou simplement avec une base de données vide?
Fiver
2
Je l'ai essayé avec des données existantes, mais juste quelques lignes sur sqlite dans mon environnement local (lorsque je passerai à la production, j'ai l'intention de tout effacer, y compris les fichiers de migration)
wasabigeek
4
Vous n'êtes pas obligé de modifier le nom du modèle dans les fichiers de migration si vous les utilisez apps.get_model. m'a pris beaucoup de temps pour comprendre cela.
ahmed
9
Dans django 2.0, si vous changez le nom de votre modèle, la ./manage.py makemigrations myappcommande vous demandera si vous avez renommé votre modèle. Exemple: avez-vous renommé le modèle myapp.Foo en Bar? [o / N] Si vous répondez «y», votre migration contiendra les migration.RenameModel('Foo', 'Bar')mêmes
nombres
1
manage.py makemigrations myapppeut toujours échouer: "Vous devrez peut-être ajouter manuellement ceci si vous changez le nom du modèle et un certain nombre de ses champs à la fois; dans le détecteur automatique, cela ressemblera à vous avez supprimé un modèle avec l'ancien nom et en avez ajouté un nouveau avec un nom différent, et la migration qu'il crée perdra toutes les données de l'ancienne table. " Django 2.1 Docs Pour moi, il suffisait de créer une migration vide, d'y ajouter le nom du modèle, puis de l'exécuter makemigrationscomme d'habitude.
hlongmore
37

Au début, je pensais que la méthode de Fiver fonctionnait pour moi parce que la migration fonctionnait bien jusqu'à l'étape 4. Cependant, les changements implicites «ForeignKeyField (Foo)» en «ForeignKeyField (Bar)» n'étaient liés à aucune migration. C'est pourquoi la migration a échoué lorsque j'ai voulu renommer les champs de relation (étape 5-8). Cela peut être dû au fait que mon 'AnotherModel' et 'YetAnotherModel' sont distribués dans d'autres applications dans mon cas.

J'ai donc réussi à renommer mes modèles et mes champs de relation en suivant les étapes ci-dessous:

J'ai adapté la méthode à partir de cela et en particulier le truc de l'otranzer.

Donc, comme Fiver, disons que nous avons dans myapp :

class Foo(models.Model):
    name = models.CharField(unique=True, max_length=32)
    description = models.TextField(null=True, blank=True)

Et dans myotherapp :

class AnotherModel(models.Model):
    foo = models.ForeignKey(Foo)
    is_awesome = models.BooleanField()


class YetAnotherModel(models.Model):
    foo = models.ForeignKey(Foo)
    is_ridonkulous = models.BooleanField()

Étape 1:

Transformez chaque OneToOneField (Foo) ou ForeignKeyField (Foo) en IntegerField (). (Cela conservera l'identifiant de l'objet Foo associé comme valeur du champ entier).

class AnotherModel(models.Model):
    foo = models.IntegerField()
    is_awesome = models.BooleanField()

class YetAnotherModel(models.Model):
    foo = models.IntegerField()
    is_ridonkulous = models.BooleanField()

ensuite

python manage.py makemigrations

python manage.py migrate

Étape 2: (comme l'étape 2-4 de Fiver)

Changer le nom du modèle

class Bar(models.Model):  # <-- changed model name
    name = models.CharField(unique=True, max_length=32)
    description = models.TextField(null=True, blank=True)

Créez une migration vide:

python manage.py makemigrations --empty myapp

Puis modifiez-le comme:

class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '0001_initial'),
    ]

    operations = [
        migrations.RenameModel('Foo', 'Bar')
    ]

Finalement

python manage.py migrate

Étape 3:

Transformez votre IntegerField () en leur ancien ForeignKeyField ou OneToOneField mais avec le nouveau modèle de barre. (Le champ entier précédent stockait l'id, donc django comprend cela et rétablit la connexion, ce qui est cool.)

class AnotherModel(models.Model):
    foo = models.ForeignKey(Bar)
    is_awesome = models.BooleanField()

class YetAnotherModel(models.Model):
    foo = models.ForeignKey(Bar)
    is_ridonkulous = models.BooleanField()

Alors fais:

python manage.py makemigrations 

Très important, à cette étape, vous devez modifier toutes les nouvelles migrations et ajouter la dépendance aux migrations RenameModel Foo-> Bar. Donc, si AnotherModel et YetAnotherModel sont tous les deux dans myotherapp, la migration créée dans myotherapp doit ressembler à ceci:

class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '00XX_the_migration_of_myapp_with_renamemodel_foo_bar'),
        ('myotherapp', '00xx_the_migration_of_myotherapp_with_integerfield'),
    ]

    operations = [
        migrations.AlterField(
            model_name='anothermodel',
            name='foo',
            field=models.ForeignKey(to='myapp.Bar'),
        ),
        migrations.AlterField(
            model_name='yetanothermodel',
            name='foo',
            field=models.ForeignKey(to='myapp.Bar')
        ),
    ]

ensuite

python manage.py migrate

Étape 4:

Finalement, vous pouvez renommer vos champs

class AnotherModel(models.Model):
    bar = models.ForeignKey(Bar) <------- Renamed fields
    is_awesome = models.BooleanField()


class YetAnotherModel(models.Model):
    bar = models.ForeignKey(Bar) <------- Renamed fields
    is_ridonkulous = models.BooleanField()

puis renommer automatiquement

python manage.py makemigrations

(django devrait vous demander si vous avez réellement renommé le nom du modèle, dites oui)

python manage.py migrate

Et c'est tout!

Cela fonctionne sur Django1.8

v.thorey
la source
3
Je vous remercie! Cela a été extrêmement utile. Mais une note - j'ai également dû renommer et / ou supprimer manuellement les index de champs PostgreSQL car, après avoir renommé Foo en Bar, j'ai créé un nouveau modèle nommé Bar.
Anatoly Scherbakov
Merci pour ça! Je pense que la partie clé consiste à convertir toutes les clés étrangères, dans ou hors du modèle à renommer, en IntegerField. Cela a parfaitement fonctionné pour moi et a l'avantage supplémentaire de les recréer avec le nom correct. Naturellement, je conseillerais de revoir toutes les migrations avant de les exécuter!
zelanix
Je vous remercie! J'ai essayé de nombreuses stratégies différentes afin de renommer un modèle sur lequel d'autres modèles ont des clés étrangères (étapes 1 à 3), et c'est la seule qui a fonctionné.
MSH
La modification ForeignKeyde IntegerFields a sauvé ma journée aujourd'hui!
mehmet
8

J'avais besoin de faire la même chose et de suivre. J'ai changé le modèle en une seule fois (étapes 1 et 5 ensemble de la réponse de Fiver). Ensuite, vous avez créé une migration de schéma, mais l'avez modifiée comme suit:

class Migration(SchemaMigration):
    def forwards(self, orm):
        db.rename_table('Foo','Bar')

    def backwards(self, orm):
        db.rename_table('Bar','Foo')

Cela a parfaitement fonctionné. Toutes mes données existantes sont apparues, toutes les autres tables référencées Bar fine.

à partir d'ici: https://hanmir.wordpress.com/2012/08/30/rename-model-django-south-migration/

John Q
la source
Super, merci pour le partage. Assurez-vous de +1 wasibigeek si cette réponse vous a aidé.
Fiver
7

Pour Django 1.10, j'ai réussi à changer deux noms de classe de modèle (y compris un ForeignKey et avec des données) en exécutant simplement Makemigrations, puis Migrate pour l'application. Pour l'étape Makemigrations, j'ai dû confirmer que je voulais changer les noms de table. Migrate a changé les noms des tables sans problème.

Ensuite, j'ai changé le nom du champ ForeignKey pour qu'il corresponde, et Makemigrations m'a de nouveau demandé de confirmer que je voulais changer le nom. Migrer que faire le changement.

J'ai donc pris cela en deux étapes sans aucune modification de fichier spéciale. J'ai eu des erreurs au début parce que j'ai oublié de changer le fichier admin.py, comme mentionné par @wasibigeek.

excyberlabber
la source
Merci beaucoup! Parfait pour Django 1.11 aussi
Francisco
6

J'ai également fait face au problème décrit par v.thorey et j'ai trouvé que son approche est très utile mais peut être condensée en moins d'étapes qui sont en fait les étapes 5 à 8 comme Fiver décrit sans les étapes 1 à 4 sauf que l'étape 7 doit être modifiée comme mon ci-dessous l'étape 3. Les étapes générales sont les suivantes:

Étape 1: modifiez les noms de champs associés dans models.py

class Bar(models.Model):
    name = models.CharField(unique=True, max_length=32)
    description = models.TextField(null=True, blank=True)


class AnotherModel(models.Model):
    bar = models.ForeignKey(Bar)  # <-- changed field name
    is_awesome = models.BooleanField()


class YetAnotherModel(models.Model):
    bar = models.ForeignKey(Bar)  # <-- changed field name
    is_ridonkulous = models.BooleanField()

Étape 2: créer une migration vide

python manage.py makemigrations --empty myapp

Étape 3: modifiez la classe de migration dans le fichier de migration créé à l'étape 2

class Migration(migrations.Migration):

dependencies = [
    ('myapp', '0001_initial'), 
]

operations = [
    migrations.AlterField(
        model_name='AnotherModel',
        name='foo',
        field=models.IntegerField(),
    ),
    migrations.AlterField(
        model_name='YetAnotherModel',
        name='foo',
        field=models.IntegerField(),
    ),
    migrations.RenameModel('Foo', 'Bar'),
    migrations.AlterField(
        model_name='AnotherModel',
        name='foo',
        field=models.ForeignKey(to='myapp.Bar'),
    ),
    migrations.AlterField(
        model_name='YetAnotherModel',
        name='foo',
        field=models.ForeignKey(to='myapp.Bar'),
    ),
    migrations.RenameField('AnotherModel', 'foo', 'bar'),
    migrations.RenameField('YetAnotherModel', 'foo', 'bar')
]

Étape 4: appliquer la migration

python manage.py migrate

Terminé

PS j'ai essayé cette approche sur Django 1.9

Curtis Lo
la source
5

J'utilise Django version 1.9.4

J'ai suivi les étapes suivantes: -

Je viens de renommer le modèle oldName en NewName Run python manage.py makemigrations. Il vous demandera de Did you rename the appname.oldName model to NewName? [y/N]sélectionner Y

Courez python manage.py migrateet il vous demandera

Les types de contenu suivants sont obsolètes et doivent être supprimés:

appname | oldName
appname | NewName

Tous les objets liés à ces types de contenu par une clé étrangère seront également supprimés. Voulez-vous vraiment supprimer ces types de contenu? Si vous n'êtes pas sûr, répondez «non».

Type 'yes' to continue, or 'no' to cancel: Select No

Il renomme et migre toutes les données existantes vers une nouvelle table nommée pour moi.

Piyush S. Wanare
la source
Merci mec, j'étais confus parce que rien ne s'est passé après avoir frappé "non"
farhawa
3

Malheureusement, j'ai trouvé des problèmes (chaque django 1.x) avec la migration de changement de nom qui laissent d'anciens noms de table dans la base de données.

Django n'essaye même rien sur l'ancienne table, il suffit de renommer son propre modèle. Le même problème avec les clés étrangères et les index en général - les changements ne sont pas suivis correctement par Django.

La solution la plus simple (solution de contournement):

class Foo(models.Model):
     name = models.CharField(unique=True, max_length=32)
     ...
Bar = Foo  # and use Bar only

La vraie solution (un moyen simple de basculer tous les index, contraintes, déclencheurs, noms, etc. en 2 commits, mais plutôt pour des tables plus petites ):

commettre A:

  1. créer le même modèle que l'ancien
# deprecated - TODO: TO BE REMOVED
class Foo(model.Model):
    ...

class Bar(model.Model):
    ...
  1. changer le code pour qu'il fonctionne Baruniquement avec le nouveau modèle . (y compris toutes les relations sur le schéma)

Dans la migration, préparez RunPython, qui copient les données de Foo vers Bar (y compris idde Foo)

  1. optimisation optionnelle (si nécessaire pour des tables plus grandes)

commit B: (pas de précipitation, faites-le quand une équipe entière est migrée)

  1. dépôt sécurisé de l'ancien modèle Foo

nettoyage supplémentaire:

  • squash sur les migrations

bug dans Django:

Sławomir Lenart
la source
3

Je voulais juste confirmer et ajouter un commentaire ceasaro. Django 2.0 semble faire cela automatiquement maintenant.

Je suis sur Django 2.2.1, tout ce que j'avais à faire pour renommer le modèle et l'exécuter makemigrations .

Ici, il demande si j'avais renommé la classe spécifique de Aà B, j'ai choisi oui et j'ai exécuté migrate et tout semble fonctionner.

Remarque Je n'ai renommé l'ancien nom de modèle dans aucun des fichiers du dossier project / migrations.

Peheje
la source
1

J'avais besoin de renommer quelques tables. Mais un seul changement de nom de modèle a été remarqué par Django. Cela s'est produit parce que Django itère sur les modèles ajoutés, puis supprimés. Pour chaque paire, il vérifie si elles appartiennent à la même application et ont des champs identiques . Une seule table n'avait aucune clé étrangère vers les tables à renommer (les clés étrangères contiennent le nom de la classe de modèle, comme vous vous en souvenez). En d'autres termes, une seule table n'avait aucun changement de champ. C'est pourquoi cela a été remarqué.

La solution consiste donc à renommer une table à la fois, en modifiant models.pyéventuellement le nom de la classe de modèle views.pyet en effectuant une migration. Après cela, inspectez votre code pour d'autres références (noms de classe de modèle, noms (de requête) associés, noms de variables). Effectuez une migration, si nécessaire. Ensuite, combinez éventuellement toutes ces migrations en une seule (assurez-vous de copier également les importations).

x-yuri
la source
1

Je ferais des mots @ceasaro, les miens sur son commentaire sur cette réponse .

Les nouvelles versions de Django peuvent détecter les changements et demander ce qui a été fait. J'ajouterais également que Django pourrait mélanger l'ordre d'exécution de certaines commandes de migration.

Il serait sage d'appliquer de petits changements et courir makemigrationset migratesi l'erreur se produit le fichier de migration peut être modifié.

L'ordre d'exécution de certaines lignes peut être modifié pour éviter les erreurs.

diogosimao
la source
Il est bon de noter que cela ne fonctionne pas si vous changez les noms de modèle et qu'il y a des clés étrangères définies, etc.
Dean Kayton
Expansion sur le commentaire précédent: Si tout ce que je fais est de changer les noms de modèle et d'exécuter makemigrations, j'obtiens 'NameError: name' <oldmodel> 'is not defined' dans les clés étrangères, etc ... Si je change cela et lance makemigrations, j'obtiens des erreurs d'importation dans admin.py ... si je corrige cela et exécute à nouveau makemigrations, j'obtiens des invites `` Avez-vous renommé le modèle <app.oldmodel> en <newmodel> '' Mais alors en appliquant les migrations, j'obtiens `` ValueError: The field <app .newmodel.field1> a été déclaré avec une référence paresseuse à '<app.oldmodel>', mais l'application '<app>' ne fournit pas le modèle '<oldmodel>', etc ... '
Dean Kayton
Cette erreur semble que vous devez renommer les références dans vos migrations historiques.
mhatch
@DeanKayton dirait que cela migrations.SeparateDatabaseAndStatepeut aider?
diogosimao
1

Si vous utilisez un bon IDE comme PyCharm, vous pouvez faire un clic droit sur le nom du modèle et faire un refactor -> renommer. Cela vous évite d'avoir à parcourir tout votre code qui fait référence au modèle. Ensuite, exécutez makemigrations et migrez. Django 2+ confirmera simplement le changement de nom.

Josh
la source
-10

J'ai mis à jour Django de la version 10 à la version 11:

sudo pip install -U Django

( -Upour "mise à niveau") et cela a résolu le problème.

Muhammad Hafid
la source