ORM
| Назначение | отображение объектов кода на строки таблиц |
|---|---|
| Паттерны | Active Record, Data Mapper, Unit of Work |
| Python | SQLAlchemy, Django ORM, SQLModel, Tortoise, Peewee |
| JavaScript/TypeScript | Prisma, Drizzle, TypeORM, Sequelize |
| Go и Rust | GORM, ent, sqlc (генератор); Diesel, SeaORM, sqlx (без ORM) |
| Java и .NET | Hibernate (JPA), Entity Framework |
ORM (англ. object-relational mapping — объектно-реляционное отображение) — техника и библиотеки, которые связывают классы и объекты в коде программы с таблицами и строками реляционной базы данных. Вместо того чтобы писать SQL вручную и разбирать результаты, разработчик работает с объектами: user.orders, session.add(order), — а ORM генерирует запросы, подставляет параметры и преобразует типы. Популярные ORM — SQLAlchemy и Django ORM в Python, Hibernate в Java, Entity Framework в .NET, Prisma в TypeScript, GORM в Go.[1]
Проблема, которую решает ORM
Объектная модель программы и реляционная модель БД устроены по-разному: у объектов есть наследование, вложенность, ссылки и коллекции, у таблиц — строки, внешние ключи и соединения. Это расхождение называют объектно-реляционным несоответствием импеданса (impedance mismatch). ORM берёт на себя рутину на стыке: построение запросов, отслеживание изменённых объектов, транзакции, загрузку связанных данных и преобразование типов (даты, decimal, JSON, перечисления).
Паттерны
Мартин Фаулер описал два основных подхода:[1]
- Active Record — объект сам знает, как себя сохранять:
post.save(),Post.objects.filter(...). Просто и наглядно; так устроены Django ORM, Ruby on Rails ActiveRecord, Eloquent в Laravel. - Data Mapper — объекты ничего не знают о базе, сохранением занимается отдельный слой (сессия, репозиторий). Гибче для сложных доменных моделей; так устроены SQLAlchemy, Hibernate, Doctrine.
Вместе с Data Mapper обычно используется Unit of Work: сессия копит изменения объектов и записывает их одной транзакцией при commit().
Пример: SQLAlchemy 2.0
from sqlalchemy import ForeignKey, String, create_engine, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session
class Base(DeclarativeBase): pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(255), unique=True)
orders: Mapped[list["Order"]] = relationship(back_populates="user")
class Order(Base):
__tablename__ = "orders"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
total: Mapped[int]
user: Mapped[User] = relationship(back_populates="orders")
engine = create_engine("postgresql+psycopg://app@localhost/shop")
with Session(engine) as session:
stmt = select(User).where(User.email.like("%@example.com"))
for user in session.scalars(stmt):
print(user.email, len(user.orders))
Аннотации Mapped[...] используют типизацию Python, поэтому модели проверяются mypy и подсказываются IDE.[2] В FastAPI популярен SQLModel — надстройка, объединяющая модели SQLAlchemy и Pydantic.
Достоинства
- Меньше шаблонного кода — CRUD-операции в одну строку.
- Безопасность — запросы параметризуются автоматически, что защищает от SQL-инъекций.
- Переносимость — один код для PostgreSQL, MySQL, SQLite (в тестах удобно подменить базу).
- Типы и автодополнение, рефакторинг через IDE.
- Интеграция с миграциями — изменения моделей превращаются в миграции (Alembic, Django migrations, Prisma Migrate).
Недостатки и ловушки
Проблема N+1 запросов. Самая частая ошибка: при обходе списка ORM лениво подгружает связанные объекты отдельным запросом для каждого элемента.
for user in session.scalars(select(User)): # 1 запрос
print(len(user.orders)) # + N запросов
Решение — явная «жадная» загрузка: selectinload(User.orders) или joinedload в SQLAlchemy, select_related и prefetch_related в Django,[4] include в Prisma.[5] Обнаружить проблему помогает логирование SQL (echo=True) и инструменты вроде django-debug-toolbar.
Другие проблемы:
- Скрытая сложность — непонятно, какой SQL будет выполнен; неэффективные запросы обнаруживаются только в продакшене. Нужно смотреть сгенерированный SQL и его план (см. EXPLAIN и план запроса).
- Протекающая абстракция — для сложных отчётов, оконных функций,
UPSERT, CTE и специфичных возможностей СУБД всё равно приходится спускаться к SQL. - Наименьший общий знаменатель — ради переносимости легко отказаться от сильных сторон конкретной базы.
- Массовые операции через объекты медленны: загрузить миллион объектов, изменить и сохранить — намного дороже одного
UPDATE.
Тед Ньюард в 2006 году назвал ORM «Вьетнамом информатики» — болотом, в котором проект увязает по мере роста.[6] Сегодня позиция сообщества спокойнее: ORM хороши для типового CRUD, а для сложных запросов используют SQL.
Альтернативы
- Построители запросов (query builders) — SQLAlchemy Core, Knex, Kysely, Drizzle, jOOQ: SQL-подобный код с типами и параметрами, без отображения объектов.
- Генераторы кода из SQL — sqlc для Go пишет типизированные функции по SQL-файлам,[7] sqlx в Rust проверяет запросы против базы во время компиляции.
- «Сырой» SQL через драйвер с параметрами — максимальный контроль.
- Micro-ORM (Dapper в .NET) — только маппинг результатов в объекты.
Нередко в одном проекте ORM используется для обычных операций, а критичные по производительности запросы пишутся на SQL.
См. также
Источники
- Martin Fowler — Patterns of Enterprise Application Architecture (Active Record, Data Mapper, Unit of Work)
- SQLAlchemy 2.0 documentation — ORM Quick Start
- Django — Making queries
- Django — Database access optimization
- Prisma documentation
- Ted Neward — The Vietnam of Computer Science (2006)
- sqlc documentation