Рассказываем про обновление опенсорс-проекта SQLAlchemy, с помощью которого миллионы Python-разработчиков работают с базами данных.
- Новый интерфейс запросов
- Обновленные менеджеры контекста
- Аннотации типов
- Отношения Write-Only
- Поддержка асинхронности
В начале 2023 года вышла SQLAlchemy 2.0 — библиотека на Python для работы с реляционными СУБД, которая работает с Object Relational Mapper (объектно-реляционным отображением). Основная задача SQLAlchemy — синхронизация объектов Python с данными в БД.
То есть с помощью SQLAlchemy можно описывать структуры БД и работать с их данными на объектно-ориентированном коде на Python без использования чистого SQL. Другая важная особенность SQLAlchemy — код для работы с базой данных будет одинаковым вне зависимости от БД, которую использует разработчик. Такой подход позволяет без проблем мигрировать с одной базой данных на другую.
Полный список нововведений в SQLAlchemy 2.0 можно посмотреть в официальной документации сервиса.
Читайте также: Программирование на Python: особенности обучения, перспективы, ситуация на рынке труда
Новый интерфейс запросов
В SQLAlchemy 2.0 появился новый интерфейс запросов. Если быть точным, эта функция была представлена в релизе SQLAlchemy 1.4 как способ помочь разработчикам перейти на версию 2.0.
До этого основной (но теперь уже устаревший) способ выполнения запросов в ORM SQLAlchemy заключался в использовании объекта Query
, доступного из метода Session.query()
. Либо метода Model.query
, если разработчик использовал расширение Flask-SQLAlchemy для микрофреймворка Flask.
# using native SQLAlchemy
user = session.query(User).filter(User.username == 'susan').first()
# using Flask-SQLAlchemy
user = User.query.filter(User.username == 'susan').first()
В релизе SQLAlchemy 2.0 это теперь считается устаревшим способом выполнения запросов. Разработчики по-прежнему могут делать запросы этими методами, но в документации к SQLAlchemy такой подход уже называется «API запросов 1.x» или «устаревший API запросов».
Новый Query API имеет четкое разделение между самими запросами и средой выполнения, в которой они выполняются. Приведенный выше запрос для поиска пользователя по атрибуту username
теперь можно записать вот так:
query = select(User).where(User.username == 'susan')
В этом примере запрос сохраняется в переменной query
. При этом сейчас запрос еще не выполнен и даже пока не связан с сеансом. Для выполнения этого запроса его нужно передать в метод execute()
объекта сеанса:
results = session.execute(query)
Возвращаемое значение из execute()
— это объект Result
, который функционирует как итерируемый объект, возвращающий объекты Row
с интерфейсом как у кортежа. При этом в самом Python нет функции с таким названием, под Row обычно понимают «строку» или «запись» в контексте работы с базами данных. Если же разработчик хочет получить результаты, не дублируя их, есть несколько методов, которые можно вызвать для этого объекта:
- Метод
all()
позволяет вернутьlist
с объектом строки для каждой строки результата - Метод
first()
вернет первую строку результата - Метод
one()
вернет первую строку результата и вызовет исключение, если в ответе нет результата. Либо в объекте есть несколько одинаковых объектов, которые подходят под результат - Метод
one_or_none()
вернет первую строку результата,None
— если результатов нет, или вызовет исключение, если есть более чем один подходящий к результату объект.
Работа с результатами в виде кортежей имеет смысл, когда каждая строка результата может содержать несколько подходящих значений. Однако, когда у нас есть только одно значение на строку, то извлекать данные из одноэлементных кортежей может быть достаточно утомительно. Поэтому новый интерфейс имеет два дополнительных метода, которые делают работу со строками с одним значением более удобной:
- Метод
scalars()
возвращаетScalarResult
объект с первым значением каждой строки результата. И перечисленные выше методы остаются доступны для этого нового объекта результата. - Метод
scalar()
возвращает первое значение первой строки результата.
Поэтому устаревший запрос может быть теперь выполнен вот так:
user = session.scalar(query)
Обновленные менеджеры контекста
Менеджеры контекста сеанса впервые появились, как и интерфейс запросов, еще в SQLAlchemy 1.4. Но теперь этот подход стал стандартом, который используется в SQLAlchemy.
Раньше сеанс с локальной областью видимости был основным шаблоном для работы с сеансами. Расширение Flask-SQLAlchemy, например, включило его в переменную db.session
, которая является его сигнатурой. То есть раньше сеанс имел ограниченную зону видимости и привязывался к потоку. Даже несмотря на то, что условная жизнь сеанса может быть намного короче, чем потока — и лечилось это только ручным управлением.
Теперь сеанс можно инициировать с помощью менеджера контекста, что позволяет прозрачно наблюдать за началом сеанса и его концом:
with Session() as session:
session.add(user)
session.commit()
Здесь сеанс закрывается, когда заканчивается блок диспетчера контекста. И, если внутри него возникает ошибка, то сессия откатывается.
Вариант этого шаблона можно использовать для сеанса, который автоматически фиксируется в конце, но все еще откатывается при ошибках:
with Session() as session:
with session.begin():
session.add(user)
Аннотации типов
Еще одно интересное изменение, представленное в версии SQLAlchemy 2.0, — это возможность использовать подсказки при вводе данных во время объявления столбцов и связей в моделях. По сути, это введение элементов строгой типизации. Рассмотрим это на модели User
:
class User(Model):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(64), index=True, unique=True, nullable=False)
email = Column(String(120), index=True, unique=True, nullable=False)
password_hash = Column(String(128))
about_me = Column(String(140))
В версии 2.0 тип столбца можно определить с помощью Mapped-подсказки. Если есть какие-то дополнительные опции, их можно указать в mapped_column()
.
import sqlalchemy as sa
import sqlalchemy.orm as so
class User(Model):
__tablename__ = 'users'
id: so.Mapped[int] = so.mapped_column(primary_key=True)
username: so.Mapped[str] = so.mapped_column(String(64), index=True, unique=True)
email: so.Mapped[str] = so.mapped_column(String(120), index=True, unique=True)
password_hash: so.Mapped[Optional[str]] = so.mapped_column(String(128))
about_me: so.Mapped[Optional[str]] = so.mapped_column(String(140))
Применение подсказок для типов данных может дать разработчикам несколько преимуществ:
- При использовании IDE, которая выполняет статический анализ кода и предлагает изменения по мере ввода, строго типизированная модель поможет вашей IDE лучше понять код.
- Это позволяет проще работать с
dataclasses
, которые также опираются на выбранный разработчиком тип данных. - Это приводит к тому, что из SQLAlchemy теперь нужно импортировать меньше символов. Потому что теперь для столбцов, которые принимают данные только в числах, датах, времени или даже UUID, можно указывать формат в виде сразу в виде подсказки.
Как и в случае с запросами, SQLAlchemy по-прежнему поддерживает старый способ определения столбцов и связей.
Читайте также: Как создатель Python Гвидо ван Россум устроился в Microsoft и теперь работает над развитием CPython
Отношения Write-Only
Динамические отношения считаются устаревшими в SQLAlchemy 2.0, поскольку они несовместимы с новым интерфейсом запросов. Вместо этого рекомендуемым решением является новый тип отношений под названием Write-Only («Только для записи»). Вот как определить отношения только для записи:
class User(Model):
# ...
tokens: WriteOnlyMapped['Token'] = relationship(back_populates='user')
Отличие от старой динамической связи в том, что связь Write-Only не загружает и не читает связанные объекты. Она только предоставляет методы add()
и remove()
для внесения изменений или для записи в них.
С таким типом отношений можно получать связанные объекты через метод select()
, который возвращает запрос, его можно выполнить в сеансе — например, после добавления фильтров, сортировки или разбивки на страницы.
Вот пример того, как получить объекты tokens
, связанные с пользователем, и отсортированные по сроку их действия:
tokens = session.scalars(user.tokens.select().order_by(Token.expiration)).all()
Поддержка асинхронности
SQLAlchemy 1.4 представила бета-версию расширения asyncio, которая показала асинхронные версии объектов Engine
и Session
. В версии 2.0 это расширение больше не считается бета-версией.
Большая часть настройки асинхронного решения в SQLAlchemy включает в себя предотвращение всех способов неявной работы с БД для того, чтобы минимизировать количество возможных ошибок. Поэтому для использования расширения asyncio нужно, чтобы разработчик хотя бы базово понимал, как работает asyncio под капотом и умел разбираться над выводом ошибок. Подробнее про асинхронность в SQLAlchemy можно почитать тут.
Этот текст — адаптированный перевод материала What's New in SQLAlchemy 2.0? с сайта blog.miguelgrinberg.com
Изучите Python на Хекслете Пройдите нашу профессию «Python-разработчик», чтобы поменять свою жизнь и стать бэкенд-программистом.