Дополнительные возможности ORM

При знакомстве с SQL вы строили запросы через объединение таблиц JOIN и применяли агрегирующие функции MAXMINCOUNT etc. Посмотрим, как эти инструменты работают в Django ORM.

Вывод запросов в консоли

Для более наглядного понимания того, как Django ORM работает с базой данных, активируем вывод отладочной информации. В виртуальном окружении запустите python shell:

(venv) $ python manage.py shell 

Следующая команда импортирует библиотеку logging, входящую в стандартный набор модулей, и включит вывод отладочной информации для той части Django, которая делает запросы к базе:

>>> import logging
>>> log = logging.getLogger('django.db.backends')
>>> log.setLevel(logging.DEBUG)
>>> log.addHandler(logging.StreamHandler()) 

Этот режим останется активным до конца работы в консоли. По ссылке вы можете посмотреть документацию о работе библиотеки logging. Теперь в консоли будут отображаться SQL-запросы, сформированные Django ORM при обращении к базе. Импортируем модели и создадим переменную latest:

>>> from posts.models import Post, User
>>> latest = Post.objects.all() 

Однако SQL-запрос не отобразился в консоли! Дело в том, что мы просто создали код запроса, но никаких данных не запросили. Это достаточно разумная оптимизация: Django старается лишний раз не дёргать базу.Запустим в терминале код шаблона, который выводит на главную страницу свежие записи. Чтобы не загромождать вывод текстами записей — сохраним их во временной строке tmp, но не будем выводить эту строку на экран:

>>> for post in latest:
...     tmp = f'{post.text} Автор {post.author.username}'
... 
(0.000) SELECT "posts_post"."id", "posts_post"."text", "posts_post"."pub_date", "posts_post"."author_id", "posts_post"."group_id" FROM "posts_post"; args=()
(0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 2 LIMIT 21; args=(2,)
[...skip...] 

Разберёмся с этим выводом.Мы запросили данные для модели Post. Её свойство author — ссылка на модель User, в author хранится первичный ключ записи из таблицы User.При попытке обработать данные Django поступил как ленивый сотрудник: ему сказали принести из базы данных содержимое таблицы posts, он сходил и принёс; ему сказали принести из модели User свойство username, он опять сходил и принёс. За каждой записью он ходил по очереди. Наверное, чтобы приготовить омлет, он ходит в магазин за каждым яйцом отдельно. А ведь связей может быть несколько: в нашем проекте есть модель для сообществ, и если будет нужно показать принадлежность поста к сообществу, мы получим ещё один запрос. В результате ресурсоёмкость проекта может увеличиться многократно: для отображения одной страницы потребуются сотни SQL-запросов! Поочерёдное получение записей из базы требует гораздо больших ресурсов, чем единый запрос. Нам срочно нужен JOIN для запросов через Django ORM!

Загрузка связанных записей

Django предлагает два способа загрузки связанных записей:

  • select_related(relation) — загрузка связанных данных с помощью JOIN. В результате обработки получается один запрос, который, помимо основной модели, загружает и связанные данные из дополнительных таблиц.
  • prefetch_related(relation) — «ленивая» подгрузка связанных данных с помощью дополнительных запросов. В этом случае Django ORM сперва запрашивает данные из основной таблицы, запоминает первичные ключи связанных записей, а затем делает ещё один запрос для загрузки связанных данных, ключи которых есть в первой выборке.

Выбор подходящего варианта зависит от характеристики данных и ситуации. Если данных мало, то один запрос создаст меньшую нагрузку на базу и на Python, который потом эти данные будет обрабатывать. Выбор в пользу select_related(). А в ситуации, когда в таблице связанной модели лежат огромные объекты (например, файлы с картинками или большие тексты), можно оптимизировать нагрузку, не пересылая одни и те же данные много раз.Представьте, что в модели User, помимо username и прочей служебной информации, хранятся портреты пользователей, большие картинки. Мы запросили список постов, и при получении каждого поста мы получаем из связанной таблицы информацию об авторе этого поста. Для тридцати постов информация об авторах будет передана тридцать раз. С картинками. Но в базе у нас только два автора, и каждый из постов написан одним из них! Значит, одну и ту же информацию мы запрашиваем многократно, впустую расходуя ресурсы сервера, увеличивая трафик и время обработки данных. Избежать таких расходов поможет запрос prefetch_related(). Посмотрим, как это выглядит в коде. Методам select_related() и prefetch_related() параметром передаём имя поля, в котором хранятся ключи связанной модели. Сравните запросы:

>>> related = Post.objects.select_related('author').all()
>>> for post in related:
...     tmp = f'{post.text} Автор {post.author.username}'
... 
# запрашиваем данные FROM "posts_post"
# и, дополнительно, данные автора INNER JOIN "auth_user":
# всё в одном запросе
(0.000) SELECT "posts_post"."id", "posts_post"."text", "posts_post"."pub_date", "posts_post"."author_id", "posts_post"."group_id", "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "posts_post" INNER JOIN "auth_user" ON ("posts_post"."author_id" = "auth_user"."id"); args=()
>>> 

Для select_related хватило только одного запроса! А вот так работает prefetch_related:

>>> related = Post.objects.prefetch_related('author').all()
>>> for post in related:
...     tmp = f"{post.text} Автор {post.author.username}"
... 
# запрашиваем все посты FROM "posts_post"
(0.000) SELECT "posts_post"."id", "posts_post"."text", "posts_post"."pub_date", "posts_post"."author_id", "posts_post"."group_id" FROM "posts_post"; args=()
# а теперь запрашиваем авторов FROM "auth_user", но только с перечисленными id: 
# WHERE "auth_user"."id" IN (1, 2)
# Django ORM получил список необходимых id из результатов первого запроса
(0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" IN (1, 2); args=(1, 2) 

В этот раз запросов два. Сначала Django ORM запросил посты, а потом — информацию об авторах, но только о тех, которые упомянуты в результирующей выборке первого запроса. Вызовите цикл ещё раз и обратите внимание на то, сколько запросов будет отправлено к базе при повторном использовании переменных в зависимости от выбранной стратегии.





Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: