Suite à un mauvais choix au niveau du modèle de données, on peut être amené à vouloir changer la clé primaire d'un modèle.
Opération pas si simple que ça si ce modèle est référencé par des clés étrangères dans d'autres modèles.
Nous vous proposons de la réaliser en 6 étapes, pour cela nous allons partir d'une simple application data avec deux modèles :
from django.db import models
class Category(models.Model):
name = models.CharField(max_length=32)
class Item(models.Model):
category = models.ForeignKey(
Category,
related_name='items',
on_delete=models.CASCADE
)
1. Category: création de la nouvelle clé
La première étape consiste à créer un nouveau champ qui deviendra la nouvelle clé primaire à la fin. Dans notre exemple on ajoute un champ de type uuid.
Malheureusement on ne peut pas créer directement un champ uuid unique :
+import uuid
from django.db import models
class Category(models.Model):
...
+ uuid = models.UUIDField(
+ max_length=36,
+ unique=True,
+ default=uuid.uuid4,
+ editable=False
+ )
Avec la migration suivante:
class Migration(migrations.Migration):
dependencies = [
('data', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='category',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
),
]
Nous aurions cette erreur pendant la migration:
django.db.utils.IntegrityError: could not create unique index "data_category_uuid_key"
DETAIL: Key (uuid)=(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) is duplicated.
Comme expliqué dans la documentation django:
L’application d’une « simple » migration qui ajoute un champ unique non nul à une table qui contient déjà des enregistrements, va produire une erreur. Car la valeur utilisée pour remplir les enregistrements existants n’est générée qu’une seule fois, ce qui va à l’encontre de la contrainte d’unicité.
Nous allons donc devoir éditer la migration pour créer un champ de type uuid avec null=True, renseigner ce champ puis le passer à null=False.
def create_uuid(apps, schema_editor):
Category = apps.get_model('data', 'Category')
for category in Category.objects.all():
category.uuid = uuid.uuid4()
category.save()
class Migration(migrations.Migration):
dependencies = [
('data', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='category',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, editable=False, null=True),
),
migrations.RunPython(create_uuid, reverse_code=migrations.RunPython.noop),
migrations.AlterField(
model_name='category',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
)
]
2. Item: transformer la clé étrangère category en IntegerField
class Item(models.Model):
- category = models.ForeignKey(
- Category,
- related_name='items',
- on_delete=models.CASCADE
- )
+ category = models.IntegerField()
class Migration(migrations.Migration):
dependencies = [
('data', '0002_...'),
]
operations = [
migrations.AlterField(
model_name='item',
name='category',
field=models.IntegerField(),
),
]
3. Category: le champ uuid devient la clé primaire
Passer le champ uuid du modèle Category en clé primaire, sans oublier d'ajouter le champ id=models.IntegerField() pour éviter que django le supprime automatiquement.
class Category(models.Model):
uuid = models.UUIDField(
max_length=36,
- unique=True,
+ primary_key=True,
default=uuid.uuid4,
editable=False
)
+ id = models.IntegerField()
class Migration(migrations.Migration):
dependencies = [
('data', '0003_...'),
]
operations = [
migrations.AlterField(
model_name='category',
name='id',
field=models.IntegerField(),
),
migrations.AlterField(
model_name='category',
name='uuid',
field=models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False
),
),
]
4. Item: renommer le champ category en category_old
class Item(models.Model):
- category = models.IntegerField()
+ category_old = models.IntegerField()
class Migration(migrations.Migration):
dependencies = [
('data', '0004_...'),
]
operations = [
migrations.RenameField(
model_name='item',
old_name='category',
new_name='category_old',
),
]
5. Item: ajouter la clé étrangère category
Ajouter la clé étrangère category avec null=True et le remplir à l'aide du champ category_old.
class Item(models.Model):
...
+ category = models.ForeignKey(
+ Category,
+ related_name='items',
+ on_delete=models.CASCADE,
+ null=True
+ )
def copy_category_uuid(apps, schema_editor):
Item = apps.get_model('data', 'Item')
Category = apps.get_model('data', 'Category')
category_uuid = Category.objects.filter(id=models.OuterRef('category_old')) \
.values_list('uuid')[:1]
Item.objects.update(category=models.Subquery(category_uuid))
class Migration(migrations.Migration):
dependencies = [
('data', '0005_auto_20191002_0909'),
]
operations = [
migrations.AddField(
model_name='item',
name='category',
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='items',
to='data.Category'
),
),
migrations.RunPython(
copy_category_uuid,
reverse_code=migrations.RunPython.noop
),
]
6. Fin !
6ème et dernière étape, il ne reste plus qu'à supprimer les champs inutiles des modèles et que la clé étrangère category du modèle Item soit à null=False.
class Category(models.Model):
name = models.CharField(max_length=32)
uuid = models.UUIDField(
max_length=36,
primary_key=True,
default=uuid.uuid4,
editable=False
)
- id = models.IntegerField()
class Item(models.Model):
- category_old = models.IntegerField()
category = models.ForeignKey(
Category,
related_name='items',
on_delete=models.CASCADE,
- null=True
)