Recently, I took on the task of internationalizing a Django app. The app included the following two simple models for tagging user Profile
s with a set of pre-defined Tag
s:
class Profile(models.Model):
name = models.CharField()
tags = models.ManyToManyField('Tag', blank=True)
class Tag(models.Model):
text = models.CharField(max_length=255)
slug = models.SlugField(primary_key=True)
“Wait a minute…”, I thought to myself while scrolling through the models.py
module for the first time. “How am I supposed to add a translation for slug
when it is the primary key?” After all, multilingual URLs were part of the requirement and so all slugs used in URLs had to be translatable as well.
I figured that the cleanest option was adding a translation slug
field for each supported language and introducing a surrogate key instead of having a SlugField
as the primary key.
As a wise Stack Overflow user once put it, changing the primary key of a model is like “doing open-heart surgery”. If you have very complex and/or intertwined models with lots of instances, it’s probably best to follow the approach presented in the linked Stack Overflow answer.
If, on the other hand, you are dealing with only a few models and instances, feel free to try out the following steps to achieve the task of replacing a Django model’s natural key with a surrogate key. It goes without saying that you should have a database backup ready and test everything multiple times before you migrate your production database.
- Create a second, nearly identical model with a surrogate primary key by copy-and-pasting the old model:
class Tag(models.Model):
text = models.CharField(max_length=255)
slug = models.SlugField(primary_key=True)
class Tag2(models.Model):
text = models.CharField(max_length=255)
slug = models.SlugField(unique=True)
- Add a new
ForeignKey('Tag2', …)
orManyToManyField('Tag2', …)
field to each model that already has a foreign key pointing to the originalTag
model (e.g.Profile
):
class Profile(models.Model):
name = models.CharField()
tags = models.ManyToManyField('Tag', blank=True, related_name='profiles')
tags2 = models.ManyToManyField('Tag2', blank=True, related_name='profiles_2')
- Generate a new migration, but don’t run it yet:
[zepp@arch myapp]$ ./manage.py makemigrations myapp
- Instead, add a
RunPython
operation to the newly created migration. This operation should create one instance of the new model for each instance of the old model, thereby basically converting the old data to the new format:
from django.db import migrations, models
import django.db.models.deletion
def convert_tags(apps, schema_editor):
Tag = apps.get_model('myapp', 'Tag')
Tag2 = apps.get_model('myapp', 'Tag2')
for tag in Tag.objects.all():
tag_2 = Tag2.objects.create(text=tag.text, slug=tag.slug, set=tag.set)
for p in tag.profiles.all():
p.tags2.add(tag_2)
class Migration(migrations.Migration):
dependencies = [
('myapp', '0002_auto_20200402_0423'),
]
operations = [
migrations.CreateModel(
name='Tag2',
fields=[
('text', models.CharField(max_length=255)),
('slug', models.SlugField(unique=True)),
],
options={
'verbose_name': 'Tag',
'verbose_name_plural': 'Tags',
'abstract': False,
},
),
migrations.AddField(
model_name='profile',
name='tags2',
field=models.ManyToManyField(blank=True, related_name='profiles_2', to='myapp.Tag2'),
),
),
migrations.RunPython(convert_tags),
]
- At this point, you have essentially copied the original model (
Tag
) as well as its instances into a new model (Tag2
) with a surrogate primary key. - It’s now time to get rid of the old model and data by deleting the original model. You’ll probably have to comment out a few lines of code so as to make the
makemigrations
command run again. It’s probably a good idea to mark those lines with a specific comment so you can find them again quickly. Once you are done, run themakemigrations
command:
[zepp@arch myapp]$ ./manage.py makemigrations myapp
Migrations for 'myapp':
myapp/migrations/0004_auto_20200426_0705.py
- Remove field tags from profile
- Delete model Tag
- Next, rename the new model to have the same name as the old one:
[zepp@arch myapp]$ ./manage.py makemigrations myapp
Did you rename the myapp.Tag2 model to Tag? [y/N] y
Migrations for 'myapp':
myapp/migrations/0005_auto_20200426_0707.py
- Rename model Tag2 to Tag
- The same step is necessary for the fields referencing
Tag2
(e.g.tags2
onProfile
):
[zepp@arch myapp]$ ./manage.py makemigrations myapp
Did you rename profile.tags2 to profile.tags (a ManyToManyField)? [y/N] y
Migrations for 'myapp':
myapp/migrations/0006_auto_20200426_0709.py
- Rename field tags2 on profile to tags
- Uncomment the lines of code you commented out earlier and, if applicable, adapt any other places in your code that rely on the old (natural) primary key.
- Finally, run the
migrate
Django command to apply the created migrations:
[zepp@arch myapp]$ ./manage.py migrate myapp
Operations to perform:
Apply all migrations: myapp
Running migrations:
Applying myapp.0003_auto_20200426_0659... OK
Applying myapp.0004_auto_20200426_0705... OK
Applying myapp.0005_auto_20200426_0707... OK
Applying myapp.0006_auto_20200426_0709... OK