摘要
看来我们可以在Django模型上创建一个property,并为该模型的查询集添加一个完全相同名称的annotation .
例如,我们的 FooBarModel
有 foo = property(...)
,最重要的是我们可以做 FooBarModel.objects.annotate(foo=...)
.
请注意,名称是相同的:如果 foo
是普通属性,而不是 property
, annotate(foo=...)
将引发 ValueError
. 在这种情况下,没有这样的错误 .
初步测试表明这种方法有效,允许我们创建例如准可过滤 property .
我想知道的:这是一个好方法,还是会导致冲突或某种意外的惊喜?
背景
我们有一个带有 FooBarModel
with field bar
的现有数据库,它在我们项目代码的许多地方使用,主要用于过滤 FooBarModel
查询集 .
现在,出于某种原因,我们需要在我们的模型中添加一个新字段 foo
,该字段应该用于代替 bar
字段 . bar
字段仍然有用,所以我们也需要保留它 . 如果尚未设置 foo
(例如,对于现有数据库条目),我们将回退到 bar
.
这必须适用于新的(未保存的)模型实例和查询集 .
注意:虽然简单的data migration将是下面提供的特定示例(回退)的替代解决方案,但是存在更复杂的情况,其中数据迁移不是一种选择 .
实施
现在,为了使这项工作,我们使用 foo
property
, _foo
模型字段, set_foo
方法和提供回退逻辑的 get_foo
方法实现模型(如果尚未设置 _foo
,则返回 bar
) .
但是,据我所知, property
不能在查询集过滤器中使用,因为 foo
不是实际的数据库字段( _foo
是,但是没有回退逻辑) . 以下SO问题似乎支持这一点:Filter by property,Django empty field fallback,Do properties work on django model fields,Django models and python properties,Cannot resolve keyword
因此,我们添加了一个自定义管理器,它使用Coalesce类注释初始查询集 . 这会在数据库级别上复制 get_foo
(空字符串除外)的回退逻辑,并可用于过滤 .
最终结果如下(在Python 2.7 / 3.6和Django 1.9 / 2.0中测试):
from django.db import models
from django.db.models.functions import Coalesce
class FooManager(models.Manager):
def get_queryset(self):
# add a `foo` annotation (with fallback) to the initial queryset
return super(FooManager,self).get_queryset().annotate(foo=Coalesce('_foo', 'bar'))
class FooBarModel(models.Model):
objects = FooManager() # use the extended manager with 'foo' annotation
_foo = models.CharField(max_length=30, null=True, blank=True) # null=True for Coalesce
bar = models.CharField(max_length=30, default='something', blank=True)
def get_foo(self):
# fallback logic
if self._foo:
return self._foo
else:
return self.bar
def set_foo(self, value):
self._foo = value
foo = property(fget=get_foo, fset=set_foo, doc='foo with fallback to bar')
现在我们可以在新的(未保存的) FooBarModel
实例上使用 foo
属性,例如 FooBarModel(bar='some old value').foo
,我们也可以使用 foo
过滤 FooBarModel
查询集,例如 FooBarModel.objects.filter(foo='what I'm looking for')
.
注意:不幸的是,后者在related objects上过滤时似乎不起作用,例如: SomeRelatedModel.objects.filter(foobar__foo='what I'm looking for')
,因为在这种情况下manager is not used .
这种方法的一个缺点是 get_foo
中的"fallback"逻辑需要在自定义管理器中复制(以 Coalesce
的形式) .
我想也可以将这个"property+annotation"模式应用于更复杂的 property
逻辑,在注释部分使用Django的conditional expressions .
问题
上面的方法模拟了可过滤的模型属性 . 但是,名为 foo
的注释会将 property
命名为 foo
,我不确定这是否会在某些时候导致冲突或其他意外 . 有人知道吗?
如果我们向查询集添加一个"normal"注释,即一个与属性名称不匹配的注释,例如 z
,则 z
显示为属性(当我在该查询集的实例上调用 vars()
时) . 但是,如果我们在 FooBarModel.objects
的实例上使用 vars()
,则它不会显示名为 foo
的属性 . 这是否意味着 get_foo
方法会覆盖注释?