Если бы я использовал СУБД (например, SQL Server) для хранения данных источника событий, как могла бы выглядеть схема?
Я видел несколько вариантов, о которых говорилось в абстрактном смысле, но ничего конкретного.
Например, предположим, что у кого-то есть сущность «Продукт», и изменения в этом продукте могут иметь форму: Цена, Стоимость и Описание. Я не понимаю, могу ли я:
- Имейте таблицу «ProductEvent», в которой есть все поля для продукта, где каждое изменение означает новую запись в этой таблице, плюс «кто, что, где, почему, когда и как» (WWWWWH) в зависимости от ситуации. Когда стоимость, цена или описание изменяются, для представления Продукта добавляется новая строка.
- Храните стоимость продукта, цену и описание в отдельных таблицах, связанных с таблицей Product с помощью отношения внешнего ключа. Когда происходят изменения в этих свойствах, при необходимости напишите новые строки с WWWWWH.
- Сохраните WWWWWH, а также сериализованный объект, представляющий событие, в таблице ProductEvent, то есть само событие должно быть загружено, десериализовано и повторно воспроизведено в моем коде приложения, чтобы восстановить состояние приложения для данного продукта. ,
Особенно меня беспокоит вариант 2 выше. В крайнем случае, таблица продуктов будет состоять почти из одной таблицы на свойство, где для загрузки состояния приложения для данного продукта потребуется загрузить все события для этого продукта из каждой таблицы событий продукта. Этот взрыв стола кажется мне неправильным.
Я уверен, что «это зависит от обстоятельств», и хотя нет единого «правильного ответа», я пытаюсь понять, что приемлемо, а что совершенно неприемлемо. Я также знаю, что здесь может помочь NoSQL, где события могут храниться для совокупного корня, что означает только один запрос к базе данных, чтобы получить события для восстановления объекта, но мы не используем базу данных NoSQL в момент, так что я ищу альтернативы.
источник
Ответы:
Хранилище событий не должно знать о конкретных полях или свойствах событий. В противном случае каждая модификация вашей модели привела бы к необходимости переноса вашей базы данных (как в старом добром постоянстве на основе состояний). Поэтому я бы вообще не рекомендовал варианты 1 и 2.
Ниже приведена схема, используемая в Ncqrs . Как видите, таблица «События» хранит связанные данные как CLOB (например, JSON или XML). Это соответствует вашему варианту 3 (только то, что нет таблицы "ProductEvents", потому что вам нужна только одна общая таблица "События". В Ncqrs сопоставление с вашими агрегированными корнями происходит через таблицу "EventSources", где каждый EventSource соответствует фактическому Совокупный корень.)
Механизм сохранения SQL в реализации хранилища событий Джонатана Оливера состоит в основном из одной таблицы с именем «Commits» с полем BLOB «Payload». Это почти то же самое, что и в Ncqrs, только сериализует свойства события в двоичном формате (который, например, добавляет поддержку шифрования).
Грег Янг рекомендует аналогичный подход, как это подробно описано на веб-сайте Грега .
Схема его прототипной таблицы «События» гласит:
источник
В проекте GitHub CQRS.NET есть несколько конкретных примеров того, как вы можете создавать EventStores с помощью нескольких различных технологий. На момент написания существует реализация на SQL с использованием Linq2SQL и соответствующей схемы SQL , одна для MongoDB , одна для DocumentDB (CosmosDB, если вы находитесь в Azure) и одна с использованием EventStore (как упоминалось выше). В Azure есть больше, например хранилище таблиц и хранилище BLOB-объектов, которое очень похоже на хранилище плоских файлов.
Думаю, главное здесь то, что все они соответствуют одному и тому же принципалу / контракту. Все они хранят информацию в одном месте / контейнере / таблице, они используют метаданные для идентификации одного события от другого и «просто» сохраняют все событие в том виде, в каком оно было - в некоторых случаях сериализовано, в поддерживающих технологиях, как оно было. Таким образом, в зависимости от того, выберете ли вы базу данных документов, реляционную базу данных или даже плоский файл, существует несколько разных способов достижения одного и того же намерения хранилища событий (это полезно, если вы передумаете в любой момент и обнаружите, что вам нужно выполнить миграцию или поддержку более одной технологии хранения).
Как разработчик проекта я могу поделиться некоторыми мыслями о том, что мы сделали.
Во-первых, мы обнаружили (даже с уникальными UUID / GUID вместо целых чисел) по многим причинам последовательные идентификаторы возникают по стратегическим причинам, поэтому просто наличие идентификатора не было достаточно уникальным для ключа, поэтому мы объединили наш основной столбец ключа идентификатора с данными / тип объекта для создания того, что должно быть действительно (в смысле вашего приложения) уникальным ключом. Я знаю, что некоторые люди говорят, что вам не нужно его хранить, но это будет зависеть от того, являетесь ли вы новым участком или должны сосуществовать с существующими системами.
Мы остановились на одном контейнере / таблице / коллекции из соображений удобства обслуживания, но мы поигрались с отдельной таблицей для каждой сущности / объекта. На практике мы обнаружили, что это означает, что либо приложению требуются разрешения «СОЗДАТЬ» (что, вообще говоря, не очень хорошая идея ... как правило, всегда есть исключения / исключения), либо каждый раз, когда новая сущность / объект возникает или развертывается, новая контейнеры для хранения / таблицы / коллекции необходимо сделать. Мы обнаружили, что это было мучительно медленно для локальной разработки и проблематично для производственного развертывания. Возможно, нет, но это был наш реальный опыт.
Также следует помнить, что запрос на выполнение действия X может привести к возникновению множества различных событий, поэтому мы знаем все события, сгенерированные командой / событием / тем, что когда-либо было полезно. Они также могут относиться к разным типам объектов, например, нажатие кнопки «Купить» в корзине для покупок может инициировать запуск событий учетной записи и складирования. Приложение-потребитель может захотеть узнать все это, поэтому мы добавили CorrelationId. Это означало, что потребитель мог запрашивать все события, возникшие в результате его запроса. Вы увидите это на схеме .
В частности, с помощью SQL мы обнаружили, что производительность действительно становится узким местом, если индексы и разделы не используются должным образом. Помните, что события необходимо будет транслировать в обратном порядке, если вы используете снимки. Мы попробовали несколько разных индексов и обнаружили, что на практике некоторые дополнительные индексы необходимы для отладки производственных приложений реального мира. Вы снова увидите это на схеме .
Другие производственные метаданные были полезны во время производственных исследований, отметки времени давали нам представление о порядке, в котором события сохранялись и возникали. Это дало нам некоторую помощь в системе, особенно сильно управляемой событиями, которая генерировала огромное количество событий, давая нам информацию о производительности таких вещей, как сети, и о распределении систем по сети.
источник
Что ж, вы могли бы взглянуть на Datomic.
Datomic - это база данных гибких, основанных на времени фактов , поддерживающих запросы и объединения, с эластичной масштабируемостью и транзакциями ACID.
Я написал подробный ответ здесь
Вы можете посмотреть выступление Стюарта Хэллоуэя, объясняющее дизайн Datomic здесь
Поскольку Datomic хранит факты во времени, вы можете использовать его для сценариев использования источников событий и многого другого.
источник
Я думаю, что решение (1 и 2) может очень быстро стать проблемой по мере развития вашей модели предметной области. Создаются новые поля, некоторые меняют значение, а некоторые больше не используются. В конце концов, ваша таблица будет иметь десятки полей, допускающих значение NULL, и загрузка событий будет беспорядочной.
Кроме того, помните, что хранилище событий следует использовать только для записи, вы запрашиваете его только для загрузки событий, а не свойств агрегата. Это разные вещи (в этом суть CQRS).
Решение 3 то, что люди обычно делают, есть много способов добиться этого.
Например, EventFlow CQRS при использовании с SQL Server создает таблицу со следующей схемой:
где:
Однако, если вы создаете с нуля, я бы порекомендовал следовать принципу YAGNI и создавать с минимальными необходимыми полями для вашего варианта использования.
источник
Возможный намек - дизайн, за которым следует «Медленно изменяющееся измерение» (type = 2), должно помочь вам охватить:
Функцию левого сворачивания также можно реализовать, но вам нужно подумать о будущей сложности запроса.
источник
Я считаю, что это будет поздний ответ, но я хотел бы указать, что использование СУБД в качестве хранилища источников событий вполне возможно, если ваши требования к пропускной способности не высоки. Я просто покажу вам примеры реестра источников событий, которые я создаю для иллюстрации.
https://github.com/andrewkkchan/client-ledger-service Вышеупомянутая веб-служба реестра источников событий. https://github.com/andrewkkchan/client-ledger-core-db И выше я использую РСУБД для вычисления состояний, чтобы вы могли пользоваться всеми преимуществами РСУБД, такими как поддержка транзакций. https://github.com/andrewkkchan/client-ledger-core-memory И у меня есть еще один потребитель, который будет обрабатывать в памяти для обработки всплесков.
Можно утверждать, что фактическое хранилище событий, указанное выше, все еще существует в Kafka - поскольку СУБД медленно вставляется, особенно когда вставка всегда добавляется.
Я надеюсь, что код поможет вам проиллюстрировать это помимо уже предоставленных очень хороших теоретических ответов на этот вопрос.
источник