Что такое StreamId в EventSourcing, когда событие домена влияет на несколько агрегатов в одном и том же ограниченном контексте?
Streams
Некоторые авторы предлагают классифицировать события в "потоках", а многие авторы идентифицируют "поток" с "совокупным идентификатором".
Скажи событие car.repainted
под этим мы подразумеваем, что мы перекрасили машину с идентификатором 12345
в {color:red}
,
В этом примере идентификатор потока, вероятно, будет что-то вроде car.12345
или если у вас есть универсальные уникальные идентификаторы, то просто 12345
,
Некоторые авторы фактически предлагают сохранить поток событий в таблице со структурой, более или менее похожей на следующую (если вы используете реляционную):
| writeIndex | event | cachedEventId | cachedTimeStamp | cachedType | cachedStreamId |
| 1 | JSON | abcd | xxxx | car.repainted | 12345 |
event
Столбец имеет "исходный" объект значения события, наиболее вероятно сериализованный в JSON, если это реляционная БД.writeIndex
только для администрирования БД и не имеет ничего общего с самим доменом. Вы можете "сбросить" ваши события в другую БД и переписать writeIndex без побочных эффектов.cached*
поля предназначены для простого поиска и фильтрации событий, и все они могут быть рассчитаны на основе самого события.- Особого упоминания
cachedStreamId
который будет использоваться - согласно некоторым авторам - для сопоставления с "агрегированным идентификатором, которому принадлежит событие". В данном случае "автомобиль идентифицирован12345
".
Если вы не используете реляционную систему, вы, вероятно, сохранили бы свое событие "как документ" в хранилище данных / хранилище событий / хранилище документов / или "позвоните как хотите" (mongo, redis,asticsearch...) и затем вы создаете группы, группы, выборки или фильтры для извлечения некоторых событий по критерию (и один из критериев - "какой идентификатор объекта / агрегата мне интересен" => stream Id снова).
Воспроизведение
При воспроизведении событий для создания новых проекций у вас просто есть несколько подписчиков на тип события (и, возможно, версию), и если это для вас, вы читаете полный оригинал документа события, вы обрабатываете его, вычисляете и обновляете проекция. И если событие не для вас, вы просто пропустите его.
При воспроизведении вы восстанавливаете агрегатные таблицы чтения, которые вы хотите перестроить, в известный начальный набор (возможно, "все пусто"), затем выбираете один или несколько потоков, выбираете события в хронологическом порядке и итеративно обновляете состояние агрегатов.
Окей...
Все это кажется мне разумным. Никаких новостей пока здесь.
Вопрос
Но... теперь у меня в голове есть какое-то короткое замыкание... Это такое простое короткое замыкание, что, вероятно, ответ настолько очевиден, что я буду чувствовать себя глупо, если не смогу увидеть это сейчас...
Что происходит... если событие "одинаково важно" для двух агрегатов разных типов (при условии, что они находятся в одном и том же ограниченном контексте) или даже если оно относится к двум экземплярам одного и того же агрегатного типа.
Пример 2 одинаково важных разных агрегатов:
Представьте, что вы работаете в железнодорожной индустрии и у вас есть такие агрегаты:
Locomotive
Wagon
На мгновение представьте, что один локомотив может перевозить 0 или 1 вагон, но не так много вагонов.
И у вас есть эти команды:
Attach( locomotiveId, wagonId )
Detach( locomotiveId, wagonId )
Присоединение может быть отклонено, если локомотив и вагон уже были прикреплены к чему-либо, и отсоединение может быть отклонено, если команда выдается, когда они не присоединены.
События, очевидно, соответствующие:
AttachedEvent( locomotiveId, wagonId )
DetachedEvent( locomotiveId, wagonId )
Q:
Какой там идентификатор потока? и локомотив, и вагон имеют одинаковое значение, это не событие "локомотива" или "вагона". Это событие домена, которое затрагивает этих двоих! Какой из них является stream Id и почему?
Пример с 2 агрегатами одного типа
Скажи трекер проблем. У вас есть этот агрегат:
Issue
И эти команды:
MarkAsRelated( issueAId, issueBId )
UnmarkAsRelated( issueAId, issueBId )
И отметка отклоняется, если отметка уже была, а отметка отклоняется, если ранее не было отметки.
И те события:
MarkedAsRelatedEvent( issueAId, issueBId )
UnmarkedAsRelatedEvent( issueAId, issueBId )
Q:
Тот же вопрос здесь: дело не в том, что отношения "принадлежат" проблеме А или В. Они либо связаны, либо нет. Но его двунаправленный. Если A относится к B, то B относится к A. Какой здесь идентификатор потока и почему?
История написана один раз
В любом случае, я не вижу создания ДВУХ событий по одному для каждого. Это вопрос калькуляторов...
Если мы видим определение "история" (не в компьютерах, в общем!), Оно говорит "последовательность событий, которые произошли". В бесплатном словаре написано: "Хронологическая запись событий" ( https://www.thefreedictionary.com/history).
Поэтому, когда идет война между социальной группой A и социальной группой B и, скажем, B бьет A, вы не пишете 2 события: lost(A)
а также won(B)
, Вы просто пишете одно событие warFinished( wonBy:B, lostBy:A )
,
Вопрос
Итак, как вы обрабатываете потоки событий, когда событие влияет на несколько объектов одновременно, и это не значит, что оно "принадлежит" одному, а другое является дополнением к нему, но оно действительно равно обоим?
2 ответа
Что происходит... если событие "одинаково важно" для двух агрегатов разных типов (при условии, что они находятся в одном и том же ограниченном контексте) или даже если оно относится к двум экземплярам одного и того же агрегатного типа
Event-Sourcing - простая (примечание: не простая) идея. Вместо того, чтобы перезаписывать предыдущее состояние, когда мы сохраняем агрегат в нашем стабильном хранилище, мы пишем новую версию, связанную с предыдущей версией. Кроме того, вместо того, чтобы выписывать полную копию новой версии, мы записываем diff, и diff выражается в зависимости от предметной области.
Следовательно, сохранение агрегата в потоке аналогично сохранению представления агрегата в виде документа в хранилище значений ключей или в виде строк в реляционной базе данных.
Когда вы спрашиваете "к какому потоку" он принадлежит: он принадлежит потоку агрегата, который изменился, так же, как и в любой другой стратегии хранения.
Если вы не уверены, какой агрегат изменился, то проблема в моделировании, а не в источнике событий.
Оба ваших примера описывают введение отношения между двумя агрегатами; это аналогично тому, что между двумя таблицами в базе данных есть отношение многие ко многим. Так кому принадлежит таблица M2M?
Что ж, если ни одному агрегату не нужна эта информация для обеспечения собственного инварианта, то таблица M2M может быть агрегатом сама по себе.
Представьте себе представление контракта между двумя сторонами - может оказаться, что обе стороны являются случайными, и "Контракт" является важной идеей, достойной того, чтобы ее смоделировали как свою собственную вещь.
Если отношение явно является "частью" одного агрегата (этот агрегат защищает инварианты, которые зависят от состояния отношения), то этот агрегат будет отвечать за редактирование новой таблицы, а другой агрегат будет игнорировать ее.
Если оба агрегата заботятся об отношениях, то у вас одна из двух проблем
1) Ваш анализ домена неверен - вы наметили свои совокупные границы не в том месте. Доберись до белой доски и начни рисовать вещи.
2) У вас есть две копии отношения - по одной для каждого агрегата, но эти копии не обязательно соответствуют друг другу.
Вот важная эвристика: если у вас действительно есть два разных агрегата, вы сможете хранить их в двух совершенно разных базах данных. Они не могут делиться данными друг друга, но могут хранить свои собственные версионные / временные / кэшированные копии данных других парней.
Таким образом, агрегат левой руки вносит изменения, и "слесарное дело" отправляет сообщение "агрегат изменен слева" агрегату справа, затем агрегат справа обновляет свой кэш.
Обратите внимание, как это будет работать в случае, если мы думаем, что контракт - это первоклассная задача, которая управляет своим собственным состоянием. Модель обновляет контракт, сохраняя изменения в его состоянии, а затем приходит сантехника и доставляет копию изменений для каждого из агрегатов слева и справа агрегата.
Просто Не обязательно просто.
Я не думаю, что это как-то связано с источником событий как таковым. Возможно, дизайн можно немного повозить.
Я бы пошел с чем-то вроде этого для локомотива:
public class Locomotive
{
Guid Id { get; private set; }
Guid? AttachedWagonId { get; private set; }
public WagonAttached Attach(Guid wagonId)
{
return On(
new WagonAttached
{
Id = wagonId
});
}
private WagonAttached On(WagonAttached wagonAttached)
{
AttachedWagonId = wagonAttached.Id;
return wagonAttached;
}
}
Поток событий для Locomotive
где WagonAttached
событие будет проживать. Каким образом Wagon
Совокупность зависит от этого события, что является предметом обсуждения. Я бы сказал, что вагон, вероятно, не так уж сильно заботится о Product
не слишком обеспокоен тем, какие Order
(может в этом случае) это связано с. Совокупность Order
та сторона, которая кажется более подходящей для OrderItem
ассоциативный субъект. Я предполагаю, что ваши отношения локомотив-вагон, вероятно, будут следовать той же схеме, учитывая, что к локомотиву будет прикреплено более одного вагона. Возможно, немного больше о дизайне, но я собираюсь предположить, что это гипотетические примеры.
То же самое касается Issue
, Если бы можно было прикрепить несколько, то Order
в Product
Концепция вступает в игру. Несмотря на то, что затрагиваются две проблемы, существует своего рода направление, учитывая, что одна проблема, как подчиненная, связана с основной проблемой. Возможно событие с RelationshipType
такие как Dependency
, Impediment
и т. д. В таком случае можно было бы использовать объект-значение для представления этого:
public class Issue
{
public class RelatedIssue
{
public enum RelationshipType
{
Dependency = 0,
Impediment = 1
}
public Guid Id { get; private set; }
public RelationshipType Type { get; private set; }
public RelatedIssue(Guid id, RelationshipType type)
{
Id = id;
Type = type;
}
}
private readonly List<RelatedIssue> _relatedIssues = new List<RelatedIssue>();
public Guid Id { get; private set; }
public IEnumerable<RelatedIssue> GetRelatedIssues()
{
return new ReadOnlyCollection<RelatedIssue>(_relatedIssues);
}
public IssueRelated Relate(Guid id, RelationshipType type)
{
// probably an invariant to check for existence of related issue
return On(
new IssueRelated
{
Id = id,
Type = (int)type
});
}
private IssueRelated On(IssueRelated issueRelated)
{
_relatedIssues.Add(
new RelatedIssue(
issueRelated.Id,
(RelatedIssue.RelationshipType)issueRelated.Type));
return issueRelated;
}
}
Дело в том, что событие принадлежит одному агрегату, но все еще представляет отношение. Вам просто нужно определить сторону, которая имеет больше всего смысла.
События могут (или должны) быть опубликованы с использованием некоторого метода управляемой событиями архитектуры (скажем, служебной шины), чтобы другие заинтересованные стороны могли получать уведомления.