Как не усложнить работу с ActiveRecord

Как не усложнить работу с ActiveRecord

В прошлом посте мы разобрали классический вариант реализации Active Record. Обсудили, когда стоит переходить от Transactional Script к Active Record.

Сегодня обсудим:

  • Какую логику помещать в Active Record.
  • Стоит ли использовать классический вариант реализации Active Record.

Несколько важных правил, чтобы Active Record не превратился в монстра

📌 Внутри Active Record помещайте только простые инварианты: проверки статусов, ограничения значений, пересчет полей, базовые проверки.

📌 Не включайте сложную бизнес-логику: расчёты цен с кучей правил, сложные жизненные циклы. Для этого уже нужен Domain Model.

📌 Избегайте анемии: не делайте getters/setters публичными без необходимости. Лучше использовать методы вроде order.addItem(...), а не order.getItems().add(...).

📌 Не превращайте сущность в "божественный объект" (God object) — если методов больше 10-15 и большая часть из них не про состояние самого объекта, пора задуматься о вынесении логики.

Логика сохранения самого себя в базу: актуально ли?

В классическом варианте Active Record содержит логику по сохранению и загрузке самого себя из и в базу данных. В настоящий момент использование такого подхода — уже не лучшая идея. Такая реализация нарушает принципы единой ответственности и вносит дополнительные сложности в поддержку и развитие функциональности.

В настоящий момент лучше делегировать логику по сохранению и загрузке объектов репозиторию, а не оставлять ее в Active Record. Но это уже не будет классическим паттерном Active Record, а, скорее всего, что-то среднее между Active Record и Domain Model.

Если мы оставляем работу с БД внутри объекта, мы получаем следующие проблемы:

✘ Смешивание инфраструктуры и бизнес-логики: объект знает и о том, как посчитать скидку, и о том, как сгенерировать SQL-запрос (или обратиться к EntityManager).

✘ Сложность тестирования: вам придется использовать моки базы данных или поднимать Testcontainers даже для проверки простой математики.

✘ Связность (Coupling): доменная логика становится намертво привязана к конкретному ORM-фреймворку.

Реализация Active Record с делегированием логики в репозитории

Прошлый пример в новом подходе будет выглядеть так:

@Entity @Table(name = "orders") public class Order { private UUID id; private UUID customerId; private OrderStatus status; private List<OrderItem> items = new ArrayList<>(); private BigDecimal total = BigDecimal.ZERO; // Бизнес-логика внутри сущности public void addItems(List<OrderItem> newItems) { this.items.addAll(newItems); recalculateTotal(); } public void applyPromoCode(PromoCode promo) { if (promo != null && promo.isValidFor(this.total)) { BigDecimal discount = promo.getDiscountAmount(); this.total = this.total.subtract(discount); } } private void recalculateTotal() { this.total = items.stream() .map(OrderItem::getSubtotal) .reduce(BigDecimal.ZERO, BigDecimal::add); } // ... @Service @Transactional public class OrderService { private final OrderRepository orderRepository; private final PromoCodeRepository promoRepository; private final ApplicationEventPublisher eventPublisher; public OrderDto createOrder(CreateOrderRequest request) { validateRequest(request); Order order = new Order(); order.setCustomerId(request.getCustomerId()); order.addItems(request.getItems()); if (request.hasPromoCode()) { PromoCode promo = promoRepository.findByCode(request.getPromoCode()); order.applyPromoCode(promo); } // Сохранение через репозиторий order = orderRepository.save(order); eventPublisher.publish(new OrderCreatedEvent(order.getId())); return toDto(order); } }

Когда стоит оставить Active Record и не переходить дальше:

  • Бизнес-логика остаётся небольшой — несколько проверок + расчёты.
  • Поддомен не основной (core), а вспомогательный (supporting) или общий (generic).
  • Важны скорость разработки и минимум абстракций.
  • Проект небольшой или средний, нет планов на очень сложную доменную модель.
  • Уже используете ORM, где Active Record — естественный стиль (Panache, Eloquent и т. п.).

В core-доменах с высокой скоростью изменений и большой ценой ошибки переходите к Domain Model. В остальном Active Record — это разумный компромисс между простотой и выразительностью.

А вы где чаще останавливаетесь — на Transactional Script, Active Record или сразу прыгаете в богатую модель?

Подпишись на мой канал в telegram

Начать дискуссию