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
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.
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.
-
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.
Créez le nouveau schéma. Cela ne créera que les tables qui changent.
-
É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.
Renommez toutes vos anciennes tables.
Créez l'ensemble du nouveau schéma.
É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.
Supprimez les anciennes tables renommées.
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.
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)
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:
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.
-
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
Supprimez la clé étrangère et la table D'emplacement avec un la migration
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'...
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
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...
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:
- 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. -
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)]
-
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) ), ]
-
É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
Champclass 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>'), ]
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:
-
Modifiez la classe modèle comme ceci:
class Something(models.Model): name = models.CharField(max_length=64, unique=True)
-
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), ]
Où
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()
- Appliquer la nouvelle migration
, Vous pouvez trouver le code complet dans cette repo. J'ai aussi écrit à ce sujet dans mon blog.