понедельник, 9 ноября 2009 г.

CQS – конец монополии систем реляционных баз данных

Продолжая раскапывать возможности архитектуры, основанной на принципе разделения команд и запросов, попробуем порассуждать, как все это вяжется с проектированием базы данных.

Краткое содержание предыдущих серий

В общем случае, используя CQS где-то на уровне кода, в 80% случаев мы придем примерно к такой схеме:

CQS в первом приближении

Данные из базы данных запрашиваются для отображения, а на основе увиденного пользователем генерируется поток команд, который в конце концов приводит к изменению исходных данных в БД.

Но если пойти дальше и применить CQS даже к базам данных, то мы получим две различных структуры для хранения данных: Reporting Database и Domain Database (да, здесь нет никакой ссылки :)). В сочетании с Domain Events картинка выше преобразится следующим образом:

CQS и Domain Events

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

Итак, что мы получаем в итоге:

  1. Существует некая база данных, структура которой максимально адаптирована под конкретные задачи отображения данных;
  2. База для хранения данных домена, которая не видна пользователю, со структурой, непосредственно заточенной под хранение всех этих entities и value objects.

Вместо базы данных — репозиторий

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

Чаще всего любые манипуляции над доменом начинаются с того, что мы либо создаем новые экземпляры aggregate roots, либо, «достав» из репозитория нужный экземпляр, производим над ним какие-то манипуляции.

Таким образом типичный репозиторий будет выглядеть так:

interface IOrderRepository
{
   void Add(Order newOrder);
   Order Get(OrderId orderId);
}

И в некоторых особых случаях репозиторий может поддерживать удаление или какие-то другие специальные методы. Все. Ничего лишнего.

А теперь фокусы

Подводим итоги из всего вышесказанного и пробуем сделать выводы:

  1. Domain Database предназначена исключительно для доступа к ней инфраструктуры репозиториев.

    А значит никаких сложных sql-запросов и нестандартных структур выходных данных. Все запросы строго стандартны, все выходные структуры данных определены структурой типов самого домена. Более того, структура хранилища подразумевает наличие всего нескольких ключевых сущностей или же наборов сущностей с непересекающимися структурами, с которыми работают, как с едиными целыми. А значит нам ничего не мешает провести «денормализацию» такого рода:

    Денормализация

    Где OrderSnapshot — это просто сериализованное состояние нашего aggregate root со всей его иерархией. Таким образом все наши сложные иерархии и множества объектов превращаются в самую обычную хэш-таблицу, не оставляя и следа от реляционности.

  2. Стандартными операциями в репозитории являются Add и Get.

    Если рассматривать Aggregate Root как атом, с которым нам приходится работать, используя лишь методы add/get, то для этих нам совершенно не обязательно пользоваться именно реляционными базами данных. Мы можем обратить свой взор в сторону документо-ориентированных, или в сторону объектных баз данных.

  3. Для отчетов используются оптимизированные для этих целей структуры

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

В итоге

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

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

Но как быть, когда частные случаи являют собой достаточно сложную материю, с чем мы сталкиваемся ежедневно, пытаясь смоделировать сложную предметную область?

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

Применение принципе CQS в общем случае и помогает нам перейти от поисков генериков к решению вполне себе частных задач частными средствами. И, как мы видим, принцип этот так же удачно применим и к дизайну баз данных.

Ссылки

  1. Unshackle Your Domain
  2. Eager Read Derivation
  3. CQRS à la Greg Young example code
  4. Domain-Driven Design Group
Опубликовать

