Цепочка нескольких filter() в Django, это ошибка?

Я всегда предполагал, что цепочка нескольких вызовов 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() дважды присоединяется к модели Inventory, эффективно создавая операцию ИЛИ между двумя условиями, тогда как второй набор запросов объединяет два условия вместе. Я ожидал, что первый запрос также будет И с двумя условиями. Это ожидаемое поведение или это ошибка в Django?

Ответ на связанный с этим вопрос Есть ли обратная сторона использовать .filter().filter().filter()... в Django? кажется, указывает на то, что два набора запросов должны быть эквивалентны.


person gerdemb    schedule 17.11.2011    source источник


Ответы (6)


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

Посмотрите на пример здесь:

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

особенно:

Все внутри одного вызова filter() применяется одновременно для фильтрации элементов, соответствующих всем этим требованиям. Последовательные вызовы filter() еще больше ограничивают набор объектов.

...

Во втором примере (filter(A).filter(B)) первый фильтр ограничил набор запросов до (A). Второй фильтр дополнительно ограничил набор блогов теми, которые также относятся к категории (B). Записи, выбранные вторым фильтром, могут совпадать или не совпадать с записями в первом фильтре.

person Timmy O'Mahony    schedule 17.11.2011
comment
Означает ли это, что ссылка на вопрос SO на самом деле неверна? - person Timmy O'Mahony; 17.11.2011
comment
Такое поведение, хотя и задокументировано, похоже, нарушает принцип наименьшего удивления. Множественные фильтры () И вместе, когда поля находятся в одной модели, но затем ИЛИ вместе, когда охватывают отношения. - person gerdemb; 17.11.2011
comment
Я полагаю, что в первом абзаце вы ошиблись: filter(A, B) - это ситуация AND ("lennon" AND 2008 в документах), а filter(A).filter(B) - это ситуация OR ( Леннон ИЛИ 2008). Это имеет смысл, когда вы смотрите на запросы, сгенерированные в вопросе - случай .filter(A).filter(B) создает соединения дважды, что приводит к ИЛИ. - person Sam; 29.01.2014
comment
filter(A, B) - фильтр AND (A).filter(B) - OR - person WeizhongTu; 12.03.2014
comment
filter(A,B) не работает должным образом, когда A и B объекты Q(), содержащие операторы И или ИЛИ. - person Austin A; 21.04.2015
comment
так further restrict означает less restrictive? - person boh; 13.07.2016
comment
Этот ответ неверен. Это не ИЛИ. Это предложение Второй фильтр ограничил набор блогов теми, которые также являются (B). ясно упоминает, что они также (B). Если вы наблюдаете поведение, похожее на ИЛИ в этом конкретном примере, это не обязательно означает, что вы можете обобщать собственную интерпретацию. Пожалуйста, посмотрите ответы Кевина 3112 и Джонни Цанга. Я считаю, что это правильные ответы. - person 1man; 17.07.2016
comment
Да, это задокументировано в Django. Но документирование плохого дизайна не делает его хорошим. Бывают ситуации, когда фильтры оказываются вложенными. Например, в DRF я добавляю некоторую фильтрацию в методе get_queryset(self) набора представлений, а также использую бэкенд фильтра DRF из коробки. Фильтры кажутся вложенными, и они хорошо работают, если применяются к одной и той же модели, но ломаются (как задумано) после того, как фильтрация выполняется для связанной модели. Это определенно нарушение принципа наименьшего удивления (как минимум). - person Dmitry Mugtasimov; 09.05.2019
comment
В Django есть встроенные объекты Q, которые позволяют осуществлять такой поиск. Использование: from django.db.models import Q Item.objects.filter(Q(A),Q(B)); // A Or B Item.objects.filter(A,B); // A AND B - person anouar es-sayid; 17.09.2020
comment
@anouares-sayid Думаю, вы ошибаетесь, Q(A) | Q(B) означает ИЛИ. - person damd; 03.02.2021

Эти два стиля фильтрации в большинстве случаев эквивалентны, но при запросе объектов на основе 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()
    ...

objects
Предположим, что здесь есть объекты блога и записи.
  введите здесь описание изображения

запросы

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)  
    

Для первого запроса (с одним фильтром) он соответствует только blog1.

Для второго запроса (связанные фильтры один) он отфильтровывает blog1 и blog2.
Первый фильтр ограничивает набор запросов до blog1, blog2 и blog5; второй фильтр дополнительно ограничивает набор блогов до blog1 и blog2.

И вы должны понимать, что

Мы фильтруем элементы блога с каждым оператором фильтра, а не элементы записей.

