Явное изменение версии хороший вариант - просто и понятно.
С одной стороны да, а с другой - появляется лишний мусор, который никаким образом не относится в бизнес модели. Текущая вещь больше инфраструктурная, поэтому домен не должен знать ни о каких версиях агрегата. Да и в каждом тесте модели придётся делать проверку изменения агрегата.
Чтобы люди не забывали инкрементить версию можно написать юнит тест который найдет все энтити у которых есть методы начинающиеся с add и проверить что они инкрементят версию либо вызывав их , либо тупо посчитав что количество методов равно количеству инкрементов или количество инкрементов равно количеству @OneToMany аннотаций x 3.
В этих случаях мы явно заставляем все использования доктрины проставлять версию агрегата. А что делать с простыми доменами, которое реализуется не по DDD, а простым CRUD. Там вообще нет такого понятия, как агрегат… Unit тест не будет проходить.
Зачем?) Это очень странно. В Yii2 есть ArrayDataProvider. Вам достаточно было отделить слой работы с моделью изменения от слоя чтения - аналог CQRS. Тогда бы у вас было меньше связанности. А так у вас может утекать какая-то логика в ваши DtoBuilder. Например, в User у нас есть статусы (активный, заблокирован). А в UI мам нужно только isActive чтобы показать что-то или скрыть. Тогда получается что вы эту проверку добавите в DtoBuilder. И как это тестировать?
Пойдём дальше, а что если к User нужно ещё добавить например список его комментариев из другого модуля? У вас уже будут совмещённые DtoBuilder или два билдера как-то объединять данные. А с разными репозиториями на чтение мы бы вызвали два репозитория, которые отдают нам две DTO: User и Comment и уже их бы передали в view.
Вот как раз над этим и озаботились и эта проблема вышла в данную статью. Ищем лаконичный способ следить за изменениями. Можно пойти простым путём вроде:
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, даже если он не нужен или не написан в действующем проекте.
К сожалению, такой способ не работает. Пример метода 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()
}
По-моему OrderLine вообще не должен знать какому order'у он принадлежит (вложенные сущности не должны знать о том, куда их засунули — это не их дело). Завтра OrderLine окажется в другом агрегате, что будете делать?
Соглашусь с Вами что, возможно, пример не самый удачный, но всем понятный. Здесь рассматривал проблему не в том, что OrderLine может жить в другом агрегате или OrderLine сделать агрегатом. Проектирование ответственности агрегата всегда рассматривается индивидуально. Проблема здесь заключается в изменении любых коллекций-сущностей в агрегате, при которых не будет работать оптимистическая блокировка. Ведь такая проблема есть. Проблема, как мне кажется, не архитектуры. Любая вложенная коллекция в виде сущности даст нам эту проблему. Это может быть UserRoles, UserNetworks и так далее.
Я бы думал в сторону вычисления хэша агрегата по его содержимому (у вас наоборот: вы через аннотации учите вложенную сущность определять какому агрегату она принадлежит). Добавляете в супертип AggregateRoot метод getHash(). Доктрина, когда решает увеличивать версию или нет, должна сверять этот хэш: если изменился — увеличивает версию.
Хорошая идея. О таком не подумали. Пока что пришли к такому решению. Посмотрим в сторону предложенного вами решения. Возможно, из этого что-то получится. Здесь только есть одна проблема, когда вложенная сущность изменяется агрегат не попадает в Unit Of Work. Поэтому пришлось придумать обратную ссылку на агрегат, чтобы знать к какому агрегату принадлежит изменённая сущность. Но есть идея как получить rootEntity по другому, а именно вот так:
Но пока что это ничего не даёт) Надо её ID этого агрегата.
Решение 2: вручную увеличивать версию при изменении агрегата (так же, как это делается в Unit Of Work)
Выбрасывайте события при изменениях, а когда ловите — обновляйте версию соотв.агрегата
Да. О таком решении знали. Иногда даже изменение версии делаются при публикации событий. Такое тоже практикуется некоторыми командами. Однако шли от того, что есть множество агрегатов, которые имеют данную проблему и хотели её решить наименьшим изменением в коде. С данным примером нам удалось достичь этого.
В любом случае спасибо вам за ёмкий комментарий. Здесь есть над чем подумать и придти к каком-то более лаконичному решению, особенно с хэшем агрегата. Если есть что-то добавить — напишите.
С одной стороны да, а с другой - появляется лишний мусор, который никаким образом не относится в бизнес модели. Текущая вещь больше инфраструктурная, поэтому домен не должен знать ни о каких версиях агрегата. Да и в каждом тесте модели придётся делать проверку изменения агрегата.
В этих случаях мы явно заставляем все использования доктрины проставлять версию агрегата. А что делать с простыми доменами, которое реализуется не по DDD, а простым CRUD. Там вообще нет такого понятия, как агрегат… Unit тест не будет проходить.
Зачем?) Это очень странно. В Yii2 есть ArrayDataProvider. Вам достаточно было отделить слой работы с моделью изменения от слоя чтения - аналог CQRS. Тогда бы у вас было меньше связанности. А так у вас может утекать какая-то логика в ваши DtoBuilder. Например, в User у нас есть статусы (активный, заблокирован). А в UI мам нужно только isActive чтобы показать что-то или скрыть. Тогда получается что вы эту проверку добавите в DtoBuilder. И как это тестировать?
Пойдём дальше, а что если к User нужно ещё добавить например список его комментариев из другого модуля? У вас уже будут совмещённые DtoBuilder или два билдера как-то объединять данные. А с разными репозиториями на чтение мы бы вызвали два репозитория, которые отдают нам две DTO: User и Comment и уже их бы передали в view.
Выше пример тоже не работает. Order агрегат и вряд ли когда-то будет новым, при вызове его из em:
Вот как раз над этим и озаботились и эта проблема вышла в данную статью. Ищем лаконичный способ следить за изменениями. Можно пойти простым путём вроде:
Но пока от этого способа отошли, так как это требует постоянно изменять версию в каждом методе. При наличии большого количества агрегатов в действующем проекте - это достаточно долго и много изменений. Хотя тоже не плохой вариант.
Ещё был вариант изменениие версии при публикации события:
Этот способ дополнительно требует наличие DomainEvent, даже если он не нужен или не написан в действующем проекте.
К сожалению, такой способ не работает. Пример метода
addLine
:При таком использовании поле version остаётся прежней - не меняется. Вроде бы пример правильный.
На счёт коллекций думал иначе. Сделать свою коллекцию, которая будет вызывать корень агрегата вроде такого:
Соглашусь с Вами что, возможно, пример не самый удачный, но всем понятный. Здесь рассматривал проблему не в том, что OrderLine может жить в другом агрегате или OrderLine сделать агрегатом. Проектирование ответственности агрегата всегда рассматривается индивидуально. Проблема здесь заключается в изменении любых коллекций-сущностей в агрегате, при которых не будет работать оптимистическая блокировка. Ведь такая проблема есть. Проблема, как мне кажется, не архитектуры. Любая вложенная коллекция в виде сущности даст нам эту проблему. Это может быть UserRoles, UserNetworks и так далее.
Хорошая идея. О таком не подумали. Пока что пришли к такому решению. Посмотрим в сторону предложенного вами решения. Возможно, из этого что-то получится. Здесь только есть одна проблема, когда вложенная сущность изменяется агрегат не попадает в Unit Of Work. Поэтому пришлось придумать обратную ссылку на агрегат, чтобы знать к какому агрегату принадлежит изменённая сущность. Но есть идея как получить rootEntity по другому, а именно вот так:
Но пока что это ничего не даёт) Надо её ID этого агрегата.
Да. О таком решении знали. Иногда даже изменение версии делаются при публикации событий. Такое тоже практикуется некоторыми командами. Однако шли от того, что есть множество агрегатов, которые имеют данную проблему и хотели её решить наименьшим изменением в коде. С данным примером нам удалось достичь этого.
В любом случае спасибо вам за ёмкий комментарий. Здесь есть над чем подумать и придти к каком-то более лаконичному решению, особенно с хэшем агрегата. Если есть что-то добавить — напишите.
Благодарю) Не заметил, исправил)