我正在尝试在 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 中的硬限制吗?有没有办法像我想要的那样提供课程?
而且,我想要这种构造开始的事实是否表明我在代码设计中犯了错误?我是否以错误的方式看待这个问题——将一些可重用逻辑封装到定义该字段的辅助类中没有意义吗?
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 迁移系统处理默认值方式的限制。
如果您想保持
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))