Прив’язуючи кілька фільтрів () у Django, це помилка?


103

Я завжди припускав, що зв'язування декількох викликів filter () у Django завжди було таким самим, як збирання їх в один дзвінок.

# Equivalent
Model.objects.filter(foo=1).filter(bar=2)
Model.objects.filter(foo=1,bar=2)

але я зіткнувся зі складним набором запитів у своєму коді, коли це не так

class Inventory(models.Model):
    book = models.ForeignKey(Book)

class Profile(models.Model):
    user = models.OneToOneField(auth.models.User)
    vacation = models.BooleanField()
    country = models.CharField(max_length=30)

# Not Equivalent!
Book.objects.filter(inventory__user__profile__vacation=False).filter(inventory__user__profile__country='BR')
Book.objects.filter(inventory__user__profile__vacation=False, inventory__user__profile__country='BR')

Створений SQL є

SELECT "library_book"."id", "library_book"."asin", "library_book"."added", "library_book"."updated" FROM "library_book" INNER JOIN "library_inventory" ON ("library_book"."id" = "library_inventory"."book_id") INNER JOIN "auth_user" ON ("library_inventory"."user_id" = "auth_user"."id") INNER JOIN "library_profile" ON ("auth_user"."id" = "library_profile"."user_id") INNER JOIN "library_inventory" T5 ON ("library_book"."id" = T5."book_id") INNER JOIN "auth_user" T6 ON (T5."user_id" = T6."id") INNER JOIN "library_profile" T7 ON (T6."id" = T7."user_id") WHERE ("library_profile"."vacation" = False  AND T7."country" = BR )
SELECT "library_book"."id", "library_book"."asin", "library_book"."added", "library_book"."updated" FROM "library_book" INNER JOIN "library_inventory" ON ("library_book"."id" = "library_inventory"."book_id") INNER JOIN "auth_user" ON ("library_inventory"."user_id" = "auth_user"."id") INNER JOIN "library_profile" ON ("auth_user"."id" = "library_profile"."user_id") WHERE ("library_profile"."vacation" = False  AND "library_profile"."country" = BR )

Перший набір запитів з ланцюговими filter()дзвінками приєднується до моделі Інвентаризації двічі, ефективно створюючи АБО між двома умовами, тоді як другий набір запитів AND - це два умови разом. Я очікував, що перший запит також буде І двома умовами. Це очікувана поведінка чи це помилка в Джанго?

Відповідь на відповідне запитання Чи є недолік використання ".filter (). Filter (). Filter () ..." у Django? Здається, вказує, що два набори запитів повинні бути рівнозначними.

Відповіді:


117

Наскільки я розумію, це те, що вони тонко відрізняються за дизайном (і я, безумовно, відкритий для виправлення): filter(A, B)спочатку фільтрують згідно з A, а потім підфільтрують відповідно до B, тоді як filter(A).filter(B)повернуть рядок, що відповідає A 'та' потенційно іншим рядок, який відповідає B.

Подивіться на приклад тут:

https://docs.djangoproject.com/en/dev/topics/db/queries/#spanning-multi-valued-relationships

зокрема:

Все, що знаходиться в одному виклику фільтра (), застосовується одночасно для фільтрації елементів, що відповідають усім цим вимогам. Послідовні виклики filter () додатково обмежують набір об'єктів

...

У цьому другому прикладі (filter (A) .filter (B)) перший фільтр обмежив набір запитів (A). Другий фільтр обмежив набір блогів далі тим, що також є (B). Записи, вибрані другим фільтром, можуть бути або не збігатися з записами у першому фільтрі. "


18
Хоча така поведінка, хоча і задокументована, порушує принцип найменшого здивування. Кілька фільтрів () - І разом, коли поля знаходяться в одній моделі, але потім АБО разом, коли охоплюють відносини.
гердем

3
Я вважаю, що у першому абзаці це неправильно - фільтр (A, B) - це ситуація AND ("lennon" AND 2008 у документах), тоді як filter (A) .filter (B) - ситуація АБО ( 'lennon' АБО 2008). Це має сенс, коли ви дивитесь на запити, згенеровані у запитанні, - випадок .filter (A) .filter (B) створює приєднання двічі, в результаті чого АБО.
Сем

17
filter (A, B) - це фільтр AND (A) .фільтр (B) - АБО
WeizhongTu

3
значить, further restrictзначить less restrictive?
бог

7
Ця відповідь неправильна. Це не "АБО". Це речення "Другий фільтр обмежив набір блогів далі тим, що також є (B)." чітко згадується "які також є (В)". Якщо ви спостерігаєте поведінку, подібну до АБО у цьому конкретному прикладі, це не обов'язково означає, що ви можете узагальнити власну інтерпретацію. Будь ласка, подивіться на відповіді "Кевін 3112" та "Джонні Цанг". Я вважаю, що це правильні відповіді.
1man

66

Ці два стилі фільтрації в більшості випадків еквівалентні, але при запитах на базі об'єктів на ForeignKey або ManyToManyField вони трохи відрізняються.

Приклади з документації .

модель
Блог до вступу - це співвідношення один до багатьох.

