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

J'ai une application qui est en mode bêta. Le modèle de cette application a quelques classes avec une primary_key explicite. Par conséquent, Django utilise les champs et ne crée pas automatiquement d'id.

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

, je pense que c'était une mauvaise idée (voir unicode erreur lors de la sauvegarde d'un objet dans django admin) et j'aimerais passer en arrière et avoir un id 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 (remplacer chaque primary_key = True par db_index=True) et Je veux migrer la base de données avec sud - .

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. Toutes les suggestions?

Merci pour votre aide

29
demandé sur Community 2010-01-13 13:00:16

6 réponses

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

La clé primaire formelle doit toujours être une clé de substitution. Jamais rien d'autre. [Mots forts. Concepteur de base de données depuis les années 1980. Important lessoned appris est ceci: tout est modifiable, même lorsque les utilisateurs jurent sur les tombes de leurs mères que la valeur ne peut pas être modifiée est vraiment une clé naturelle qui peut être prise comme primaire. Il n'est pas primaire. Seuls les substituts peuvent être primaires.]

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. Utiliser le propre interne de Django django-admin.py outils pour cela. Vous devez créer un fichier de déchargement pour chaque change et chaque table qui dépend d'une clé qui est en cours de création. Des fichiers séparés rendent cela légèrement plus facile à faire.

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

    Tables qui dépendent de ces les tables auront leur FK changé; vous pouvez soit mettez à jour les lignes en place ou-il peut être plus simple-de supprimer et de réinsérer ces lignes, aussi.

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

  4. Écrire des scripts pour lire et recharger les données avec les nouvelles clés. Ce sont de 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 de la ligne de tuple JSON les objets qui ont été créés pour vous. Vous pouvez ensuite les insérer dans la base de données.

    Vous avez deux cas.

    • Les Tables avec le changement de PK changé seront insérées et obtiendront de nouveaux PK. ceux-ci doivent être "cascadés" à d'autres tables pour s'assurer que les FK de L'autre table sont également modifiés.

    • Les Tables avec FK de cette modification devront localiser la ligne dans la table étrangère et mettre à jour leur FK référence.

Alternative.

  1. Renommez toutes vos anciennes tables.

  2. Créez l'ensemble 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.

 

56
répondu S.Lott 2015-08-28 23:10:59

Pour changer la clé primaire avec south, vous pouvez utiliser south.DB.commande create_primary_key dans datamigration. Pour changer votre CharField PK personnalisé en AutoField standard, vous devez faire:

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 à 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 la migration Sud automatique et migrer:

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

3) Créer un nouveau datamigration

./manage.py datamigration <your_appname> fill_id

Dans tis datamigration 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 (si ce n'est pas nécessaire) créer la migration automatique et migrer.

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

6) dernière étape-restaurer les relations clés fireign. Créez à nouveau un champ FK réel et supprimez votre fake_fk champ, créer 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.

8
répondu user920391 2013-11-29 09:32:29

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

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

  • ajoute 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)
  • marque la nouvelle colonne clé primaire et supprime l'ancien pk colonne si elle est devenue inutile (migration ddl)
6
répondu Tobu 2012-07-07 21:49:40

J'ai eu le même problème aujourd'hui et je suis arrivé à une solution inspirée par les 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 à"Location".

Maintenant, je veux corriger cette erreur et donner à L'emplacement un primaire auto-incrémenté ordinaire clé.

Mesures prises:

  1. Créer un Charfield ScheduledMeasurement.temp_location_unique_id et un modèle TempLocation, et les migrations pour les créer. TempLocation a la structure que je veux que L'emplacement ait.

  2. Créer 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 à TempLocation

  3. Supprimez la clé étrangère et la table D'emplacement avec un la migration

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

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

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

Et éditer tout le code qui suppose unique_id était unique (tous les objets.get (unique_id=...) des trucs), et qui a utilisé unique_id autrement...

6
répondu RemcoGerlich 2012-09-03 12:08:00

J'ai réussi à le faire avec les migrations django 1.10.4 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. Voici comment:

  1. ajout d'un 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 un 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 pour 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>'),
        ]
    
3
répondu Nikolay Markov 2017-10-01 22:00:30

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 étapes que j'ai prises étaient:

  1. Modifiez la classe 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 cette repo. J'ai aussi écrit à ce sujet dans mon blog.

1
répondu salvalcantara 2017-09-09 05:31:45