76

I'm creating a data migration using the RunPython method. However when I try to run a method on the object none are defined. Is it possible to call a method defined on a model using RunPython?

chhantyal
  • 11,874
  • 7
  • 51
  • 77
user2954587
  • 4,661
  • 6
  • 43
  • 101

7 Answers7

74

Model methods are not available in migrations, including data migrations.

However there is workaround, which should be quite similar to calling model methods. You can define functions inside migrations that mimic those model methods you want to use.

If you had this method:

class Order(models.Model):
    '''
    order model def goes here
    '''

    def get_foo_as_bar(self):
        new_attr = 'bar: %s' % self.foo
        return new_attr

You can write function inside migration script like:

def get_foo_as_bar(obj):
    new_attr = 'bar: %s' % obj.foo
    return new_attr


def save_foo_as_bar(apps, schema_editor):
    old_model = apps.get_model("order", "Order")

    for obj in old_model.objects.all():
        obj.new_bar_field = get_foo_as_bar(obj)
        obj.save()

Then use it in migrations:

class Migration(migrations.Migration):

    dependencies = [
        ('order', '0001_initial'),
    ]

    operations = [
        migrations.RunPython(save_foo_as_bar)
    ]

This way migrations will work. There will be bit of repetition of code, but it doesn't matter because data migrations are supposed to be one time operation in particular state of an application.

devnull
  • 393
  • 4
  • 20
chhantyal
  • 11,874
  • 7
  • 51
  • 77
  • 28
    You could just say: "copy+paste your methods". It's obvious... but doesn't solve the problem. What if your method is more complex? What if you import a function that calls this method...? – Andrey St Sep 09 '19 at 17:03
  • 9
    @AndreySt it makes sense actually. your application code might change over time.. but if the code used in any given migration changes, subsequent migrations might fail, leaving you in an inconsistent state if you need to rebuild your database from scratch. – connorbode Apr 12 '20 at 04:54
  • 2
    Django doc official info related to this: https://docs.djangoproject.com/en/3.1/topics/migrations/#historical-models – Quitiweb Sep 22 '20 at 11:29
15

did you call your model like said in the documentation ?

def combine_names(apps, schema_editor):
    # We can't import the Person model directly as it may be a newer
    # version than this migration expects. We use the historical version.
    Person = apps.get_model("yourappname", "Person")
    for person in Person.objects.all():
        person.name = "%s %s" % (person.first_name, person.last_name)
        person.save()

Data-Migration Because at this point, you can't import your Model directly :

from yourappname.models import Person

Update

The internal Django code is in this file django/db/migrations/state.py django.db.migrations.state.ModelState#construct_fields

def construct_fields(self):
    "Deep-clone the fields using deconstruction"
    for name, field in self.fields:
        _, path, args, kwargs = field.deconstruct()
        field_class = import_string(path)
        yield name, field_class(*args, **kwargs)

There is only fields that are clones in a "fake" model instance:

MyModel.__module__ = '__fake__'

Github Django

devnull
  • 393
  • 4
  • 20
Azman0101
  • 310
  • 2
  • 6
  • 18
    yes. you can access fields on a model but not the models methods – user2954587 Feb 28 '15 at 15:55
  • Upvoted for the useful comment in ```combine_names```. I'd totally forgotten that you can't import the model directly - did a lot of head-scratching trying to get migrations to work before I was reminded of this. – Jihoon Baek Feb 18 '19 at 04:37
14

The fine print is laid in Historical Models

Because it’s impossible to serialize arbitrary Python code, these historical models will not have any custom methods that you have defined.

It was quite a surprise when I first encountered it during migration and didn't read the fine print because it seems to contradict their Design Philosophy (adding functions around models)

user2829759
  • 3,372
  • 2
  • 29
  • 53
10

As of Django 1.8, you can make model managers available to migrations by setting use_in_migrations = True on the model manager. See the migrations documentation.

Ryan Knight
  • 1,288
  • 1
  • 11
  • 18
4

This does not answer the OP, but might still be of use to someone.

Not only are custom model methods unavailable in migrations, but the same holds for other model attributes, such as class "constants" used for model field choices. See examples in the docs.

In this specific edge case, we cannot access the historical values of the choices directly, during migration, but we can get the historical values from the model field, using the model _meta api, because those values are contained in migrations.

Given Django's Student example:

class Student(models.Model):
    FRESHMAN = 'FR'
    ...
    YEAR_IN_SCHOOL_CHOICES = [(FRESHMAN, 'Freshman'), ...]
    year_in_school = models.CharField(
        max_length=2,
        choices=YEAR_IN_SCHOOL_CHOICES,
        default=FRESHMAN,
    )

We can get the historic value of Student.FRESHMAN inside a migration as follows:

...
Student = apps.get_model('my_app', 'Student')
YEAR_IN_SCHOOL_CHOICES = Student._meta.get_field('year_in_school').choices
...
djvg
  • 11,722
  • 5
  • 72
  • 103
0

Something useful that worked for me when you have many complex methods calling each other and you need them available via your object:

First copy those model methods over into your migration file

def A(self):
    return self.B() + self.C()

def B(self):
    return self.name

def C(self):
    return self.description

Then in your migration function:

def do_something_to_your_objects(apps, schema_editor):
    MyModel = apps.get_model("my_app", "MyModel")
    MyModel.A = A
    MyModel.B = B
    MyModel.C = C
    
    for my_object in MyModel.objects.all():
         my_object.name_and_decription = my_object.C()
         my_object.save()

class Migration(migrations.Migration):

    dependencies = [
        ('initial', '0001_initial'),
    ]

    operations = [
        migrations.RunPython(do_something_to_your_objects)
    ]

rymanso
  • 869
  • 1
  • 9
  • 21
0

If you are like me, who came here because you got the error ValueError: RunPython must be supplied with a callable It's because you put "()" at the end of the function that you are assigning to code in migrations.RunPython

Error e.g. migrations.RunPython(code=do_something(), reverse=noop)

It should be: migrations.RunPython(code=do_something, reverse=noop) without the ()

Benson Mathew
  • 119
  • 2
  • 7