Django: Как аннотировать поля M2M или OneToMany, используя подзапрос? - PullRequest
0 голосов
/ 25 февраля 2019

У меня есть 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 () оба предназначены в качестве оптимизации для конкретного использованияcase: извлечение подмножества данных без затрат на создание экземпляра модели.Эта метафора разваливается при работе со многими ко многим и другими многозначными отношениями (такими как отношение «один ко многим» обратного внешнего ключа), потому что предположение «одна строка, один объект» не выполняется.

Ответы [ 2 ]

0 голосов
/ 26 февраля 2019

ArrayAgg будет хорошо, если вы хотите извлечь только одну переменную (т.е. имя) из всех статей.Если вам нужно больше, для этого есть лучший вариант:

prefetch_related

Вместо этого вы можете выполнять предварительную выборку для каждой Order, lates OrderOperation as a whole object. This adds the ability to easily get any field from 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') к первому набору запросов, чтобы повысить производительность и уменьшить количество запросов к базе данных.

0 голосов
/ 25 февраля 2019

К моему удивлению, ваша идея с 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 функция.Чтобы последующая фильтрация работала, вам также может потребоваться преобразовать поля 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;В приведенном выше запросе рассматриваются все операции для соответствующего заказа.

...