DEV Community

loading...
Cover image for Detecting code that will break Django migrations

Detecting code that will break Django migrations

djangodoctor profile image Django Doctor Updated on ・2 min read

Can you see the problems with this Django migration?

# 0006_populate_has_chickens.py
from django.db import migrations
from territory import models

def forwards(apps, schema_editor):
    for item in in models.ChickenCoopLocations.objects.all():
        item.has_chickens = does_have_chickens(item.pk)
        item.save()

class Migration(migrations.Migration):
    dependencies = [("cases", "0005_auto_does_have_chickens.py")]
    operations = [migrations.RunPython(forwards)]


def does_have_chickens(pk):
    ...
Enter fullscreen mode Exit fullscreen mode

That's right - no backwards migrations is specified, and directly it's importing from models.py. Lets delve into why the latter is bad for maintainability.

Out of step

The fields in Django's models.py must agree with the schema in the database. When Django performs database read or write operations it uses the shape of the model in models.py to determine what fields to SELECT and INSERT. If models.py includes fields that are not yet in the database schema then the database will throw an error.

This is easily missed if the code is reviewed while in a rush, because when 0006_populate_has_chickens is ran this bug will not happen. Indeed, at that point in time models.py does agree with the schema, but what happens when in one weeks time your team member adds a new field to ChickenCoopLocations? From that point on whenever 0006_populate_has_chickens is ran, models.py will have a have a field that the database schema does not yet have, so migrations will fail. This will happen when setting up a new database when all the migrations run from scratch such as in the CI, when a new developer joins the team, or when you replaced your bricked laptop.

Future proof

In 0006_populate_has_chickens.py it's better to use apps.get_model, which asks Django to construct a simplified time-traveling model whose fields will reflect the fields in the database even if models.py is vastly out of step with the schema:

# 0006_populate_has_chickens.py
from django.db import migrations

def forwards(apps, schema_editor):
    ChickenCoopLocations = apps.get_model("territory", "ChickenCoopLocations")
    for item in in ChickenCoopLocations.objects.all():
        item.has_chickens = does_have_chickens(item.pk)
        item.save()

class Migration(migrations.Migration):
    dependencies = [("cases", "0005_auto_foo.py")]
    operations = [migrations.RunPython(forwards)]

def does_have_chickens(pk):
    ...
Enter fullscreen mode Exit fullscreen mode

So directly importing models in migrations is flaky and in a few migrations time will probably fail because during migrations the code in models.py is out of step with the database schema: the models.py can have a field defined that does not yet exist in the database because the required migration has not yet ran.

Does your codebase import use live models.py in migrations?

Over time it's easy for tech debt to slip into your codebase. I can check that for you at django.doctor, or can review your GitHub PRs:

Alt Text

Or try out Django refactor challenges.

Discussion

pic
Editor guide