After a wrong choice on a data model, we can be tempted of changing the primary key of this model.
Operation not so easy if this model is referenced by foreign keys in other models.
We propose you to achieve this in 6 steps. For this, we'll start from a simple data application with two models:
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: creation of the new key
The first step consist in creating a new field which will become the new primary key in the end. In our example, we add a field with the type uuid.
Unfortunately, we cannot directly create a field uuid with a unique constraint :
+import uuid
from django.db import models
class Category(models.Model):
...
+ uuid = models.UUIDField(
+ max_length=36,
+ unique=True,
+ default=uuid.uuid4,
+ editable=False
+ )
With the following migration :
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),
),
]
We would have this error during the migration:
django.db.utils.IntegrityError: could not create unique index "data_category_uuid_key"
DETAIL: Key (uuid)=(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) is duplicated.
As explained in the django documentation:
The application of a « simple » migration which adds a unique not null field to a table which contains already some rows, will produce an error. Because the value used to fill existing rows is generated only once, which is contrary to unique constraint.
We will have to edit the migration, in order to create a field of type uuid with null=True, fill this field, then migrate this field to 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: transform the foreign key category into an 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: the uuid field become the primary key
Turn uuid field of Category model into a primary key, without forgotting to add the id=models.IntegerField() field in order to avoid that django delete it automatically.
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: rename the category field to 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: add the foreign key category
Add the foreign key category with null=True and fill it with the content of category_old field.
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. The end !
6th and last step, we only need to delete useless fields from models and transform the foreign key category of Item model to 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
)