web-dev-qa-db-fra.com

Quelle est la meilleure approche pour changer les clés primaires dans une application Django existante?

J'ai une application qui est en mode BETA. Le modèle de cette application comporte certaines classes avec une clé primaire explicite. En conséquence Django utilise les champs et ne crée pas automatiquement un identifiant.

class Something(models.Model):
    name = models.CharField(max_length=64, primary_key=True)

Je pense que c'était une mauvaise idée (voir erreur unicode lors de l'enregistrement d'un objet dans Django admin ) et je voudrais revenir en arrière et avoir un identifiant pour chaque classe de mon modèle.

class Something(models.Model):
    name = models.CharField(max_length=64, db_index=True)

J'ai apporté les modifications à mon modèle (remplacez chaque primary_key = True par db_index = True) et je veux migrer la base de données avec south .

Malheureusement, la migration échoue avec le message suivant: ValueError: You cannot add a null=False column without a default value.

J'évalue les différentes solutions de contournement pour ce problème. Aucune suggestion?

Merci de votre aide

37
luc

D'accord, votre modèle est probablement faux.

La clé primaire formelle doit toujours être une clé de substitution. Jamais autre chose. [Mots forts. Concepteur de bases de données depuis les années 80. Il est important de retenir les leçons apprises: tout est variable, même lorsque les utilisateurs jurent sur les tombes de leur mère que la valeur ne peut pas être modifiée est vraiment une clé naturelle qui peut être considérée comme principale. Ce n'est pas primaire. Seuls les substituts peuvent être principaux.]

Vous faites une chirurgie à cœur ouvert. Ne jouez pas avec la migration de schéma. Vous remplacez le schéma.

  1. Déchargez vos données dans des fichiers JSON. Pour cela, utilisez les propres outils Django-admin.py internes de Django. Vous devez créer un fichier de déchargement pour chacun qui sera modifié et pour chaque table qui dépend d'une clé en cours de création. Des fichiers séparés rendent cela un peu plus facile à faire.

  2. Supprimez les tables que vous allez modifier de l'ancien schéma.

    Les tables qui dépendent de ces tables verront leurs FK modifiés; vous pouvez soit mettre à jour les lignes en place, soit - cela pourrait être plus simple - supprimer et réinsérer également ces lignes.

  3. Créez le nouveau schéma. Cela ne créera que les tables qui changent.

  4. Écrivez des scripts pour lire et recharger les données avec les nouvelles clés. Celles-ci sont courtes et très similaires. Chaque script utilisera json.load() pour lire les objets du fichier source; vous créerez ensuite vos objets de schéma à partir des objets JSON Tuple-line qui ont été créés pour vous. Vous pouvez ensuite les insérer dans la base de données.

    Vous avez deux cas.

    • Les tableaux avec le changement de PK modifié seront insérés et obtiendront de nouveaux PK. Ceux-ci doivent être "mis en cascade" vers d'autres tables pour garantir que les FK de l'autre table soient également modifiés.

    • Les tables avec des FK qui changent devront localiser la ligne dans la table étrangère et mettre à jour leur référence FK.

Alternative.

  1. Renommez toutes vos anciennes tables.

  2. Créez l'intégralité du nouveau schéma.

  3. Écrivez SQL pour migrer toutes les données de l'ancien schéma vers le nouveau schéma. Cela devra réaffecter intelligemment les clés au fur et à mesure.

  4. Supprimez les anciennes tables renommées.

68
S.Lott

Pour modifier la clé primaire avec south, vous pouvez utiliser la commande south.db.create_primary_key dans la migration de données. Pour changer votre pack CharField personnalisé en AutoField standard, vous devez:

1) créez un nouveau champ dans votre modèle

class MyModel(Model):
    id = models.AutoField(null=True)

1.1) si vous avez une clé étrangère dans un autre modèle vers ce modèle, créez également un nouveau faux champ fk sur ce modèle (utilisez IntegerField, il sera ensuite converti)

class MyRelatedModel(Model):
    fake_fk = models.IntegerField(null=True)

2) créer une migration automatique vers le sud et migrer:

./manage.py schemamigration --auto
./manage.py migrate

3) créer une nouvelle migration de données

./manage.py datamigration <your_appname> fill_id

dans cette migration de données, remplissez ces nouveaux champs id et fk avec des nombres (il suffit de les énumérer)

    for n, obj in enumerate(orm.MyModel.objects.all()):
        obj.id = n
        # update objects with foreign keys
        obj.myrelatedmodel_set.all().update(fake_fk = n)
        obj.save()

    db.delete_primary_key('my_app_mymodel')
    db.create_primary_key('my_app_mymodel', ['id'])

