Как стать автором
Обновить

Комментарии 11

По-моему OrderLine вообще не должен знать какому order'у он принадлежит (вложенные сущности не должны знать о том, куда их засунули — это не их дело). Завтра OrderLine окажется в другом агрегате, что будете делать?


AggregateEntity не должно существовать.


Проблема: когда вы меняете что-то в модели, доктрина об этом не догадывается


Решение1: учить доктрину ловить ваши изменения (найти место, где она ищет изменения в модели, и расширить так, чтобы она учитывала те изменения, которые вам нужны).


Я бы думал в сторону вычисления хэша агрегата по его содержимому (у вас наоборот: вы через аннотации учите вложенную сущность определять какому агрегату она принадлежит). Добавляете в супертип AggregateRoot метод getHash(). Доктрина, когда решает увеличивать версию или нет, должна сверять этот хэш: если изменился — увеличивает версию.


Внутри доктрины вроде как свой UoW, пока она не видит ваши изменения — там много чего может сломаться и работать не так.


Решение 2: вручную увеличивать версию при изменении агрегата (так же, как это делается в Unit Of Work)


Выбрасывайте события при изменениях, а когда ловите — обновляйте версию соотв.агрегата

По-моему OrderLine вообще не должен знать какому order'у он принадлежит (вложенные сущности не должны знать о том, куда их засунули — это не их дело). Завтра OrderLine окажется в другом агрегате, что будете делать?

Соглашусь с Вами что, возможно, пример не самый удачный, но всем понятный. Здесь рассматривал проблему не в том, что OrderLine может жить в другом агрегате или OrderLine сделать агрегатом. Проектирование ответственности агрегата всегда рассматривается индивидуально. Проблема здесь заключается в изменении любых коллекций-сущностей в агрегате, при которых не будет работать оптимистическая блокировка. Ведь такая проблема есть. Проблема, как мне кажется, не архитектуры. Любая вложенная коллекция в виде сущности даст нам эту проблему. Это может быть UserRoles, UserNetworks и так далее.

Я бы думал в сторону вычисления хэша агрегата по его содержимому (у вас наоборот: вы через аннотации учите вложенную сущность определять какому агрегату она принадлежит). Добавляете в супертип AggregateRoot метод getHash(). Доктрина, когда решает увеличивать версию или нет, должна сверять этот хэш: если изменился — увеличивает версию.

Хорошая идея. О таком не подумали. Пока что пришли к такому решению. Посмотрим в сторону предложенного вами решения. Возможно, из этого что-то получится. Здесь только есть одна проблема, когда вложенная сущность изменяется агрегат не попадает в Unit Of Work. Поэтому пришлось придумать обратную ссылку на агрегат, чтобы знать к какому агрегату принадлежит изменённая сущность. Но есть идея как получить rootEntity по другому, а именно вот так:

$meta = $em->getClassMetadata(get_class($aggregateRoot));
$meta->rootEntityName; // App/Entity/Order

Но пока что это ничего не даёт) Надо её ID этого агрегата.

Решение 2: вручную увеличивать версию при изменении агрегата (так же, как это делается в Unit Of Work)

Выбрасывайте события при изменениях, а когда ловите — обновляйте версию соотв.агрегата

Да. О таком решении знали. Иногда даже изменение версии делаются при публикации событий. Такое тоже практикуется некоторыми командами. Однако шли от того, что есть множество агрегатов, которые имеют данную проблему и хотели её решить наименьшим изменением в коде. С данным примером нам удалось достичь этого.

В любом случае спасибо вам за ёмкий комментарий. Здесь есть над чем подумать и придти к каком-то более лаконичному решению, особенно с хэшем агрегата. Если есть что-то добавить — напишите.

Код класса OrderLine дублируется, вместо второго примера возможно Line должен был быть.

Благодарю) Не заметил, исправил)

Предлагаю более простое решение для:

для изменений полей самого агрегата — она работает, но если изменяется сущность-коллекция внутри агрегата — блокировка перестаёт работать и версия не увеличивается на +1.

