У меня не настолько большой опыт проектирования систем, и, когда столкнулся с ситуацией, возникли сомнения, как её верно разрешить.
Есть таблица Role - роли пользователей.
ПО условно разделено на два уровня - пользовательский код и api (тоже код, но выделенный отдельно как ядро системы).
В апи есть метод AddRoleToUser - передаем в метод идентификатор юзера, идентификатор роли -> метод проверяет, существуют ли роль и пользователь -> в случае успеха назначает роль путем записи еще в парочку второстепенных таблиц, затем рассылает письма по электронке. Запросы внутри AddRoleToUser обернуты в transactionScope (scope1).
Пользовательский код делает следующее:
1. Создает свой transactionScope (scope2) в режиме Requires, Serializable;
2. Пишет данные во второстепенные таблицы таблица1 и таблица2;
3. Пишет напрямую в таблицу Role, создавая там роль.
4. Вызывает AddRoleToUser, передавая ему идентификатор созданной роли и идентификатор какого-то пользователя (заведомо существующего);
5. Выходит из scope2 без commit (то есть происходит rollback).
У меня возникло непонимание, как правильно поступать внутри AddRoleToUser с чтением данных из базы. Могут возникнуть следующие сценарии:
1) scope1 в режиме requires, уровень изоляции любой - роль считывается без проблем, данные записываются, письмо отправляется, но после rollback в scope2 откатываются все записи, созданные в AddRoleToUser. Итог: роли нет, назначения роли нет, письмо есть.
2) scope1 в режиме suppress, уровень изоляции read uncommitted - роль читается, данные записываются, назначение роли создается, письмо отправляется, rollback ничего не отказывает. Итог: роли нет, назначение роли есть, письмо есть.
3) scope1 в режиме suppress, уровень изоляции любой, кроме read uncommitted - роль не читается, запрос падает по таймауту ожидания. Итог: роли нет, назначения нет, письма нет.
Сценарий 3 выглядит самым "чистым" с точки зрения последствий, но является ли он верным?
Возможно, правильнее сначала коммитить scope2, затем вызывать AddRoleToUser? Но что, если логика внутри scope2 зависит от результата работы AddRoleToUser?
Один из напрашивающихся выводов - Role тоже должен создаваться внутри методов api. Да, в идеале должен, но сейчас ситуация не та.
гуглите паттерн Unit of Work
или вот для начала - Паттерн Unit of Work
UoW есть в EntityFramework, можете переиспользовать его.
если у вас микросервисная архитекрура, то гуглите паттерн Saga
ну или вот почитайте - Паттерн: Сага
а вообще, письмо об измении роли должен рассылать отдельный микросервис, а не код, который пишет в БД.
на основе события, значит в идеале надо поднимать шину (RabbitMQ) и связывать их асинхронно.
Сага выглядит очень привлекательно, но капец как трудоемко. Да, это оправданно для микросервисов, но как быть с монолитным приложением?
Давайте предположим, что у нас нет обращений к базе, есть только изменение состояния сущностей. То есть откатить все изменения путем rollback-а транзакции не выйдет.
Выходит, что есть некий набор атомарных операций, выполняемых последовательно. В конце они все должны быть сохранены или отменены. Это похоже на сагу, похоже на UoW. На UoW похоже даже больше, чем на все остальное - в конце все изменения должны быть проанализированы и применены. Или отменены. Но это еще и на паттерн Хранитель похоже.
При этом у вас будут репозитории, которые непосредственно работают каждый со своей сущностью.
Не один на всех! А три сущности - три репозитория.
> Давайте предположим, что у нас нет обращений к базе
хорошо, но тогда каждая сущность должна иметь некое Стабильное состояние, которое нельзя откатить, и Временное состояние, которое можно откатить до последнего Стабильного.
Реализация UoW делает только изменение флага с Временного состояния на Стабильное. И больше ничего. Ей не нужно знать какие поля изменились, какие это сущности, кто от кого зависит и т.д.
Схема такая:
1. Каждый Репозиторий берет из памяти/БД сущность. Ее Стабильное состояние.
2. Затем вы дергаете каждую сущность согласно бизнес логике.
3. Вы отдаете их обратно Репозиториям - и они выставляют им Временое состояние.
Фактически в памяти начинает висеть 2 копии Сущности (Стабильное старое и Временное новое)
4. Вы даете приказ UoW реализации применить изменения. - Происходит подмена ссылок - Стабильные версии сущностей начинают указывать на новое Временное состояние. Старое Стабильное - убивается.
При этом каждая сущность имела внутри себя Коллекцию Событий (Отправить письмо на email и т.д.)
UoW реализация делает рассылку Событий после применения нового состояния.
Дмитрий Петров, если Коммит всех изменений в UoW или в Репозитории завершится неудачей, то ничего страшного. GC или какой другой сервис прихлопнет Временные сущности, потому что скоро на них не будет ссылок. Как только закончится using (UnitOfWork...