摘要

看来我们可以在Django模型上创建一个property,并为该模型的查询集添加一个完全相同名称的annotation .

例如,我们的 FooBarModelfoo = property(...) ,最重要的是我们可以做 FooBarModel.objects.annotate(foo=...) .

请注意,名称是相同的:如果 foo 是普通属性,而不是 propertyannotate(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 propertyDjango empty field fallbackDo properties work on django model fieldsDjango models and python propertiesCannot 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 方法会覆盖注释?