diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java index e039fb9a..33ffa818 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java @@ -15,6 +15,7 @@ */ package org.springframework.data.jdbc.core; +import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.relational.core.conversion.AggregateChange; @@ -110,6 +111,10 @@ class AggregateChangeExecutor { throw new RuntimeException("unexpected action"); } } catch (Exception e) { + + if (e instanceof OptimisticLockingFailureException) { + throw e; + } throw new DbActionExecutionException(action, e); } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java index 1b6c2b51..9b516a84 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java @@ -17,6 +17,7 @@ package org.springframework.data.jdbc.core; import java.util.Optional; +import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException; import org.springframework.data.domain.Example; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -41,6 +42,8 @@ public interface JdbcAggregateOperations { * @param instance the aggregate root of the aggregate to be saved. Must not be {@code null}. * @param the type of the aggregate root. * @return the saved instance. + * @throws IncorrectUpdateSemanticsDataAccessException when the instance is determined to be not new and the resulting + * update does not update any rows. */ T save(T instance); @@ -50,6 +53,8 @@ public interface JdbcAggregateOperations { * @param instances the aggregate roots to be saved. Must not be {@code null}. * @param the type of the aggregate root. * @return the saved instances. + * @throws IncorrectUpdateSemanticsDataAccessException when at least one instance is determined to be not new and the + * resulting update does not update any rows. * @since 3.0 */ Iterable saveAll(Iterable instances); @@ -78,6 +83,11 @@ public interface JdbcAggregateOperations { /** * Deletes a single Aggregate including all entities contained in that aggregate. + *

+ * Since no version attribute is provided this method will never throw a + * {@link org.springframework.dao.OptimisticLockingFailureException}. If no rows match the generated delete operation + * this fact will be silently ignored. + *

* * @param id the id of the aggregate root of the aggregate to be deleted. Must not be {@code null}. * @param domainType the type of the aggregate root. @@ -87,7 +97,12 @@ public interface JdbcAggregateOperations { /** * Deletes all aggregates identified by their aggregate root ids. - * + *

+ * Since no version attribute is provided this method will never throw a + * {@link org.springframework.dao.OptimisticLockingFailureException}. If no rows match the generated delete operation + * this fact will be silently ignored. + *

+ * * @param ids the ids of the aggregate roots of the aggregates to be deleted. Must not be {@code null}. * @param domainType the type of the aggregate root. * @param the type of the aggregate root. @@ -100,6 +115,9 @@ public interface JdbcAggregateOperations { * @param aggregateRoot to delete. Must not be {@code null}. * @param domainType the type of the aggregate root. Must not be {@code null}. * @param the type of the aggregate root. + * @throws org.springframework.dao.OptimisticLockingFailureException when {@literal T} has a version attribute and the + * version attribute of the provided entity does not match the version attribute in the database, or when + * there is no aggregate root with matching id. In other cases a NOOP delete is silently ignored. */ void delete(T aggregateRoot, Class domainType); @@ -116,6 +134,9 @@ public interface JdbcAggregateOperations { * @param aggregateRoots to delete. Must not be {@code null}. * @param domainType type of the aggregate roots to be deleted. Must not be {@code null}. * @param the type of the aggregate roots. + * @throws org.springframework.dao.OptimisticLockingFailureException when {@literal T} has a version attribute and for at least on entity the + * version attribute of the entity does not match the version attribute in the database, or when + * there is no aggregate root with matching id. In other cases a NOOP delete is silently ignored. */ void deleteAll(Iterable aggregateRoots, Class domainType); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java index f825f5c6..dd2987aa 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java @@ -866,11 +866,11 @@ class JdbcAggregateTemplateIntegrationTests { assertThatThrownBy(() -> template.save(new AggregateWithImmutableVersion(id, 0L))) .describedAs("saving an aggregate with an outdated version should raise an exception") - .hasRootCauseInstanceOf(OptimisticLockingFailureException.class); + .isInstanceOf(OptimisticLockingFailureException.class); assertThatThrownBy(() -> template.save(new AggregateWithImmutableVersion(id, 2L))) .describedAs("saving an aggregate with a future version should raise an exception") - .hasRootCauseInstanceOf(OptimisticLockingFailureException.class); + .isInstanceOf(OptimisticLockingFailureException.class); } @Test // GH-1137 @@ -915,12 +915,12 @@ class JdbcAggregateTemplateIntegrationTests { assertThatThrownBy( () -> template.delete(new AggregateWithImmutableVersion(id, 0L), AggregateWithImmutableVersion.class)) .describedAs("deleting an aggregate with an outdated version should raise an exception") - .hasRootCauseInstanceOf(OptimisticLockingFailureException.class); + .isInstanceOf(OptimisticLockingFailureException.class); assertThatThrownBy( () -> template.delete(new AggregateWithImmutableVersion(id, 2L), AggregateWithImmutableVersion.class)) .describedAs("deleting an aggregate with a future version should raise an exception") - .hasRootCauseInstanceOf(OptimisticLockingFailureException.class); + .isInstanceOf(OptimisticLockingFailureException.class); // This should succeed template.delete(aggregate, AggregateWithImmutableVersion.class); @@ -1060,12 +1060,12 @@ class JdbcAggregateTemplateIntegrationTests { reloadedAggregate.setVersion(toConcreteNumber.apply(initialId)); assertThatThrownBy(() -> template.save(reloadedAggregate)) .withFailMessage("saving an aggregate with an outdated version should raise an exception") - .hasRootCauseInstanceOf(OptimisticLockingFailureException.class); + .isInstanceOf(OptimisticLockingFailureException.class); reloadedAggregate.setVersion(toConcreteNumber.apply(initialId + 2)); assertThatThrownBy(() -> template.save(reloadedAggregate)) .withFailMessage("saving an aggregate with a future version should raise an exception") - .hasRootCauseInstanceOf(OptimisticLockingFailureException.class); + .isInstanceOf(OptimisticLockingFailureException.class); } private Long count(String tableName) {