13 комментариев:

  1. Мне совсем не нравится схема с сепарированными "Reports Data" и "Domain Data". Почему? Потому что, не существует способа позволяющего получить 100% гарантию еквивалентности данных хранящихся в этих двух хранилищах. А для бизнес приложения нет хуже ошибки, чем несоответствие данных в отчетах реальному положению дел. Такая ошибка сразу и навсегда убивает доверие пользователей к системе.
    Да, разделение БД на OLAP и OLTP имеет место, но обычно по причине невозможности решить OLAP задачи на OLTP базе. Но рекомендовать такой подход в качестве шаблонного и всеобщего я бы не стал.

    ОтветитьУдалить
  2. Я как раз постарался избежать вывода какого-то шаблонного решения, показав лишь, что RDBMS могут быть не единственным способом хранения данных при таком раскладе в дизайне системы.

    А проблема эквивалентности дублирующихся данных всегда появляется в тот самый момент, когда в табличке Order появляется колонка Total :)

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

    И не был бы столь категоричен в неприятии такого разделения. Потому на другой чаше весов с актуальностью данных находится высокая степень доступности системы (та, которая high availability). И применимость того или иного решения зависит исключительно специфики домена и от важности этих показателей для бизнеса.

    ОтветитьУдалить
  3. На самом то деле, если уже бояться проблемы эквивалентности/актуальности данных, то надо отключить кеши всех уровней, сделать запрос на получение данных с подтверждением. Фактически базаданных репортов может расматриваться как кеш. Кеш с возможностью принудительной актуализацией данных. Собсно я так понимаю именно для этого и рождаються паттерны на подобие Event Sourcing.

    ОтветитьУдалить
  4. На самом деле, схема репликации Master(RW) - Subscriber(RO) (для снижения нагрузки) организует что-то подобное, только там по прагматическим причинам используются RDB и для того звена, что у вас названо ReportingDB.
    Подход в данном случае интересный, но спорный.

    ОтветитьУдалить
  5. Игорь, а как ваша архитектура будет обслуживать такие требования бизнеса, которые строятся на отчетной информации.
    Наприме: Клиент автоматически получает скидку, равную 3% от средней стоимости заказов за последний год(месяц, квартал, эру...).

    ОтветитьУдалить
  6. 2Meowth:

    А никто и не говорит об абсолютной новизне подхода. Тут все дело лишь в точке приложения этого подхода: если приложение сильно data-centric, то все будет делаться на основе Master/Subscriber. Если же приложение behaviour-centric, то это будет называться CQS.

    Нужно отметить, что схема Master/Subscriber вполне себе может сочетаться и с CQS на уровне кода. Оно все и так сильно оторвано друг от друга. Никаких догм здесь нет - во всем следует быть прагматиком :)

    ОтветитьУдалить
  7. To C...R...a...S...H

    Тут следует отличать действительно отчетные данные, которые никак не влияют на логику домена, и данные, которые домену нужны.

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

    А вообще, вы затронули интересную тему, о которой я умышлено не написал.

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

    А как быть, если, скажем на любом из уровней, добавляются новые типы "аккумуляторов", которые должны работать так, как если бы они были в системе последние лет 10...

    Вот тут есть о чем поговорить :)

    ОтветитьУдалить
  8. Игорь, можете поподробнее описать QuarterDiscount.

    ОтветитьУдалить
  9. например, так. хотя, все зависит от случая.

    class QuarterDiscount : Entity
    {
    Percentage Percentage { get; set; }

    public UpdatePercentage(Percentage newValue)
    {
    Percentage = newValue;
    }

    public Amount Apply(Amount amount)
    {
    return amount * percentage;
    }
    }

    ОтветитьУдалить
  10. Пораскинув мозгами, и попробовав наструячить пару тестовых примеров по вашему предложению, мне стало уже 100% понятно, что кроме геморроя предложенный вами(или не вами) способ, больше ничего не приносит.
    То есть если мы даже хоть какой, то мнимый плюс при работе с моделью. То этот плюс превращается в огромный минус, когда дело касается реализации данных методов.
    И в целом мне непонятна ситуация, зачем такой огород городить. Например, выделение OLAP служит для увеличения скорости построения отчетов, и основной костыль с которым все мирятся – что OLAP база может содержать устаревшую информацию.
    Но в вашем случае вы делает 2 базы и еще Notify между ними, что бы их держать в одинаковом состоянии, но зачем это надо, когда можно используя инструменты базы данных, такие как View и StoreProc реализовать точно такую, же систему, то есть у вас структура базы одна, но для приложения она совершенно другая.

    ОтветитьУдалить
  11. К какому именно случаю вы попытались все это применить? Возможно, проблема была именно в не совсем подходящем примере.

    Еще, возможно вы найдете ответы на некоторые вопросы здесь. Ну и пройдитесь еще раз по указанным ссылкам.

    ОтветитьУдалить
  12. Вот еще встретилось и может быть полезно http://myshop.codeplex.com/

    Семпл по мотивам CQS

    ОтветитьУдалить
  13. Прочитал с огромным интересом почти весь блог, а в этой серии, ещё не дочитав первый пост, понял, что в эту архитектуру идеально вписывается CoushDB. Она хранит все данные в общей куче (одна база данных - одно B-дерево) в виде (по крайней мере внешне, со стороны потребителя) пар "идентификатор"->"JSON сериализация объекта, включая вложенные (списки и т. п., в общем дерево целиком, а не только его корень)", при этом эту сериализацию она понимает и позволяет искать по значению свойств. Основной механизм получения данных - или "GetById", или так называемые View (JavaScript функции с активным использованием map/reduce функций, индексы для которых обновляются при каждой операции). "Бесплатные плюшки" - встроенная по дефолту версионность, репликации, вертикальная масштабируемость, простой REST интерфейс. Кого заинтересовалo CQS - настоятельно рекомендую попробовать CouchDB.

    ОтветитьУдалить