【问题标题】:Django: How to annotate M2M or OneToMany fields using a SubQuery?Django:如何使用子查询注释 M2M 或 OneToMany 字段?
【发布时间】:2019-07-18 16:39:58
【问题描述】:

我有 Order 对象和 OrderOperation 对象,它们代表对订单的操作(创建、修改、取消)。

从概念上讲,一个订单有 1 到多个订单操作。每次对订单进行操作时,都会在此操作中计算总数。这意味着当我需要查找订单的属性时,我只需使用子查询获取最后一个订单操作属性。

简化代码

class OrderOperation(models.Model):
    order = models.ForeignKey(Order)
    total = DecimalField(max_digits=9, decimal_places=2)

class Order(models.Model)
    # ...

class OrderQuerySet(query.Queryset):

    @staticmethod
    def _last_oo(field):
        return Subquery(OrderOperation.objects
                        .filter(order_id=OuterRef("pk"))
                        .order_by('-id')
                        .values(field)
                        [:1])

    def annotated_total(self):
        return self.annotate(oo_total=self._last_oo('total'))

这样,我可以运行my_order_total = Order.objects.annotated_total()[0].oo_total。效果很好。

问题

计算总数很容易,因为它是一个简单的值。但是,当有 M2M 或 OneToMany 字段时,此方法不起作用。例如,使用上面的例子,让我们添加这个字段:

class OrderOperation(models.Model):
    order = models.ForeignKey(Order)
    total = DecimalField(max_digits=9, decimal_places=2)
    ordered_articles = models.ManyToManyField(Article,through='orders.OrderedArticle')                                       

编写如下内容不起作用,因为它仅返回 1 个外键(不是所有 FK 的列表):

def annotated_ordered_articles(self):
    return self.annotate(oo_ordered_articles=self._last_oo('ordered_articles'))

目的

整个目的是允许用户在所有订单中进行搜索,在输入中提供列表或文章。例如:“请查找所有至少包含第 42 条或第 43 条的订单”,或“请查找所有恰好包含第 42 条和第 43 条的订单”等。

如果我能得到类似的东西:

>>> Order.objects.annotated_ordered_articles()[0].oo_ordered_articles
<ArticleQuerySet [<Article: Article42>, <Article: Article43>]>

甚至:

>>> Order.objects.annotated_ordered_articles()[0].oo_ordered_articles
[42,43]

这将解决我的问题。

我目前的想法

  • 也许像 ArrayAgg(我正在使用 pgSQL)之类的东西可以解决问题,但我不确定如何在我的情况下使用它。
  • 也许这与values() 方法有关,该方法似乎不是为了处理文档中所述的 M2M 和 1TM 关系:

values() 和 values_list() 都是为了优化 特定用例:检索数据子集而无需 创建模型实例。这个比喻在处理时分崩离析 多对多和其他多值关系(例如一对多 反向外键的关系),因为“一行,一个对象” 假设不成立。

【问题讨论】:

  • 你想要的行为是什么?
  • @GwynBleidD 问题已更新为“目的”部分。
  • 应该只从最后一个OrderOperation得到它们吗?
  • @GwynBleidD 是的。行为应该类似于total。除了totalOrderOperation 中返回一个简单的十进制值,而ordered_articles 是多对多字段的管理器,所以我想处理起来更复杂:)

标签: python django django-queryset


【解决方案1】:

ArrayAgg 如果您只想从所有文章中获取一个变量(即名称),那就太好了。如果您需要更多,还有一个更好的选择:

prefetch_related

相反,您可以将每个Order、最新的OrderOperation 作为一个整体对象进行预取。这增加了从OrderOperation 轻松获取任何字段的能力,而无需额外的魔法。

唯一需要注意的是,当所选订单没有任何操作时,您总是会得到一个包含一个操作的列表或一个空列表。

为此,您应该使用prefetch_related 查询集模型和Prefetch object 以及OrderOperation 的自定义查询。示例:

from django.db.models import Max, F, Prefetch

last_order_operation_qs = OrderOperation.objects.annotate(
    lop_pk=Max('order__orderoperation__pk')
).filter(pk=F('lop_pk'))

orders = Order.objects.prefetch_related(
    Prefetch('orderoperation_set', queryset=last_order_operation_qs, to_attr='last_operation')
)

然后您可以使用order.last_operation[0].ordered_articles 获取特定订单的所有已订购文章。您可以将prefetch_related('ordered_articles') 添加到第一个查询集以提高性能并减少对数据库的查询。

【讨论】:

  • 请注意,这实际上是我的answer 对您之前的问题的改进,但它不允许过滤使用to_attr 预取的任何内容。
  • 再次非常感谢,非常有帮助.. 由于我的目的是获取订单,给定文章列表作为输入,您如何看待用此解决方案替换答案的“预取部分” :order_pks = last_order_operation_qs.filter(ordered_articles__in=[1,4]).select_related('order').values('order__pk') 然后Order.objects.filter(pk__in=order_pks)?
【解决方案2】:

令我惊讶的是,您对ArrayAgg 的想法是正确的。我不知道有一种方法可以用数组进行注释(而且我相信除了 Postgres 之外还没有其他后端)。

from django.contrib.postgres.aggregates.general import ArrayAgg

qs = Order.objects.annotate(oo_articles=ArrayAgg(
            'order_operation__ordered_articles__id',
            'DISTINCT'))

然后您可以使用ArrayField lookups 过滤生成的查询集:

# Articles that contain the specified array
qs.filter(oo_articles__contains=[42,43])
# Articles that are identical to the specified array
qs.filter(oo_articles=[42,43,44])
# Articles that are contained in the specified array
qs.filter(oo_articles__contained_by=[41,42,43,44,45])
# Articles that have at least one element in common
# with the specified array
qs.filter(oo_articles__overlap=[41,42])

'DISTINCT' 仅在操作可能包含重复文章时才需要。

您可能需要调整传递给ArrayAgg 函数的字段的确切名称。为了使后续过滤起作用,您可能还需要将ArrayAgg 中的id 字段转换为int,否则Django 会将id 数组转换为::serial[],而我的Postgres 抱怨type "serial[]" does not exist

from django.db.models import IntegerField
from django.contrib.postgres.fields.array import ArrayField
from django.db.models.functions import Cast

ArrayAgg(Cast('order_operation__ordered_articles__id', IntegerField()))
# OR
Cast(ArrayAgg('order_operation__ordered_articles__id'), ArrayField(IntegerField()))

更仔细地查看您发布的代码,您还必须过滤您感兴趣的OrderOperation;上面的查询会查看相关订单的所有操作。

【讨论】:

  • 谢谢。这是最实用、最直观的解决方案。
  • 完美!通过使用ArrayAgg(field_name, distinct=True),我在最新的 Django 中获得了不同的值。文档here.
猜你喜欢
  • 2020-11-24
  • 1970-01-01
  • 2023-01-27
  • 2020-07-09
  • 2020-11-26
  • 1970-01-01
  • 2020-07-31
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多