Обеспечение согласованности транзакций с DDD

9

Я начинаю с DDD и понимаю, что совокупные корни используются для обеспечения транснациональной согласованности. Мы не должны изменять несколько агрегатов в одном сервисе приложений.

Однако я хотел бы знать, как справиться со следующей ситуацией.

У меня есть совокупный корень под названием Продукты.

Существует также совокупный корень, называемый группой.

Оба имеют идентификаторы и могут редактироваться независимо.

Несколько продуктов могут указывать на одну и ту же группу.

У меня есть служба приложений, которая может изменить группу продуктов:

ProductService.ChangeProductGroup(string productId, string groupId)

  1. Проверка группы существует
  2. Получить продукт из хранилища
  3. Установить свою группу
  4. Написать продукт обратно в хранилище

У меня также есть служба приложений, где группа может быть удалена:

GroupService.DeleteGroup(string groupId) 1. Получить продукты из репозитория, для которого groupId установлен на предоставленный groupId, убедиться, что счетчик равен 0 или прервать 2. Удалить группу из репозитория групп 3. Сохранить изменения

Мой вопрос следующий сценарий, что произойдет, если:

В ProductService.ChangeProductGroup мы проверяем, что группа существует (она существует), затем сразу после этой проверки отдельный пользователь удаляет группу ProductGroup (через другой GroupService.DeleteGroup). В этом случае мы устанавливаем ссылку на продукт, который был только что удален?

Является ли это недостатком моего дизайна в том, что я должен использовать другой дизайн домена (при необходимости добавляя дополнительные элементы), или мне придется использовать транзакции?

morleyc
источник

Ответы:

2

У нас та же проблема.

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

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

То, как вы поступите, зависит от вашей системы и бизнес-логики.
Параллелизм - задача не из легких.
Даже если вы сможете обнаружить проблему, как вы ее решите? Просто отменить операцию или позволить пользователю «объединить» изменения?

Мы используем Entity Framework, а EF6 по умолчанию использует read_commited_snapshot, поэтому два последовательных чтения из хранилища могут дать нам противоречивые данные. Мы просто помним об этом в будущем, когда бизнес-процессы будут более четко очерчены, и мы сможем принять взвешенное решение. И да, мы по-прежнему проверяем согласованность на уровне модели, как и вы. Это как минимум позволяет тестировать BL отдельно от БД.

Я также предлагаю вам продумать согласованность репозитория на случай, если у вас «длинные» бизнес-транзакции. Как оказалось, это довольно сложно.

Павел Воронин
источник
1

Я думаю, что это не вопрос, специфичный для предметно-ориентированного проектирования, а вопрос управления ресурсами.

Ресурсы просто должны быть заблокированы по мере их приобретения .

Это верно, когда служба приложения заранее знает, что полученный ресурс ( Groupагрегат в вашем примере) будет изменен. В доменно-управляемом дизайне блокировка ресурсов является аспектом, который принадлежит хранилищу.

rucamzu
источник
Нет, блокировка не обязательна. На самом деле это довольно ужасная вещь, связанная с длительными транзакциями. Вам лучше использовать оптимистичный параллелизм, который поддерживается всеми современными ORM. И отличительной особенностью оптимистичного параллелизма является то, что он также работает с нетранзакционными базами данных, если они поддерживают атомарные обновления.
Aaronaught
1
Я согласен с недостатками блокировки внутри длительных транзакций, но сценарий, описанный @ g18c, намекает на совершенно противоположное, то есть на короткие операции над отдельными агрегатами.
rucamzu
0

Почему вы не храните идентификаторы продуктов в группе? Таким образом, вы имеете дело только с одним агрегатом, который облегчит задачу.

Затем вам нужно будет реализовать некоторый тип шаблона параллелизма, например, если вы выбираете оптимистичный параллелизм, просто добавьте свойство версии к объекту группы и сгенерируйте исключение, если версии не совпадают при обновлении, т.е.

Добавить товар в группу

  1. Проверьте, существует ли продукт
  2. Получить группу из репозитория (включая его свойство id версии)
  3. Group.Add (ProductID)
  4. Сохранить группу с помощью репозитория (обновите с новым идентификатором версии и добавьте предложение where, чтобы версия не изменилась.)

Многие ORM имеют оптимистичный параллелизм, встроенный с использованием идентификаторов версий или временных меток, но вы легко можете сделать свою собственную. Вот хороший пост о том, как это делается в Nhibernate http://ayende.com/blog/3946/nhibernate-mapping-concurrency .

Скотт Миллетт
источник
0

Хотя транзакции могут помочь, есть и другой способ, особенно если ваш сценарий ограничен несколькими случаями.

Вы можете включить проверки в запросы, которые выполняют мутации, эффективно превращая каждую пару check-and-mutate в атомарную операцию.

Внутренний запрос на обновление продукта присоединяется к (вновь ссылающейся) группе. Это заставляет его ничего не обновлять, если эта группа исчезла.

Запрос на удаление группы объединяет любые продукты, указывающие на него, и имеет дополнительное условие, что (любой столбец) результат объединения должен быть нулевым. Это приводит к тому, что он ничего не удаляет, если на него указывают какие-либо продукты.

Запросы возвращают количество строк, которые были сопоставлены или были обновлены. Если результат равен 0, то вы проиграли условие гонки и ничего не сделали. Он может выдать исключение, и прикладной уровень может обработать то, что он хочет, например, повторив попытку с самого начала.

Timo
источник