SqlAlchemy 1.4 теперь поддерживает asyncio

Калух! Callay!¹ SqlAlchemy 1.4 теперь поддерживает asyncio. Проделанная ими работа впечатляет.

В течение многих лет я использовал SqlAlchemy с Tornado, и мне пришлось прибегнуть к ThreadPoolExecutor, чтобы мои запросы выполнялись параллельно. Больше не надо. Теперь он будет асинхронным вплоть до последней черепахи. Если, конечно, вы не решите использовать ORM. Тогда зелень решит все ваши проблемы. Но давайте сделаем шаг назад и обсудим редакционное решение, которое вы принимаете при разработке API.

Функционировать или класс?

Первое редакционное решение часто является делом привычки. Некоторые, которых я знаю, не допустили бы накладных расходов на классы — это все наследование и ненужная сложность. Они иногда ошибаются. Элегантная иерархия классов часто может облегчить задачу навигации по графу кода перед вами. В другое время куча функций подойдет.

Возьмем, к примеру, простоту изучения Java. Все было классом, и все требовало реализации интерфейса. Затем вы смотрите на пакет Python — какой стиль использовал этот Уолли? функционал, классы или — о, горе мое. Документация помогает, но бывают случаи, когда я не мог понять, чего они ожидали, не читая исходный код.

Разглагольствуйте, давайте посмотрим на наши варианты. Мы пытаемся создать простое приложение для учета рабочего времени. Я решил смоделировать это следующим образом:

Почему? Потому что пользователь будет регистрировать использование своего времени и делать это в течение дня. Только пользователь может регистрировать время, и только пользователь может редактировать свое время. Мы изолировали действия в пространстве имен пользователя; деятельность не делится. Таким образом, журнал — это просто композиция временной метки и действия. Пользователь также будет сохранять заметки о дне.

Так как же нам генерировать представления для этой модели? Собираемся ли мы предоставлять CRUD-подобный интерфейс или предполагать контекст? Давайте сначала посмотрим на представление журнала:

Во-первых, кто является текущим зарегистрированным пользователем? Затем давайте упорядочим некоторые даты, которые могут быть None и могут быть плохо отформатированы. Я позволил python-dateutil решить эту проблему за меня. Затем мы переходим к нашему запросу. Мы используем новый оператор 1.4 Session и select. Во-первых, он контекстно-зависим. Он закрывается, когда покидает контекст, и вместо передачи массива выбираемых мы используем *args для их передачи. Это чисто и интуитивно понятно. Присоединяемся, добавляем where и order_by. Затем этот курсор имеет метод all(), и, поскольку каждая строка имеет интерфейс элементов, мы можем деконструировать их в аргументы для нашего конструктора dto. Боже мой, это похоже на SQL. Ну разве не в этом дело - писать sql на своем питоне.

Давайте теперь посмотрим на эквивалентный запрос в мире ORM.

Вы можете видеть, что запрос стал немного более компактным и что наш класс модели знает о различных поддерживаемых им представлениях. Есть некоторая сложность в использовании options для выбора - какую стратегию загрузки использовать? Но подождите - нет никакого ожидания. Это потому, что мы используем абстракцию гринлета, встроенную в SQLAlchemy 1.4, которая называется: session.run_sync. Это позволяет нам обращаться с нашей асинхронной моделью так, как если бы это был старый синхронный стиль. Теперь вот еще одно забавное расширение библиотеки:

Наш класс Log объявляет метаданные __table__, что позволяет использовать как таблицу на языке выражений, так и модель в ORM! Поэтому, если вам не нужны накладные расходы ORM, не используйте их. Если вы хотите ORM и его кеш сеанса - используйте его. Попытка объединить эти миры в цельную метафору поразительна, но документация по-прежнему фрагментарна и непонятна.

Так что ответ оба. Определите свою схему с помощью таблицы, дополните ее моделью и напишите функции, которые используют преимущества обоих. Почему функции? Потому что мы создаем API.

О контексте и связях

Контекст — это расширенное пространство в Python, начиная с версии 3.7. Наш contextvars теперь использует как thread.local, так и task.local. Таким образом, мы можем поместить текущего пользователя в ContextVar, и независимо от того, запускаем ли мы задачу или работаем в ThreadPoolExecutor, каждый раз, когда мы обращаемся к переменной, это будет правильно. То же самое происходит и с Session.

Мы можем настроить это async_session_factory из наших переменных среды:

Или, когда будем тестировать, забрать их из нашего setup.cfg:

