serverlesstechnology / cqrs

A lightweight, opinionated CQRS and event sourcing framework.
Other
346 stars 36 forks source link

How to check for aggregate existence? #52

Closed sancho20021 closed 1 year ago

sancho20021 commented 1 year ago

If I understand correctly, implementations of Store are intended to return Default::default() in case the aggregate is not found. Sometimes in handling commands (say, extending someones fitness club card, where card is an aggregate) you have to check for the entity existence.

Workaround: make the aggregate an enum with two variants to simulate Option.

The case where this is needed seems very common, so I would suggest to modify the framework to satisfy these needs. But I have almost no experience in ES/CQRS, so I may be missing something.

davegarred commented 1 year ago

Hi @sancho20021, for any feedback you'll want to use a query, this is a very simple one to configure. The most basic setup would be to use a GenericQuery to serialize a view with this list.

struct ExistingAggregatesView {
  aggregate_ids: Vec<String>
}

impl View<MyAggregate> for ExistingAggregatesView {
    fn update(&mut self, event: &EventEnvelope<MyAggregate>) {
        self.aggregate_ids.push(event.aggregate_id);
    }
}

This obviously only works with small number of aggregates. For a production system you'd probably want to use a database table with a single aggregate_id column and create a custom query.

struct ExistingAggregatesQuery {
    database_client: DatabaseClient,
}
#[async_trait]
impl Query<MyAggregate> for ExistingAggregatesQuery {
    async fn dispatch(&self, aggregate_id: &str, events: &[EventEnvelope<MyAggregate>]) {
        for event in events {
            self.database_client.execute("INSERT INTO aggregate_id_table VALUES (?)", event.aggregate_id);
        }
    }
}
sancho20021 commented 1 year ago

Thank you. Will I be able to access these queries /views inside the handle of a command?

davegarred commented 1 year ago

Accessing the view from within the aggregate is accomplished in a similar fashion by configuring your aggregate's Services associated type.

For the latter example this might look like:

struct MyServices {
  db_client: Box<dyn AbstractDatabaseClient>,
}

Note the database client should be a boxed dynamic type since you'll need to swap it out with a mock client in your tests.

This client is then available within the handle method of your aggregate.

Take a look at our services example in the demo application for a complete example (client test implementation is here).

sancho20021 commented 1 year ago

Hi @sancho20021, for any feedback you'll want to use a query, this is a very simple one to configure. The most basic setup would be to use a GenericQuery to serialize a view with this list.

struct ExistingAggregatesView {
  aggregate_ids: Vec<String>
}

impl View<MyAggregate> for ExistingAggregatesView {
    fn update(&mut self, event: &EventEnvelope<MyAggregate>) {
        self.aggregate_ids.push(event.aggregate_id);
    }
}

Okay, but how can I create a Query which will use this View in memory, without any ViewRepository?

https://doc.rust-cqrs.org/application_persisted_views.html in the tutorial they wrap similar view in a PostgresViewRepository, but I would like to have an in-memory query which I would use in handling requests for my server.

davegarred commented 1 year ago

Okay, but how can I create a Query which will use this View in memory, without any ViewRepository?

https://doc.rust-cqrs.org/application_persisted_views.html in the tutorial they wrap similar view in a PostgresViewRepository, but I would like to have an in-memory query which I would use in handling requests for my server.

See the SimpleLoggingQuery example in our demo application. Something similar can be used to accept new events and persist information in memory.