Фильтры в Django – filter(A, B) vs filter(A).filter(B)

В этой статье мы рассмотрим довольно сложную тему в Django ORM. И после прочтения статьи вы будете лучше понимать, как работает Django ORM, в частности, как он обрабатывает джойны.
Допустим, у нас есть проект Django с двумя простыми моделями:

from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models

class Course(models.Model):
    title = models.CharField(max_length=255)
    price = models.DecimalField(max_digits=10, decimal_places=2)

class Review(models.Model):
    course = models.ForeignKey(
        'Course',
        related_name='reviews',
        on_delete=models.CASCADE
    )
    value = models.PositiveSmallIntegerField(
        validators=[MinValueValidator(1), MaxValueValidator(5)]
    )
    date = models.DateField()

Теперь давайте немного поиграем с Django ORM и воспользуемся методом фильтрации.

Сейчас я открою shell, которая печатает SQL-запросы по мере их выполнения, и этот shell не является встроенной в Django. Вам необходимо установить библиотеку django-extensions, если вы хотите иметь такой же shell, как у меня:

pip install django-extensions

Давайте просто отфильтруем курсы только по их собственным полям. Это самый простой фильтр, который мы можем сделать:

>>> Course.objects.filter(title__contains='Course', price__gte=20)
SELECT "courses_course"."id",
       "courses_course"."title",
       "courses_course"."price"
  FROM "courses_course"
 WHERE ("courses_course"."price" >= '20' AND "courses_course"."title" LIKE '%Course%' ESCAPE '\\\\')
 LIMIT 21
Execution time: 0.000410s [Database: default]
<QuerySet [<Course: Course object (1)>, <Course: Course object (2)>]>

>>> Course.objects.filter(title__contains='Course').filter(price__gte=20)
SELECT "courses_course"."id",
       "courses_course"."title",
       "courses_course"."price"
  FROM "courses_course"
 WHERE ("courses_course"."title" LIKE '%Course%' ESCAPE '\\\\' AND "courses_course"."price" >= '20')
 LIMIT 21
Execution time: 0.000513s [Database: default]
<QuerySet [<Course: Course object (1)>, <Course: Course object (2)>]>

Если посмотреть на сгенерированные запросы, то они выглядят абсолютно одинаково. Таким образом, не имеет значения, будем ли мы использовать цепочку фильтров или передадим все условия в один вызов фильтра. Результат будет одинаковым в обоих случаях.

А вот когда мы фильтруем объекты на основе поля ManyToManyField или на основе обратного ForeignKey, все становится сложнее.

В качестве иллюстрации попробуем отфильтровать курсы по отзывам:

>>> Course.objects.filter(reviews__value=5, reviews__date__year=2020)
SELECT "courses_course"."id",
       "courses_course"."title",
       "courses_course"."price"
  FROM "courses_course"
 INNER JOIN "courses_review"
    ON ("courses_course"."id" = "courses_review"."course_id")
 WHERE ("courses_review"."date" BETWEEN '2020-01-01' AND '2020-12-31' AND "courses_review"."value" = 5)
 LIMIT 21
Execution time: 0.000248s [Database: default]
<QuerySet [<Course: Course object (1)>]>

>>> Course.objects.filter(reviews__value=5).filter(reviews__date__year=2020)
SELECT "courses_course"."id",
       "courses_course"."title",
       "courses_course"."price"
  FROM "courses_course"
 INNER JOIN "courses_review"
    ON ("courses_course"."id" = "courses_review"."course_id")
 INNER JOIN "courses_review" T3
    ON ("courses_course"."id" = T3."course_id")
 WHERE ("courses_review"."value" = 5 AND T3."date" BETWEEN '2020-01-01' AND '2020-12-31')
 LIMIT 21
Execution time: 0.000254s [Database: default]
<QuerySet [<Course: Course object (1)>, <Course: Course object (2)>]>

Как видно, результаты фильтрации курсов различны. Первый фильтр нашел только один курс, а второй - два.

Также отличаются и запросы, которые были сформированы и отправлены в базу данных. Запрос для второго фильтра, как вы видите, немного сложнее.

Чтобы понять, почему эти фильтры дали разные результаты, сначала покажем данные, которые мы имеем сейчас в базе данных:

У нас есть таблицы курсов и таблицы рецензий. Для нас сейчас интересна таблица рецензий. В ней 8 строк. И у каждого курса есть 2 отзыва, кроме курса с id 3. У него нет ни одного отзыва.

Кроме того, я выделил некоторые важные ячейки зеленым и синим цветами. Эти ячейки важны из-за значений, которые в них содержатся. Мы использовали эти значения при фильтрации курсов в примерах, которые я показывал ранее.

Давайте вернемся к нашим примерам. Давайте снова посмотрим на наш первый фильтр:

>>> Course.objects.filter(reviews__value=5, reviews__date__year=2020)
SELECT "courses_course"."id",
       "courses_course"."title",
       "courses_course"."price"
  FROM "courses_course"
 INNER JOIN "courses_review"
    ON ("courses_course"."id" = "courses_review"."course_id")
 WHERE ("courses_review"."date" BETWEEN '2020-01-01' AND '2020-12-31' AND "courses_review"."value" = 5)
 LIMIT 21
