簡體   English   中英

Django:如何使用子查詢注釋 M2M 或 OneToMany 字段?

[英]Django: How to annotate M2M or OneToMany fields using a SubQuery?

我有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() 都旨在針對特定用例進行優化:檢索數據子集而無需創建模型實例的開銷。 當處理多對多和其他多值關系(例如反向外鍵的一對多關系)時,這個比喻就失效了,因為“一行一個對象”假設不成立。

如果您只想從所有文章中獲取一個變量(即名稱),則ArrayAgg會很棒。 如果您需要更多,有一個更好的選擇:

prefetch_related

相反,您可以將每個Order 、最新的OrderOperation預取為一個整體對象。 這增加了無需額外魔法即可輕松從OrderOperation獲取任何字段的能力。

唯一需要注意的是,當所選訂單沒有操作時,您將始終獲得一個包含一個操作的列表或一個空列表。

為此,您應該將prefetch_related查詢集模型與Prefetch對象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')添加到第一個查詢集以提高性能並減少對數據庫的查詢。

令我驚訝的是,您對ArrayAgg的想法是正確的。 我不知道有一種方法可以用數組進行注釋(而且我相信除了 Postgres 之外還沒有其他后端)。

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

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

然后,您可以使用ArrayField 查找過濾結果查詢集

# 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 字段ArrayAggint ,否則 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 上面的查詢查看相關訂單的所有操作。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM