dk.cloudcreate.essentials.components.eventsourced.aggregates.flex.FlexAggregateRepository Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of eventsourced-aggregates Show documentation
Show all versions of eventsourced-aggregates Show documentation
This library focuses on providing different flavors of eventsourced aggregrates
/*
* Copyright 2021-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dk.cloudcreate.essentials.components.eventsourced.aggregates.flex;
import dk.cloudcreate.essentials.components.eventsourced.aggregates.*;
import dk.cloudcreate.essentials.components.eventsourced.eventstore.postgresql.*;
import dk.cloudcreate.essentials.components.eventsourced.eventstore.postgresql.eventstream.*;
import dk.cloudcreate.essentials.components.eventsourced.eventstore.postgresql.persistence.*;
import dk.cloudcreate.essentials.components.eventsourced.eventstore.postgresql.transaction.EventStoreUnitOfWorkFactory;
import dk.cloudcreate.essentials.components.eventsourced.eventstore.postgresql.types.EventOrder;
import dk.cloudcreate.essentials.components.foundation.transaction.*;
import dk.cloudcreate.essentials.shared.reflection.Reflector;
import dk.cloudcreate.essentials.types.LongRange;
import org.slf4j.*;
import java.util.*;
import static dk.cloudcreate.essentials.shared.FailFast.requireNonNull;
/**
* Opinionated {@link FlexAggregate} Repository that's built to persist and load a specific {@link FlexAggregate} type in combination
* with {@link EventStore}, {@link EventStoreUnitOfWorkFactory} and a {@link FlexAggregateRepository}.
*
* Here's how to create an {@link FlexAggregateRepository} instance that can persist an {@link FlexAggregate}
* of type Order
which has an aggregate id of type OrderId
:
*
{@code
* FlexAggregateRepository repository =
* FlexAggregateRepository.from(
* eventStores,
* standardSingleTenantConfiguration(
* AggregateType.of("Orders"),
* new JacksonJSONEventSericreateObjectMapper(),
* AggregateIdSerializer.serializerFor(OrderId.class),
* IdentifierColumnType.UUID,
* JSONColumnType.JSONB),
* unitOfWorkFactory,
* OrderId.class,
* Order.class
* );
* }
* Here's a typical usage pattern for when you want to persist an new {@link FlexAggregate} instance
* (i.e. the {@link EventStore} doesn't contain an events related to the given Aggregate id):
* {@code
* unitOfWorkFactory.usingUnitOfWork(unitOfWork -> {
* var eventsToPersist = Order.createNewOrder(orderId, CustomerId.random(), 123);
* repository.persist(eventsToPersist);
* });
* }
* Here's the typical usage pattern for {@link FlexAggregateRepository} for already existing {@link FlexAggregate}
* instance (i.e. an instance that has events in the {@link EventStore}):
* {@code
* unitOfWorkFactory.usingUnitOfWork(unitOfWork -> {
* var order = repository.load(orderId);
* var eventsToPersist = order.accept();
* repository.persist(eventsToPersist);
* });
* }
*
*
* @param the aggregate id type
* @param the aggregate type
* @see FlexAggregate
*/
public interface FlexAggregateRepository> {
/**
* Create a {@link FlexAggregateRepository} - the {@link EventStore} will be configured with the supplied eventStreamConfiguration
.
*
* @param the aggregate ID type
* @param the concrete aggregate type (MUST be a subtype of {@link FlexAggregate})
* @param eventStore The {@link EventStore} instance to use
* @param aggregateEventStreamConfiguration the configuration for the event stream that will contain all the events related to the aggregate type
* @param unitOfWorkFactory The factory that provides {@link UnitOfWork}'s
* @param aggregateIdType the concrete aggregate ID type
* @param aggregateImplementationType the concrete aggregate type (MUST be a subtype of {@link FlexAggregate})
* @return a repository instance that can be used load, add and query aggregates of type aggregateImplementationType
*/
static > FlexAggregateRepository from(ConfigurableEventStore eventStore,
CONFIG aggregateEventStreamConfiguration,
EventStoreUnitOfWorkFactory unitOfWorkFactory,
Class aggregateIdType,
Class aggregateImplementationType) {
return new DefaultFlexAggregateRepository<>(eventStore, aggregateEventStreamConfiguration, unitOfWorkFactory, aggregateIdType, aggregateImplementationType);
}
/**
* Create a {@link FlexAggregateRepository} - if missing, the {@link EventStore} will be configured with the
* default {@link AggregateEventStreamConfiguration} based on the {@link AggregateEventStreamConfigurationFactory} that the {@link EventStore}'s {@link AggregateEventStreamPersistenceStrategy}
* is configured with
*
* @param the aggregate ID type
* @param the concrete aggregate type (MUST be a subtype of {@link FlexAggregate})
* @param eventStore The {@link EventStore} instance to use
* @param aggregateType the aggregate type being handled by this repository
* @param unitOfWorkFactory The factory that provides {@link UnitOfWork}'s
* @param aggregateIdType the concrete aggregate ID type
* @param aggregateImplementationType the concrete aggregate type (MUST be a subtype of {@link FlexAggregate})
* @return a repository instance that can be used load, add and query aggregates of type aggregateImplementationType
*/
static > FlexAggregateRepository from(ConfigurableEventStore eventStore,
AggregateType aggregateType,
EventStoreUnitOfWorkFactory unitOfWorkFactory,
Class aggregateIdType,
Class aggregateImplementationType) {
return new DefaultFlexAggregateRepository<>(eventStore, aggregateType, unitOfWorkFactory, aggregateIdType, aggregateImplementationType);
}
// -------------------------------------------------------------------------------------------------------------------------------------------------
/**
* Try to load an {@link FlexAggregate} instance with the specified aggregateId
from the underlying {@link EventStore}
* If the aggregate instance exists it will be associated with the {@link UnitOfWorkFactory#getRequiredUnitOfWork()}
* and any changes to will be tracked and will be persisted to the underlying {@link EventStore} when the {@link UnitOfWork}
* is committed.
*
* @param aggregateId the id of the aggregate we want to load
* @param expectedLatestEventOrder the expected {@link PersistedEvent#eventOrder()} of the last event stored in relation to the given aggregate instance
* @return an {@link Optional} with the matching {@link FlexAggregate} instance if it exists, otherwise it will return an {@link Optional#empty()}
* @throws OptimisticAggregateLoadException in case the {@link PersistedEvent#eventOrder()} of the last event stored in relation to the given aggregate instance
* is different from the expectedLatestEventOrder
*/
default Optional tryLoad(ID aggregateId, long expectedLatestEventOrder) {
return tryLoad(aggregateId, Optional.of(expectedLatestEventOrder));
}
/**
* Try to load an {@link FlexAggregate} instance with the specified aggregateId
from the underlying {@link EventStore}
* If the aggregate instance exists it will be associated with the {@link UnitOfWorkFactory#getRequiredUnitOfWork()}
* and any changes to will be tracked and will be persisted to the underlying {@link EventStore} when the {@link UnitOfWork}
* is committed.
*
* @param aggregateId the id of the aggregate we want to load
* @param expectedLatestEventOrder Optional with the expected {@link PersistedEvent#eventOrder()} of the last event stored in relation to the given aggregate instance (if any)
* @return an {@link Optional} with the matching {@link FlexAggregate} instance if it exists, otherwise it will return an {@link Optional#empty()}
* @throws OptimisticAggregateLoadException in case the {@link PersistedEvent#eventOrder()} of the last event stored in relation to the given aggregate instance
* is different from the expectedLatestEventOrder
*/
Optional tryLoad(ID aggregateId, Optional expectedLatestEventOrder);
/**
* Try to load an {@link FlexAggregate} instance with the specified aggregateId
from the underlying {@link EventStore}
* If the aggregate instance exists it will be associated with the {@link UnitOfWorkFactory#getRequiredUnitOfWork()}
* and any changes to will be tracked and will be persisted to the underlying {@link EventStore} when the {@link UnitOfWork}
* is committed.
*
* @param aggregateId the id of the aggregate we want to load
* @return an {@link Optional} with the matching {@link FlexAggregate} instance if it exists, otherwise it will return an {@link Optional#empty()}
* @throws OptimisticAggregateLoadException in case the {@link PersistedEvent#eventOrder()} of the last event stored in relation to the given aggregate instance
* is different from the expectedLatestEventOrder
*/
default Optional tryLoad(ID aggregateId) {
return tryLoad(aggregateId, Optional.empty());
}
/**
* Load an {@link FlexAggregate} instance with the specified aggregateId
from the underlying {@link EventStore}
* The loaded aggregate instance will be associated with the {@link UnitOfWorkFactory#getRequiredUnitOfWork()}
* and any changes to will be tracked and will be persisted to the underlying {@link EventStore} when the {@link UnitOfWork}
* is committed.
*
* @param aggregateId the id of the aggregate we want to load
* @return an {@link Optional} with the matching {@link FlexAggregate} instance
* @throws AggregateNotFoundException in case a matching {@link FlexAggregate} doesn't exist in the {@link EventStore}
*/
default AGGREGATE_TYPE load(ID aggregateId) {
return tryLoad(aggregateId).orElseThrow(() -> new AggregateNotFoundException(aggregateId, aggregateRootImplementationType(), aggregateType()));
}
/**
* The type of {@link FlexAggregate} implementation this repository handles
*/
Class aggregateRootImplementationType();
/**
* The type of {@link AggregateType} this repository is using to persist Events
*/
AggregateType aggregateType();
/**
* Load an {@link FlexAggregate} instance with the specified aggregateId
from the underlying {@link EventStore}
* The loaded aggregate instance will be associated with the {@link UnitOfWorkFactory#getRequiredUnitOfWork()}
* and any changes to will be tracked and will be persisted to the underlying {@link EventStore} when the {@link UnitOfWork}
* is committed.
*
* @param aggregateId the id of the aggregate we want to load
* @param expectedLatestEventOrder the expected {@link PersistedEvent#eventOrder()} of the last event stored in relation to the given aggregate instance
* @return an {@link Optional} with the matching {@link FlexAggregate} instance
* @throws AggregateNotFoundException in case a matching {@link FlexAggregate} doesn't exist in the {@link EventStore}
*/
default AGGREGATE_TYPE load(ID aggregateId, long expectedLatestEventOrder) {
return tryLoad(aggregateId, expectedLatestEventOrder).orElseThrow(() -> new AggregateNotFoundException(aggregateId, aggregateRootImplementationType(), aggregateType()));
}
/**
* Associate a newly created and not yet persisted {@link FlexAggregate} instance with the {@link UnitOfWorkFactory#getRequiredUnitOfWork()}
* Any changes to will be tracked and will be persisted to the underlying {@link EventStore} when the {@link UnitOfWork}
* is committed.
*
* @param eventsToPersist the events to persist to the underlying {@link EventStore} (a result of a Command method
* invocation on an {@link FlexAggregate} instance
*/
void persist(EventsToPersist eventsToPersist);
// TODO: Add loadReadOnly (that doesn't require a UnitOfWork), getAllAggregateId's, loadByIds, loadAll, query, etc.
/**
* The type of aggregate ID this repository uses
*/
Class aggregateIdType();
// -------------------------------------------------------------------------------------------------------------------------------------------------
class DefaultFlexAggregateRepository> implements FlexAggregateRepository {
private static final Logger log = LoggerFactory.getLogger(FlexAggregateRepository.class);
private final EventStore eventStore;
private final Class aggregateRootImplementationType;
private final Class aggregateIdType;
private final EventStoreUnitOfWorkFactory unitOfWorkFactory;
private final FlexAggregateRepositoryUnitOfWorkLifecycleCallback unitOfWorkCallback;
private final AggregateType aggregateType;
/**
* Create an {@link FlexAggregateRepository} - the {@link EventStore} will be configured with the supplied eventStreamConfiguration
.
*
* @param eventStore the event store that can load and persist events related to the aggregateType
* @param aggregateEventStreamConfiguration the configuration for the event stream that will contain all the events related to the aggregate type
* @param unitOfWorkFactory The factory that provides {@link UnitOfWork}'s
* @param aggregateIdType the concrete aggregate ID type
* @param aggregateRootImplementationType the concrete aggregate type (MUST be a subtype of {@link FlexAggregate})
*/
private DefaultFlexAggregateRepository(ConfigurableEventStore eventStore,
CONFIG aggregateEventStreamConfiguration,
EventStoreUnitOfWorkFactory unitOfWorkFactory,
Class aggregateIdType,
Class aggregateRootImplementationType) {
this.eventStore = requireNonNull(eventStore, "You must supply an EventStore instance");
this.aggregateType = requireNonNull(aggregateEventStreamConfiguration, "You must supply an aggregateType").aggregateType;
this.unitOfWorkFactory = requireNonNull(unitOfWorkFactory, "You must supply a UnitOfWorkFactory instance");
this.aggregateRootImplementationType = requireNonNull(aggregateRootImplementationType, "You must supply an aggregateImplementationType");
this.aggregateIdType = requireNonNull(aggregateIdType, "You must supply an aggregateIdType");
this.unitOfWorkCallback = new FlexAggregateRepositoryUnitOfWorkLifecycleCallback();
eventStore.addAggregateEventStreamConfiguration(aggregateEventStreamConfiguration);
}
/**
* Create an {@link FlexAggregateRepository} - if missing, the {@link EventStore} will be configured with the
* default {@link AggregateEventStreamConfiguration} based on the {@link AggregateEventStreamConfigurationFactory} that the {@link EventStore}'s {@link AggregateEventStreamPersistenceStrategy}
* is configured with
*
* @param eventStore the event store that can load and persist events related to the aggregateType
* @param aggregateType the aggregate type being handled by this repository - the {@link EventStore} must contain a corresponding {@link AggregateEventStreamConfiguration}
* @param unitOfWorkFactory The factory that provides {@link UnitOfWork}'s
* @param aggregateIdType the concrete aggregate ID type
* @param aggregateRootImplementationType the concrete aggregate type (MUST be a subtype of {@link FlexAggregate})
*/
private DefaultFlexAggregateRepository(ConfigurableEventStore eventStore,
AggregateType aggregateType,
EventStoreUnitOfWorkFactory unitOfWorkFactory,
Class aggregateIdType,
Class aggregateRootImplementationType) {
this.eventStore = requireNonNull(eventStore, "You must supply an EventStore instance");
this.aggregateType = requireNonNull(aggregateType, "You must supply an aggregateType");
this.unitOfWorkFactory = requireNonNull(unitOfWorkFactory, "You must supply a UnitOfWorkFactory instance");
this.aggregateRootImplementationType = requireNonNull(aggregateRootImplementationType, "You must supply an aggregateImplementationType");
this.aggregateIdType = requireNonNull(aggregateIdType, "You must supply an aggregateIdType");
this.unitOfWorkCallback = new FlexAggregateRepositoryUnitOfWorkLifecycleCallback();
if (eventStore.findAggregateEventStreamConfiguration(aggregateType).isEmpty()) {
eventStore.addAggregateEventStreamConfiguration(aggregateType,
aggregateIdType);
}
}
/**
* The {@link EventStore} that's being used for support
*/
protected EventStore eventStore() {
return eventStore;
}
@Override
public AggregateType aggregateType() {
return aggregateType;
}
@Override
public String toString() {
return "FlexAggregateRepository{" +
"aggregateType=" + aggregateType() +
", aggregateIdType=" + aggregateIdType() +
", aggregateImplementationType=" + aggregateRootImplementationType().getName() +
'}';
}
@Override
public Optional tryLoad(ID aggregateId, Optional expectedLatestEventOrder) {
log.trace("Trying to load {} with id '{}' and expectedLatestEventOrder {}", aggregateRootImplementationType.getName(), aggregateId, expectedLatestEventOrder);
UnitOfWork unitOfWork = unitOfWorkFactory.getRequiredUnitOfWork();
var potentialPersistedEventStream = eventStore.fetchStream(aggregateType,
aggregateId,
LongRange.from(EventOrder.FIRST_EVENT_ORDER.longValue())); // TODO: Support for looking up a snapshot version of the aggregate, where we only need to load events not included in the snapshot
if (potentialPersistedEventStream.isEmpty()) {
log.trace("Didn't find a {} with id '{}'", aggregateRootImplementationType.getName(), aggregateId);
return Optional.empty();
} else {
var persistedEventsStream = potentialPersistedEventStream.get();
if (expectedLatestEventOrder.isPresent()) {
PersistedEvent lastEventPersisted = persistedEventsStream.eventList().get(persistedEventsStream.eventList().size() - 1);
if (lastEventPersisted.eventOrder().longValue() != expectedLatestEventOrder.get()) {
log.trace("Found {} with id '{}' but expectedLatestEventOrder {} != actualLatestEventOrder {}",
aggregateRootImplementationType.getName(),
aggregateId,
expectedLatestEventOrder.get(),
lastEventPersisted.eventOrder());
throw new OptimisticAggregateLoadException(aggregateId,
aggregateRootImplementationType,
expectedLatestEventOrder.map(EventOrder::of).get(),
lastEventPersisted.eventOrder());
}
}
log.debug("Found {} with id '{}' and expectedLatestEventOrder {}", aggregateRootImplementationType.getName(), aggregateId, expectedLatestEventOrder);
AGGREGATE_TYPE aggregate = Reflector.reflectOn(aggregateRootImplementationType).newInstance();
return Optional.of(aggregate.rehydrate(persistedEventsStream));
}
}
@Override
public Class aggregateRootImplementationType() {
return aggregateRootImplementationType;
}
@Override
public void persist(EventsToPersist eventsToPersist) {
log.debug("Adding {} with id '{}' to the current UnitOfWork so it will be persisted at commit time",
aggregateRootImplementationType.getName(),
eventsToPersist.aggregateId);
unitOfWorkFactory.getRequiredUnitOfWork()
.registerLifecycleCallbackForResource(eventsToPersist, unitOfWorkCallback);
}
@Override
public Class aggregateIdType() {
return aggregateIdType;
}
/**
* The {@link UnitOfWorkLifecycleCallback} that's responsible for persisting {@link EventsToPersist} that were a side effect of command methods invoked on
* {@link FlexAggregate} instances during the {@link UnitOfWork} - see {@link FlexAggregateRepository#persist(EventsToPersist)}
*/
private class FlexAggregateRepositoryUnitOfWorkLifecycleCallback implements UnitOfWorkLifecycleCallback> {
@Override
public void beforeCommit(UnitOfWork unitOfWork, List> associatedResources) {
log.trace("beforeCommit processing {} '{}' registered with the UnitOfWork being committed", associatedResources.size(), aggregateRootImplementationType.getName());
associatedResources.forEach(eventsToPersist -> {
log.trace("beforeCommit processing '{}' with id '{}'", aggregateRootImplementationType.getName(), eventsToPersist.aggregateId);
if (eventsToPersist.events.isEmpty()) {
log.trace("No changes detected for '{}' with id '{}'", aggregateRootImplementationType.getName(), eventsToPersist.aggregateId);
} else {
if (log.isTraceEnabled()) {
log.trace("Persisting {} event(s) related to '{}' with id '{}': {}", eventsToPersist.events.size(), aggregateRootImplementationType.getName(), eventsToPersist.aggregateId, eventsToPersist.events.stream().map(persistableEvent -> persistableEvent.getClass().getName()).reduce((s, s2) -> s + ", " + s2));
} else {
log.debug("Persisting {} event(s) related to '{}' with id '{}'", eventsToPersist.events.size(), aggregateRootImplementationType.getName(), eventsToPersist.aggregateId);
}
eventStore.appendToStream(aggregateType,
eventsToPersist.aggregateId,
eventsToPersist.eventOrderOfLastRehydratedEvent,
eventsToPersist.events);
eventsToPersist.markEventsAsCommitted();
}
});
}
@Override
public void afterCommit(UnitOfWork unitOfWork, List> associatedResources) {
}
@Override
public void beforeRollback(UnitOfWork unitOfWork, java.util.List> associatedResources, Exception causeOfTheRollback) {
}
@Override
public void afterRollback(UnitOfWork unitOfWork, java.util.List> associatedResources, Exception causeOfTheRollback) {
}
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy