Что такое StreamId в EventSourcing, когда событие домена влияет на несколько агрегатов в одном и том же ограниченном контексте? - PullRequest
0 голосов
/ 01 сентября 2018

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 ...) и затем вы создаете группы, группы, выборки или фильтры для извлечения некоторых событий по критерию (и один из критериев - «какой идентификатор объекта / агрегата интересует меня» => streamId снова ).

1034 * Воспроизведение * При воспроизведении событий для создания новых проекций у вас просто есть несколько подписчиков на тип события (и, вероятно, версию), и если это для вас, вы читаете полный оригинал документа события, вы обрабатываете его, рассчитать и обновить прогноз. И если событие не для вас, просто пропустите его. При воспроизведении вы восстанавливаете совокупные таблицы чтения, которые вы хотите перестроить, в известный начальный набор (возможно, «все пусто»), затем выбираете один или несколько потоков, выбираете события в хронологическом порядке и итеративно обновляете состояние агрегаты. Окей ...

Все это кажется мне разумным. Пока новостей нет.

Вопрос

Но ... у меня сейчас есть короткое замыкание в моей голове ... Это такое простое короткое замыкание, что, вероятно, ответ настолько очевиден, что я буду чувствовать глупость из-за того, что не смогу увидеть его сейчас ...

Что происходит ... если событие "одинаково важно" для двух агрегатов разных типов (при условии, что они находятся в одном и том же ограниченном контексте) или даже если оно ссылается на два экземпляра одного и того же типа агрегата.

Пример 2 одинаково важных разных агрегатов:

Представьте, что вы работаете в железнодорожной отрасли, и у вас есть эти совокупности:

Locomotive
Wagon

На мгновение представьте, что один локомотив может перевозить 0 или 1 вагон, но не так много вагонов.

И у вас есть эти команды:

Attach( locomotiveId, wagonId )
Detach( locomotiveId, wagonId )

Присоединение может быть отклонено, если локомотив и вагон уже были прикреплены к чему-либо, и отсоединение может быть отклонено, если команда выдается, когда они не присоединены.

События, очевидно, соответствуют:

AttachedEvent( locomotiveId, wagonId )
DetachedEvent( locomotiveId, wagonId )

Q:

Какой там идентификатор потока? и локомотив, и вагон имеют одинаковое значение, это не событие "локомотива" или "вагона". Это событие домена, которое затрагивает этих двоих! Какой из них является streamId и почему?

Пример с двумя агрегатами одного типа

Скажите, что проблема отслеживания. У вас есть этот агрегат:

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 ]

0 голосов
/ 01 сентября 2018

Что происходит ... если событие "одинаково важно" для двух агрегатов разных типов (при условии, что они находятся в одном и том же ограниченном контексте) или даже если оно ссылается на два экземпляра одного и того же типа агрегата

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

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

Когда вы спрашиваете «к какому потоку» он принадлежит: он принадлежит потоку агрегата, который изменился, так же, как и в любой другой стратегии хранения.

Если вы не уверены, какой агрегат изменился , то проблема в моделировании, а не в источнике событий.

Оба ваших примера описывают введение отношения между двумя агрегатами; это аналогично тому, что между двумя таблицами в базе данных есть отношение многие ко многим. Так кому принадлежит таблица M2M?

Ну, если ни одному агрегату не нужна эта информация для обеспечения собственного инварианта, то таблица M2M может быть агрегатом сама по себе.

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

Если отношение явно является «частью» одного агрегата (этот агрегат защищает инварианты, которые зависят от состояния отношения), то этот агрегат будет отвечать за редактирование новой таблицы, а другой агрегат будет игнорировать ее.

Если оба агрегатов заботятся об отношениях, то у вас есть одна из двух проблем

1) Ваш анализ домена неверен - вы наметили совокупные границы не в том месте. Подведи тебя к белой доске и начни рисовать вещи.

2) У вас есть две копии отношения - по одной для каждого агрегата, но эти копии не обязательно соответствуют друг другу.

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

Таким образом, агрегат левой руки вносит изменение, и «слесарное дело» отправляет сообщение «изменено агрегатом слева» в агрегат справа, затем агрегат справа обновляет свой кэш.

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

Simple . Не обязательно просто.

0 голосов
/ 01 сентября 2018

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

Я бы пошел с чем-то вроде этого для локомотива:

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;
    }
}

Дело в том, что событие относится к одному агрегату, но все еще представляет отношение. Вам просто нужно определить сторону, которая имеет больше всего смысла.

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

...