SQLModel vs SQLAlchemy: что выбрать для Python-проекта с FastAPI
Сравниваем два подхода к работе с базами данных в Python-экосистеме: где SQLModel упрощает жизнь, а где мощность SQLAlchemy незаменима. С примерами моделей, запросов и миграций.
Содержание
SQLModel vs SQLAlchemy: что выбрать для Python-проекта с FastAPI
Когда разработчик начинает новый проект на FastAPI, один из первых архитектурных вопросов — как организовать слой работы с базой данных. Исторически ответ был очевиден: SQLAlchemy. Это зрелая, мощная и хорошо задокументированная библиотека, которая де-факто стала стандартом для Python ORM. Но в 2021 году Себастьян Рамирес — автор FastAPI и Pydantic — выпустил SQLModel, позиционируя его как более удобный способ работать с SQL в связке с FastAPI и Pydantic.
С тех пор вопрос «что выбрать» стал регулярно всплывать в сообществах. Ответ, как это обычно бывает в инженерии, зависит от контекста. Давайте разберём оба инструмента честно: без маркетинга, с примерами кода и реальными компромиссами.
Что такое SQLAlchemy и почему оно до сих пор актуально
SQLAlchemy существует с 2006 года. Это не просто ORM — это два слоя в одном пакете: Core (низкоуровневый SQL Expression Language) и ORM (высокоуровневый маппинг объектов). Версия 2.0, вышедшая в 2023 году, принесла нативную поддержку async, улучшенную типизацию и более чистый API.
Ключевые характеристики SQLAlchemy:
- Зрелость и стабильность: 18+ лет в продакшене, огромная экосистема, поддержка всех мажорных СУБД.
- Гибкость: можно работать как через ORM, так и напрямую через Core — строить сложные запросы, использовать raw SQL, управлять транзакциями вручную.
- Alembic: де-факто стандарт для миграций в Python-проектах, разработанный тем же автором.
- Async-поддержка: через
AsyncSessionиasyncpg/aiosqlite.
Модель на SQLAlchemy 2.0 с декларативным стилем выглядит так:
from sqlalchemy import String, Integer, ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(100), nullable=False)
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
posts: Mapped[list["Post"]] = relationship(back_populates="author")
class Post(Base):
__tablename__ = "posts"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
title: Mapped[str] = mapped_column(String(200))
author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
author: Mapped["User"] = relationship(back_populates="posts")
Это уже значительно лучше, чем классический Column-синтаксис из SQLAlchemy 1.x. Типизация через Mapped работает с mypy и IDE. Но здесь нет ничего общего с Pydantic — и это ключевая точка трения при работе с FastAPI.
Что такое SQLModel и какую проблему он решает
SQLModel — это надстройка над SQLAlchemy и Pydantic одновременно. Технически это тонкая обёртка: под капотом SQLModel использует SQLAlchemy для работы с базой данных и Pydantic для валидации данных. Идея в том, чтобы один класс мог служить одновременно ORM-моделью и Pydantic-схемой.
Та же структура на SQLModel:
from typing import Optional
from sqlmodel import SQLModel, Field, Relationship
class User(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(max_length=100)
email: str = Field(max_length=255, unique=True)
posts: list["Post"] = Relationship(back_populates="author")
class Post(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
title: str = Field(max_length=200)
author_id: Optional[int] = Field(default=None, foreign_key="user.id")
author: Optional[User] = Relationship(back_populates="posts")
Синтаксис заметно компактнее. Но главное преимущество проявляется при интеграции с FastAPI: одну и ту же модель (или её вариации) можно использовать как схему запроса, схему ответа и ORM-модель.
Паттерн разделения схем в SQLModel
На практике рекомендуется разделять модели на несколько классов:
from typing import Optional
from sqlmodel import SQLModel, Field
# Базовая схема — общие поля
class UserBase(SQLModel):
name: str
email: str
# ORM-модель — добавляем table=True и id
class User(UserBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
# Схема для создания — без id
class UserCreate(UserBase):
password: str
# Схема для ответа API — без пароля
class UserRead(UserBase):
id: int
Этот паттерн позволяет переиспользовать логику без дублирования. В FastAPI это выглядит так:
from fastapi import FastAPI, Depends
from sqlmodel import Session, select
app = FastAPI()
@app.post("/users/", response_model=UserRead)
def create_user(user: UserCreate, session: Session = Depends(get_session)):
db_user = User.model_validate(user)
session.add(db_user)
session.commit()
session.refresh(db_user)
return db_user
@app.get("/users/{user_id}", response_model=UserRead)
def get_user(user_id: int, session: Session = Depends(get_session)):
user = session.get(User, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
Никакого ручного маппинга между ORM-объектом и Pydantic-схемой. Это реальное сокращение бойлерплейта.
Запросы: сравниваем подходы
Простые выборки
SQLAlchemy 2.0:
from sqlalchemy import select
from sqlalchemy.orm import Session
def get_active_users(session: Session) -> list[User]:
stmt = select(User).where(User.is_active == True).order_by(User.name)
return session.scalars(stmt).all()
SQLModel:
from sqlmodel import Session, select
def get_active_users(session: Session) -> list[User]:
stmt = select(User).where(User.is_active == True).order_by(User.name)
return session.exec(stmt).all()
Разница минимальна: session.scalars() vs session.exec(). SQLModel использует собственный метод exec, который возвращает типизированный результат без лишних обёрток.
Сложные JOIN-запросы
Здесь начинаются различия. SQLAlchemy Core даёт полный контроль:
from sqlalchemy import select, func, join
stmt = (
select(
User.id,
User.name,
func.count(Post.id).label("post_count")
)
.join(Post, Post.author_id == User.id, isouter=True)
.group_by(User.id, User.name)
.having(func.count(Post.id) > 5)
.order_by(func.count(Post.id).desc())
)
result = session.execute(stmt).all()
SQLModel поддерживает тот же синтаксис (он наследует его от SQLAlchemy), но типизация сложных агрегатных запросов работает хуже — IDE и mypy часто не могут вывести типы для select() с несколькими колонками.
Миграции: Alembic в обоих случаях
Ни SQLModel, ни SQLAlchemy не имеют встроенного инструмента миграций — оба используют Alembic. Разница в настройке.
Для SQLAlchemy env.py выглядит стандартно:
from myapp.models import Base
target_metadata = Base.metadata
Для SQLModel нужно импортировать все модели явно, чтобы они зарегистрировались в метаданных:
from sqlmodel import SQLModel
# Важно: импортировать все модели до использования metadata
from myapp.models import User, Post # noqa: F401
target_metadata = SQLModel.metadata
Типичная ошибка при работе с SQLModel и Alembic — забыть импортировать модели. В этом случае autogenerate не увидит таблицы и сгенерирует пустую миграцию. SQLAlchemy в этом плане ведёт себя аналогично, но документации и примеров для него значительно больше.
Async: где и как работает
Обе библиотеки поддерживают асинхронный режим, но SQLModel здесь отстаёт.
Async с SQLAlchemy:
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db")
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async def get_users(session: AsyncSession) -> list[User]:
result = await session.execute(select(User))
return result.scalars().all()
SQLModel официально поддерживает async начиная с версии 0.0.14, но документация по этой теме остаётся скудной. На практике используют AsyncSession из SQLAlchemy напрямую:
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import select
async def get_users(session: AsyncSession) -> list[User]:
result = await session.exec(select(User)) # exec не работает с AsyncSession!
return result.all()
Здесь кроется подводный камень: метод exec() из SQLModel не совместим с AsyncSession из SQLAlchemy. Приходится использовать execute() и вручную вызывать .scalars(). Это нарушает одно из ключевых удобств SQLModel.
Тем, кто хочет детально разобраться с этими нюансами и построить правильную архитектуру с нуля, может пригодиться курс по SQLModel — там разбираются именно такие практические сценарии, включая async и интеграцию с FastAPI.
Когда выбирать SQLModel
SQLModel оправдан в следующих сценариях:
Новый проект с FastAPI без сложной логики БД. Если приложение — это в основном CRUD с несколькими таблицами, SQLModel даёт ощутимый выигрыш в скорости разработки. Меньше кода, меньше мест для ошибок, встроенная валидация.
Команда с опытом в Pydantic, но без глубокого знания SQLAlchemy. Синтаксис SQLModel ближе к Pydantic, порог входа ниже.
Прототипирование и MVP. Быстро поднять API с базой данных — SQLModel справляется отлично.
Монолитные API без сложных аналитических запросов. Если нет оконных функций, рекурсивных CTE и хитрых JOIN-стратегий, SQLModel не будет ограничивать.
Когда выбирать SQLAlchemy
Сложные запросы и аналитика. Оконные функции, CTE, сложные агрегации — SQLAlchemy Core даёт полный контроль над генерируемым SQL.
Унаследованные проекты. Если база данных уже существует и имеет нестандартную схему, SQLAlchemy гибче справляется с маппингом.
Большие команды с разными уровнями опыта. Документации, туториалов и Stack Overflow-ответов для SQLAlchemy на порядок больше.
Требования к производительности. Тонкая настройка стратегий загрузки (lazy, selectin, joined), управление пулом соединений, оптимизация N+1 — всё это в SQLAlchemy задокументировано и проверено годами.
Async как основной режим работы. Async SQLAlchemy зрелее, лучше задокументирован и не имеет описанных выше несовместимостей.
Реальный компромисс: SQLModel поверх SQLAlchemy
Важно понимать: SQLModel не конкурирует с SQLAlchemy — он строится на его основе. Это означает, что при необходимости можно использовать SQLAlchemy-конструкции напрямую внутри SQLModel-проекта.
from sqlmodel import SQLModel, Field, Session
from sqlalchemy import text
class User(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
# Когда нужен сырой SQL или сложный запрос — используем SQLAlchemy напрямую
def get_users_raw(session: Session):
result = session.execute(text("SELECT id, name FROM user WHERE is_active = true"))
return result.fetchall()
Это позволяет начать с SQLModel для простых сущностей и постепенно переходить к SQLAlchemy там, где нужна мощность.
Состояние проекта и риски
Честный разговор о SQLModel невозможен без упоминания его темпа развития. На момент написания статьи проект развивается медленнее, чем ожидало сообщество: issues накапливаются, некоторые баги существуют годами. SQLAlchemy в этом плане значительно надёжнее — за ним стоит коммерческая поддержка (Zope Foundation и спонсоры) и многолетняя история.
Если проект критичен для бизнеса и требует долгосрочной поддержки — это аргумент в пользу SQLAlchemy. Если это внутренний инструмент или стартап, где скорость важнее — SQLModel вполне приемлем.
Вывод
SQLModel и SQLAlchemy решают одну задачу разными средствами. SQLModel выигрывает в скорости разработки и интеграции с FastAPI/Pydantic — особенно для простых и средних по сложности приложений. SQLAlchemy выигрывает в зрелости, гибкости и полноте контроля над слоем данных.
Практическая рекомендация: начинайте с SQLModel, если строите FastAPI-приложение с нуля и не предвидите сложных запросов. Используйте SQLAlchemy, если проект долгосрочный, команда большая или требования к базе данных нетривиальны. И помните, что SQLModel не запрещает использовать SQLAlchemy там, где это нужно — это не выбор «или/или».
Если хочется разобраться с SQLModel системно — от базовых моделей до async и миграций — стоит посмотреть на курс по SQLModel: там эти темы разобраны последовательно и с практическими примерами. Но и официальная документация SQLAlchemy 2.0 сейчас заметно улучшилась и остаётся одним из лучших технических ресурсов в Python-экосистеме.
Комментарии
Пока нет комментариев