Запрос Django, который получает самые последние объекты из разных категорий

У меня две модели A и B. Все B объекты имеют внешний ключ для A объекта. Учитывая набор A объектов, можно ли каким-либо образом использовать ORM для получения набора B объектов, содержащих самый последний объект, созданный для каждого A объекта.

Вот упрощенный пример:

class Bakery(models.Model):
    town = models.CharField(max_length=255)

class Cake(models.Model):
    bakery = models.ForeignKey(Bakery, on_delete=models.CASCADE)
    baked_at = models.DateTimeField()

Итак, я ищу запрос, который возвращает самый последний торт, испеченный в каждой пекарне в Anytown, США.


person Zach    schedule 15.01.2010    source источник
comment
Я тоже хотел бы это увидеть :-)   -  person gruszczy    schedule 15.01.2010


Ответы (7)


Насколько мне известно, в Django ORM нет одношагового способа сделать это.

Но вы можете разделить его на два запроса:

from django.db.models import Max

bakeries = Bakery.objects.annotate(
    hottest_cake_baked_at=Max('cake__baked_at')
) 
hottest_cakes = Cake.objects.filter(
    baked_at__in=[b.hottest_cake_baked_at for b in bakeries]
)

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

from django.db.models import Max

hottest_cake_ids = Bakery.objects.annotate(
    hottest_cake_id=Max('cake__id')
).values_list('hottest_cak‌​e_id', flat=True)

hottest_cakes = Cake.objects.filter(id__in=hottest_cake_ids)

Кстати, за это следует отдать должное Дэниелу Розману, который однажды ответил на мой вопрос:

http://groups.google.pl/group/django-users/browse_thread/thread/3b3cd4cbad478d34/3e4c87f336696054?hl=pl&q=

Если вышеуказанный метод слишком медленный, то я знаю и второй метод - вы можете написать собственный SQL, производящий только те торты, которые являются самыми популярными в соответствующих пекарнях, определить его как просмотр базы данных, а затем написать для него неуправляемую модель Django. Это также упоминается в вышеупомянутой ветке django-users. Прямая ссылка на оригинальную концепцию находится здесь:

http://web.archive.org/web/20130203180037/http://wolfram.kriesing.de/blog/index.php/2007/django-nice-and-critical-article#comment-48425

Надеюсь это поможет.

person Tomasz Zieliński    schedule 16.01.2010
comment
Я, вероятно, выберу второй набор вопросов, которые вы предложили. Спасибо. - person Zach; 18.01.2010
comment
Это было бы более эффективно, если бы вы использовали список значений для первого запроса: hottest_cake_ids = Bakery.objects.annotate (hottest_cake_id = Max ('cake__id')). Values_list ('hottest_cake_id', flat = True); hottest_cakes = Cake.objects.filter (id__in = hottest_cake_ids) - person dbn; 28.06.2014
comment
Кроме того, если вы используете PostGreSQL, есть одношаговое решение. - person dbn; 30.01.2015
comment
Разве первое решение не создает проблему, когда последняя дата для одного предшествует самой поздней дате для другого, но существует в другом? A = [1, 2, 3], B = [1, 2]. A latest = 3, B latest = 2. Кажется, что первый запрос получает 2 и 3 A, а также 2 B. - person kaungst; 27.03.2015
comment
Начиная с Django 1.11 теперь есть односторонний шаг. Проверьте мой ответ. - person Todor; 12.05.2017

Начиная с Django 1.11 и благодаря подзапросу и OuterRef, и мы наконец можем создать latest-per-group запрос с использованием ORM.

hottest_cakes = Cake.objects.filter(
    baked_at=Subquery(
        (Cake.objects
            .filter(bakery=OuterRef('bakery'))
            .values('bakery')
            .annotate(last_bake=Max('baked_at'))
            .values('last_bake')[:1]
        )
    )
)

#BONUS, we can now use this for prefetch_related()
bakeries = Bakery.objects.all().prefetch_related(
    Prefetch('cake_set',
        queryset=hottest_cakes,
        to_attr='hottest_cakes'
    )
)

#usage
for bakery in bakeries:
    print 'Bakery %s has %s hottest_cakes' % (bakery, len(bakery.hottest_cakes))
person Todor    schedule 11.05.2017
comment
Это сработало отлично, хотя мой вариант использования был немного другим. Что мне нравится в этом подходе, так это 1) он сохраняет результирующий набор запросов в экземпляре целевой модели и 2) он не исключает экземпляры модели, которые не имеют связанных данных (в контексте вопроса, пекарни, у которых нет пекла еще ничего). - person Supra621; 07.09.2019
comment
ты самый умный парень - person yang zhou; 01.04.2020
comment
Таким образом, это дает все самые популярные торты в виде QuerySet. Очень интересно. Мне тоже это было нужно, но потом нужно было отфильтровать их в пекарне на основе агрегата ('cake__topping'). Вроде нормально работает. - person gabn88; 08.03.2021

Если вы используете PostGreSQL, вы можете использовать Интерфейс Django для DISTINCT ON:

recent_cakes = Cake.objects.order_by('bakery__id', '-baked_at').distinct('bakery__id')

