Detecting code that will break Django migrations
Tags: django, python, webdev, codequality
Published: 2020-11-09 13:50:27

Can you see the problems with this Django migration?

```py
# 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):
    ...
```

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:

```py
# 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):
    ...
```


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](https://django.doctor), or can [review your GitHub PRs](https://github.com/marketplace/django-doctor/):

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/d10epulva9d373jgldhu.png)

Or try out [Django refactor challenges](https://django.doctor/challenge).