Библиотека SQLAlchemy: возможности и примеры использования
SQLAlchemy - это мощная библиотека для работы с базами данных в Python, предоставляющая как высокоуровневый ORM (Object-Relational Mapping), так и низкоуровневый Core (SQL Expression Language). Она позволяет разработчикам выбирать между полной абстракцией таблиц в виде объектов Python и прямым написанием SQL-запросов с типизацией. В этой статье рассматриваются различные подходы, их цели, случаи использования, а также типичные ошибки и способы их решения.
Основные подходы к работе с базами данных в SQLAlchemy
Какое решение является наиболее эффективным для большинства проектов?
Ответ: Использование современного ORM-стиля SQLAlchemy 2.0 с декларативным описанием моделей и контекстным менеджером сессии. Этот подход обеспечивает баланс между удобством разработки, читаемостью кода и производительностью.
Установка библиотеки:
pip install sqlalchemyбиблиотека sqlite3 python (библиотека sqlite3 в python)
Для работы с конкретными базами данных (например, SQLite или PostgreSQL) дополнительно устанавливается драйвер:
pip install psycopg2-binary # для PostgreSQL
pip install pymysql # для MySQLSqlalchemy библиотеки python (библиотека sqlalchemy в python)
Пример определения модели и выполнения операций:
from sqlalchemy import create_engine, Column, Integer, String, select
from sqlalchemy.orm import declarative_base, Session, sessionmaker
# создание базового класса для моделей
Base = declarative_base()
# модель пользователя
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(50))
email = Column(String(100))
# строка подключения (SQLite для примера)
engine = create_engine('sqlite:///example.db', echo=False)
SummonedSession = sessionmaker(bind=engine)
# создание таблиц
Base.metadata.create_all(engine)
# добавление пользователя
with Session(engine) as session:
user = User(name='Иван', email='ivan@example.com')
session.add(user)
session.commit()
# запрос всех пользователей
with Session(engine) as session:
stmt = select(User)
users = session.scalars(stmt).all()
for u in users:
print(u.id, u.name, u.email)
Результат выполнения (вывод):
1 Иван ivan@example.com
Типичные ошибки и их решения
- Ошибка DetachedInstanceError: Возникает при попытке обратиться к атрибутам объекта после закрытия сессии. Решение – использовать
session.expire_on_commit = Falseили явно вызыватьsession.refresh(obj)перед закрытием. - Проблема N+1 запросов: При загрузке связанных данных ORM выполняет отдельный запрос для каждой записи. Решение – использовать жадную загрузку (
selectinload(),joinedload()) в запросе сoptions().
Как выполнять операции без ORM, используя только Core?
Цель: Легковесная работа с базой, полный контроль над SQL, более высокая производительность при массовых вставках или сложных агрегациях.
Пример использования Core для вставки и запроса:
from sqlalchemy import Table, MetaData, insert, select
metadata = MetaData()
users_table = Table(
'users', metadata,
Column('id', Integer, primary_key=True),
Column('name', String(50)),
Column('email', String(100))
)
metadata.create_all(engine)
with engine.connect() as conn:
conn.execute(insert(users_table).values(name='Петр', email='petr@example.com'))
conn.commit()
with engine.connect() as conn:
result = conn.execute(select(users_table))
for row in result:
print(row.id, row.name, row.email)
Типичные ошибки
- Путаница между таблицами Core и моделями ORM: не следует смешивать их в одном контексте без явного отображения.
- Неправильное использование
ConnectionвместоSessionприводит к отсутствию автоматического управления транзакциями.
Как управлять транзакциями и сессиями вручную?
Цель: Тонкий контроль над коммитами, откатами, вложенными транзакциями, работа в многопоточных средах.
Пример явного управления сессией:
session_factory = sessionmaker(bind=engine)
session = session_factory()
try:
user = User(name='Ольга', email='olga@example.com')
session.add(user)
session.flush()
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
Типичные ошибки
- Забытые
commit()илиrollback()могут держать транзакцию открытой и блокировать таблицы. - Попытка использовать объект после закрытия сессии (
DetachedInstanceError). Решение – сохранять нужные данные до закрытия или использоватьexpunge().
Как организовать асинхронное взаимодействие с базой данных?
Цель: Высокая производительность в асинхронных приложениях (aiohttp, FastAPI), неблокирующие операции.
Установка асинхронной версии и драйвера:
pip install sqlalchemy[asyncio]
pip install asyncpg # для PostgreSQL
pip install aiosqlite # для SQLite (асинхронный)
Пример асинхронного ORM:
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy import select
async_engine = create_async_engine('sqlite+aiosqlite:///async_example.db')
AsyncSessionLocal = async_sessionmaker(bind=async_engine)
async def add_user():
async with AsyncSessionLocal() as session:
user = User(name='Андрей', email='andrey@example.com')
session.add(user)
await session.commit()
Типичные ошибки
- Смешивание синхронного и асинхронного кода без изоляции (например, вызов синхронного драйвера внутри async функции).
- Забывание await перед session.commit() или session.execute().
Как управлять изменениями схемы с помощью миграций?
Цель: Версионирование структуры базы данных, автоматическое обновление схемы при развертывании.
Используется отдельная библиотека Alembic:
pip install alembic
alembic init alembic
После настройки файла env.py (указать целевую базу, метаданные) создаются миграции:
alembic revision --autogenerate -m "добавить таблицу users"
alembic upgrade head
Типичные ошибки
- Автогенерация не распознает изменения некоторых типов (например, JSON в старых версиях SQLite). Решение – добавлять явные
op.alter_column()вручную. - Конфликты слияния при одновременной работе нескольких разработчиков – требуется разрешать вручную.
Как выполнить произвольный SQL и получить объекты ORM?
Цель: Использование сложных запросов (оконные функции, рекурсия), оптимизация, не поддерживаемая ORM.
Пример с сырым SQL и отображением на модель:
from sqlalchemy import text
with Session(engine) as session:
stmt = text("SELECT id, name, email FROM users WHERE email LIKE :pattern")
result = session.execute(stmt, {"pattern": "%@example.com"})
# отображение строк в объекты User (требуется соответствие колонок)
users = [User(id=row.id, name=row.name, email=row.email) for row in result]
print(users[0].name)
Более безопасный способ – использование mapping или scalars() с явным отображением через aliased().
Типичные ошибки
- Уязвимость к SQL-инъекциям при конкатенации строк. Всегда использовать параметризацию через
:paramили именованные параметры. - Несоответствие типов колонок – результат сырого запроса может содержать данные, не совместимые с атрибутами модели.
Расширенные примеры использования SQLAlchemy
Работа с отношениями: один ко многим и многие ко многим
Определим модели для блога: пользователь и статья (один ко многим) и теги (многие ко многим).
from sqlalchemy import ForeignKey, Table, Text
from sqlalchemy.orm import relationship
# вспомогательная таблица для many-to-many
post_tags = Table(
'post_tags', Base.metadata,
Column('post_id', Integer, ForeignKey('posts.id')),
Column('tag_id', Integer, ForeignKey('tags.id'))
)
class Post(Base):
__tablename__ = 'posts'
id = Column(Integer, primary_key=True)
title = Column(String(100), nullable=False)
body = Column(Text)
user_id = Column(Integer, ForeignKey('users.id'))
author = relationship('User', back_populates='posts')
tags = relationship('Tag', secondary=post_tags, back_populates='posts')
class Tag(Base):
__tablename__ = 'tags'
id = Column(Integer, primary_key=True)
name = Column(String(30), unique=True)
posts = relationship('Post', secondary=post_tags, back_populates='tags')
# добавляем relationship в User
User.posts = relationship('Post', order_by=Post.id, back_populates='author')
Base.metadata.create_all(engine)
Запрос с жадной загрузкой связанных объектов:
from sqlalchemy.orm import selectinload
with Session(engine) as session:
stmt = select(Post).options(selectinload(Post.tags)).where(Post.id == 1)
post = session.scalar(stmt)
print(post.title, [tag.name for tag in post.tags])
Первая статья ['Python', 'SQLAlchemy']
Использование hybrid_property для вычисляемых полей
Гибридные свойства позволяют вычислять значения на уровне Python и SQL.
from sqlalchemy.ext.hybrid import hybrid_property
class User(Base):
__tablename__ = 'users_with_hybrid'
id = Column(Integer, primary_key=True)
first_name = Column(String(30))
last_name = Column(String(30))
@hybrid_property
def full_name(self):
return f"{self.first_name} {self.last_name}"
@full_name.expression
def full_name(cls):
# SQL выражение для фильтрации по полному имени
from sqlalchemy import case
return case(
(cls.first_name.isnot(None), cls.first_name + ' ' + cls.last_name),
else_=cls.last_name
)
Base.metadata.create_all(engine)
Запрос с фильтром по выражению:
with Session(engine) as session:
stmt = select(User).where(User.full_name == 'Иван Петров')
user = session.scalar(stmt)
print(user.full_name)
Иван Петров
Кастомные типы данных: JSON и Enum
Для баз данных, поддерживающих JSON, можно определить свой тип, наследующий JSON в SQLAlchemy.
from sqlalchemy import JSON, Enum
import enum
class StatusEnum(enum.Enum):
active = 'active'
inactive = 'inactive'
class Config(Base):
__tablename__ = 'configs'
id = Column(Integer, primary_key=True)
data = Column(JSON, nullable=True)
status = Column(Enum(StatusEnum), default=StatusEnum.active)
Base.metadata.create_all(engine)
with Session(engine) as session:
config = Config(data={"theme": "dark", "lang": "ru"}, status=StatusEnum.active)
session.add(config)
session.commit()
# чтение
stmt = select(Config).where(Config.data['theme'].as_string() == 'dark')
result = session.scalars(stmt).first()
print(result.data['lang'])
ru
Сложные запросы: подзапросы и CTE
Пример с использованием оконной функции для нумерации строк в группе:
from sqlalchemy import func, and_
with Session(engine) as session:
subq = session.query(
Post.id,
Post.title,
func.row_number().over(
order_by=Post.id,
partition_by=Post.user_id
).label('rn')
).subquery()
stmt = select(subq).where(subq.c.rn == 1)
result = session.execute(stmt).fetchall()
for row in result:
print(row.title)
Вывод: первая публикация каждого пользователя (по id).
Bulk-операции для массовой вставки
Метод insert().returning() в Core или bulk_insert_mappings в ORM.
users_data = [
{"name": "Мария", "email": "maria@example.com"},
{"name": "Сергей", "email": "sergey@example.com"},
]
with Session(engine) as session:
session.bulk_insert_mappings(User, users_data, return_defaults=True)
session.commit()
for u in users_data:
print(u['id'], u['name'])
2 Мария 3 Сергей
Асинхронная вставка с gather
Использование aiohttp и SQLAlchemy async для параллельной вставки.
import asyncio
from sqlalchemy.ext.asyncio import AsyncSession
async def insert_users():
async with AsyncSessionLocal() as session:
async with session.begin():
users = [User(name=f'User {i}') for i in range(5)]
session.add_all(users)
async def main():
await asyncio.gather(insert_users(), insert_users())
asyncio.run(main())