Итак, это не одно и то же, потому что Блог и Запись — многозначные отношения.

Ссылка: https://docs.djangoproject.com/en/1.8/topics/db/queries/#spanning-multi-valued-relationships
Если что-то не так, поправьте меня.

Изменить: изменена версия 1.6 на версию 1.8, поскольку ссылки на версию 1.6 больше не доступны.

person Kevin_wyx    schedule 31.01.2015
comment
Вы, кажется, путаетесь между совпадениями и фильтрами. Если бы вы придерживались этого запроса, это было бы намного яснее. - person OrangeDog; 03.01.2018

Как вы можете видеть в сгенерированных операторах SQL, разница не в «ИЛИ», как некоторые могут подозревать. Именно так размещаются WHERE и JOIN.

Пример 1 (та же объединенная таблица):

(пример из 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') И (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

person Johnny Tsang    schedule 14.06.2012
comment
Второй пример на самом деле не соответствует действительности. Все связанные фильтры применяются к запрашиваемым объектам, т. е. в запросе они объединяются вместе. - person Janne; 20.01.2014
comment
Я считаю, что пример 2 верен, и на самом деле это объяснение, взятое из официальных документов Django, как указано. Возможно, я не лучший объяснитель, и я прошу прощения за это. Пример 1 представляет собой прямое И, как и следовало ожидать при обычном написании SQL. Пример 1 дает что-то вроде этого: 'ВЫБЕРИТЕ запись блога ПРИСОЕДИНЯЙТЕСЬ ГДЕ entry.head_line LIKE Lennon AND entry.year == 2008 Пример 2 дает что-то вроде этого: 'ВЫБЕРИТЕ запись блога ПРИСОЕДИНЯЙТЕСЬ WHERE entry.head_list LIKE < i>Леннон ОБЪЕДИНЕНИЕ ВЫБЕРИТЕ блог ПРИСОЕДИНЯЙТЕСЬ к записи ГДЕ entry.head_list НРАВИТСЯ Леннон' - person Johnny Tsang; 27.01.2014
comment
Сэр, вы совершенно правы. В спешке я пропустил тот факт, что наши критерии фильтрации указывают на отношение «один ко многим», а не на сам блог. - person Janne; 27.01.2014

Из документов Django:

Чтобы справиться с обеими этими ситуациями, в Django есть согласованный способ обработки вызовов filter(). Все внутри одного вызова filter() применяется одновременно для фильтрации элементов, соответствующих всем этим требованиям. Последующие вызовы filter() дополнительно ограничивают набор объектов, но для многозначных отношений они применяются к любому объекту, связанному с первичной моделью, не обязательно к тем объектам, которые были выбраны предыдущим вызовом filter().

  • Ясно сказано, что несколько условий в одном filter() применяются одновременно. Это означает, что выполнение:
objs = Mymodel.objects.filter(a=True, b=False)

вернет набор запросов с необработанными данными из модели Mymodel, где a=True И b=False.

  • Последующие filter() в некоторых случаях дадут тот же результат. Делает :
objs = Mymodel.objects.filter(a=True).filter(b=False)

вернет набор запросов с необработанными данными из модели Mymodel, где a=True И b=False тоже. Поскольку вы сначала получаете набор запросов с записями, которые имеют a=True, а затем он ограничивается теми, у кого одновременно есть b=False.

  • Разница в цепочке filter() возникает, когда есть multi-valued relations, что означает, что вы просматриваете другие модели (например, пример, приведенный в документах, между моделями Blog и Entry). Говорят, что в таком случае (...) they apply to any object linked to the primary model, not necessarily those objects that were selected by an earlier filter() call.

Это означает, что он применяет последующие filter() непосредственно к целевой модели, а не к предыдущим filter().

Если я возьму пример из документов:

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

помните, что фильтруется модель Blog, а не Entry. Таким образом, он будет обрабатывать 2 filter() независимо.

Например, он вернет набор запросов с блогами, в которых есть записи, содержащие «Леннон» (даже если они не из 2008 г.), и записи из 2008 г. (даже если их заголовок не содержит «Леннон»).

ЭТОТ ОТВЕТ идет еще дальше в объяснении. И исходный вопрос аналогичен.

person lbris    schedule 03.12.2020

Увидел это в комментарии и подумал, что это самое простое объяснение.

filter(A, B) - это AND ; фильтр(A).фильтр(B) ИЛИ

person chia yongkang    schedule 26.12.2020

Иногда вы не хотите объединять несколько фильтров вместе, например:

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)

Это решение также дешевле для больших наборов запросов.

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

person Tobias Ernst    schedule 27.04.2019