[英]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 字段ArrayAgg
為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
; 上面的查詢查看相關訂單的所有操作。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.