使用属性和查询集注释在Django中模拟按属性过滤

时间:2018-12-10 12:21:25

标签: python django properties django-queryset

摘要

看来我们可以在Django模型上创建一个property,然后 向该查询集添加具有完全相同名称annotation模型。

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

请注意,名称相同:如果foo是普通属性,而不是property,则annotate(foo=...)将引发ValueError。在这种情况下,不会出现此类错误。

初步测试表明,这种方法行之有效,使我们能够创建例如准可过滤属性。

我想知道的是:这是一个好方法,还是可能导致某种冲突或意想不到的意外?

背景

我们有一个现有的数据库,该数据库的FooBarModel字段为bar,在我们的项目代码中的许多地方都使用了该数据库,主要用于过滤FooBarModel查询集。

现在,由于某种原因,我们需要向模型添加一个新字段foo,而不是bar字段。 bar字段仍然有目的,因此我们也要保留该目的。如果尚未设置foo(例如,对于现有数据库条目),我们将退回到bar

这必须同时适用于新的(未保存的)模型实例和查询集。

注意:尽管对于下面提供的特定示例,一个简单的data migration可以作为替代解决方案(备用),但是在更复杂的情况下,数据迁移是不可行的。

实施

现在,为了完成这项工作,我们使用foo property_foo模型字段,set_foo方法和get_foo来实现模型提供后备逻辑的方法(如果尚未设置bar,则返回_foo

但是,据我所知,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')

现在,我们可以在新的(未保存的)foo实例上使用FooBarModel属性,例如FooBarModel(bar='some old value').foo,我们也可以使用FooBarModel过滤foo个查询集,例如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中的“后备”逻辑需要在自定义管理器中复制(以Coalesce的形式)。

我想也可以在批注部分使用Django的conditional expressions,将此“属性+批注”模式应用于更复杂的property逻辑。

问题

上面的方法模拟了可过滤的模型属性。但是,名为foo的注释遮盖了名为property的{​​{1}},因此我不确定这是否会在某些时候导致冲突或其他意外情况。有人知道吗?

如果我们向查询集添加“正常”注释,即与属性名称(例如foo不匹配)的z会显示为属性(当我对该查询集的实例调用z时)。但是,如果我们在vars()的实例上使用vars(),它将显示名为FooBarModel.objects no 属性。这是否意味着foo方法会覆盖注释?

0 个答案:

没有答案