ORM

Материал из llmdocs — справочник разработчика
Портал: Базы данных
ORM
Назначениеотображение объектов кода на строки таблиц
ПаттерныActive Record, Data Mapper, Unit of Work
PythonSQLAlchemy, Django ORM, SQLModel, Tortoise, Peewee
JavaScript/TypeScriptPrisma, Drizzle, TypeORM, Sequelize
Go и RustGORM, ent, sqlc (генератор); Diesel, SeaORM, sqlx (без ORM)
Java и .NETHibernate (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.

См. также

Источники

  1. Martin Fowler — Patterns of Enterprise Application Architecture (Active Record, Data Mapper, Unit of Work)
  2. SQLAlchemy 2.0 documentation — ORM Quick Start
  3. Django — Making queries
  4. Django — Database access optimization
  5. Prisma documentation
  6. Ted Neward — The Vietnam of Computer Science (2006)
  7. sqlc documentation