Вы можете спросить, что такое Config и command? Это Alembic, и мы готовим базу данных в фикстуре pytest, вроде как django.unittest. Используя alembic для нашей миграции базы данных, мы получаем возможность тестирования, а также миграцию схемы, и она управляется из table.metadata. Alembic поддерживает асинхронность с новым шаблоном. Я злоупотребляю системой, помещая каталог сценариев в свой пакет Python и переименовывая файл alembic.ini в setup.cfg. В сегодняшнем мире .cfg и .toml трудно понять, где должна находиться вся конфигурация. Мне не удалось избавиться от setup.cfg, хотя setup.py уже нет. pyproject.toml отлично, но никто так и не смог разобраться, кто чем занимается и где. Я уверен, что в конце концов все будет ясно, но на данный момент мне все еще нужен setup.cfg, формат, понятный alembic, и pyproject.toml, потому что black не будет смотреть в .cfg. Имейте в виду, что это только для тестирования. В рабочей среде большая часть конфигурации загружается из переменных среды или аргументов командной строки.

Итак, теперь мы можем протестировать наши таблицы и модель и инициализировать нашу базу данных. Итак, давайте протестируем что-нибудь.

Тестирование

Для тестирования асинхронного кода вам понадобится расширение pytest. Я использовал pytest.torn-async и pytest.asyncio. Поскольку мы еще не работаем с tornado, я буду использовать pytest.asyncio. Он ожидает конфигурацию и посмотрит на ваш setup.cfg — добавьте это:

[tool:pytest]
asyncio_mode = strict

В противном случае он будет жаловаться с предупреждением об обесценивании. Теперь мы собираемся идти. Давайте напишем функцию register для регистрации пользователя:

Мы делаем вставку и выбор, чтобы выбрать значения по умолчанию из базы данных. Существует тенденция использовать returning, что сделало бы это ненужным, но мои серверные части (mysql и sqlite)² не поддерживают его. Я абстрагировался от select, так как он используется во многих местах. И я поднял IntegrityError, который, возможно, следует абстрагироваться. Мы также зашифровали пароль — его, вероятно, следует посолить. Но мы можем проверить это:

Наше устройство init_db просто совершает набеги на setup.cfg, понижает и обновляет базу данных. Декоратор asyncio сообщает pytest.asyncio, что мы должны работать асинхронно. И мы можем проверить, что возвращаемый класс содержит значения по умолчанию, которые мы не указывали. Мы также не можем зарегистрироваться дважды.

Итак, если бы register был методом класса в model.User, как бы выглядел наш тест?

Мы написали синхронную функцию в нашем тесте и назвали ее session.run_sync. Итак, мы ответили на вопрос? Это модели или таблицы? функции или классы? Еще оба.

Надеюсь, вы видите, к чему все идет. Мы создаем приложение, в котором пользователь будет актором UML в нашей системе. Этот пользователь будет регистрировать действия — нам не нужно знать идентификатор действия, первичный ключ базы данных, просто текст, который они использовали.

REST создает конечные точки, а CRUD предоставляет нашим акторам реализацию нашей базы данных. Вместе они составляют ленивую абстракцию наших моделей. В этом мире нужно было бы создать действие, прежде чем можно будет создать журнал. Журнал будет:

{id, activity_id, created}

или хуже:

{id, activity:{id, name, user:{id, name}}, created}

Контекст API упрощает получение результатов и позволяет предоставить функциональный интерфейс. Каждая функция возвращает график, но это не просто копия модели базы данных. Итак, наш журнал {id,activity, created}.

Итак, как же выглядит эта мифическая функция log:

Это версия модели log. Он более компактен, чем функциональная версия. Мы создаем now, передаем его с текущим пользователем и предоставленным activity_name в класс, который мы добавляем в сеанс, и commit! Потом мы с rpc.broadcast сообщаем всем устройствам жены, что она добавила лог и даже тот, который она использовала для добавления. Если бы была какая-то ошибка, только одно устройство - вызывающее - справилось бы с ней, в противном случае они все равны и связаны!

Если вы дочитали до этого места, вам захочется увидеть код. Он есть на Replit³ вместе с рабочей версией. При этом используется rpc, описанный в разделе Удаленные процедуры, пожалуйста.

Заключить

Модели касаются состояния и функции — инкапсуляции. Они делают управление отношениями в графе менее подверженным ошибкам. Это видно по тому, что к модели таблицы добавлены только отношения — плюс, конечно, несколько представлений. Но мы живем в функциональном мире — мире удаленных процедур. И часто понятная стоимость ORM не нужна. Если вы знаете sql, сопоставьте его с языком выражений и await ваша радость.

Авторы SQLAlchemy заслуживают похвалы за проделанную ими работу. Я не мог быть счастливее, используя их пакет, чтобы проложить свой путь. Пользуюсь 1.4 уже год и только хихикаю от радости. В сочетании с торнадо и vue можно делать что угодно. Действительно vorpal¹ клинок.

В следующей статье мы соберем все воедино и покажем конечный продукт. Предыдущая статья:



[1] https://www.poetryfoundation.org/poems/42916/jabberwocky

[2] https://docs.sqlalchemy.org/en/14/orm/persistence_techniques.html

[3] скоро.