serverlesstechnology / cqrs

A lightweight, opinionated CQRS and event sourcing framework.
Other
370 stars 40 forks source link

Advised way to extend view retrieval functionality? #70

Closed liamwh closed 8 months ago

liamwh commented 1 year ago

My understanding is that the only way to use the load functionality with a View Repository is by supplying the view ID, which must be used because of the necessity to implement the ViewRepository trait (below) required by the GenericQuery struct.

#[async_trait]
pub trait ViewRepository<V, A>: Send + Sync
where
    V: View<A>,
    A: Aggregate,
{
    /// Returns the current view instance.
    async fn load(&self, view_id: &str) -> Result<Option<V>, PersistenceError>;

    /// Returns the current view instance and context, used by the `GenericQuery` to update
    /// views with committed events.
    async fn load_with_context(
        &self,
        view_id: &str,
    ) -> Result<Option<(V, ViewContext)>, PersistenceError>;

    /// Updates the view instance and context, used by the `GenericQuery` to update
    /// views with committed events.
    async fn update_view(&self, view: V, context: ViewContext) -> Result<(), PersistenceError>;
}

But what if the view_id is not known upfront? For example, if I have a table user_query, where across all rows, the email field in the payload must be unique, how should I check if user_query table contains a row where the payload's email value does not match a provided input?

Am I meant to create a user service and add such methods to that service?

davegarred commented 1 year ago

In this instance a custom query should be used to lookup the view_id (in this example, the email) in another query and then use that value to make the appropriate view change. This could be composed of a ViewRepository to do the lookup and a GenericQuery to make the change.

Example:

struct MyCompoundQuery {
  email_lookup: EmailViewLookup,
  base_query: GenericQuery<...>
}

#[async_trait]
impl Query<BankAccount> for MyCompoundQuery {
    async fn dispatch(&self, aggregate_id: &str, events: &[EventEnvelope<BankAccount>]) {
        let email_id = self.email_lookup.load(aggregate_id).await.unwrap().email_id;
        self.base_query.dispatch(email_id, events).await;
    }
}

Note: I've updated the name of the EmailViewLookup component due to my previous choice being confusing.

liamwh commented 1 year ago

Thank you for helping. I'm sorry for my ignorance here. It is still not entirely clear to me.

Here is my user struct:

pub struct User {
    pub id: Uuid, (view_id)
    pub email: String,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    pub token_salt: Uuid,
}

So I we should agree that we don't necessarily want another aggregate for an email right? I would expect a user to be the aggregate in this case.

If we say that the user is the aggregate, I should have a User Repository, and a User View, right? The problem is, the User View only allows looking up by the id (uuid).

In your suggestion, are you suggesting I make another aggregate for the email so I can have an EmailViewRepository? Or can you please help me understand how the compound query helps me here?

Thanks very much in advance, I'm very grateful for you help 🙏

davegarred commented 1 year ago

Thank you for the clarification, I think I see part of the issue.

The GenericQuery is a little misleading because it's sort of set up as a single source lookup. This is meant to simplify the initial build-out of a service. In real systems looking up the correct view may be done from a variety of sources.

One solution is to extend the GenericQuery and add additional lookup methods rather than just using the UserId (in this case, via an email).

The solution I recommended is adding a EmailViewLookup that contains only two fields, UserId and Email. The backing database should set the email field as an index. This can be updated using a GenericQuery but you'll need build a custom tool to retrieve the UserId based on that secondary index.

This could be extended to use any number of fields that you might need to use to lookup the ViewId in question.

Generally a modified GenericQuery is faster as it reduces the database calls needed to update each view. The latter option is simpler (so a bit easier to maintain) and, if extended, may be used as a lookup for multiple different views with widely differing view ids (just make sure it gets updated from all aggregate sources).

Hope that helps, let me know if I'm still not getting to the root of your problem.