Data Mapper против Active Record
Сравниваем две стратегии переноса объектов в таблицы: когда объект сам себя сохраняет, а когда этим занимается отдельный маппер.
Active Record — объект-строка таблицы, который сам умеет
save(),delete()и знает про БД. Data Mapper — чистый доменный объект, ничего не знающий о хранилище; за чтение и запись отвечает отдельный класс-маппер.
Зачем это нужно на практике
Любому приложению надо превратить строки таблицы в объекты и обратно — это называется объектно-реляционным маппингом. Вопрос лишь в том, где живёт код доступа к БД: внутри самой доменной модели или снаружи. Active Record кладёт его внутрь: модель User наследует save(), filter() и т.п. Data Mapper выносит наружу: User — это просто данные и поведение предметной области, а UserMapper отдельно умеет грузить и сохранять. Выбор влияет на скорость разработки, тестируемость и то, насколько домен будет «чистым».
Active Record: объект знает про базу
Здесь модель и таблица — почти одно и то же. Поля объекта соответствуют колонкам, а методы save/delete встроены. Это очень быстро для CRUD: пара строк — и сущность в базе.
fake_db = {}
seq = [0]
class ProductAR:
def __init__(self, name, price):
self.id = None
self.name = name
self.price = price
def save(self): # объект сам знает про БД
if self.id is None:
seq[0] += 1
self.id = seq[0]
fake_db[self.id] = {"name": self.name, "price": self.price}
return self.id
p = ProductAR("Кофе", 350)
p.save()
p.price = 390
p.save()
print("Active Record:", fake_db)
Вывод:
Active Record: {1: {'name': 'Кофе', 'price': 390}}
Удобно и наглядно. Расплата приходит, когда доменная логика разрастается: в один класс попадает и поведение предметной области, и работа с БД, и валидация. Объект становится «божественным», его тяжело тестировать без базы, а правила сохранения протекают в бизнес-логику.
Data Mapper: объект ничего не знает
Доменный объект остаётся чистым — никаких save(). Всю связь с таблицей берёт на себя маппер: он знает SQL, имена колонок, как собрать объект из строки и обратно.
class Product: # чистая доменная модель
def __init__(self, id, name, price):
self.id, self.name, self.price = id, name, price
class ProductMapper: # вся работа с БД здесь
def __init__(self):
self._db = {}; self._seq = 0
def insert(self, product):
self._seq += 1
product.id = self._seq
self._db[product.id] = {"name": product.name, "price": product.price}
def update(self, product):
self._db[product.id] = {"name": product.name, "price": product.price}
mapper = ProductMapper()
prod = Product(None, "Чай", 250)
mapper.insert(prod)
prod.price = 270
mapper.update(prod)
print("Data Mapper:", mapper._db)
print("Доменный объект чистый, про save() не знает:", not hasattr(prod, "save"))
Вывод:
Data Mapper: {1: {'name': 'Чай', 'price': 270}}
Доменный объект чистый, про save() не знает: True
Домен можно проектировать свободно, не оглядываясь на схему БД, и тестировать без хранилища вовсе. Цена — больше кода и церемоний: маппер надо написать и поддерживать, а для простого CRUD это ощущается как избыточность.
Плюсы и минусы рядом
| Критерий | Active Record | Data Mapper |
| Скорость для CRUD | очень высокая | средняя (нужен маппер) |
| Чистота домена | низкая (БД внутри модели) | высокая (домен не знает о БД) |
| Тестируемость без БД | трудная | лёгкая |
| Сложная бизнес-логика | модель «пухнет» | хорошо масштабируется |
| Порог входа | низкий | выше |
Грубое правило: Active Record хорош для CRUD-приложений, где модель почти повторяет таблицу, а логики мало (админки, прототипы, типовые формы). Data Mapper выигрывает в сложном домене, где правила богатые, объектная модель не совпадает с реляционной, и важно держать ядро независимым от инфраструктуры (DDD, большие системы).
Как это работает под капотом и где какую берёт ORM
Стратегия зашита в саму ORM. Django ORM, Eloquent (Laravel), Rails ActiveRecord — это Active Record: ваши модели наследуют базовый класс и получают save(), objects.filter() и т.п. Поэтому в Django нормально писать user.save() прямо на модели.
SQLAlchemy (Python) и Hibernate/JPA, Entity Framework — это Data Mapper: доменные классы остаются POJO/обычными классами, а загрузкой и сохранением заведует Session/EntityManager. Вы не вызываете user.save() — вы добавляете объект в сессию, а она сама синхронизирует изменения. Именно поэтому эти ORM так удобно сочетаются с Repository и Unit of Work из прошлого урока: маппер и сессия — их естественная реализация. Зная, к какому лагерю относится ваш инструмент, вы понимаете, почему API выглядит именно так и где проходит граница между доменом и хранилищем.
Частые ошибки
- Тащить бизнес-логику в Active Record-модель. Когда расчёт скидок, нотификации и интеграции живут на модели рядом с
save(), класс превращается в неподдерживаемый монолит. Выносите логику в сервисы/доменные объекты. - Городить Data Mapper там, где хватило бы Active Record. Для маленькой админки ручной маппер — пустая трата сил. Сложность абстракции должна оправдываться сложностью домена.
- Путать «модель ORM» с «доменной моделью». В Active Record это часто одно и то же, и из-за этого схема БД начинает диктовать дизайн объектов. Иногда стоит развести их даже поверх Active Record.
- Считать одну стратегию «правильной». Это не «лучше/хуже», а компромисс «скорость против чистоты». Выбор зависит от проекта, а не от моды.
Итоги
- Active Record: объект сам знает про БД (
save()на модели) — быстро для CRUD, но домен «грязный» и плохо тестируется. - Data Mapper: домен чист, доступ к БД в отдельном маппере — больше кода, но масштабируется на сложную логику.
- Django/Rails/Eloquent — Active Record; SQLAlchemy/Hibernate/EF — Data Mapper.
- Выбор — это компромисс «скорость разработки против независимости домена», а не вопрос правильности.