4) dans vos modèles, définissez primary_key = True sur votre nouveau champ pk

id = models.AutoField(primary_key=True)

5) supprimer l'ancien champ de clé primaire (s'il n'est pas nécessaire) créer une migration automatique et migrer.

5.1) si vous avez des clés étrangères - supprimez également les anciens champs de clé étrangère (migrer)

6) Dernière étape - restaurer les relations clés Fireign. Créez à nouveau un vrai champ fk et supprimez votre champ fake_fk, créez une migration automatique MAIS NE PAS MIGRER (!) - vous devez modifier la migration automatique créée: au lieu de créer un nouveau fk et de supprimer fake_fk - renommer la colonne fake_fk

# in your models
class MyRelatedModel(Model):
    # delete fake_fk
    # fake_fk = models.InegerField(null=True)
    # create real fk
    mymodel = models.FoeignKey('MyModel', null=True)

# in migration
    def forwards(self, orm):
        # left this without change - create fk field
        db.add_column('my_app_myrelatedmodel', 'mymodel',
                  self.gf('Django.db.models.fields.related.ForeignKey')(default=1, related_name='lots', to=orm['my_app.MyModel']),keep_default=False)

        # remove fk column and rename fake_fk
        db.delete_column('my_app_myrelatedmodel', 'mymodel_id')
        db.rename_column('my_app_myrelatedmodel', 'fake_fk', 'mymodel_id')

donc fake_fk précédemment rempli devient une colonne, qui contient des données de relation réelles, et il ne se perd pas après toutes les étapes ci-dessus.

10
user920391

J'ai eu le même problème aujourd'hui et suis parvenu à une solution inspirée des réponses ci-dessus.

Mon modèle a une table "Location". Il a un CharField appelé "unique_id" et j'en ai bêtement fait une clé primaire, l'année dernière. Bien sûr, ils ne se sont pas révélés aussi uniques que prévu à l'époque. Il existe également un modèle "ScheduledMeasurement" qui a une clé étrangère pour "Location".

Maintenant, je veux corriger cette erreur et donner à Location une clé primaire ordinaire à incrémentation automatique.

Mesures prises:

  1. Créez un CharField ScheduledMeasurement.temp_location_unique_id et un modèle TempLocation, et migrations pour les créer. TempLocation a la structure que je veux que Location ait.

  2. Créez une migration de données qui définit tous les temp_location_unique_id à l'aide de la clé étrangère et qui copie toutes les données de Location vers TempLocation

  3. Supprimez la clé étrangère et la table Emplacement avec une migration

  4. Recréez le modèle d'emplacement comme je le souhaite, recréez la clé étrangère avec null = True. Renommé 'unique_id' en 'location_code' ...

  5. Créez une migration de données qui remplit les données dans Location à l'aide de TempLocation et remplit les clés étrangères dans ScheduledMeasurement à l'aide de temp_location

  6. Supprimez temp_location, TempLocation et null = True dans la clé étrangère

Et modifiez tout le code qui supposait que unique_id était unique (tous les objets objects.get (unique_id = ...)), et qui utilisait unique_id sinon ...

6
RemcoGerlich

J'ai réussi à le faire avec Django 1.10.4 migrations et mysql 5.5, mais ce n'était pas facile.

