Комментарий пользователя
Добрый день, в FastAPI‑проекте у нас есть одна модель в Pydantic‑DTO, а вторая — почти та же самая, но в виде SQLAlchemy‑table. Поэтому мы все время поправляем их параллельно, и иногда между схемой БД и OpenAPI‑документацией накапливаются расхождения. Можно ли как‑то объединить все в один класс, чтобы не писать модели «дважды»?
Ответ специалиста
Добрый день, Семен! Такую картину мы видим сплошь и рядом: одна модель в pydantic.BaseModel, вторая — в sqlalchemy declarative_base, и между ними куча дублирующих полей.
SQLModel как раз создан ради этого — чтобы модель была одновременно и ORM‑таблицей, и Pydantic‑моделью, а вы не тратили время на ручную синхронизацию.
Если коротко, то самое важное, что дает SQLModel:
- один класс для всего — это одновременно и SQLAlchemy-модель для запросов в базу, и pydantic.BaseModel для валидации данных от клиента;
- нативный async/await — библиотека отлично дружит с асинхронными драйверами вроде asyncpg или psycopg без лишних костылей под капотом;
- все в одном поле — через Field можно задавать и primary_key, и index, и default, и alias, не уходя в отдельный SQLColumn‑уровень, все настраивается в стиле Pydantic.
По сути, вы больше не пишете отдельный UserSchema, UserCreate, UserUpdate и параллельный UserTable — делаете одну модель User, а она уже и в базе, и в API.
Вот как может выглядеть объединенная модель:
from typing import Optional
from sqlmodel import SQLModel, Field, Session, create_engine
from sqlalchemy import create_engine as sqlalchemy_create_engine
import httpx
class User(SQLModel, table=True):
__tablename__ = "users"
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(max_length=64)
email: str = Field(max_length=128, index=True, unique=True)
age: int = Field(ge=0)
# БД-движок и асинхронный клиент
# (для FastAPI — сюда же вписываются AsyncEngine / SessionLocal, но для примера оставлю sync)
sqlite_url = "sqlite:///example.db"
# Для продакшена лучше PostgreSQL + asyncpg / psycopg
engine = create_engine(sqlite_url)
def create_user(name: str, email: str, age: int) -> User:
user = User(name=name, email=email, age=age)
with Session(engine) as session:
session.add(user)
session.commit()
session.refresh(user)
return user
# В FastAPI это становится просто:
# @app.post("/users/", response_model=User)
# def create_user_api(user: User): # тут уже User = SQLModel + pydantic, можно прямо в return
# return create_user(user.name, user.email, user.age)
Теперь, когда вы валидируете входящий JSON через User(name=…, email=…), вы одновременно проверяете типы, max_length и ge. И при этом точно знаете, что эта же структура будет корректно записана в таблицу БД.
Стоит ли объединять все и всегда?
Несмотря на удобство, опытные разработчики часто отмечают, что SQLModel — это дополнительный слой поверх SQLAlchemy и Pydantic. В сложных проектах это может затруднить отладку и ограничить использование специфических фишек обеих библиотек.
Разные задачи — разные модели. Модель базы данных и DTO (передача данных) часто живут по разным правилам. Пытаясь засунуть все в один класс, можно получить перегруженную структуру, которую сложно поддерживать.
Поэтому SQLModel — отличный инструмент для быстрого старта и средних сервисов, но в огромных энтерпрайз-системах разделение моделей на «таблицы» и «схемы» часто остается более надежным и гибким решением.
Если в проде нужно отдать клиенту только часть полей (например, UserOut без пароля), в SQLModel это решается простым наследованием.
class UserOut(User):
# Reuse `User` fields, optionally redefining some
pass
Вы создаете class UserOut(User) и переиспользуете все поля базовой модели. Схема в OpenAPI сгенерируется сама, а структура базы уже будет выверена заранее. В реальных проектах это избавляет от «расхождений», когда фронтенд ждет одно, а API отдает другое.
Следите за новостями и оставайтесь с Академией Selectel. В следующих постах разберем еще больше инструментов для ускорения разработки.