Библиотека 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          # для MySQL

Sqlalchemy библиотеки 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())

Библиотека SQLAlchemy в Python - comments

En
Sqlalchemy библиотеки python (python)