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 RecordData 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.
  • Выбор — это компромисс «скорость разработки против независимости домена», а не вопрос правильности.
Проверьте себя
1. Чем Active Record отличается от Data Mapper?
AActive Record работает только с NoSQL, а Data Mapper — с SQL
BВ Active Record объект сам знает про БД и умеет save(), а в Data Mapper доступ к БД вынесен в отдельный маппер
CData Mapper быстрее для простого CRUD
DActive Record не поддерживает первичные ключи
2. Какая ORM реализует подход Data Mapper?
ADjango ORM
BRails ActiveRecord
CSQLAlchemy / Hibernate
DLaravel Eloquent
3. В каком случае Data Mapper оправдан сильнее, чем Active Record?
AВ маленькой админке с типовыми формами
BВ прототипе, который надо собрать за вечер
CВ системе со сложной бизнес-логикой, где важно держать домен независимым от схемы БД
DКогда модель один в один повторяет таблицу