Execution time: 0.000248s [Database: default]
<QuerySet [<Course: Course object (1)>]>

Здесь мы объединяем таблицу курсов с таблицей рецензий, а затем применяем предложение "WHERE". По сути, мы просто пытаемся получить курсы, у которых есть хотя бы одна рецензия, которая одновременно имеет значение, равное 5, и год, когда она была создана, - 2020.

И у нас есть только одна рецензия, в которой одновременно присутствуют значения 5 и 2020, и это первая рецензия. Этот отзыв привязан к курсу с id 1, и поэтому в QuerySet у нас был только этот курс.

Теперь я бы сказал, что этот фильтр достаточно интуитивен и прост для понимания. Итак, давайте рассмотрим цепочку фильтров:

>>> Course.objects.filter(reviews__value=5).filter(reviews__date__year=2020)
SELECT "courses_course"."id",
       "courses_course"."title",
       "courses_course"."price"
  FROM "courses_course"
 INNER JOIN "courses_review"
    ON ("courses_course"."id" = "courses_review"."course_id")
 INNER JOIN "courses_review" T3
    ON ("courses_course"."id" = T3."course_id")
 WHERE ("courses_review"."value" = 5 AND T3."date" BETWEEN '2020-01-01' AND '2020-12-31')
 LIMIT 21
Execution time: 0.000254s [Database: default]
<QuerySet [<Course: Course object (1)>, <Course: Course object (2)>]>

С цепочечными фильтрами дело обстоит сложнее. Как видите, мы дважды объединяем таблицу "Курс" с таблицей "Рецензия", и если посмотреть на предложение "WHERE", то можно увидеть, что в первом условии используется первая объединенная таблица, а во втором - вторая объединенная таблица.

Понять этот запрос довольно сложно, но давайте попробуем его визуализировать. Посмотрим на результат аналогичного SELECT, если бы в нем не было пункта "WHERE":

SELECT "courses_course"."id" as "T1 id",
       "courses_course"."title" as "T1 title",
       "courses_course"."price" as "T1 price",
       "courses_review"."value" as "T2 value",
       "courses_review"."date" as "T2 date",
       "T3"."value" as "T3 value",
       "T3"."date" as "T3 date"
  FROM "courses_course"
 INNER JOIN "courses_review"
    ON ("courses_course"."id" = "courses_review"."course_id")
 INNER JOIN "courses_review" T3
    ON ("courses_course"."id" = T3."course_id")

Как видно, каждый курс имеет 4 строки. По сути, мы имеем все возможные комбинации значений. И если мы попробуем добавить в наш SELECT предложение "WHERE":

SELECT "courses_course"."id" as "T1 id",
       "courses_course"."title" as "T1 title",
       "courses_course"."price" as "T1 price",
       "courses_review"."value" as "T2 value",
       "courses_review"."date" as "T2 date",
       "T3"."value" as "T3 value",
       "T3"."date" as "T3 date"
  FROM "courses_course"
 INNER JOIN "courses_review"
    ON ("courses_course"."id" = "courses_review"."course_id")
 INNER JOIN "courses_review" T3
    ON ("courses_course"."id" = T3."course_id")
 WHERE ("courses_review"."value" = 5 AND T3."date" BETWEEN '2020-01-01' AND '2020-12-31')

В итоге мы получим такой результат:

Это связано с тем, что у нас есть только две строки, в которых столбец "T2 value" имеет значение 5, а столбец "T3 date" - 2020.

В принципе, когда мы выстраиваем цепочку фильтров, эти несколько вызовов фильтров применяются независимо друг от друга. Мы начинаем с первого фильтра. Когда мы применяем этот первый фильтр, то получаем курсы, которые имеют отзывы со значением, равным 5. И у нас есть 3 курса с такими отзывами. Курсы с идентификаторами 1, 2 и 5.

Затем мы применяем другой фильтр. Он фильтрует по дате 2020 года. И у нас есть только два курса, у которых есть отзывы со значением, равным 5, и отзывы с датой 2020. Первый и второй курсы. Курс с идентификатором 5 не имеет отзывов с датой 2020 года, поэтому его нет в результатах.

Заключение

В Django, если мы хотим отфильтровать данные, мы используем метод filter. Этот метод фильтрации работает по-разному в зависимости от того, как мы его используем, и от того, какие отношения существуют в наших моделях.

Если мы фильтруем модель Course по ее собственным полям, по отношению "один к одному" или по внешнему ключу, то результат будет одинаковым независимо от того, как мы его используем. Мы можем выстроить цепочку фильтров или передать все условия в один вызов метода фильтрации. Это не имеет значения, результат будет один и тот же.

Однако когда мы начинаем фильтровать по обратному внешнему ключу или по отношениям "многие-ко-многим", все становится сложнее.

Когда мы передаем все условия в один вызов метода фильтрации, эти условия применяются одновременно. Когда же мы выстраиваем фильтры в цепочку, эти несколько вызовов фильтра применяются независимо друг от друга.

Прокрутить вверх