использовать immutable значения внутри сущностей, то есть чтобы UoW видел изменение, то при изменении коллекции надо, чтобы это была новая коллекция... и всего-то

Это как-то более явно

К сожалению, такой способ не работает. Пример метода addLine:

public function addLine(Line $line): void
{
    $items = new ArrayCollection();
    $items->add(new OrderLine($this, $line));
    $this->items = $items;
}

При таком использовании поле version остаётся прежней - не меняется. Вроде бы пример правильный.

На счёт коллекций думал иначе. Сделать свою коллекцию, которая будет вызывать корень агрегата вроде такого:

public function __construct(string $id)
{
	$this->items = new MyArrayCollection($this); //Передаём агрегат ($s), чтобы вызвать у него внутри $aggregate->updateAggregateVersion()
}

Должен UoW видеть изменение сущности

public function addLine(Line $line): void 
{
  $items = clone $this->items;
  $items->add(new OrderLine($this, $line));
  $this->items = $items;
}

а ну да, если через корень делаете изменения, то сам Order тоже должен быть новым объектом, ведь по нему смотрим изменения

крч следите за тем, что сущность должна меняться

Выше пример тоже не работает. Order агрегат и вряд ли когда-то будет новым, при вызове его из em:

$order = $this->em->getRepository(self::ORDER)->find($id)

крч следите за тем, что сущность должна меняться

Вот как раз над этим и озаботились и эта проблема вышла в данную статью. Ищем лаконичный способ следить за изменениями. Можно пойти простым путём вроде:

public function addLine(Line $line): void
{
    $this->items->add(new OrderLine($this, $line));
    $this->aggregateVersion++; //Обновляем версию агрегата
}

public function editLine(string $id, Line $line): void
{
    foreach ($this->items as $item) {
      if ($item->getId() === $id) {
        $item->edit($line);
        $this->aggregateVersion++; //Обновляем версию агрегата
        return;
      }
    }
  	throw new DomainException('Order line not found.');
}

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

Ещё был вариант изменениие версии при публикации события:

public function releaseEvents(): array
{
    $events = $this->recordedEvents;
    $this->recordedEvents = [];
    $this->updateAggregateVersion(); // Обновляем версию
    return $events;
}

Этот способ дополнительно требует наличие DomainEvent, даже если он не нужен или не написан в действующем проекте.

Явное изменение версии хороший вариант - просто и понятно.

Изменение каждого метода равноценно изменению всех классов с добавлением аннотаций по трудоемкости.

Чтобы люди не забывали инкрементить версию можно написать юнит тест который найдет все энтити у которых есть методы начинающиеся с add и проверить что они инкрементят версию - либо вызывав их , либо тупо посчитав что количество методов равно количеству инкрементов или количество инкрементов равно количеству @OneToMany аннотаций x 3.

Явное изменение версии хороший вариант - просто и понятно.

С одной стороны да, а с другой - появляется лишний мусор, который никаким образом не относится в бизнес модели. Текущая вещь больше инфраструктурная, поэтому домен не должен знать ни о каких версиях агрегата. Да и в каждом тесте модели придётся делать проверку изменения агрегата.

Чтобы люди не забывали инкрементить версию можно написать юнит тест который найдет все энтити у которых есть методы начинающиеся с add и проверить что они инкрементят версию либо вызывав их , либо тупо посчитав что количество методов равно количеству инкрементов или количество инкрементов равно количеству @OneToMany аннотаций x 3.

В этих случаях мы явно заставляем все использования доктрины проставлять версию агрегата. А что делать с простыми доменами, которое реализуется не по DDD, а простым CRUD. Там вообще нет такого понятия, как агрегат… Unit тест не будет проходить.

Мне кажется, в этой ситуации будет правильнее делать нормальную блокировку через FOR SHARE/FOR UPDATE, а не имитировать ее с помощью фиктивных полей. Может быть это медленнее, зато стабильнее и без всяких откатов и усложнения кода.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории