Запит 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.