django自定义字段中,给构造函数传入class类型的入参是无效的吗?

问题描述 投票:0回答:1

我正在尝试在 Django 中定义一个自定义字段,并且我想使用辅助对象来初始化它。字段类接收辅助对象的初始化参数,并将其存储在内部。像这样工作的东西:

# define helper class
class Foo:
   # ...

# instance of helper class
myFoo = Foo()

# model with field accepting helper class
class MyModel(models.Model):
    foo = FooField(foo=myFoo)

在我看来,这是一件非常合理的事情。
但在实践中,它完全破坏了迁移工具。

我看到的情况是这样的:只要任何模型使用这个字段,Django 就会认为它始终处于改变的状态(我假设是因为它不认为不同的对象是等效的)。因此,它一如既往地注册有需要新迁移的更改。

有什么办法解决这个问题,有什么办法表明定义是一致的吗?还是说自定义字段只能用原语初始化是一个全面的限制?
或者也许有一个原因我没有看到将对象实例传递到自定义 Field 实现是一个坏主意,我不应该这样做?

演练和细节

我正在定义一个简单的自定义 Django 字段,如下所示:

class FooField(CharField):
    def __init__(self, foo : Foo, *args, **kwargs) -> None:
        self.foo : Foo = foo
        super().__init__(*args, **kwargs)
    
    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        kwargs["foo"]=self.foo
        return name, path, args, kwargs

其中

Foo
是一个简单的对象,例如:

class Foo:
    def __init__(self, n):
        self.n=n
    def __repr__(self):
        return f"Foo({self.n})"

现在我将这个新的字段类型添加到模型中(已编写支持类

FooSerializer
和方法
default_foo()
,以支持迁移 - 支持下面的代码):

from django.db import models
from .utils.foo_field import Foo, FooField, default_foo

fooA= Foo(1)
fooB = Foo(2)

class MyModel(models.Model):
    n1 = models.IntegerField() # preexisting field
    n2 = models.IntegerField() # preexisting field
    foo1 = FooField(foo=fooA, default=default_foo) # new FooField
    foo2 = FooField(foo=fooB, default=default_foo) # new FooField

运行

makemigrations
会生成一个迁移文件,如预期的那样:

    operations = [
        migrations.AddField(
            model_name='mymodel',
            name='foo1',
            field=myproject.utils.foo_field.FooField(default=myproject.utils.foo_field.default_foo, foo=Foo(1)),
        ),
        migrations.AddField(
            model_name='mymodel',
            name='foo2',
            field=myproject.utils.foo_field.FooField(default=myproject.utils.foo_field.default_foo, foo=Foo(2)),
        ),
    ]

但是,再次运行

makemigrations
将生成 另一个 迁移文件。再次运行它会产生第三个。对于此字段定义,没有最终状态。所有新的迁移将如下所示:

    operations = [
        migrations.AlterField(
            model_name='mymodel',
            name='foo1',
            field=myproject.utils.foo_field.FooField(default=myproject.utils.foo_field.default_foo, foo=Foo(1)),
        ),
        migrations.AlterField(
            model_name='mymodel',
            name='foo2',
            field=myproject.utils.foo_field.FooField(default=myproject.utils.foo_field.default_foo, foo=Foo(2)),
        ),
    ]

问题

这是 Django 中的硬限制吗?有没有办法像我想要的那样提供课程?

而且,我想要这种构造开始的事实是否表明我在代码设计中犯了错误?我是否以错误的方式看待这个问题——将一些可重用逻辑封装到定义该字段的辅助类中没有意义吗?

参考文献

  • 由于默认可调用对象不匹配而导致的无限 AlterField 迁移是一个现有的票证,它帮助我找出当前的问题是什么(尽管我仍然缺少基本原理和解决方法,或者了解为什么不应该这样做)周围)
  • non_db_attrs
    是一个相对较新的属性,可让您标记“不影响列定义的字段属性”。这对这种情况有什么帮助吗?我尝试添加这个并没有看到任何改进,但也许我误解了用法。

支持代码

我的默认 Foo 可调用(默认对象 可以是可调用对象,但不能是可变对象):

def default_foo():
    return Foo(0)

Foo 序列化器(参考此处):

from django.db.migrations.serializer import BaseSerializer
from django.db.migrations.writer import MigrationWriter

class FooSerializer(BaseSerializer):
    def serialize(self):
        return repr(self.value), {"from myproject.utils.foo_field import Foo"}


MigrationWriter.register_serializer(Foo, FooSerializer)
django django-models django-migrations
1个回答
0
投票

回答你的主要问题:你在 Django 中遇到的限制并不是一个硬限制,因为你不能提供一个类作为字段的默认值。然而,这是 Django 迁移系统处理默认值方式的限制。

如果您想保持

default_foo
函数不变,您可以修改模型字段定义以使用 lambda 函数作为默认值,为
default_foo
提供必要的参数。

class FooField(CharField):
    def __init__(self, *args, **kwargs) -> None:
        default = kwargs.get("default", default_foo())
        kwargs["default"] = default_foo_callable(default)
        super().__init__(*args, **kwargs)
    
    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        if kwargs.get("default") == default_foo_callable():
            del kwargs["default"]
        return name, path, args, kwargs

def default_foo_callable(value=0):
    return lambda: default_foo(value)

def default_foo(n):
    return Foo(n)

修改模型定义以使用可调用对象作为默认值:

class MyModel(models.Model):
    n1 = models.IntegerField()
    n2 = models.IntegerField()
    foo1 = FooField(default=default_foo_callable(1))
    foo2 = FooField(default=default_foo_callable(2))
© www.soinside.com 2019 - 2024. All rights reserved.