Запит Django, який отримує найновіші об'єкти з різних категорій


79

У мене є дві моделі 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()

Тож я шукаю запит, який повертає найновіший пиріг, випечений у кожній пекарні в місті Антаун, США.


6
Я б теж хотів це побачити :-)
— gruszczy

Відповіді:


35

Наскільки я знаю, в Django ORM це неможливо зробити в один крок.

Але ви можете розділити його на два запити:

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, ви можете спростити та розрізнити описаний вище код (у випадку, якщо одночасно приходить два торти, ви можете отримати обидва):

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

Сподіваюся, це допомагає.


Можливо, я піду з другим набором запитів, який ви запропонували. Дякую.
— Зак

Це було б ефективніше, якщо б ви використовували список значень для першого запиту: 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)
— dbn

Крім того, якщо ви випадково використовуєте PostGreSQL, є однокрокове рішення.
— dbn

2
Хіба перше рішення не створює проблеми, коли остання дата для однієї є до останньої дати іншої, але існує в іншій? A = [1, 2, 3], B = [1, 2]. Останній = 3, B останній = 2. Перший запит, здається, отримує 2 і 3 A, а також B 2.
— kaungst

1
Починаючи з Django 1.11цього часу, це робиться в один бік. Перевірте мою відповідь.
— Тодор,

32

Починаючи з 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))

Це спрацювало чудово, хоча мій варіант використання дещо відрізнявся. Мені подобається в цьому підході: 1) він зберігає результуючий набір запитів у екземплярі цільової моделі, і 2) він не виключає екземпляри моделей, які не мають пов’язаних даних (у контексті питання, пекарні, які не ще нічого не спекла).
— Supra621

ти найрозумніший хлопець
— ян чжоу

19

Якщо ви випадково використовуєте PostGreSQL, ви можете використовувати інтерфейс Django, щоб ВИЗНАЧИТИ :

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

Як сказано в документах , ви повинні вводити order byті самі поля, що і ви distinct on. Як зазначив Саймон нижче, якщо ви хочете зробити додаткове сортування, вам доведеться це робити в Python-просторі.


Любіть підхід - дякую. Просто зробив незначне виправлення щодо остаточного замовлення. Залежно від загального розміру QS, це може бути краще чи гірше прийнятої відповіді. У моєму випадку: краще :)
— Саймон Штейнбергер

Я думаю, що це непотрібне ускладнення коду і виходить за рамки того, на що відповідають. Я збираюся припустити, що люди можуть зрозуміти, як сортувати отримані дані.
— dbn

Я багато грав з подібною проблемою, пробуючи 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з підзапиту :(
— Іван Класс

так, з цієї причини, я думаю, що якщо ви не використовуєте postGreSQL, відповідь Томаша Зелінського - це шлях.
— dbn

5

Це повинно зробити роботу:

from django.db.models import Max
Bakery.objects.annotate(Max('cake__baked_at'))

5
Я ще не тестував, але, схоже, це позначить час, коли кожна пекарня нещодавно випікала торт. Я шукаю фактичні об'єкти торта. Я неправильно трактую вашу відповідь?
— Зак

Так, ви праві. Я забув попередню відповідь, яку розмістив для Томаша :-)
— Даніель Роузман,

1
Я вважаю, що це спрацює лише в тому випадку, якщо сортування тортів за ідентифікаторами та за датою призведе до того самого замовлення. У загальному випадку, коли послідовність первинного ключа не відповідає хронологічному порядку, визначеному полем дати, це не спрацює.
— Дмитро Б.

3

Я боровся з подібною проблемою і нарешті дійшов наступного рішення. Він не покладається order_byі distinctтому може бути відсортований за бажанням на стороні db, а також може бути використаний як вкладений запит для фільтрації. Я також вважаю, що ця реалізація не залежить від движка db, оскільки вона базується на стандартному HAVINGреченні sql . Єдиним недоліком є ​​те, що він поверне кілька найгарячіших пиріжків на пекарню, якщо вони випікаються в цій пекарні точно одночасно.

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'))

0
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 .


0

Наведене вище рішення @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]
)

За допомогою цього підходу ви також можете реалізувати інші речі, такі як фільтр, замовлення, розмітка сторінок для тортів

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.