Update a Django Primary Key to UUID

Photo by Mikołaj on Unsplash

Update a Django Primary Key to UUID

EDIT 3-10-24: As u/heppy pointed out on Reddit, there was an error with my initial implementation. I deleted the author's id before populating the book's new author foreign key, meaning that book's reference to author id no longer held any meaning. I've since fixed this. See the original comment here.

This is a tricky operation, especially if other models have a foreign key to the target model. A short example:

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=255)

class Book(models.Model):
    title = models.CharField(max_length=255)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

It is tricky because Book references an Author via id, which is currently an integer. When it changes to a uuid though, then Book will have an integer reference to a uuid field, and the database will raise errors. We work around this by downgrading the foreign key to a regular IntegerField and slowly massaging it to use the new uuid primary key of Book.

Steps:

  1. Add uuid field to Author while preserving the id field as an integer

  2. Populate uuid in the migration per https://docs.djangoproject.com/en/5.0/howto/writing-migrations/#migrations-that-add-unique-fields

  3. Alter Book's author foreign key reference to Author to be an integer

  4. Make uuid the Author's primary key

  5. Rename Book's author to author_old

  6. Add a new author foreign key field to Book

  7. Create new empty migration

    1. pythonmanage.pymakemigrations --empty library
  8. Add a RunPython step to empty migration that populates author from author_old for each book row

  9. Remove Author'sid

  10. Rename Author's uuid to id

  11. Drop author_old

  12. Make the Book's author null again

  13. Squash the new migrations (https://docs.djangoproject.com/en/5.0/topics/migrations/#migration-squashing). This ensures that the database is not in a weird state if only half of the migrations get applied.

    1. pythonmanage.pysquashmigrations library 0002 0012

    2. Follow the steps stated in the command output and the migration file to copy the custom populate_* functions into the squashed migration

    3. Delete old migrations that the squashed migration replaces

    4. Rename the squashed migration

Bonus points for using ulid (https://www.bitecode.dev/p/python-libs-that-i-wish-were-part) instead of the standard uuid.uuid4!

See the git commit history of my example repository, which can be found at https://github.com/thuibr/django-uuid-migration-example. This is a good way to see the result of each step.