使用相关模型的自定义管理器过滤Django相关字段

10
如何在通过相关字段筛选时,应用自定义管理器查询集中的注释和过滤器?以下是一些代码以演示我的意思。
管理器和模型:
from django.db.models import Value, BooleanField

class OtherModelManager(Manager):
    def get_queryset(self):
        return super(OtherModelManager, self).get_queryset().annotate(
            some_flag=Value(True, output_field=BooleanField())
        ).filter(
            disabled=False
        )

class MyModel(Model):
    other_model = ForeignKey(OtherModel)

class OtherModel(Model):
    disabled = BooleanField()

    objects = OtherModelManager()

尝试使用管理器过滤相关字段
# This should only give me MyModel objects with related 
# OtherModel objects that have the some_flag annotation 
# set to True and disabled=False
my_model = MyModel.objects.filter(some_flag=True)

如果你尝试上面的代码,你会得到以下错误:
TypeError: 相关字段收到了无效的查找:some_flag。
进一步澄清一下,基本上相同的问题被报告为一个错误,但没有回应如何实现这个目标:https://code.djangoproject.com/ticket/26393
我知道可以通过直接在MyModel过滤器中使用管理器的筛选和注释来实现这一点,但关键是要保持DRY,并确保在访问此模型时每个地方都重复这种行为(除非明确指示不这样做)。

不,我想看看你声称可以正常工作的代码,但它不够DRY,我希望我可以让它DRY。你的“显而易见”的例子肯定是错误的,因为它在内部编译为SELECT app_mymodel.id,... , True AS "other_model__some_flag" FROM ... SQL可以通过any_queryset.query.get_compiler('default').as_sql()轻松验证。也许清楚地解释一下,例如通过SQL,你想得到什么。是的,简化的例子很有用,但经过验证的例子是没有其他无关错误的,例如在def get_queryset之后缺少return - hynekcer
  1. 不应该以这种方式覆盖基本管理器。阅读为什么用户不鼓励这样做。--- 预计Django ORM足够强大,但正确的解决方案可能完全不同,您可以早期重新制定问题,但现在可能为时已晚。
- hynekcer
1
@hynekcer 很抱歉让你失望,但你混淆了注释和聚合。注释的常见工作负载是在行级别添加计算字段(例如 amount=F('price')+F('quantity')(简化版)。正如你所说,聚合是针对一组行进行操作的。 - user1600649
抱歉,我只是不理解这个问题,即使我已经理解了一半的django.db源代码,这让我感到沮丧。我尝试重现错误信息等,最终我为浪费的时间感到遗憾,因为我仍然不理解,并白白地等待着上下文。 - hynekcer
很抱歉,@hynekcer,你不理解这个问题,我已经尝试用几种方式来解释了。我只是想避免在相关模型上进行复制粘贴过滤,而是应该始终应用,类似地注释应始终可用于相关模型的字段(除非明确指示否则)。自定义管理器可以实现此目的,但在使用.filter()时不适用于相关模型。 - Ben
显示剩余4条评论
3个回答

4

如果你的后端是MySQL,那么运行嵌套查询(或两个查询)如何呢?(性能方面)

第一个查询用于获取相关的OtherModel对象的主键。

第二个查询用于根据获取到的主键筛选Model对象。

other_model_pks = OtherModel.objects.filter(some_flag=...).values_list('pk', flat=True)
my_model = MyModel.objects.filter(other_model__in=other_model_pks)
# use (...__in=list(other_model_pks)) for MySQL to avoid a nested query.

2
我不认为你想要的是可能的。
1)我认为你误解了注释的作用。

为查询集中的每个项目生成聚合

生成摘要值的第二种方法是为查询集中的每个对象生成独立的摘要。例如,如果您正在检索书籍列表,则可能想知道有多少作者为每本书做出了贡献。每本书都与作者具有多对多的关系;我们想为查询集中的每本书总结这种关系。

可以使用annotate()子句生成每个对象的摘要。指定annotate()子句时,查询集中的每个对象都将以指定的值进行注释。

这些注释的语法与用于aggregate()子句的语法相同。每个传递给annotate()的参数描述了要计算的聚合。

所以当你说:

MyModel.objects.annotate(other_model__some_flag=Value(True, output_field=BooleanField()))

您没有在other_model上注释some_flag
也就是说,您不会得到:mymodel.other_model.some_flag

您正在对mymodel进行注释,注释的是other_model__some_flag
也就是说,您将得到:mymodel.other_model__some_flag

2)我不确定您对SQL的熟悉程度,但为了保持MyModel.objects.filter(other_model__some_flag=True)可行,即在进行JOIN时保留注释,ORM需要在子查询上进行JOIN,类似于:

INNER JOIN 
(
    SELECT other_model.id, /* more fields,*/ 1 as some_flag
    FROM other_model
) as sub on mymodel.other_model_id = sub.id

这可能会非常慢,我不奇怪他们没有这样做。

可能的解决方案

不要注释您的字段,而是将其作为常规字段添加到您的模型中。


实际上,我认为在注释之后我们应该有mymodel.some_flag而不是mymodel.other_model__some_flag,我在示例中犯了这个错误。此外,你所假设的SQL取决于被注释的内容 - 例如,如果它是通过F("some_field")进行的字段引用,则不需要子查询;聚合也是如此。连接和子查询也不是特别慢,这只是一种教条主义;无论如何,这对Django开发人员来说都是无关紧要的 - ORM在这里是为了做我需要它做的事情,包括如果需要的话,自己给自己挖坑 :) - Ben
无论如何,您的解决方案显然可以用于删除任何注释,但这并没有解决对“disabled”字段的隐式过滤。我担心你可能是对的,“不可能”,至少不使用ORM或进行一些可怕的“.extra()”。 - Ben
@Ben 我没有涉及隐式过滤,因为它将在关系之间保持,只有注释不会。 - Todor
啊,你说得对,这似乎是来自 1.10 的新行为,我仍在使用1.8,所以我错过了那个版本。我进行了一些测试,似乎没有办法恢复旧的行为。总之,显式比隐式好 ;) - Todor

1
简单来说,模型对字段集合具有权威性,而管理器对模型集合具有权威性。为了使其DRY,您使其变得WET,因为您在管理器中更改了字段集合。
为了修复它,您需要使用Lookup API教授模型查找并进行操作
现在我假设您实际上没有使用固定值进行注释,因此如果该注释实际上可以还原为字段,则您可能只需完成它,因为最终需要将其映射到数据库表示形式。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接