CQRS: чтение модели, когда источник событий связан с отношением Parent-Child-GrandChild...
Я нахожусь в процессе написания моего первого приложения CQRS, скажем, моя система отправляет следующие команды:
- CreateContingent (Id, Name)
- CreateTeam (идентификатор, имя)
- AssignTeamToContingent (TeamId, ContingentId)
- CreateParticipant (Id, Name)
- AssignParticipantToTeam (ParticipantId, TeamId)
В настоящее время они приводят к идентичным событиям, просто сформулированным в прошедшем времени (ContingentCreated, TeamCreated и т. Д.), Но содержат те же свойства. (Я не уверен, что это правильно и это один из моих вопросов)
Моя проблема заключается в прочитанных моделях.
У меня есть модель чтения Contingents, которая подписывается на ContingentCreated и имеет следующие методы:
List<ContingentQueries.Contingent> GetContingents();
ContingentQueries.Contingent GetContingent(System.Guid id);
ContingentQueries.Contingent GetContingent(string province);
Класс Contingent, возвращаемый этими командами, выглядит следующим образом:
public class Contingent
{
public Guid Id { get; internal set; }
public string Province { get; internal set; }
}
Это хорошо работает для листинга, но когда я начинаю думать о модели чтения для представления одного контингента и всех его команд, все начинает запутываться. Учитывая методы, это кажется тривиальным:
ContingentQueries.Contingent GetContingent(System.Guid id);
ContingentQueries.Contingent GetContingent(string province);
И я создаю класс Contingent для этих запросов с набором команд:
public class Contingent
{
public Guid Id { get; internal set; }
public string Province { get; internal set; }
public IList<Team> Teams { get; internal set; }
}
public class Team
{
public Guid Id { get; internal set; }
public string Name { get; internal set; }
}
Теоретически мне нужно только подписаться на ContingentCreated и TeamAssignedToContingent, но у события TeamAssignedToContingent есть только идентификаторы команды и условного обозначения... поэтому я не могу установить свойство Team.Name этой модели чтения.
Я тоже подписываюсь на TeamCreated? Добавить сохранить еще одну копию команды для использования здесь?
Или когда события возникают, должны ли они иметь больше информации в них? Должен ли обработчик команд AddTeamToContingent запрашивать информацию о команде для добавления к событию? Который может еще не существовать, потому что модель чтения еще не была обновлена с командой.
Что если бы я хотел показать название команды и участника, назначенного команде, и назвал капитана в этом представлении Contingent? Я также храню участников в прочитанной модели? Это похоже на тонну дублирования.
Для некоторого дополнительного контекста. Участники не обязательно являются частью какого-либо контингента или команды, они могут быть просто гостями; или делегаты и / или запасные части контингента. Однако роли могут поменяться. Делегат, который не является членом команды, также может быть запасным, и из-за травмы может быть назначен команде, однако он все еще является делегатом. Именно поэтому используется общий "Участник".
Я понимаю, что хочу, чтобы модели чтения были не тяжелее, чем необходимо, но у меня возникают трудности с тем фактом, что мне могут понадобиться данные о событиях, на которые у меня нет подписки (TeamCreated), и теоретически не должно быть, но потому что будущего мероприятия (TeamAssignedToContingent) Мне нужна эта информация, что мне делать?
ОБНОВЛЕНИЕ: думал об этом быстро, и кажется, что все варианты, о которых я думал, плохи. Там должно быть другое.
Вариант 1: добавить больше данных к вызванным событиям
- Начинают ли обработчики команд использовать модели чтения, которые еще не могут быть записаны, чтобы они могли получать эти данные?
- Что если будущему подписчику на событие понадобится еще больше информации? Я не могу добавить это!
Вариант 2. Обработчики событий подписываются на другие события?
- Это приводит к тому, что модель чтения хранит данные, о которых она может не беспокоиться?
- Пример: Хранение участников в модели чтения ContingentView, поэтому, когда человек назначен в команду и помечен как капитан, мы знаем его имя.
Вариант 3. Обработчики событий запрашивают другие модели чтения?
- Это похоже на лучший подход, но кажется неправильным. Должно ли условное представление запрашивать представление участника, чтобы получить имя, если и когда оно понадобится? Это устраняет недостатки в 1 и 2.
Варианты 4:...?
2 ответа
То, что вы, вероятно, собираетесь закончить, это комбинация 1 и 2. Нет абсолютно никаких проблем с улучшением или обогащением событий, чтобы иметь больше информации в них. Это на самом деле довольно полезно.
- Что если будущему подписчику на событие понадобится еще больше информации? Я не могу добавить это!
Вы можете добавить его к событиям в будущем. Если вам нужны исторические данные, вам нужно найти их где-нибудь еще. Вы не можете строить события со всеми данными в мире только на случай, если они понадобятся вам в будущем. Можно даже вернуться и добавить больше данных к вашим историческим событиям. Некоторые люди не одобряют это как переписывание истории, но это не так, вы просто улучшаете историю. Пока вы не измените существующие данные, все будет в порядке.
Я подозреваю, что ваши обработчики событий также будут нуждаться в подписке на другие события с течением времени. Например, команды должны быть переименованы? Если это так, то вам нужно обработать это событие. Не бойтесь иметь несколько копий одних и тех же данных в разных режимах чтения. Исходя из реляционной базы данных, такое дублирование данных является одной из самых сложных вещей, к которым нужно привыкнуть.
В итоге будьте прагматичны. Если вы испытываете боль при применении CQRS, измените способ его применения.
Другой вариант, который приходит мне в голову:
На стороне чтения вы сначала строите нормализованные представления по событиям на стороне записи.
Например:
contingents table:
-----------
id, name
teams table:
-----------
id, name
contigents_teams table:
-----------
contigent_id, team_id
В случае события ContingentCreated вы просто вставляете запись в таблицу условных обозначений.
На событии TeamCreated вы просто вставляете запись в таблицу команд.
В событии TeamAssignToContingent вы вставляете запись в таблицу contigents_teams.
На событии TeamNameChanged вы обновляете соответствующую запись в таблице команд.
И так далее.
Таким образом, ваши данные (в конечном итоге) согласованы и синхронизированы со стороной записи.
Да, вам нужно использовать соединения на стороне чтения для извлечения данных...
Если это нормализованное представление не удовлетворяет требованиям производительности чтения, то вы можете построить денормализованное представление из этих нормализованных данных. для создания денормализованного представления требуется дополнительный шаг, но для меня это самое простое решение, которое я могу себе представить.
Может быть, вам даже не понадобится денормализованное представление.
В конце концов (на мой взгляд), главная ценность ES заключается в том, чтобы захватить намерения пользователя, уверенность в ваших данных (никаких разрушительных операций, единый источник правды), способность отвечать на вопросы, о которых никто не мог думать в прошлом, большое значение для аналитики,
Как я уже сказал, если вам нужно оптимизировать производительность, вы всегда можете построить денормализованное представление из нормализованного представления со стороны чтения.