All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dk.cloudcreate.essentials.components.eventsourced.aggregates.snapshot.PostgresqlAggregateSnapshotRepository Maven / Gradle / Ivy

The newest version!
/*
 * 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.snapshot;

import dk.cloudcreate.essentials.components.eventsourced.eventstore.postgresql.ConfigurableEventStore;
import dk.cloudcreate.essentials.components.eventsourced.eventstore.postgresql.eventstream.*;
import dk.cloudcreate.essentials.components.eventsourced.eventstore.postgresql.persistence.AggregateEventStreamConfiguration;
import dk.cloudcreate.essentials.components.eventsourced.eventstore.postgresql.serializer.json.JSONEventSerializer;
import dk.cloudcreate.essentials.components.eventsourced.eventstore.postgresql.types.EventOrder;
import dk.cloudcreate.essentials.components.foundation.postgresql.PostgresqlUtil;
import dk.cloudcreate.essentials.components.foundation.transaction.jdbi.*;
import dk.cloudcreate.essentials.shared.collections.Lists;
import dk.cloudcreate.essentials.shared.reflection.Classes;
import dk.cloudcreate.essentials.types.NumberType;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;
import org.slf4j.*;

import java.sql.*;
import java.time.*;
import java.util.*;
import java.util.stream.Collectors;

import static dk.cloudcreate.essentials.shared.FailFast.*;
import static dk.cloudcreate.essentials.shared.MessageFormatter.msg;


/**
 * Postgresql specific version of {@link AggregateSnapshotRepository}
 *
 * Security
* The user can supply a custom {@code snapshotTableName}, which controls the name of the table where {@link AggregateSnapshot}'s will be stored
*
* To support customization of storage table name, the {@code snapshotTableName} will be directly used in constructing SQL statements * through string concatenation, which exposes the component to SQL injection attacks.
*
* It is the responsibility of the user of this component to sanitize the {@code snapshotTableName} * to ensure the security of all the SQL statements generated by this component.
* The {@link PostgresqlAggregateSnapshotRepository} component will * call the {@link PostgresqlUtil#checkIsValidTableOrColumnName(String)} method to validate the table name as a first line of defense.
* The {@link PostgresqlUtil#checkIsValidTableOrColumnName(String)} provides an initial layer of defense against SQL injection by applying naming conventions intended to reduce the risk of malicious input.
* However, Essentials components as well as {@link PostgresqlUtil#checkIsValidTableOrColumnName(String)} does not offer exhaustive protection, nor does it assure the complete security of the resulting SQL against SQL injection threats.
* The responsibility for implementing protective measures against SQL Injection lies exclusively with the users/developers using the Essentials components and its supporting classes.
* Users must ensure thorough sanitization and validation of API input parameters, column, table, and index names.
* Insufficient attention to these practices may leave the application vulnerable to SQL injection, potentially endangering the security and integrity of the database.
* It is highly recommended that the {@code snapshotTableName} value is only derived from a controlled and trusted source.
* To mitigate the risk of SQL injection attacks, external or untrusted inputs should never directly provide the {@code snapshotTableName} value.
* Failure to adequately sanitize and validate this value could expose the application to SQL injection * vulnerabilities, compromising the security and integrity of the database. */ @SuppressWarnings("unchecked") public class PostgresqlAggregateSnapshotRepository implements AggregateSnapshotRepository { private static final Logger log = LoggerFactory.getLogger(dk.cloudcreate.essentials.components.eventsourced.aggregates.snapshot.PostgresqlAggregateSnapshotRepository.class); public static final String DEFAULT_AGGREGATE_SNAPSHOTS_TABLE_NAME = "aggregate_snapshots"; private final ConfigurableEventStore eventStore; private final HandleAwareUnitOfWorkFactory unitOfWorkFactory; private final String snapshotTableName; private final JSONEventSerializer jsonSerializer; private final AggregateSnapshotRowMapper aggregateSnapshotWithSnapshotPayloadRowMapper; private final AggregateSnapshotRowMapper aggregateSnapshotWithoutSnapshotPayloadRowMapper; private final AddNewAggregateSnapshotStrategy addNewSnapshotStrategy; private final AggregateSnapshotDeletionStrategy snapshotDeletionStrategy; /** * Create a new durable Postgresql version of the {@link PostgresqlAggregateSnapshotRepository} * that will persist {@link AggregateSnapshot}'s into the {@link #DEFAULT_AGGREGATE_SNAPSHOTS_TABLE_NAME} table
* Adding new {@link AggregateSnapshot}'s when it's behind by 10 events AND * Deleting All Historic {@link AggregateSnapshot}'s * * @param eventStore the event store responsible for persisting aggregate event stream * @param unitOfWorkFactory unit of work factory for controlling transactions * @param jsonSerializer JSON serializer that will be used to serialize Aggregate instances */ public PostgresqlAggregateSnapshotRepository(ConfigurableEventStore eventStore, HandleAwareUnitOfWorkFactory unitOfWorkFactory, JSONEventSerializer jsonSerializer) { this(eventStore, unitOfWorkFactory, Optional.empty(), jsonSerializer, AddNewAggregateSnapshotStrategy.updateWhenBehindByNumberOfEvents(10), AggregateSnapshotDeletionStrategy.deleteAllHistoricSnapshots()); } /** * Create a new durable Postgresql version of the {@link PostgresqlAggregateSnapshotRepository}
* Adding new {@link AggregateSnapshot}'s when it's behind by 10 events AND * Deleting All Historic {@link AggregateSnapshot}'s * * @param eventStore the event store responsible for persisting aggregate event stream * @param unitOfWorkFactory unit of work factory for controlling transactions * @param snapshotTableName the name of the table where {@link AggregateSnapshot}'s will be stored
* Note:
* To support customization of storage table name, the {@code snapshotTableName} will be directly used in constructing SQL statements * through string concatenation, which exposes the component to SQL injection attacks.
*
* Security Note:
* It is the responsibility of the user of this component to sanitize the {@code snapshotTableName} * to ensure the security of all the SQL statements generated by this component.
* The {@link PostgresqlAggregateSnapshotRepository} component will * call the {@link PostgresqlUtil#checkIsValidTableOrColumnName(String)} method to validate the table name as a first line of defense.
* The {@link PostgresqlUtil#checkIsValidTableOrColumnName(String)} provides an initial layer of defense against SQL injection by applying naming conventions intended to reduce the risk of malicious input.
* However, Essentials components as well as {@link PostgresqlUtil#checkIsValidTableOrColumnName(String)} does not offer exhaustive protection, nor does it assure the complete security of the resulting SQL against SQL injection threats.
* The responsibility for implementing protective measures against SQL Injection lies exclusively with the users/developers using the Essentials components and its supporting classes.
* Users must ensure thorough sanitization and validation of API input parameters, column, table, and index names.
* Insufficient attention to these practices may leave the application vulnerable to SQL injection, potentially endangering the security and integrity of the database.
* It is highly recommended that the {@code snapshotTableName} value is only derived from a controlled and trusted source.
* To mitigate the risk of SQL injection attacks, external or untrusted inputs should never directly provide the {@code snapshotTableName} value.
* Failure to adequately sanitize and validate this value could expose the application to SQL injection * vulnerabilities, compromising the security and integrity of the database. * @param jsonSerializer JSON serializer that will be used to serialize Aggregate instances */ public PostgresqlAggregateSnapshotRepository(ConfigurableEventStore eventStore, HandleAwareUnitOfWorkFactory unitOfWorkFactory, String snapshotTableName, JSONEventSerializer jsonSerializer) { this(eventStore, unitOfWorkFactory, Optional.ofNullable(snapshotTableName), jsonSerializer, AddNewAggregateSnapshotStrategy.updateWhenBehindByNumberOfEvents(10), AggregateSnapshotDeletionStrategy.deleteAllHistoricSnapshots()); } /** * Create a new durable Postgresql version of the {@link PostgresqlAggregateSnapshotRepository} * * @param eventStore the event store responsible for persisting aggregate event stream * @param unitOfWorkFactory unit of work factory for controlling transactions * @param snapshotTableName the name of the table where {@link AggregateSnapshot}'s will be stored
* Note:
* To support customization of storage table name, the {@code snapshotTableName} will be directly used in constructing SQL statements * through string concatenation, which exposes the component to SQL injection attacks.
*
* Security Note:
* It is the responsibility of the user of this component to sanitize the {@code snapshotTableName} * to ensure the security of all the SQL statements generated by this component.
* The {@link PostgresqlAggregateSnapshotRepository} component will * call the {@link PostgresqlUtil#checkIsValidTableOrColumnName(String)} method to validate the table name as a first line of defense.
* The {@link PostgresqlUtil#checkIsValidTableOrColumnName(String)} provides an initial layer of defense against SQL injection by applying naming conventions intended to reduce the risk of malicious input.
* However, Essentials components as well as {@link PostgresqlUtil#checkIsValidTableOrColumnName(String)} does not offer exhaustive protection, nor does it assure the complete security of the resulting SQL against SQL injection threats.
* The responsibility for implementing protective measures against SQL Injection lies exclusively with the users/developers using the Essentials components and its supporting classes.
* Users must ensure thorough sanitization and validation of API input parameters, column, table, and index names.
* Insufficient attention to these practices may leave the application vulnerable to SQL injection, potentially endangering the security and integrity of the database.
* It is highly recommended that the {@code snapshotTableName} value is only derived from a controlled and trusted source.
* To mitigate the risk of SQL injection attacks, external or untrusted inputs should never directly provide the {@code snapshotTableName} value.
* Failure to adequately sanitize and validate this value could expose the application to SQL injection * vulnerabilities, compromising the security and integrity of the database. * @param addNewSnapshotStrategy the strategy determining when a new {@link AggregateSnapshot} will be stored * @param snapshotDeletionStrategy the strategy determining when an existing {@link AggregateSnapshot} will be deleted */ public PostgresqlAggregateSnapshotRepository(ConfigurableEventStore eventStore, HandleAwareUnitOfWorkFactory unitOfWorkFactory, String snapshotTableName, JSONEventSerializer jsonSerializer, AddNewAggregateSnapshotStrategy addNewSnapshotStrategy, AggregateSnapshotDeletionStrategy snapshotDeletionStrategy) { this(eventStore, unitOfWorkFactory, Optional.ofNullable(snapshotTableName), jsonSerializer, addNewSnapshotStrategy, snapshotDeletionStrategy); } /** * Create a new durable Postgresql version of the {@link PostgresqlAggregateSnapshotRepository} * that will persist {@link AggregateSnapshot}'s into the {@link #DEFAULT_AGGREGATE_SNAPSHOTS_TABLE_NAME} table * * @param eventStore the event store responsible for persisting aggregate event stream * @param unitOfWorkFactory unit of work factory for controlling transactions * @param jsonSerializer JSON serializer that will be used to serialize Aggregate instances * @param addNewSnapshotStrategy the strategy determining when a new {@link AggregateSnapshot} will be stored * @param snapshotDeletionStrategy the strategy determining when an existing {@link AggregateSnapshot} will be deleted */ public PostgresqlAggregateSnapshotRepository(ConfigurableEventStore eventStore, HandleAwareUnitOfWorkFactory unitOfWorkFactory, JSONEventSerializer jsonSerializer, AddNewAggregateSnapshotStrategy addNewSnapshotStrategy, AggregateSnapshotDeletionStrategy snapshotDeletionStrategy) { this(eventStore, unitOfWorkFactory, Optional.empty(), jsonSerializer, addNewSnapshotStrategy, snapshotDeletionStrategy); } /** * Create a new durable Postgresql version of the {@link PostgresqlAggregateSnapshotRepository} * * @param eventStore the event store responsible for persisting aggregate event stream * @param unitOfWorkFactory unit of work factory for controlling transactions * @param snapshotTableName the name of the table where {@link AggregateSnapshot}'s will be stored
* Note:
* To support customization of storage table name, the {@code snapshotTableName} will be directly used in constructing SQL statements * through string concatenation, which exposes the component to SQL injection attacks.
*
* Security Note:
* It is the responsibility of the user of this component to sanitize the {@code snapshotTableName} * to ensure the security of all the SQL statements generated by this component.
* The {@link PostgresqlAggregateSnapshotRepository} component will * call the {@link PostgresqlUtil#checkIsValidTableOrColumnName(String)} method to validate the table name as a first line of defense.
* The {@link PostgresqlUtil#checkIsValidTableOrColumnName(String)} provides an initial layer of defense against SQL injection by applying naming conventions intended to reduce the risk of malicious input.
* However, Essentials components as well as {@link PostgresqlUtil#checkIsValidTableOrColumnName(String)} does not offer exhaustive protection, nor does it assure the complete security of the resulting SQL against SQL injection threats.
* The responsibility for implementing protective measures against SQL Injection lies exclusively with the users/developers using the Essentials components and its supporting classes.
* Users must ensure thorough sanitization and validation of API input parameters, column, table, and index names.
* Insufficient attention to these practices may leave the application vulnerable to SQL injection, potentially endangering the security and integrity of the database.
* It is highly recommended that the {@code snapshotTableName} value is only derived from a controlled and trusted source.
* To mitigate the risk of SQL injection attacks, external or untrusted inputs should never directly provide the {@code snapshotTableName} value.
* Failure to adequately sanitize and validate this value could expose the application to SQL injection * vulnerabilities, compromising the security and integrity of the database. * @param jsonSerializer JSON serializer that will be used to serialize Aggregate instances * @param addNewSnapshotStrategy the strategy determining when a new {@link AggregateSnapshot} will be stored * @param snapshotDeletionStrategy the strategy determining when an existing {@link AggregateSnapshot} will be deleted */ public PostgresqlAggregateSnapshotRepository(ConfigurableEventStore eventStore, HandleAwareUnitOfWorkFactory unitOfWorkFactory, Optional snapshotTableName, JSONEventSerializer jsonSerializer, AddNewAggregateSnapshotStrategy addNewSnapshotStrategy, AggregateSnapshotDeletionStrategy snapshotDeletionStrategy) { this.eventStore = requireNonNull(eventStore, "No eventStore instance provided"); this.unitOfWorkFactory = requireNonNull(unitOfWorkFactory, "No unitOfWorkFactory instance provided"); this.snapshotTableName = requireNonNull(snapshotTableName, "No snapshotTableName provided") .orElse(DEFAULT_AGGREGATE_SNAPSHOTS_TABLE_NAME).toLowerCase(); this.jsonSerializer = requireNonNull(jsonSerializer, "No jsonSerializer instance provided"); this.addNewSnapshotStrategy = requireNonNull(addNewSnapshotStrategy, "No snapshotUpdateStrategy instance provided"); this.snapshotDeletionStrategy = requireNonNull(snapshotDeletionStrategy, "No snapshotDeletionStrategy instance provided"); aggregateSnapshotWithSnapshotPayloadRowMapper = new AggregateSnapshotRowMapper(true); aggregateSnapshotWithoutSnapshotPayloadRowMapper = new AggregateSnapshotRowMapper(false); initializeStorage(); } private void initializeStorage() { PostgresqlUtil.checkIsValidTableOrColumnName(snapshotTableName); unitOfWorkFactory.withUnitOfWork(uow -> uow.handle().execute("CREATE TABLE IF NOT EXISTS " + this.snapshotTableName + " (\n" + "aggregate_impl_type TEXT NOT NULL,\n" + "aggregate_id TEXT NOT NULL,\n" + "aggregate_type TEXT NOT NULL,\n" + "last_included_event_order bigint NOT NULL,\n" + "snapshot JSONB NOT NULL,\n" + "created_ts TIMESTAMP WITH TIME ZONE NOT NULL,\n" + "statistics JSONB,\n" + "PRIMARY KEY (aggregate_impl_type, " + " aggregate_id," + " last_included_event_order)\n" + ")")); log.info("Ensured that aggregate snapshot table '{}' exists", snapshotTableName); } @Override public Optional> loadSnapshot(AggregateType aggregateType, ID aggregateId, EventOrder withLastIncludedEventOrderLessThanOrEqualTo, Class aggregateImplType) { requireNonNull(aggregateType, "No aggregateType supplied"); requireNonNull(aggregateId, "No aggregateId supplied"); requireNonNull(withLastIncludedEventOrderLessThanOrEqualTo, "No withLastIncludedEventOrderLessThanOrEqualTo supplied"); requireNonNull(aggregateImplType, "No aggregateImplType supplied"); var config = eventStore.getAggregateEventStreamConfiguration(aggregateType); var serializedAggregateId = config.aggregateIdSerializer.serialize(aggregateId); return unitOfWorkFactory.withUnitOfWork(uow -> uow.handle().createQuery("SELECT * FROM " + this.snapshotTableName + " WHERE aggregate_impl_type = :aggregate_impl_type AND " + "aggregate_id = :aggregate_id AND " + "last_included_event_order <= :last_included_event_order") .bind("aggregate_impl_type", aggregateImplType.getName()) .bind("aggregate_id", serializedAggregateId) .bind("last_included_event_order", withLastIncludedEventOrderLessThanOrEqualTo) .map(aggregateSnapshotWithSnapshotPayloadRowMapper) .map(snapshot -> (AggregateSnapshot) snapshot) .findOne()); } @Override public void aggregateUpdated(AGGREGATE_IMPL_TYPE aggregate, AggregateEventStream persistedEvents) { requireNonNull(aggregate, "No aggregate instance supplied"); requireNonNull(persistedEvents, "No persistedEvents stream supplied"); unitOfWorkFactory.usingUnitOfWork(uow -> { var aggregateType = persistedEvents.aggregateType(); var config = eventStore.getAggregateEventStreamConfiguration(aggregateType); var serializedAggregateId = config.aggregateIdSerializer.serialize(persistedEvents.aggregateId()); var aggregateImplType = aggregate.getClass().getName(); var mostRecentlyStoredSnapshotLastIncludedEventOrder = findMostRecentLastIncludedEventOrderFor(serializedAggregateId, aggregateImplType, uow); if (shouldWeAddANewAggregateSnapshot(aggregate, persistedEvents, aggregateType, aggregateImplType, mostRecentlyStoredSnapshotLastIncludedEventOrder)) { deleteHistoricSnapShotsIfNecessary(aggregate, persistedEvents, uow, aggregateType, serializedAggregateId, aggregateImplType); var lastAppliedEventOrder = Lists.last(persistedEvents.eventList()).get().eventOrder(); var rowsUpdated = uow.handle().createUpdate("INSERT INTO " + this.snapshotTableName + "(\n" + "aggregate_impl_type, aggregate_id, aggregate_type, last_included_event_order, snapshot, created_ts" + "\n) VALUES (\n" + ":aggregate_impl_type, :aggregate_id, :aggregate_type, :last_included_event_order, :snapshot::jsonb, :created_ts" + "\n) ON CONFLICT DO NOTHING") .bind("aggregate_impl_type", aggregateImplType) .bind("aggregate_id", serializedAggregateId) .bind("aggregate_type", aggregateType.value()) .bind("last_included_event_order", lastAppliedEventOrder.longValue()) .bind("snapshot", jsonSerializer.serialize(aggregate)) .bind("created_ts", OffsetDateTime.now(Clock.systemUTC())) .execute(); if (rowsUpdated == 1) { log.debug("[{}:{}] Updated Aggregate Snapshot for '{}' and last_included_event_order {}", aggregateType, persistedEvents.aggregateId(), aggregateImplType, lastAppliedEventOrder); } else { log.debug("[{}:{}] No rows updated when trying to update Aggregate Snapshot for '{}' and last_included_event_order {}", aggregateType, persistedEvents.aggregateId(), aggregateImplType, lastAppliedEventOrder); } } }); } private boolean shouldWeAddANewAggregateSnapshot(AGGREGATE_IMPL_TYPE aggregate, AggregateEventStream persistedEvents, AggregateType aggregateType, String aggregateImplType, Optional mostRecentlyStoredSnapshotLastIncludedEventOrder) { if (addNewSnapshotStrategy.shouldANewAggregateSnapshotBeAdded(aggregate, persistedEvents, mostRecentlyStoredSnapshotLastIncludedEventOrder)) { if (log.isDebugEnabled()) { log.debug("[{}:{}] {} strategy determined to ADD a new Aggregate Snapshot for '{}' based on mostRecentlyStoredSnapshotLastIncludedEventOrder {} and persistedEvents->eventOrders: {}", aggregateType, persistedEvents.aggregateId(), addNewSnapshotStrategy, aggregateImplType, mostRecentlyStoredSnapshotLastIncludedEventOrder, persistedEvents.eventList().stream().map(persistedEvent -> persistedEvent.eventOrder().longValue()).collect(Collectors.toList())); } return true; } else { if (log.isDebugEnabled()) { log.debug("[{}:{}] {} strategy determined NOT to add a new Aggregate Snapshot for '{}' based on mostRecentlyStoredSnapshotLastIncludedEventOrder {} and persistedEvents->eventOrders: {}", aggregateType, persistedEvents.aggregateId(), addNewSnapshotStrategy, aggregateImplType, mostRecentlyStoredSnapshotLastIncludedEventOrder, persistedEvents.eventList().stream().map(persistedEvent -> persistedEvent.eventOrder().longValue()).collect(Collectors.toList())); } return false; } } protected Optional findMostRecentLastIncludedEventOrderFor(String serializedAggregateId, String aggregateImplType, HandleAwareUnitOfWork uow) { return uow.handle().createQuery("SELECT coalesce(MAX(last_included_event_order), -1) FROM " + this.snapshotTableName + " WHERE aggregate_impl_type = :aggregate_impl_type AND " + "aggregate_id = :aggregate_id") .bind("aggregate_impl_type", aggregateImplType) .bind("aggregate_id", serializedAggregateId) .mapTo(EventOrder.class) .findOne(); } private void deleteHistoricSnapShotsIfNecessary(AGGREGATE_IMPL_TYPE aggregate, AggregateEventStream persistedEvents, HandleAwareUnitOfWork uow, AggregateType aggregateType, String serializedAggregateId, String aggregateImplType) { if (snapshotDeletionStrategy.requiresExistingSnapshotDetailsToDetermineWhichAggregateSnapshotsToDelete()) { var existingSnapshots = loadAllSnapshots(serializedAggregateId, aggregateImplType, false, uow); if (log.isDebugEnabled()) { log.debug("[{}:{}] Found {} {}'s Aggregate-Snapshots with eventOrderOfLastIncludedEvent: {}", aggregateType, persistedEvents.aggregateId(), existingSnapshots.size(), aggregateImplType, existingSnapshots.stream().map(snapshot -> snapshot.eventOrderOfLastIncludedEvent.longValue()).collect(Collectors.toList())); } if (!existingSnapshots.isEmpty()) { var snapshotEventOrdersToDeleteStream = snapshotDeletionStrategy.resolveSnapshotsToDelete(existingSnapshots); var eventOrdersToDelete = snapshotEventOrdersToDeleteStream.map(snapshot -> snapshot.eventOrderOfLastIncludedEvent).collect(Collectors.toList()); if (!eventOrdersToDelete.isEmpty()) { log.debug("[{}:{}] Will delete {} Historic {}'s Aggregate-Snapshots with eventOrderOfLastIncludedEvent: {}", aggregateType, persistedEvents.aggregateId(), eventOrdersToDelete.size(), aggregateImplType, eventOrdersToDelete); deleteSnapshots(aggregateType, persistedEvents.aggregateId(), aggregate.getClass(), eventOrdersToDelete); } } } else { deleteSnapshots(aggregateType, persistedEvents.aggregateId(), aggregate.getClass()); } } @Override public List> loadAllSnapshots(AggregateType aggregateType, ID aggregateId, Class aggregateImplType, boolean includeSnapshotPayload) { requireNonNull(aggregateType, "No aggregateType supplied"); requireNonNull(aggregateId, "No aggregateId supplied"); requireNonNull(aggregateImplType, "No aggregateImplType supplied"); var config = eventStore.getAggregateEventStreamConfiguration(aggregateType); var serializedAggregateId = config.aggregateIdSerializer.serialize(aggregateId); return unitOfWorkFactory.withUnitOfWork(uow -> loadAllSnapshots(serializedAggregateId, aggregateImplType.getName(), includeSnapshotPayload, uow)); } protected List> loadAllSnapshots(String serializedAggregateId, String aggregateImplType, boolean includeSnapshotPayload, HandleAwareUnitOfWork uow) { var selectColumns = includeSnapshotPayload ? "*" : "aggregate_impl_type, aggregate_id, aggregate_type, last_included_event_order, created_ts, statistics"; return uow.handle().createQuery("SELECT " + selectColumns + " FROM " + this.snapshotTableName + " WHERE " + "aggregate_impl_type = :aggregate_impl_type AND aggregate_id = :aggregate_id " + "ORDER BY last_included_event_order ASC") .bind("aggregate_impl_type", aggregateImplType) .bind("aggregate_id", serializedAggregateId) .map(includeSnapshotPayload ? aggregateSnapshotWithSnapshotPayloadRowMapper : aggregateSnapshotWithoutSnapshotPayloadRowMapper) .map(snapshot -> (AggregateSnapshot) snapshot) .list(); } @Override public void deleteAllSnapshots(Class ofAggregateImplementationType) { requireNonNull(ofAggregateImplementationType, "No ofAggregateImplementationType supplied"); var rowsUpdated = unitOfWorkFactory.withUnitOfWork(uow -> uow.handle().createUpdate("DELETE FROM " + this.snapshotTableName + " WHERE aggregate_impl_type = :aggregate_impl_type") .bind("aggregate_impl_type", ofAggregateImplementationType.getName()) .execute()); log.debug("Deleted {} historic snapshots related to Aggregate implementation type '{}'", rowsUpdated, ofAggregateImplementationType.getName()); } @Override public void deleteSnapshots(AggregateType aggregateType, ID aggregateId, Class withAggregateImplementationType) { requireNonNull(aggregateType, "No aggregateType supplied"); requireNonNull(aggregateId, "No aggregateId supplied"); requireNonNull(withAggregateImplementationType, "No withAggregateImplementationType supplied"); var config = eventStore.getAggregateEventStreamConfiguration(aggregateType); var serializedAggregateId = config.aggregateIdSerializer.serialize(aggregateId); var rowsUpdated = unitOfWorkFactory.withUnitOfWork(uow -> uow.handle().createUpdate("DELETE FROM " + this.snapshotTableName + " WHERE aggregate_impl_type = :aggregate_impl_type AND " + "aggregate_id = :aggregate_id") .bind("aggregate_impl_type", withAggregateImplementationType.getName()) .bind("aggregate_id", serializedAggregateId) .execute()); log.debug("Deleted {} historic snapshots related to Aggregate '{}' with id '{}'", rowsUpdated, withAggregateImplementationType.getName(), aggregateId); } @Override public void deleteSnapshots(AggregateType aggregateType, ID aggregateId, Class withAggregateImplementationType, List snapshotEventOrdersToDelete) { requireNonNull(aggregateType, "No aggregateType supplied"); requireNonNull(aggregateId, "No aggregateId supplied"); requireNonNull(withAggregateImplementationType, "No withAggregateImplementationType supplied"); requireNonEmpty(snapshotEventOrdersToDelete, "snapshotEventOrdersToDelete may not be null or empty"); var config = eventStore.getAggregateEventStreamConfiguration(aggregateType); var serializedAggregateId = config.aggregateIdSerializer.serialize(aggregateId); var rowsUpdated = unitOfWorkFactory.withUnitOfWork(uow -> uow.handle().createUpdate("DELETE FROM " + this.snapshotTableName + " WHERE aggregate_impl_type = :aggregate_impl_type AND " + "aggregate_id = :aggregate_id AND last_included_event_order IN ()") .bind("aggregate_impl_type", withAggregateImplementationType.getName()) .bind("aggregate_id", serializedAggregateId) .bindList("snapshotEventOrdersToDelete", snapshotEventOrdersToDelete.stream().map(NumberType::longValue).collect(Collectors.toList())) .execute()); log.debug("Deleted {} historic snapshots related to Aggregate '{}' with id '{}' and snapshotEventOrdersToDelete: {}", rowsUpdated, withAggregateImplementationType.getName(), aggregateId, snapshotEventOrdersToDelete); } private class AggregateSnapshotRowMapper implements RowMapper { private final boolean resultSetContainsSnapshotPayload; public AggregateSnapshotRowMapper(boolean resultSetContainsSnapshotPayload) { this.resultSetContainsSnapshotPayload = resultSetContainsSnapshotPayload; } @Override public AggregateSnapshot map(ResultSet rs, StatementContext ctx) throws SQLException { var aggregateType = AggregateType.of(rs.getString("aggregate_type")); var config = eventStore.getAggregateEventStreamConfiguration(aggregateType); var aggregateImplType = Classes.forName(rs.getString("aggregate_impl_type"), jsonSerializer.getClassLoader()); var aggregateId = config.aggregateIdSerializer.deserialize(rs.getString("aggregate_id")); var snapshotPayload = deserializeSnapshot(rs, aggregateId, aggregateImplType); return new AggregateSnapshot(aggregateType, aggregateId, aggregateImplType, snapshotPayload, EventOrder.of(rs.getLong("last_included_event_order"))); } private Object deserializeSnapshot(ResultSet rs, Object aggregateId, Class aggregateImplType) throws SQLException { try { return resultSetContainsSnapshotPayload ? jsonSerializer.deserialize(rs.getString("snapshot"), aggregateImplType) : null; } catch (Exception e) { log.error(msg("Failed to deserialize '{}' with id '{}'", aggregateImplType, aggregateId), e); return new BrokenSnapshot(e); } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy