属性是不是适用于 Django 模型字段?
Posted
技术标签:
【中文标题】属性是不是适用于 Django 模型字段?【英文标题】:Do properties work on Django model fields?属性是否适用于 Django 模型字段? 【发布时间】:2010-11-30 02:18:36 【问题描述】:我认为提出这个问题的最好方法是使用一些代码...我可以这样做吗:
class MyModel(models.Model):
foo = models.CharField(max_length = 20)
bar = models.CharField(max_length = 20)
def get_foo(self):
if self.bar:
return self.bar
else:
return self.foo
def set_foo(self, input):
self.foo = input
foo = property(get_foo, set_foo)
还是我必须这样做:
class MyModel(models.Model):
_foo = models.CharField(max_length = 20, db_column='foo')
bar = models.CharField(max_length = 20)
def get_foo(self):
if self.bar:
return self.bar
else:
return self._foo
def set_foo(self, input):
self._foo = input
foo = property(get_foo, set_foo)
注意:您可以通过将 db_column 传递给模型字段,在数据库中将列名称保留为 'foo'。当您在现有系统上工作并且您不想无缘无故地进行数据库迁移时,这非常有用
【问题讨论】:
我认为这是一个很好的问题,但您应该将您的“答案”作为实际答案发布。 meta.stackexchange.com/questions/17845/…. 如果我重命名字段但使用db_column
保留列名,它仍然希望生成迁移以删除该字段并在我运行makemigrations
时添加一个新字段。我可以删除此迁移,但以后我每次运行makemigrations
时,它都会不断重新出现。有什么办法可以防止这种情况发生?
本质上,这是Django empty field fallback的复制品。
filter by property 的问题也类似,但更通用一些。
【参考方案1】:
如前所述,实现您自己的django.db.models.Field
类的正确替代方法是使用db_column
参数和自定义(或隐藏)类属性。我只是重写@Jiaaro 编辑的代码,遵循python 中更严格的OOP 约定(例如,如果_foo
实际上应该隐藏):
class MyModel(models.Model):
__foo = models.CharField(max_length = 20, db_column='foo')
bar = models.CharField(max_length = 20)
@property
def foo(self):
if self.bar:
return self.bar
else:
return self.__foo
@foo.setter
def foo(self, value):
self.__foo = value
__foo
将被解析为_MyModel__foo
(如dir(..)
所见),因此被隐藏(private)。请注意,这种形式还允许使用 @property decorator,这最终将是编写可读代码的更好方法。
再次,django 将创建_MyModel
表,其中包含两个字段foo
和bar
。
【讨论】:
这很奇怪,但在我的情况下,postgres 上的 django 1.4.5 在这种情况下我得到一个列 foo ......我没有使用 setter 和 getter 并且属性是读取的-只有 你能发布代码吗?我很迷惑。在我的理解中,属性机制是由装饰器“@property”提供的,但如果你没有使用它,那么你所说的“只读属性”必须是一个对象字段 .foo(python 中的属性) .你能或你不能真正为“my_model.foo = 'baz' 分配任何东西吗?? 你将如何访问 foo?MyMode.objects.order_by('__foo')
?
@agconti 数据库查询方法,如 order_by()、filter() 和 get() 不适用于方法属性,因为它们不直接映射到数据库表列。这是他们的强项和弱点。
请注意:--> Django 2.0 给出错误:fields.E002) 字段名称不能包含“__”'【参考方案2】:
模型字段已经是属性,所以我想说你必须用第二种方法来避免名称冲突。
当您定义 foo = property(..)
时,它实际上会覆盖 foo = models..
行,因此该字段将不再可访问。
您需要为属性和字段使用不同的名称。事实上,如果您按照示例 #1 中的方式进行操作,当您尝试访问该属性时,您将获得一个无限循环,因为它现在尝试返回自身。
编辑:也许您还应该考虑不使用_foo
作为字段名称,而是使用foo
,然后为您的属性定义另一个名称,因为属性不能在QuerySet
中使用,因此您需要例如,在进行过滤时使用实际的字段名称。
【讨论】:
关于编辑,这是个好建议 - 我正在维护现有系统,因为必须更改字段名称(或属性名称),我们放弃了这种方法。不过,这项技术在未来可能会很有用:) 不确定是否可以通过闭包或类似的方式访问它 - 诚然,我对闭包了解不多 我在下面提供了 '__foo' 的案例。使用 foo 作为 Field 和 another_foo 作为包装属性将暴露 foo 以供直接操作。尽管 Python 和 X 语言的开发人员应该知道他在做什么 - 隐藏变量被接受为语言的一部分是有原因的。【参考方案3】:这取决于您的property
是达到目的的手段还是目的本身。
如果您在过滤查询集时想要这种“覆盖”(或“回退”)行为(无需先评估它们),我认为属性不能解决问题。 As far as I know,Python 属性在数据库级别不起作用,因此它们不能在查询集过滤器中使用。请注意,您可以在过滤器中使用_foo
(而不是foo
),因为它代表一个实际的表列,但是您的get_foo()
中的覆盖逻辑将不适用。
但是,如果您的用例允许,来自 django.db.models.functions
(docs) 的 Coalesce()
类可能会有所帮助。
Coalesce()
... 接受至少两个字段名称的列表或 表达式并返回第一个非空值(注意空 string 不被视为空值)。 ...
这意味着您可以使用Coalesce('bar','foo')
将bar
指定为foo
的覆盖。这将返回bar
,除非bar
是null
,在这种情况下它返回foo
。与您的get_foo()
相同(除了它不适用于空字符串),但在数据库级别。
剩下的问题是如何实现这一点。
如果您不在很多地方使用它,只需 annotating 查询集可能是最简单的。使用你的例子,没有属性的东西:
class MyModel(models.Model):
foo = models.CharField(max_length = 20)
bar = models.CharField(max_length = 20)
然后像这样进行查询:
from django.db.models.functions import Coalesce
queryset = MyModel.objects.annotate(bar_otherwise_foo=Coalesce('bar', 'foo'))
现在您的查询集中的项目具有魔术属性bar_otherwise_foo
,可以对其进行过滤,例如queryset.filter(bar_otherwise_foo='what I want')
,或者可以直接在实例上使用,例如print(queryset.all()[0].bar_otherwise_foo)
从queryset.query
得到的SQL query 表明Coalesce()
确实在数据库级别工作:
SELECT "myapp_mymodel"."id", "myapp_mymodel"."foo", "myapp_mymodel"."bar",
COALESCE("myapp_mymodel"."bar", "myapp_mymodel"."foo") AS "bar_otherwise_foo"
FROM "myapp_mymodel"
注意:你也可以调用你的模型字段_foo
然后foo=Coalesce('bar', '_foo')
等等。使用foo=Coalesce('bar', 'foo')
会很诱人,但这会引发ValueError: The annotation 'foo' conflicts with a field on the model.
必须有几种方法来创建 DRY 实现,例如编写 custom lookup 或 custom(ized) Manager。
自定义管理器很容易实现如下(参见example in docs):
class MyModelManager(models.Manager):
""" standard manager with customized initial queryset """
def get_queryset(self):
return super(MyModelManager, self).get_queryset().annotate(
bar_otherwise_foo=Coalesce('bar', 'foo'))
class MyModel(models.Model):
objects = MyModelManager()
foo = models.CharField(max_length = 20)
bar = models.CharField(max_length = 20)
现在MyModel
的每个查询集都将自动具有bar_otherwise_foo
注释,可以如上所述使用。
但是,请注意,例如在实例上更新 bar
不会更新注释,因为这是在查询集上进行的。查询集需要首先重新评估,例如通过从查询集中获取更新的实例。
也许结合使用带有注释的自定义管理器和 Python property
可用于两全其美 (example at CodeReview)。
【讨论】:
【参考方案4】:以前的解决方案会受到影响,因为@property 会导致 admin 和 .filter(_foo) 出现问题。
一个更好的解决方案是覆盖 setattr,但这可能会导致从数据库初始化 ORM 对象时出现问题。但是,有一个技巧可以解决这个问题,而且它是通用的。
class MyModel(models.Model):
foo = models.CharField(max_length = 20)
bar = models.CharField(max_length = 20)
def __setattr__(self, attrname, val):
setter_func = 'setter_' + attrname
if attrname in self.__dict__ and callable(getattr(self, setter_func, None)):
super(MyModel, self).__setattr__(attrname, getattr(self, setter_func)(val))
else:
super(MyModel, self).__setattr__(attrname, val)
def setter_foo(self, val):
return val.upper()
秘密是'attrname in self.__dict__'。当模型从 __dict__!
从 new 或 hydrad 初始化时【讨论】:
以上是关于属性是不是适用于 Django 模型字段?的主要内容,如果未能解决你的问题,请参考以下文章