Привет! Меня зовут Ваня, я бэкенд-разработчик-стажёр в KTS.  Недавно я нашел баг в Django, создал тикет с исправлением и его приняли. В статье расскажу подробнее — над чем работал, в чём была ошибка и почему её сложно встретить. А также ещё про один баг, который по классике оказался фичей 😊. Что будет в статье: Предыстория и немного о работе Django Оптимизация запросов Ошибка Django Причины бага Тикет и Pull Request Наша оптимизация Баг, который оказался фичей Заключение У меня высшее медицинское образование и 4 года работы хирургом.  Со школы меня интересовали математика и компьютерные технологии, но этот интерес долго оставался лишь в виде хобби — например, я делал графические патчи на телефоны. На уроках информатики преподавали лишь Word и PowerPoint, а крепкой математической базы или знакомых, связанных с IT, у меня не было. Так что я не воспринимал IT как свою профессию.  В свободное время я читал статьи по разработке. Особенно нравилась тематика нейронных сетей и параллели с работой человеческого мозга. На 4-м курсе медакадемии понял, что созрел для изучения языка программирования для общего развития. Для этой цели давно присмотрел Python, и благодаря книге Марка Лутца «Изучаем Python» началось моё погружение в мир программирования. Не описать словами кайф, который я ощутил от первых работающих строк кода! Так из года в год, медленно, но верно, повышались мои знания и навыки по Python. Ещё немного игрался с Unity. Я люблю медицину, но всё время работы меня не покидало желание заниматься тем, что нравится ещё больше — программированием. В связи с большим количеством негативных моментов в работе хирурга я наконец решился сменить род деятельности и плотно занялся самообучением. Начал со школьных курсов программирования, потом изучал алгоритмы и решал задачи для подготовки к школьным олимпиадам, читал о веб-разработке. В январе 2022 года по счастливой случайности узнал о наборе в бесплатную школу начинающих бэкенд-разработчиков и буквально в последний день набора успел подать заявку. За месяц обучения познакомился с aiohttp, открыл для себя мощь баз данных, SQL и ORM. Стало намного проще писать асинхронный код. Местами было очень трудно и больно, но оно того стоило. Во время написания курсового проекта изучил работу с git.  После окончания курса меня пригласили на собеседование, в результате чего удалось попасть на стажировку.  Пользуясь случаем, хочу поблагодарить за помощь и наставления в работе коллег из KTS, особенно своего наставника Сашу 👏. Знакомиться с Django я начал буквально с первых дней стажировки. Ранее был опыт с aiohttp + SQLAlchemy/GINO.  На мой взгляд, одной из самых крутых особенностей Django является Django ORM, который преобразует запрос в базу данных из Python-кода в язык SQL. Это значительно облегчает процесс разработки, но есть одно «но». При получении данных через отношения эти запросы не оптимизируются, и возникает так называемая проблема N+1 запросов. Чем больше запросов в БД на бэкенде, тем медленнее работает приложение. Для решения необходима оптимизация, чтобы доставать нужные данные за меньшее количество запросов. Основными методами оптимизации запросов в Django являются: select_related prefetch_related (включая класс Prefetch) only Для создания API имеется 2 основных варианта: REST API и GraphQL. В рабочем проекте мы используем GraphQL.  REST API имеет довольно строгую архитектуру, поэтому запросы в БД можно оптимизировать вручную (захардкодить). GraphQL использует более гибкий подход: клиент сам определяет, какие поля необходимо получить, поэтому оптимизировать что-либо вручную довольно сложно. Можно сразу делать select_related и prefetch_related по всем полям, но тогда сложно предусмотреть все варианты запросов, будут лишние «джойны» и из БД будет извлекаться избыточное количество данных. Именно поэтому мы решили сделать оптимизацию запросов. Задача автоматической оптимизации запросов показалась мне очень интересной, поэтому я взялся за её решение. Идея состояла в том, чтобы доставать запрашиваемые поля из info (содержит всю информацию о запросе), рекурсивно проходить по внешним ключам моделей и на основании типа отношения применять нужный метод оптимизации. Вся необходимая информация о модели находится в атрибуте _meta. Так как информация о модели статична, ее можно кэшировать при помощи functools. В info все названия полей указаны в формате CamelCase, поэтому нам нужно привести их к формату snake_case. Это удобно делать при помощи регулярных выражений. В итоге waitingAsyncDjango преобразуется в waiting_async_django: Рекурсивно узнаём, какие именно поля запрашиваются в GraphQL. Для этого потребуется функция для преобразования абстрактного синтаксического дерева в словарь ast_to_dict, данные берём из info: Для работы с only находим названия всех полей модели: Для select_related находим названия полей с типом отношений many-to-one и one-to-one, включая реверсивные типы этих отношений: Для prefetch_related и Prefetch находим названия полей с типом отношений one-to-many и many-to-many, включая реверсивные типы этих отношений: Для удобства работы с данными определяем dataclass: Узнав запрашиваемые поля и типы отношений, можно приступать к рекурсивному заполнению списка полей для каждого метода оптимизации:  Для удобства мы определили менеджер моделей и абстрактный класс моделей: Теперь можно с легкостью оптимизировать почти любые GraphQL-запросы: В дальнейшем этот код оброс новым функционалом, например оптимизацией property моделей. Метод only позволяет задавать список полей, которые нужно получить из базы данных. Получая только нужные поля, можно немного увеличить производительность приложения. В only можно указывать поля и других моделей, которые будут заджойнены. Важно то, что Django автоматически достает pk (primary key) моделей, даже если это не указано в only. На основании базовой модели Django позволяет создавать любое количество таких же моделей. Они будут различаться поведением, но при этом в БД будет существовать только одна базовая модель. Это достигается за счет proxy-моделей. И вот при оптимизации запросов к proxy-модели иногда возникала такая ошибка: Исследование показало, что ошибка возникала при попытке получить proxy-модель в select_related с применением only. Ошибка возникала только с only. Судя по всему, у прокси-модели почему-то не доставался pk, хотя это должно происходить автоматически.  Я принудительно добавил pk (в нашем случае это id) proxy-модели в only, и ошибка исчезла. Стало очевидно, что проблема где-то в Django. Тогда я начал искать, где Django автоматически добавляет pk в список доставаемых полей. Поиски привели в django/db/models/sql/query.py, класс Query, функция deffered_to_data.  Во время дебага обнаружил, что pk достается у прокси-модели, а запрашиваемые поля — у базовой. Этот код показался подозрительным: Ошибка исчезла, когда pk стали доставать именно у базовой модели:  Убедившись, что в разделе Django Issues не было тикетов с подобным багом, я создал новый. Разработчики приняли его и предложили подготовить патч. Необходимо было создать pull request с фиксом бага и тестом. К счастью, в Django для этого есть отличная инструкция. Так я сделал свой первый pull request в open-source проект. В нашей реализации этот баг тоже пофиксили: сделали принудительное добавление pk в only при получении полей proxy-модели. Интересно, что мы вряд ли нашли бы эту ошибку без кастомной оптимизации запросов. Мы искали готовые реализации и библиотеки, и даже нашли одну — https://github.com/tfoxy/graphene-django-optimizer. Но она оказалась недостаточно гибкой для использования в некоторых сценариях и иногда делала даже больше запросов в БД, чем наша реализация. Больше ничего похожего мы не нашли, но если знаете, поделитесь в комментариях 😉 Во время работы я нашел еще один баг, который значительно увеличивает количество запросов в БД. Он возникает, если делать Prefetch через реверсивное отношение. При таком GraphQL запросе… … оптимизация должна быть следующей: Но в этом случае наблюдается огромное количество запросов в БД. Причина проблемы в этой строке: Стоит добавить в only реверсивный pk (publisher_id), и запросов в БД становится в 10 раз меньше (в зависимости от количества записей в БД): Найдя причину бага и способ его исправления, я создал еще один тикет. Но... Разработчики ответили, что функция prefetch_related_objects не должна неявно добавлять какие-либо столбцы, так как это может быть неожиданным и вводящим в заблуждение. Они добавили, что те, кто используют only и defer, несут полную ответственность за передаваемые в них поля.  Тикет не подлежал фиксу и был отмечен как New feature. В нашей реализации мы просто убрали использование only в Prefetch. В августе 2022 года ожидается релиз Django 4.1, где будет представлен асинхронный интерфейс для Django ORM. Под капотом запросы пока что синхронные, но разработчики продолжают работать над внедрением полноценной асинхронности. Думаю, когда это произойдет, для меня выбор будет очевидным.  💻🎓 8 августа — старт нашей бесплатной школы начинающих фронтендеров и бэкендеров.  Если вы начинаете учиться разработке и вам интересны подобные штуки, приходите!