Как документы Скажем, вы должны order by те же поля, что и вы distinct on. Как указал Саймон ниже, если вы хотите выполнить дополнительную сортировку, вам придется делать это в пространстве Python.

person dbn    schedule 21.11.2013
comment
Очень нравится подход - спасибо. Только что сделал небольшое исправление, касающееся окончательного заказа. В зависимости от общего размера QS это может быть лучше или хуже принятого ответа. В моем случае: лучше :) - person Simon Steinberger; 24.01.2015
comment
Я думаю, что это ненужное усложнение кода и выходит за рамки того, на что был дан ответ. Я предполагаю, что люди смогут понять, как отсортировать полученные данные. - person dbn; 30.01.2015
comment
Я много играл с подобной проблемой, пробуя Max аннотации и фильтруя их, но они, наконец, терпели неудачу на стороне db из-за неправильного sql после того, как оптимизатор django удалял order_by (при использовании результата в качестве подзапроса фильтра или при агрегировании, например .count()). Это решение не нарушает всех функций при извлечении recent_cakes.count() и не вызывает ошибок при выполнении Cake.objects.filter(pk__in=recent_cackes).filter(other_conditions), но последний пример возвращает случайные пирожные для каждой пекарни, которые удовлетворяют other_conditions (не самые горячие!), Потому что django удаляет order_by из подзапроса :( - person Ivan Klass; 01.04.2015
comment
да, по этой причине, я думаю, что если вы не используете postGreSQL, ответ Томаша Зелински - это то, что вам нужно. - person dbn; 04.04.2015
comment
Разве это не заказ в первую очередь по порядку bakery_id, а не по дате выпечки, что сбивает с толку? - person Chris Barry; 15.01.2021

Это должно сработать:

from django.db.models import Max
Bakery.objects.annotate(Max('cake__baked_at'))
person Daniel Roseman    schedule 15.01.2010
comment
Я еще не тестировал, но похоже, что это будет аннотировать время, когда каждая пекарня последний раз испекла торт. Я ищу настоящие торты. Я неправильно истолковал ваш ответ? - person Zach; 16.01.2010
comment
Да, ты прав. Я забыл предыдущий ответ, который я написал для Томаша :-) - person Daniel Roseman; 16.01.2010
comment
Я считаю, что это сработает только в том случае, если сортировка тортов по идентификаторам и дате приведет к тому же порядку. В общем случае, когда последовательность первичных ключей не соответствует хронологическому порядку, определенному полем даты, это не сработает. - person Dmitry B.; 03.05.2011

Я боролся с подобной проблемой и, наконец, пришел к следующему решению. Он не зависит от order_by и distinct, поэтому может быть отсортирован по желанию на стороне базы данных, а также может использоваться как вложенный запрос для фильтрации. Я также считаю, что эта реализация не зависит от движка db, потому что она основана на стандартном предложении sql HAVING. Единственный недостаток заключается в том, что он вернет несколько самых горячих пирожных для каждой пекарни, если они выпекаются в этой пекарне в одно и то же время.

from django.db.models import Max, F

Cake.objects.annotate(
    # annotate with MAX "baked_at" over all cakes in bakery
    latest_baketime_in_bakery=Max('bakery__cake_set__baked_at')
    # compare this cake "baked_at" with annotated latest in bakery
).filter(latest_baketime_in_bakery__eq=F('baked_at'))
person Ivan Klass    schedule 01.04.2015

Cake.objects.filter(bakery__town="Anytown").order_by("-created_at")[:1]

Я не строил модели со своей стороны, но теоретически это должно сработать. Сломано:

  • Cake.objects.filter(bakery__town="Anytown") Должен возвращать любые торты, принадлежащие Anytown, при условии, что страна не является частью строки. Двойное подчеркивание между bakery и town позволяет нам получить доступ к свойству town bakery.
  • .order_by("-created_at") упорядочит результаты по дате создания, самые последние сначала (обратите внимание на знак - (минус) в "-created_at". Без знака минус они будут отсортированы от самых старых к самым новым.
  • [:1] в конце вернет только 1-й элемент в возвращаемом списке (который будет списком тортов из Anytown, отсортированным по самым последним первым).

Примечание. Этот ответ предназначен для Django 1.11. Этот ответ изменен на основе показанных запросов здесь, в Django 1.11 Docs.

person twknab    schedule 25.05.2017

Решение @Tomasz Zieliński, приведенное выше, решило вашу проблему, но не решило мою, потому что мне все еще нужно отфильтровать торт. Итак, вот мое решение

from django.db.models import Q, Max

hottest_yellow_round_cake = Max('cake__baked_at', filter=Q(cake__color='yellow', cake__shape='round'))

bakeries = Bakery.objects.filter(town='Chicago').annotate(
    hottest_cake_baked_at=hottest_yellow_round_cake
)

hottest_cakes = Cake.objects.filter(
    baked_at__in=[b.hottest_cake_baked_at for b in bakeries]
)

При таком подходе вы также можете реализовать другие вещи, такие как фильтр, сортировка, разбивка на страницы для тортов.

person Nguyen Anh Vu    schedule 19.11.2020