CQRS без DDD и без (или с?) ES - что такое модель записи и что такое модель чтения?

11

Насколько я понимаю, основная идея CQRS - это две разные модели данных для обработки команд и запросов. Они называются «модель записи» и «модель чтения».

Давайте рассмотрим пример клонирования приложения Twitter. Вот команды:

  • Пользователи могут зарегистрироваться самостоятельно. CreateUserCommand(string username)излучаетUserCreatedEvent
  • Пользователи могут подписаться на других пользователей. FollowUserCommand(int userAId, int userBId)излучаетUserFollowedEvent
  • Пользователи могут создавать сообщения. CreatePostCommand(int userId, string text)излучаетPostCreatedEvent

Хотя я использую термин «событие» выше, я не имею в виду события «источник событий». Я просто имею в виду сигналы, которые запускают обновления модели чтения. У меня нет магазина событий, и я пока хочу сконцентрироваться на самом CQRS.

И вот вопросы:

  • Пользователь должен видеть список своих сообщений. GetPostsQuery(int userId)
  • Пользователь должен видеть список своих подписчиков. GetFollowersQuery(int userId)
  • Пользователь должен видеть список пользователей, за которым он следует. GetFollowedUsersQuery(int userId)
  • Пользователь должен увидеть «ленту друзей» - журнал всех действий своих друзей («ваш друг Джон только что создал новую запись»). GetFriedFeedRecordsQuery(int userId)

Чтобы справиться, CreateUserCommandмне нужно знать, существует ли такой пользователь. Итак, на данный момент я знаю, что моя модель записи должна иметь список всех пользователей.

Чтобы справиться, FollowUserCommandмне нужно знать, следует ли userA уже за userB или нет. На данный момент я хочу, чтобы моя модель записи имела список всех подключений пользователя-пользователя.

И, наконец, для обработки CreatePostCommandя не думаю, что мне нужно что-то еще, потому что у меня нет таких команд, как UpdatePostCommand. Если бы они были у меня, мне нужно было бы убедиться, что этот пост существует, поэтому мне нужен список всех постов. Но поскольку у меня нет этого требования, мне не нужно отслеживать все сообщения.

Вопрос № 1 : действительно ли правильно использовать термин «модель записи» так, как я его использую? Или «модель записи» всегда означает «хранилище событий» в случае ES? Если да, есть ли какое-либо разделение между данными, которые мне нужны для обработки команд, и данными, которые мне нужны для обработки запросов?

Чтобы справиться GetPostsQuery, мне нужен список всех сообщений. Это означает, что моя модель чтения должна иметь список всех сообщений. Я собираюсь поддержать эту модель, слушая PostCreatedEvent.

Чтобы справиться с обоими GetFollowersQueryи GetFollowedUsersQueryмне, мне нужен список всех соединений между пользователями. Для поддержания этой модели я буду слушать UserFollowedEvent. Вот вопрос № 2 : это нормально, если я использую здесь список соединений модели записи? Или мне лучше создать отдельную модель чтения, потому что в будущем мне может понадобиться больше деталей, чем в модели записи?

Наконец, для обработки GetFriendFeedRecordsQueryмне понадобится:

  • Слушать UserFollowedEvent
  • Слушать PostCreatedEvent
  • Знайте, какие пользователи следуют за какими другими пользователями

Если пользователь A следует за пользователем B, а пользователь B начинает следовать за пользователем C, должны появиться следующие записи:

  • Для пользователя A: «Ваш друг пользователь B только что подписался на пользователя C»
  • Для пользователя B: «Вы только что подписались на пользователя C»
  • Для пользователя C: «Пользователь B теперь следует за вами»

Вот вопрос № 3 : какую модель я должен использовать, чтобы получить список соединений? Должен ли я использовать модель записи? Должен ли я использовать модель чтения - GetFollowersQuery/ GetFollowedUsersQuery? Или я должен заставить GetFriendFeedRecordsQueryсаму модель обрабатывать UserFollowedEventи поддерживать свой собственный список всех соединений?

Андрей Агибалов
источник
Помните, что в системах CQRS модель данных Query и модель данных Command могут потреблять данные из разных баз данных. И обе модели могут жить независимо друг от друга (разные приложения). При этом ответ «зависит» (как обычно). Это может заинтересовать
Laiv

Ответы:

7

Грег Янг (2010)

CQRS - это просто создание двух объектов, где ранее был только один объект.

Если вы думаете с точки зрения разделения командных запросов Бертрана Мейера , вы можете представить модель с двумя различными интерфейсами: один, поддерживающий команды, и другой, поддерживающий запросы.

interface IChangeTheModel {
    void createUser(string username)
    void followUser(int userAId, int userBId)
    void createPost(int userId, string text)
}

interface IDontChangeTheModel {
    Iterable<Post> getPosts(int userId)
    Iterable<Follower> getFollowers(int userId)
    Iterable<FollowedUser> getFollowedUsers(int userId)
}

class TheModel implements IChangeTheModel, IDontChangeTheModel {
    // ...
}

Идея Грега Янга заключалась в том, что вы можете разделить это на два отдельных объекта

class WriteModel implements IChangeTheModel { ... }
class ReadModel  implements IDontChangeTheModel {...}

Разделив объекты, теперь у вас есть возможность отделить структуры данных, которые хранят состояние объекта в памяти, так что вы можете оптимизировать для каждого случая; или сохранить / сохранить состояние чтения отдельно от состояния записи.

Вопрос № 1: действительно ли правильно использовать термин «модель записи» так, как я его использую? Или «модель записи» всегда означает «хранилище событий» в случае ES? Если да, есть ли какое-либо разделение между данными, которые мне нужны для обработки команд, и данными, которые мне нужны для обработки запросов?

Под термином WriteModel обычно понимается изменчивое представление модели (т. Е. Объект, а не хранилище данных).

TL; DR: я думаю, что вы используете хорошо.

Вот вопрос № 2: это нормально, если я использую здесь список соединений модели записи?

Это "хорошо" - иш. Концептуально, нет ничего плохого в том, что модель чтения и модель записи имеют одинаковые структуры.

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

Вот вопрос № 3: какую модель я должен использовать, чтобы получить список соединений? Должен ли я использовать модель записи? Должен ли я использовать модель чтения - GetFollowersQuery / GetFollowedUsersQuery? Или я должен заставить модель GetFriendFeedRecordsQuery самостоятельно обрабатывать UserFollowedEvent и поддерживать свой собственный список всех соединений?

Использование модели записи - неправильный ответ.

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

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

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

VoiceOfUnreason
источник
1

Я бы посоветовал вам учитывать, что у вас есть одна концептуальная модель данных.

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

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


(# 1) Для CQRS модель записи не обязательно должна быть хранилищем событий.

(# 2) Я не ожидал бы, что модель чтения будет хранить что-либо, что не входит в модель записи, например, потому что в CQRS никто не обновляет модель чтения, кроме механизма пересылки, который поддерживает синхронизацию модели чтения с изменениями в напиши модель.

(# 3) В CQRS запросы должны идти против модели чтения. Вы можете сделать по-другому: все в порядке, просто не следуя CQRS.


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

Эрик Эйдт
источник