CQRS Read Model Design when Event Sourcing with a

2019-04-22 04:56发布

问题:

I'm in the process of writing my first CQRS application, let's say my system dispatches the following commands:

  • CreateContingent (Id, Name)
  • CreateTeam (Id, Name)
  • AssignTeamToContingent (TeamId, ContingentId)
  • CreateParticipant (Id, Name)
  • AssignParticipantToTeam (ParticipantId, TeamId)

Currently, these result in identical events, just worded in the past tense (ContingentCreated, TeamCreated, etc) but they contain the same properties. (I'm not so sure that is correct and is one of my questions)

My issue lies with the read models.

I have a Contingents read model, that subscribes to ContingentCreated, and has the following methods:

    List<ContingentQueries.Contingent> GetContingents();
    ContingentQueries.Contingent GetContingent(System.Guid id);
    ContingentQueries.Contingent GetContingent(string province);

The Contingent class returned by these looks like this:

    public class Contingent
    {
        public Guid Id { get; internal set; }
        public string Province { get; internal set; }
    }

This works fine for a listing, but when I start thinking about the read model for a view of a single contingent, and all it's teams, things start to get messy. Given the methods it seems trivial:

    ContingentQueries.Contingent GetContingent(System.Guid id);
    ContingentQueries.Contingent GetContingent(string province);

And I create the Contingent class for these queries with a collection of teams:

    public class Contingent
    {
        public Guid Id { get; internal set; }
        public string Province { get; internal set; }
        public IList<Team> Teams { get; internal set; }
    }

    public class Team
    {
        public Guid Id { get; internal set; }
        public string Name { get; internal set; }
    }

In theory, I should only need to subscribe to ContingentCreated, and TeamAssignedToContingent, but the TeamAssignedToContingent event only has the team and contingent ids... so I can't set the Team.Name property of this read model.

Do I also subscribe to TeamCreated? Add keep another copy of teams for use here?

Or when events are raised, should they have more information in them? Should the AddTeamToContingent command handler be querying for team information to add to the event? Which may not yet exist, because the read model might not have been updated with the team yet.

What if I wanted to show the team name and the participant that has been assigned to the team, and named the captain in this Contingent view? Do I also store the participants in the read model? This seems like a ton of duplication.

For some extra context. Participants are not necessaily part of any contingent, or a team, they could just be guests; or delegates and/or spares of a contingent. However, roles can shift. A delegate who is not a team member, could also be a spare, and due to injury be assigned to a team, however they are still a delegate. Which is why the generic "Participant" is used.

I understand I want read models to not be heavier than necessary, but I'm having difficulty with the fact that I may need data from events that I'm not subscribed to (TeamCreated), and in theory shouldn't be, but because of a future event (TeamAssignedToContingent) I do need that info, what do I do?

UPDATE: Thought about it overnight, and it seems like all the options I have thought of are bad. There must be another.

Option 1: Add more data to raised events

  • Do Command Handlers start using read models that may not yet be written so they can get that data?
  • What if a future subscriber to the event needs even more info? I can't add it!

Option 2: Have Event Handlers subscribe to more events?

  • This results in the read model storing data it might not care about?
  • Example: Storing participants in the ContingentView read model so when a person is assigned to a team, and marked as captain, we have know their name.

Option 3: Have Event Handlers query other Read Models?

  • This feels like the best approach, but feels wrong. Should the Contingent view query the Participant view to get the name if and when it needs it? It does eliminate the drabacks in 1 and 2.

Options 4: ...?

回答1:

What you're probably going to end up with is a combination of 1 and 2. There is absolutely no problem with enhancing or enriching events to have more information in them. It is actually quite helpful.

  • What if a future subscriber to the event needs even more info? I can't add it!

You can add it to the events going forward. If you need historical data you'll have to find it somewhere else. You can't build events with all the data in the world on them just on the off chance that you'll need it in the future. It is even allowable to go back and add more data to your historical events. Some people would frown on this as rewriting history but you're not, you're simply enhancing history. As long as you don't change the existing data you should be fine.

I suspect that your event handlers are also going to need to subscribe to more events as time continues. For instance are teams going to need to be renamed? If so then you'll need to handle that event. Don't be afraid to have multiple copies of the same data in different read views. Coming from a relational database background this duplication of data is one of the hardest things to get use to.

In the end be pragmatic. If you're encountering pain in applying CQRS then change how you're applying it.



回答2:

Another option that comes to my mind is:

In your read side, you first build normalized views by events from the write side.

E.g.:

contingents table:
-----------
id, name

teams table:
-----------
id, name

contigents_teams table:
-----------
contigent_id, team_id

On ContingentCreated event, you just insert the record into the contingents table.

On TeamCreated event, you just insert the record into the teams table.

On TeamAssignToContingent event, you insert the record into the contigents_teams table.

On TeamNameChanged event, you update appropriate record in the teams table.

And so on.

Thus, your data is (eventually) consistent and in sync with the write side.

Yes, you need to use joins in the read side to fetch data...

If this normalized view does not satisfy read performance needs, then you can build a denormalized view from this normalized data. it requires an extra step to build the denormalized view, but to me, it is the most simple solution I can think of for now.

Maybe you even will not need a denormalized view.

After all (in my opinion), the main value of ES is capturing user intent, confidence in your data (no destructive operations, single source of truth), ability to answer questions that nobody could think of in the past, big value for analytics.

As I said, if you need to optimize performance, you always can build a denormalized view from the normalized view of the read side.