web-dev-qa-db-fra.com

Changer le type de champ Django model de CharField en ForeignKey

Je dois changer le type d'un champ dans l'un de mes modèles Django de CharField à ForeignKey. Les champs sont déjà remplis de données, donc je me demandais quelle est la meilleure ou la bonne façon de procéder. Puis-je simplement mettre à jour le type de champ et migrer, ou y a-t-il des "accrochages" possibles à connaître? N.B: J'utilise juste les opérations de gestion Vanilla Django (makemigrations et migrate), pas South.

23
ChrisM

Il s'agit probablement d'un cas où vous souhaitez effectuer une migration en plusieurs étapes. Ma recommandation pour cela ressemblerait à quelque chose comme suit.

Tout d'abord, supposons que c'est votre modèle initial, dans une application appelée discography:

from Django.db import models

class Album(models.Model):
    name = models.CharField(max_length=255)
    artist = models.CharField(max_length=255)

Maintenant, vous vous rendez compte que vous souhaitez plutôt utiliser une clé étrangère pour l'artiste. Eh bien, comme mentionné, ce n'est pas seulement un processus simple pour cela. Cela doit se faire en plusieurs étapes.

Étape 1, ajoutez un nouveau champ pour ForeignKey, en vous assurant de le marquer comme nul:

from Django.db import models

class Album(models.Model):
    name = models.CharField(max_length=255)
    artist = models.CharField(max_length=255)
    artist_link = models.ForeignKey('Artist', null=True)

class Artist(models.Model):
    name = models.CharField(max_length=255)

... et créez une migration pour ce changement.

./manage.py makemigrations discography

Étape 2, remplissez votre nouveau champ. Pour ce faire, vous devez créer une migration vide.

./manage.py makemigrations --empty --name transfer_artists discography

Une fois cette migration vide, vous souhaitez y ajouter une seule opération RunPython afin de lier vos enregistrements. Dans ce cas, cela pourrait ressembler à ceci:

def link_artists(apps, schema_editor):
    Album = apps.get_model('discography', 'Album')
    Artist = apps.get_model('discography', 'Artist')
    for album in Album.objects.all():
        artist, created = Artist.objects.get_or_create(name=album.artist)
        album.artist_link = artist
        album.save()

Maintenant que vos données sont transférées dans le nouveau champ, vous pouvez en fait terminer et tout laisser tel quel, en utilisant le nouveau champ pour tout. Ou, si vous voulez faire un peu de nettoyage, vous voulez créer deux migrations supplémentaires.

Pour votre première migration, vous souhaiterez supprimer votre champ d'origine, artist. Pour votre deuxième migration, renommez le nouveau champ artist_link à artist.

Cela se fait en plusieurs étapes pour s'assurer que Django reconnaît correctement les opérations. Vous pouvez créer une migration manuellement pour gérer cela, mais je vous laisse le soin de comprendre.

64
Joey Wilhelm

C'est une continuation de la grande réponse de Joey. Comment renommer le nouveau champ en son nom d'origine?

Si le champ contient des données, cela signifie probablement que vous les utilisez ailleurs dans votre projet, par conséquent, cette solution vous laissera un champ nommé différemment, et vous devrez soit refactoriser le projet pour utiliser le nouveau champ, soit supprimer l'ancien champ et renommer le nouveau.

Sachez que ce processus ne vous empêchera pas de refactoriser le code. Si vous utilisiez un CharField avec CHOIX, vous accédiez à son contenu avec get_filename_display () , par exemple.

Si vous essayez de supprimer le champ pour effectuer une migration, puis de renommer l'autre champ et d'effectuer une autre migration, vous verrez Django se plaindre car vous ne pouvez pas supprimer un champ que vous utilisez dans le projet.

Créez simplement une migration vide comme l'explique Joey, et mettez-la en opération:

operations = [
    migrations.RemoveField(
        model_name='app_name',
        name='old_field_name',
    ),
    migrations.RenameField(
        model_name='app_name',
        old_name='old_field_name_link',
        new_name='old_field_name',
    ),
]

Ensuite, exécutez migrate et vous aurez les modifications apportées dans votre base de données, mais évidemment pas dans votre modèle, il est maintenant temps de supprimer l'ancien champ et de renommer le nouveau champ ForeignKey en son nom d'origine.

Je ne pense pas que faire cela soit particulièrement hacky, mais quand même, ne faites ce genre de choses que si vous comprenez parfaitement avec quoi vous jouez.

1
Pere Picornell

En plus de la réponse de Joey, des étapes détaillées pour Django 2.2.11.

Voici les modèles de mon cas d'utilisation, qui consistent en un modèle Company et Employee. Nous devons convertir designation en un champ de clé étrangère. Le nom de l'application s'appelle core

class Company(CommonFields):
    name = models.CharField(max_length=255, blank=True, null=True

class Employee(CommonFields):
    company = models.ForeignKey("Company", on_delete=models.CASCADE, blank=True, null=True)
    designation = models.CharField(max_length=100, blank=True, null=True)

Étape 1

Créer une clé étrangère designation_link dans Employee et le marquer comme null = True

class Designation(CommonFields):
    name = models.CharField(max_length=255)
    company = models.ForeignKey("Company", on_delete=models.CASCADE, blank=True, null=True)

class Employee(CommonFields):
    company = models.ForeignKey("Company", on_delete=models.CASCADE, blank=True, null=True)
    designation = models.CharField(max_length=100, blank=True, null=True)
    designation_link = models.ForeignKey("Designation", on_delete=models.CASCADE, blank=True, null=True)

Étape 2

Créez une migration vide. Utilisation de la commande:

python app_code/manage.py makemigrations --empty --name transfer_designations core

Cela créera un fichier suivant dans le répertoire migrations.

# Generated by Django 2.2.11 on 2020-04-02 05:56

from Django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('core', '0006_auto_20200402_1119'),
    ]

    operations = [
    ]

Étape

Remplissez la migration vide avec une fonction qui boucle sur tous les Employees, crée un Designation et le relie au Employee.

Dans mon cas d'utilisation, chaque Designation est également lié à un Company. Ce qui signifie que Designation peut contenir deux lignes pour les "managers", une pour la société A, une autre pour la société B.

La migration finale ressemblerait à ceci:

# core/migrations/0007_transfer_designations.py

# Generated by Django 2.2.11 on 2020-04-02 05:56

from Django.db import migrations

def link_designation(apps, schema_editor):
    Employee = apps.get_model('core', 'Employee')
    Designation = apps.get_model('core', 'Designation')
    for emp in Employee.objects.all():
        if(emp.designation is not None and emp.company is not None):
            desig, created = Designation.objects.get_or_create(name=emp.designation, company=emp.company)
            emp.designation_link = desig
            emp.save()

class Migration(migrations.Migration):

    dependencies = [
        ('core', '0006_auto_20200402_1119'),
    ]

    operations = [
        migrations.RunPython(link_designation),
    ]

Étape 4

Enfin, exécutez cette migration en utilisant:

python app_code/manage.py migrate core 0007

0
jerrymouse