четверг, 14 февраля 2019 г.

Организация асинхронной работы с РСУБД на языке Python

Нет времени ждать! Нет времени ждать блокирующие I/O операции, поэтому практически каждый backend-разработчик рано или поздно задумывается об использовании асинхронного веб-фреймворка.

На данный момент у Python-разработчика существует достаточно большой выбор фреймворков с различными реализациями event loop’а: от Twisted, больше похожего на сетевую библиотеку, до http клиента и сервера для asyncio aiohttp (>6500 звезд на GitHub), Flask-like фреймворка sanic (>11000 звезд на GitHub) и http клиента и сервера Tornado (>17000 звезд на GitHub).

Редкий веб-сервер обходится без работы с хранилищами данных. И здесь приверженцев реляционных СУБД поджидает неприятный сюрприз: SQLAlchemy ORM, самая популярная ORM для Python, не поддерживает асинхронную работу. Рассмотрим пути решения возникшей задачи удобной работы с РСУБД без использования самой популярной Python-ORM.

Техническая постановка задачи


Рассмотрим слои абстракций, отделяющие СУБД от кода в нашем Python-приложении.
  1. Драйвер базы данных (Database Driver) – это программный компонент, реализующий программный интерфейс (API) доступа к базам данных (ODBC, JDBC, DBAPI, ADO.NET и т.д.). Это адаптер, который соединяет общий интерфейс с конкретной реализацией СУБД. Может иметь как синхронную, так и асинхронную реализацию. Примеры асинхронных драйверов для Python: aiopg, asyncpg (PostgreSQL), aiomysql (MySQL). Драйвер позволяет создавать подключение к СУБД и в рамках него выполнять SQL-запросы через курсоры.
  2. Набор абстракций над SQL – это набор инструментов, обеспечивающий абстракции для различных реализаций драйверов, а также язык выражений, позволяющий выражать язык SQL с помощью выражений языка Python, включая DDL и отображение типов языка Python на типы СУБД. Пример такого инструмента: SQLAlchemy Core. Синхронность/асинхронность зависит от выбранного драйвера базы данных.
  3. ORM – инструмент, который дает возможность представлять табличные сущности в виде обычных классов и обращаться с ними как с простыми классами, практически не применяя SQL. Чем абстрактнее инструмент, тем сложнее его реализация, создатель SQLAlchemy Майк Байер подробно описал проблемы создания асинхронных ORM в своем блоге. Примеры асинхронных ORM для Python: peewee-async, GINO, Tortoise ORM.
Итого, нам нужно определиться, на каком слое становиться, и какие реализации какого слоя использовать.

Варианты решения


Базовый вариант с привлечением в проект минимума сторонних сущностей: берем драйвер для выбранной СУБД (если он существует, конечно) и пишем/генерируем сырой SQL-код. Скорее всего придется писать свою систему очистки данных (защита от SQL-инъекций, экранирование спецсимволов и т.д.). Подход хорош тем, что разработчик полностью контролирует, как его код взаимодействует с СУБД: от точных текстов SQL-запросов до понимания, когда какое действие с какой таблицей будет произведено.

Так будет выглядеть выборка данных, реализованная на курсорах:
cur = yield from conn.cursor()
yield from cur.execute("SELECT host,user FROM user")
r = yield from cur.fetchall()

Когда нам надоест писать сырой SQL, мы обратимся, например, к SQLAlchemy Core и опишем нашу таблицу на Python:
user = Table('user', metadata,
    Column('user', String(256), nullable=False),
    Column(host, String(256)),
)

Теперь мы можем собирать запросы по кирпичикам:
s = select([user])
Или:
s = user.select()

Пример посложнее:
s = select([(user.c.user +
              ": " + user.c.host).
               label('identity')]).\
       where(
              or_(
                 user.c.user.like('%admin%'),
                 user.c.host.like('%.com')
              )
       )
Но как и прежде придется выполнять их руками через курсор:
r = yield from conn.execute(s).fetchall()

Peewee-async – библиотека, позволяющая использовать замечательную понятную, легкую и мощную ORM peewee с asyncio. На данный момент peewee-async работает с Python версии 3.4 и выше, поддерживает PostgreSQL через aiopg и MySQL через aiomysql, поддерживает все базовые операции и транзакции. В процессе использования peewee-async я столкнулась с вполне предсказуемой проблемой ленивой подгрузки связанных сущностей. При обращении к полям неявно выгруженной сущности peewee-async может намертво повесить event loop, причем происходит это не каждый раз, поэтому не всегда удается заметить такое поведение во время разработки. Во избежание данной проблемы приходится делать “лишний” явный запрос для связанных сущностей, если ожидается работа с их полями.

Модели описываются следующим образом:
class User(peewee.Model):
    user = peewee.CharField()
    host = peewee.CharField()
    
    class Meta:
        database = database

Простейшая выборка будет выглядеть так:
users = await objects.execute(User.select())

Нежелание разбираться с вышеописанными вариантами может мотивировать разработчика еще раз подумать о целесообразности использования реляционной модели данных в разрабатываемой системе. Может быть, предметная область полна вложенных сущностей или древовидных структур данных? В таком случае можно облегчить себе жизнь и посмотреть в сторону MongoDB и Motor.

Вывод


Самым гибким и простым вариантом на первый взгляд является использование сырого SQL. Однако, после очередного бага в методах очистки данных или после очередного метода выборки/обновления данных, который вы по вашим ощущениям написали уже без малого сотню раз, можно начать жалеть о сделанном выборе.

Для меня оптимальным вариантом является использование ORM peewee-async. Она генерирует предсказуемый SQL, который всегда можно посмотреть на этапе разработки, и я ни разу не столкнулась с тем, чтобы оверхед от ее использования серьезно повлиял на быстродействие разрабатываемой мной системы. Такой вывод я сделала после использования peewee-async в проектах, в которых было много CRUD-операций с использованием большого количества связанных сущностей, много нетривиальных выборок, и относительно небольшие объемы данных.

Полезные ссылки


https://github.com/timofurrer/awesome-asyncio
https://techspot.zzzeek.org/2015/02/15/asynchronous-python-and-databases/
https://emptysqua.re/blog/response-to-asynchronous-python-and-databases/

Автор: Анна Вейс, Python developer @ Antida software.

Комментариев нет:

Отправить комментарий