Здравствуйте, дорогие бэкенд-разработчики на Python, у меня опять статья про django. И так будет до тех пор, пока в нём не появится нормальная поддержка асинхронности (шучу).
Вопреки распространённому мнению, что django — это фреймворк «с батарейками», но не очень поддаётся кастомизации, это не так. Необходимость поддержки разных провайдеров, поддержка так называемого multi-db (одновременное использование нескольких баз), да и просто банальное использование здравого смысла (местами) сделало этот фреймворк одним из наиболее расширяемых среди ORM.
В этой статье я расскажу, как устроен database backend — это та штука, которая отвечает за поддержку конкретной базы данных и конкретного драйвера для неё. Я сделаю это на довольно экзотическом примере: мы с вами добавим поддержку асинхронного драйвера — psycopg3. Да, именно асинхронного. А Вы думали, что django так не умеет? Читайте и судите сами.
В общем, интрига — это не мой конёк, хочу сразу рассеять туман над асинхронностью. Дело в том, что я использую похожий подход, что и sqlalchemy, именно — greenlet. Я уже писал об нём на хабре, также о нём можно прочитать здесь. В общем, есть такая древняя магия, которую поставили на службу производству.
Так вот, в силу этого выбранного подхода, написание бэкенда для асинхронного драйвера не очень уж отличается от написания оного для синхронного случая. В качестве драйвера я выбрал psycopg3: у него более дружелюбный API, близкий к DB-API2. Одиозный же asyncpg не следует ему совершенно, хотя по производительности и выигрывает. Кстати, упомянутый подход с гринлетами позволяет сделать асинхронный драйвер тоже совместимым с DB-API2. Что я и сделал.
Есть, конечно, и принципиальные отличия асинхронного бэкенда, точнее, одно отличие. В синхронном случае, каждый поток обрабатывает в данный момент времени всего один запрос. За ним закреплено определённое подключение к базе — коннекшн, и он им пользуется. В асинхронном же случае, одновременно обрабатывается большое число запросов — нельзя заранее сказать, какое. Далеко не факт, что мы сможем каждому такому обработчику предоставить по коннекшну — их просто не хватит. Вместо этого, нужно использовать пул коннекшнов, и выдавать их оттуда во временное пользование (на время одной транзакции).
Этого несложно добиться. Например, как в джанговских бэкендах обычно используется коннекшн?
with connection.cursor() as cursor: # use cursor
Для асинхронного бэкенда, можно делать то же самое, но по закрытии курсора коннекшн возвращать в пул. Обязательно нужно использовать контекстный менеджер, чтобы гарантированно вернуть коннекшн.
Кстати, с названиями в django есть явная путаница. Вот, например, как получить дефолтный коннекшн? Вы, возможно, думаете, что так:
from django.db import connection, connections connection # <- это прокси-объект connections['default'] # <- вот к этому
Так вот, ничего подобного, это database backend, а не коннекшн. Сам коннекшн находится в одноимённом атрибуте:
connection.connection
Так что, не перепутайте. Ещё встаёт вопрос об автокоммите. Дело в том, что, если мы всегда берём коннекшн на время и точно знаем, когда мы его отдадим, то зачем нам автокоммит? Мы гарантированно сможем сделать коммит сами. Вообще, мне кажется, автокоммит — одна из неудачных идей в django.
В общем, об этом, наверно, можно говорить ещё долго, а у нас есть репозиторий с кодом, вот он. В директории pgbackend лежит — Вы, наверно, догадываетесь, что. В директории proj — тестовый проект django — для тестирования этого бэкенда, kitchen — тестовый django-app для тех же целей.
Код рабочий — можете запустить и проверить, в README есть инструкции. Извиняйте,что в зависимостях для django указан гит-репозиторий — дело в том, что нужна ветка с поддержкой psycopg3, а её ещё не вмерджили в main (но скоро вмерджат!)
Так вот, если Вы не догадались, директория pgbackend — это наш бэкенд. Мало-мальски продвинутые джангонавты знают, что там нужно искать модуль base — он там есть. Открыв его, вы увидите, что он почти пуст: прописан ops_class, в котором указан модуль с «компиляторами», в конструкторе происходит создание коннекшна — и больше ничего.
Отлично, значит, в модуле base.py почти ничего нет — смотрим, есть ли что-то где-нибудь ещё.
Я упомянул компиляторы. Если что, они «компилируют» query в SQL. Компиляторы — один из основных способов кастомизации бэкенда. Вообще, по секрету, весь бэкенд — это всего один метод в компиляторе — execute_sql. Так что, вообще говоря, заменить бэкенд достаточно легко — нужно заменить эту единственную функцию. На самом деле, компилятор не один, а на каждый тип запроса свой — для SELECT, INSERT, UPDATE, DELETE. Но это уже частности. В целом, бэкенд, как я уже говорил, легко поддаётся расширению. А за то, что, по сути, нужно переопределить всего одну функцию — плюсы в карму разработчикам django.
Раз уже начал говорить про компиляторы — скажу об изменениях, которые там потребовались. В общем, есть в django такая фича, как QuerySet.iterator() — поддержка server-side cursors. Она позволяет отдавать результаты запроса (и выполнять его — тоже) по частям, чанками. Это может пригодиться, например,если результат запроса не помещается в память. Для пагинации использовать это вряд ли разумно, учитывая, что мы занимаем целый коннекшн на неопределённое время.
В общем, я решил, что эта фича ни зачем не нужна, и решил её не поддерживать — ну её. По завершении execute_sql() я просто закрываю курсор и возвращаю коннекшн в пул. Уважаемые разработчики, если Вы знаете, зачем нужны server-side cursors, пожалуйста, напишите в этот issue. Я уже сомневаюсь — возможно, я верну их обратно.
В репозитории также можно найти код враппера вокруг курсора — я им пользуюсь, чтобы превратить асинхронный курсор в синхронный. После превращения, мой курсор вполне соответствует DB-API2.
Транзакции в django не очень предназначены для переопределения в бэкенде — это обычная функция atomic и класс Atomic. Всё равно, это не стало большим препятствием: посмотрите модуль patches — узнаете, почему.
Возможно, если поискать, то найдутся ещё интересные моменты. Ну, а в остальном — скучный кодинг, что тут рассказать?
Что касается самого бэкенда — он самый что ни на есть настоящий, предназначается для продакшна. Не тестировался ещё — это правда, но это, как раз, следующий этап. Кстати, я могу использовать любые тесты из джанго-сюиты (testsuite) для тестирования своего бэкенда. Возможно, ещё своих добавлю. И — можно публиковать бета-версию. Разработчиков django только попросить не сломать его за это время. Нет, это уже лишнее.
ссылка на оригинал статьи https://habr.com/ru/post/698938/
Добавить комментарий