В этой статье мы рассмотрим довольно сложную тему в 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 по ее собственным полям, по отношению "один к одному" или по внешнему ключу, то результат будет одинаковым независимо от того, как мы его используем. Мы можем выстроить цепочку фильтров или передать все условия в один вызов метода фильтрации. Это не имеет значения, результат будет один и тот же.
Однако когда мы начинаем фильтровать по обратному внешнему ключу или по отношениям "многие-ко-многим", все становится сложнее.
Когда мы передаем все условия в один вызов метода фильтрации, эти условия применяются одновременно. Когда же мы выстраиваем фильтры в цепочку, эти несколько вызовов фильтра применяются независимо друг от друга.