Django Migration "gotcha"

The Model Class used by SomeModel = apps.get_model('some_app', 'SomeModel') is not the model class you wrote.

Consider this example.

Inventory(models.Model):
    quantity = models.IntegerField()

LogEntry(models.Model):
    text = models.TextField()
    created = models.DateTimeField(auto_now_add=True)
    inventory = models.ForeignKey(Inventory, related_name="logs")

    def __unicode__(self):
        return self.text

We want to cache the last logentry as a field on Inventory. Don't ask why, we just do.

We'll change the model like this:

Inventory(models.Model):
    quantity = models.IntegerField()
    last_log_entry = models.TextField()

LogEntry(models.Model):
    text = models.TextField()
    created = models.DateTimeField(auto_now_add=True)
    inventory = models.ForeignKey(Inventory, related_name="logs")

    def __unicode__(self):
        return self.text

    def save(self, *args, **kwargs):
        super(LogEntry, self).save(*args, **kwargs)
        self.inventory.last_log_entry = unicode(self)
        self.inventory.save()

Which is great! It does what we thought. Now let's make a migration.

$> python manage.py makemigrations

This makes the schema migration to add our new field, but we should backfill the data.

$> python manage.py makemigrations inventory --empty

This makes the data migration. You might think it would look something like this:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations


def forwards(apps, schema_editor):
    Inventory = apps.get_model('inventory', 'Inventory')
    for inv in Inventory.objects.all():
        log = inv.logs.order_by('-created').first()
        inv.last_log_text = unicode(log)
        inv.save()


class Migration(migrations.Migration):

    dependencies = [
        ('inventory', '0003_add_last_log_text'),
    ]

    operations = [
         migrations.RunPython(forwards),
    ]

But then you run the migration, and all of your Inventory objects have the following for last_log_text

<LogEntry: >

If you read the opening sentence, you're probably a little ahead of me right now. Your Model class is not the Model class that django uses during migrations.

Your forwards function should have been:

def forwards(apps, schema_editor):
    Inventory = apps.get_model('inventory', 'Inventory')
    for inv in Inventory.objects.all():
        log = inv.logs.order_by('-created').first()
        inv.last_log_text = log.text
        inv.save()

Edit: This is sort of covered here:


Comments and Messages

I won't ever give out your email address. I don't publish comments but if you'd like to write to me then you could use this form.

Issac Kelly