Django迁移RunPython无法调用模型方法

76

我正在使用RunPython方法创建数据迁移。但是,当我尝试在对象上运行方法时,没有一个被定义。是否可以使用RunPython调用在模型上定义的方法?

7个回答

75

模型方法在迁移中不可用,包括数据迁移。

但是有一种解决方法,可以相当于调用模型方法。您可以在迁移中定义函数来模仿要使用的那些模型方法。

如果您有这个方法:

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

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

你可以在迁移脚本中编写函数,例如:

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()

然后在迁移中使用它:

class Migration(migrations.Migration):

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

    operations = [
        migrations.RunPython(save_foo_as_bar)
    ]

这种迁移方式将能够正常工作。尽管代码会有点重复,但并不重要,因为数据迁移只是针对特定应用程序状态的一次性操作。


29
你可以直接说:“复制粘贴你的方法”。这很明显……但并不能解决问题。如果你的方法更加复杂呢?如果你导入了一个调用此方法的函数呢? - Andrey St
9
@AndreySt 实际上很有道理。你的应用程序代码可能随着时间而改变,但如果在任何给定的迁移中使用的代码发生更改,则后续的迁移可能会失败,如果你需要从头开始重建数据库,则可能会导致不一致的状态。 - connorbode
2
Django官方文档相关信息:https://docs.djangoproject.com/en/3.1/topics/migrations/#historical-models - Quitiweb
我花了一点时间才发现apps.get_model()的第一个参数是应用程序名称。请参阅文档https://docs.djangoproject.com/en/4.2/topics/migrations/#data-migrations - undefined

15
你是否按照文档中的要求调用了你的模型?
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()

因为在此时,您无法直接导入您的模型:

数据迁移

from yourappname.models import Person

更新

这个文件 django/db/migrations/state.py 包含了 Django 内部代码,其中的 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)

在“假”的模型实例中,只有克隆的字段:

MyModel.__module__ = '__fake__'

Github Django


18
可以访问模型中的字段,但不能访问模型的方法。 - user2954587
combine_names中,对于有用的评论点赞。我完全忘记了不能直接导入模型,之前曾经在尝试让迁移正常运行时犯了很多难题,直到被提醒才想起这一点。 - Jihoon Baek

14

细节内容在历史模型中描述。

由于不可能序列化任意的 Python 代码,因此这些历史模型将不包含您定义的任何自定义方法。

在迁移过程中遇到它时,没仔细阅读细则,这让我感到非常惊讶,因为它似乎与他们的设计哲学相矛盾(在模型周围添加函数)。


10

从Django 1.8版本开始,您可以通过在模型管理器上设置use_in_migrations = True来使模型管理器在迁移中可用。请参阅迁移文档


1
这对我有用!但请记得运行迁移以更新更改的管理器 - 并将其作为您正在编写的数据迁移的依赖项。 - erikvw
@erikvw,你能详细说明一下吗? - GreenAsJade
这与所问的问题有何关联?您能详细说明一下吗? - sajid

4

这并没有回答问题,但可能对某些人仍然有用。

不仅自定义模型方法在迁移中无法使用,其他模型属性也同样如此,例如用于模型字段choices的类“常量”。请参见文档中的示例。

在这种特定的边缘情况下,在迁移期间我们无法直接访问选择的历史值historical,但是我们可以使用model _meta api从模型字段获取历史值,因为这些值包含在迁移中。

给定Django的Student示例

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,
    )

我们可以在迁移中通过以下方式获取Student.FRESHMAN的历史值:
...
Student = apps.get_model('my_app', 'Student')
YEAR_IN_SCHOOL_CHOICES = Student._meta.get_field('year_in_school').choices
...

0

当你有许多相互调用的复杂方法,并且需要通过对象使用它们时,以下是对我有用的一些东西:

首先将这些模型方法复制到您的迁移文件中

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

def B(self):
    return self.name

def C(self):
    return self.description

然后在您的迁移函数中:

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)
    ]


0
如果你和我一样,因为出现了错误 ValueError: RunPython must be supplied with a callable 而来到这里,那么问题可能是你在给 migrations.RunPython 中的 code 赋值的函数末尾加上了 "()"。
例如:migrations.RunPython(code=do_something(), reverse=noop) 正确的写法应该是:migrations.RunPython(code=do_something, reverse=noop),不需要加上括号 "()"。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接