from django.db import models

class Blog(models.Model):
    ...

class Entry(models.Model):
    blog = models.ForeignKey(Blog)
    headline = models.CharField(max_length=255)
    pub_date = models.DateField()
    ...

об'єкти
Припустимо, що тут є деякі об’єкти блогу та входу.
введіть тут опис зображення

запитів

Blog.objects.filter(entry__headline_contains='Lennon', 
    entry__pub_date__year=2008)
Blog.objects.filter(entry__headline_contains='Lennon').filter(
    entry__pub_date__year=2008)  

Для 1-го запиту (з одним фільтром) він відповідає лише blog1.

Для 2-го запиту (ланцюговий фільтр один) він фільтрує blog1 та blog2.
Перший фільтр обмежує набір запитів до blog1, blog2 та blog5; другий фільтр обмежує набір блогів далі до blog1 та blog2.

І ви повинні це усвідомити

Ми фільтруємо елементи блогу з кожним оператором фільтра, а не елементами Entry.

Отже, це не одне і те саме, оскільки Блог та Вступ - це багатозначні відносини.

Довідка: https://docs.djangoproject.com/en/1.8/topics/db/queries/#spanning-multi-valued-relationships
Якщо щось не так, будь ласка, виправте мене.

Редагувати: Змінено v1.6 на v1.8, оскільки посилання 1.6 більше не доступні.


3
Ви ніби змішані між "матчами" та "фільтрами". Якщо ви дотримуєтесь "цього запиту повертається", це було б набагато зрозуміліше.
OrangeDog

7

Як ви бачите в створених операторах SQL, різниця не є "АБО", як деякі можуть підозрювати. Саме так розміщується ГДЕ та ПРИЄДНАЙТЕСЬ.

Example1 (та сама приєднана таблиця):

(приклад з https://docs.djangoproject.com/en/dev/topics/db/queries/#spanning-multi-valued-relationships )

Blog.objects.filter(entry__headline__contains='Lennon', entry__pub_date__year=2008)

Це дасть вам усі блоги, які мають один запис з обома (entry_ headline _contains = 'Lennon') AND (entry__pub_date__year = 2008), що ви очікуєте від цього запиту. Результат: Книга з {entry.headline: 'Життя Леннона', entry.pub_date: '2008'}

Приклад 2 (ланцюговий)

Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008)

Це охопить усі результати з Прикладу 1, але це дасть трохи більший результат. Тому що він спочатку фільтрує всі блоги з (entry_ headline _contains = 'Lennon'), а потім із фільтрами результатів (entry__pub_date__year = 2008).

Різниця полягає в тому, що вона також дає такі результати, як: Книга з {entry.headline: ' Lennon ', entry.pub_date: 2000}, {entry.headline: 'Bill', entry.pub_date: 2008 }

У вашому випадку

Я думаю, що саме це вам потрібно:

Book.objects.filter(inventory__user__profile__vacation=False, inventory__user__profile__country='BR')

А якщо ви хочете використовувати АБО, будь ласка, прочитайте: https://docs.djangoproject.com/en/dev/topics/db/queries/#complex-lookups-with-q-objects


Другий приклад насправді не відповідає дійсності. Усі ланцюгові фільтри застосовані до запитуваних об'єктів, тобто вони об'єднані в запиті разом.
Janne

Я вважаю, що Приклад 2 є правильним, і це фактично пояснення, взяті з офіційних документів Джанго, на які посилається. Я, можливо, не найкращий пояснювач, і прошую за це. Приклад 1 - це прямий І, як можна було очікувати, у звичайному написанні SQL. Приклад 1 дає щось подібне: 'ВИБЕРІТЬ блог JOIN запис WHERE entry.head_line ЯК " Леннон " І entry.year == 2008 Приклад 2 дає щось подібне:' SELECT blog JOIN entry WHERE entry.head_list ЯК " Леннон " UNION SELECT blog ПРИЄДНАЙТЕСЬ до запису WHERE entry.head_list ПОДОБАЙТЕ " Lennon " '
Джонні Цанг

Сер, ви абсолютно праві. Поспіхом я пропустив той факт, що наші критерії фільтрації вказують на відношення «багато хто», а не до самого блогу.
Янва

0

Іноді не хочеться приєднуватися до декількох фільтрів разом:

def your_dynamic_query_generator(self, event: Event):
    qs \
    .filter(shiftregistrations__event=event) \
    .filter(shiftregistrations__shifts=False)

І наступний код насправді не поверне правильну річ.

def your_dynamic_query_generator(self, event: Event):
    return Q(shiftregistrations__event=event) & Q(shiftregistrations__shifts=False)

Тепер ви можете скористатися фільтром підрахунку анотацій.

У цьому випадку ми рахуємо всі зміни, які належать до певної події.

qs: EventQuerySet = qs.annotate(
    num_shifts=Count('shiftregistrations__shifts', filter=Q(shiftregistrations__event=event))
)

Після цього ви можете фільтрувати за анотацією.

def your_dynamic_query_generator(self):
    return Q(num_shifts=0)

Це рішення також дешевше на великих запитах.

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

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