J'avais une clé primaire varchar avec plusieurs clés étrangères. J'ai ajouté un champ id, des données migrées et des clés étrangères. C'est ainsi:

  1. Ajout du futur champ de clé primaire. J'ai ajouté un champ id = models.IntegerField(default=0) à mon modèle principal et généré une migration automatique.
  2. Migration de données simple pour générer de nouvelles clés primaires:

    def fill_ids(apps, schema_editor):
       Model = apps.get_model('<module>', '<model>')
       for id, code in enumerate(Model.objects.all()):
           code.id = id + 1
           code.save()
    
    class Migration(migrations.Migration):
        dependencies = […]
        operations = [migrations.RunPython(fill_ids)]
    
  3. Migration de clés étrangères existantes. J'ai écrit une migration combinée:

    def change_model_fks(apps, schema_editor):
        Model = apps.get_model('<module>', '<model>')  # Our model we want to change primary key for
        FkModel = apps.get_model('<module>', '<fk_model>')  # Other model that references first one via foreign key
    
        mapping = {}
        for model in Model.objects.all():
            mapping[model.old_pk_field] = model.id  # map old primary keys to new
    
        for fk_model in FkModel.objects.all():
            if fk_model.model_id:
                fk_model.model_id = mapping[fk_model.model_id]  # change the reference
                fk_model.save()
    
    class Migration(migrations.Migration):
        dependencies = […]
        operations = [
            # drop foreign key constraint
            migrations.AlterField(
                model_name='<FkModel>',
                name='model',
                field=models.ForeignKey('<Model>', blank=True, null=True, db_constraint=False)
            ),
    
            # change references
            migrations.RunPython(change_model_fks),
    
            # change field from varchar to integer, drop index
            migrations.AlterField(
                model_name='<FkModel>',
                name='model',
                field=models.IntegerField('<Model>', blank=True, null=True)
            ),
        ]
    
  4. Échange de clés primaires et restauration de clés étrangères. Encore une fois, une migration personnalisée. J'ai généré automatiquement la base de cette migration lorsque j'ai a) supprimé primary_key=True de l'ancienne clé primaire et b) supprimé id champ

    class Migration(migrations.Migration):
        dependencies = […]
        operations = [
            # Drop old primary key
            migrations.AlterField(
                model_name='<Model>',
                name='<old_pk_field>',
                field=models.CharField(max_length=100),
            ),
    
            # Create new primary key
            migrations.RunSQL(
                ['ALTER TABLE <table> CHANGE id id INT (11) NOT NULL PRIMARY KEY AUTO_INCREMENT'],
                ['ALTER TABLE <table> CHANGE id id INT (11) NULL',
                 'ALTER TABLE <table> DROP PRIMARY KEY'],
                state_operations=[migrations.AlterField(
                    model_name='<Model>',
                    name='id',
                    field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
                )]
            ),
    
            # Recreate foreign key constraints
            migrations.AlterField(
                model_name='<FkModel>',
                name='model',
                field=models.ForeignKey(blank=True, null=True, to='<module>.<Model>'),
        ]
    
6
Nikolay Markov

Actuellement, vous échouez car vous ajoutez une colonne pk qui rompt les exigences NOT NULL et UNIQUE.

Vous devez diviser la migration en plusieurs étapes , en séparant les migrations de schéma et les migrations de données:

  • ajouter la nouvelle colonne, indexée mais pas la clé primaire, avec une valeur par défaut (migration ddl)
  • migrer les données: remplissez la nouvelle colonne avec la valeur correcte (migration des données)
  • marquez la nouvelle clé primaire de la colonne et supprimez l'ancienne colonne pk si elle est devenue inutile (migration ddl)
6
Tobu

J'ai moi-même rencontré ce problème et j'ai fini par écrire une migration réutilisable (spécifique à MySQL) qui prend également en compte une relation plusieurs-à-plusieurs. En résumé, les mesures que j'ai prises étaient les suivantes:

  1. Modifiez la classe de modèle comme ceci:

    class Something(models.Model):
        name = models.CharField(max_length=64, unique=True)
    
  2. Ajoutez une nouvelle migration dans ce sens:

    app_name = 'app'
    model_name = 'something'
    related_model_name = 'something_else'
    model_table = '%s_%s' % (app_name, model_name)
    pivot_table = '%s_%s_%ss' % (app_name, related_model_name, model_name)
    
    
    class Migration(migrations.Migration):
    
        operations = [
            migrations.AddField(
                model_name=model_name,
                name='id',
                field=models.IntegerField(null=True),
                preserve_default=True,
            ),
            migrations.RunPython(do_most_of_the_surgery),
            migrations.AlterField(
                model_name=model_name,
                name='id',
                field=models.AutoField(
                    verbose_name='ID', serialize=False, auto_created=True,
                    primary_key=True),
                preserve_default=True,
            ),
            migrations.AlterField(
                model_name=model_name,
                name='name',
                field=models.CharField(max_length=64, unique=True),
                preserve_default=True,
            ),
            migrations.RunPython(do_the_final_lifting),
        ]
    

    def do_most_of_the_surgery(apps, schema_editor):
        models = {}
        Model = apps.get_model(app_name, model_name)
    
        # Generate values for the new id column
        for i, o in enumerate(Model.objects.all()):
            o.id = i + 1
            o.save()
            models[o.name] = o.id
    
        # Work on the pivot table before going on
        drop_constraints_and_indices_in_pivot_table()
    
        # Drop current pk index and create the new one
        cursor.execute(
            "ALTER TABLE %s DROP PRIMARY KEY" % model_table
        )
        cursor.execute(
            "ALTER TABLE %s ADD PRIMARY KEY (id)" % model_table
        )
    
        # Rename the fk column in the pivot table
        cursor.execute(
            "ALTER TABLE %s "
            "CHANGE %s_id %s_id_old %s NOT NULL" %
            (pivot_table, model_name, model_name, 'VARCHAR(30)'))
        # ... and create a new one for the new id
        cursor.execute(
            "ALTER TABLE %s ADD COLUMN %s_id INT(11)" %
            (pivot_table, model_name))
    
        # Fill in the new column in the pivot table
        cursor.execute("SELECT id, %s_id_old FROM %s" % (model_name, pivot_table))
        for row in cursor:
            id, key = row[0], row[1]
            model_id = models[key]
    
            inner_cursor = connection.cursor()
            inner_cursor.execute(
                "UPDATE %s SET %s_id=%d WHERE id=%d" %
                (pivot_table, model_name, model_id, id))
    
        # Drop the old (renamed) column in pivot table, no longer needed
        cursor.execute(
            "ALTER TABLE %s DROP COLUMN %s_id_old" %
            (pivot_table, model_name))
    
    def do_the_final_lifting(apps, schema_editor):
        # Create a new unique index for the old pk column
        index_prefix = '%s_id' % model_table
        new_index_prefix = '%s_name' % model_table
        new_index_name = index_name.replace(index_prefix, new_index_prefix)
    
        cursor.execute(
            "ALTER TABLE %s ADD UNIQUE KEY %s (%s)" %
            (model_table, new_index_name, 'name'))
    
        # Finally, work on the pivot table
        recreate_constraints_and_indices_in_pivot_table()
    
    1. Appliquer la nouvelle migration

Vous pouvez trouver le code complet dans ce repo . J'ai également écrit à ce sujet dans mon blog .

0
salvalcantara

J'ai dû migrer certaines clés dans mon Django 1.11 application - les anciennes clés étaient déterministes, basées sur un modèle externe. Plus tard cependant, il s'est avéré que ce modèle externe pourrait changer, donc j'avais besoin de mon propres UUID.

Pour référence, je changeais une table de bouteilles de vin spécifiques aux points de vente, ainsi qu'une table de vente pour ces bouteilles de vin.

  • J'ai créé un champ supplémentaire sur toutes les tables pertinentes. Dans la première étape, j'ai dû introduire des champs qui pouvaient être Aucun, puis j'ai généré des UUID pour chacun d'eux. Ensuite, j'ai appliqué une modification via Django où le nouveau champ UUID a été marqué comme unique. Je pouvais commencer à migrer toutes les vues, etc. pour utiliser ce champ UUID comme recherche, de sorte qu'il faudrait moins changé au cours de la prochaine phase, plus effrayante, de la migration.
  • J'ai mis à jour les clés étrangères en utilisant une jointure. (dans PostgreSQL, pas Django)
  • J'ai remplacé toute mention des anciennes clés par les nouvelles clés et l'ai testée dans des tests unitaires, car ils utilisent leur propre base de données de test distincte. Cette étape est facultative pour les cowboys.
  • En accédant à vos tables PostgreSQL, vous remarquerez que les contraintes de clé étrangère ont des noms de code avec des nombres. Vous devez supprimer ces contraintes et en créer de nouvelles:

    alter table pos_winesale drop constraint pos_winesale_pos_item_id_57022832_fk;
    alter table pos_winesale rename column pos_item_id to old_pos_item_id;
    alter table pos_winesale rename column placeholder_fk to pos_item_id;
    alter table pos_winesale add foreign key (pos_item_id) references pos_poswinebottle (id);
    alter table pos_winesale drop column old_pos_item_id;
    
  • Avec les nouvelles clés étrangères en place, vous pouvez ensuite changer la clé primaire, car plus rien n'y fait référence:

    alter table pos_poswinebottle drop constraint pos_poswinebottle_pkey;
    alter table pos_poswinebottle add primary key (id);
    alter table pos_poswinebottle drop column older_key;
    
  • Faux historique de migration .

0
kokociel

Je viens d'essayer cette approche et cela semble fonctionner, pour Django 2.2.2, mais ne fonctionne que pour sqlite. Essayer cette méthode sur d'autres bases de données telles que postgres SQL mais ne fonctionne pas.

  1. Ajouter id=models.IntegerField() au modèle, effectuer des migrations et migrer, fournir une valeur par défaut unique comme 1

  2. Utilisez python Shell pour générer l'identifiant de tous les objets du modèle de 1 à N

  3. supprimez primary_key=True du modèle de clé primaire et supprimez id=models.IntegerField(). Makemigration et vérifiez la migration et vous devriez voir le champ id migrer vers autofield.

Ça devrait marcher.

Je ne savais pas ce que je faisais en mettant la clé primaire dans l'un des champs mais si vous ne savez pas comment gérer la clé primaire, je pense qu'il vaut mieux laisser Django s'en occuper pour vous.

0
Huan Ran Ng