Django: making backfills in migrations backward compatible

Adrienne Domingus
4 min readMay 9, 2020

As teams and codebases grow, things that seem straightforward can become more complex. For example, a series of database schema changes that when deployed independently and in order, work exactly as expected, can cause other developers issues when trying to get their local environments caught up. Let’s look at one example of this in Django, along with its solution: deprecating a database field, but using it as part of a backfill prior to its removal from the database schema.

The Scenario

Your workflow when deprecating a field in favor of a new one might look something like this:

In the first PR:

  • Add the new field (nullable)
  • Backfill the new field from the old one
  • Make the old field nullable if it wasn’t already, to prepare for it’s removal
  • Make sure any write paths write to both fields

In the second PR:

  • Make the new field not-nullable, if desired
  • Remove dual writes from relevant code paths
  • Deprecate the old field (depending on your deployment process, you may need to deprecate the field from code separately from deprecating it from the database schema in order to not cause downtime)

In an example, say we have a Student model, with an email field on it, but now we need to support the possibility that a student has multiple email addresses. As a first step, we might want to migrate our existing email field to a new one called primary_email.

The migration file in our first PR might look like this:

# -*- coding: utf-8 -*-from django.db import migrations, models
from django.db.models import F
from student.models import Student
def backfill_display_name(apps, schema_editor):
Student.objects.update(primary_email=F('email'))
class Migration(migrations.Migration): dependencies = [
...
]
operations = [
migrations.AddField(
model_name='student',
name='primary_email'

--

--