Чуточку облегчаем жизнь проекту
Проблема в неоптимальном использовании queryset, что приводит к излишне тяжелым запросам, либо вовсе к появлению лишних запросов. За прошедшую неделю я встретил подобное несколько раз, а значит нам нужно поговорить.
Рассмотрим один пример (в примере нас интересует исключительно queryset exist_borrower):
В этом методе кверисет exist_borrower на ровном месте делает в базу три одинаковых запроса вида:
Первый запрос формируется при вызове if exist_borrower. Да, вы можете сказать что при вызове exist_borrower.__bool__() - результат запроса кэшируется, и будете правы. Но далее по коду у exist_borrower дергаются методы .get() и .last() которые игнорируют этот кэш. В результате мы получаем три одинаковых запроса, где дергаются все поля модели.
Попробуем последовательно улучшить этот код.
Если нужно проверить что кверисет не пустой - следует всегда использовать метод .exists()
При выполнении exist_borrower.exists() получается такой запрос:
Если еще приглядеться к коду, то можно заметить что нам достаточно всего один раз получить заемщика, и на весь метод получится лишь один запрос:
exist_borrower = Borrower.objects.filter(**params).first()
В принципе на этом варианте можно остановиться. Но если вы понимаете что данный метод очень часто используется, либо модель довольно тяжелая, то можно сразу получить лишь нужные поля:
В таком случае единственный запрос будет выглядеть так:
Я умышленно не привожу в примере метод .only(), т.к. его использование требуется гораздо реже, требует от разработчика большей внимательности, и может сыграть злую шутку если неаккуратно с ним обращаться.
Еще немного
Если нужно проверить кверисет на пустоту - делаем queryset.exists()
Если нужно получить количество объектов в кверисете, то необходимо использовать только queryset.count()
и никогда не следует делать len(queryset)
(встречается и такое).
len(queryset) достает из базы объекты со всеми полями (тот самый большой запрос из начала заметки), а затем питоном получает их количество.
queryset.count() сразу спрашивает у базы количество объектов:
Не используйте обращение по индексу к объекту в кверисете. Обращение к объекту из списка по его какому-то магическому номеру - плохая практика.
Если вам нужен первый или последний объект в отсортированном кверисете - для этого есть методы .first() и .last(), они хорошо влияют на читаемость (к тому же queryset[0] необходимо оборачивать в блок try except).
Если из queryset нужно достать объект под определенным номером, скорее всего это значит что-то пошло не так в реализуемой логике. Попробуйте еще раз.
Если вы сомневаетесь в запросах которые генерируются ORM, или интересно посмотреть в каких ситуациях запросы кешируются, а в каких нет - можно запустить шелл с отладкой запросов:
./manage.py shell_plus --print-sql
(это возможно благодаря батарейке django_extensions)
Все запросы в базу будут отображаться в консоли в момент выполнения.
Итоги
Я призываю аккуратно обращаться с запросами, потому что в часто используемых методах пренебрежение этим правилом может давать ощутимые латенси, нагрузку на базу, или вовсе иметь кумулятивный эффект.
Также, если вам вдруг посчастливилось дорабатывать метод подобный тому что в примере - хорошим тоном будет навести в таком методе порядок.
Документация, чтобы освежить память:
https://djbook.ru/rel1.9/topics/db/queries.html про запросы на русском (django 1.9)
https://docs.djangoproject.com/en/2.0/topics/db/queries/ про запросы на англ. (django 2.0)