Browse Source

Correct behavior of NOOP deletes to match the specification in `CrudRepository`.

Delete operations that receive a version attribute throw an `OptimisticFailureException` when they delete zero rows.
Otherwise, the NOOP delete gets silently ignored.

Note that save operations that are determined to be an update because the aggregate is not new will still throw an `IncorrectUpdateSemanticsDataAccessException` if they fail to update any row.
This is somewhat asymmetric to the delete-behaviour.
But with a delete the intended result is achieved: the aggregate is gone from the database.
For save operations the intended result is not achieved, hence the exception.

Closes #1313
Original pull request: #1314.
See https://github.com/spring-projects/spring-data-commons/issues/2651
pull/1327/head
Jens Schauder 3 years ago committed by Mark Paluch
parent
commit
7b1f680a26
No known key found for this signature in database
GPG Key ID: 4406B84C1661DCD1
  1. 5
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java
  2. 23
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java
  3. 12
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java

5
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java

@ -15,6 +15,7 @@ @@ -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 { @@ -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);
}
}

23
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java

@ -17,6 +17,7 @@ package org.springframework.data.jdbc.core; @@ -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 { @@ -41,6 +42,8 @@ public interface JdbcAggregateOperations {
* @param instance the aggregate root of the aggregate to be saved. Must not be {@code null}.
* @param <T> 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> T save(T instance);
@ -50,6 +53,8 @@ public interface JdbcAggregateOperations { @@ -50,6 +53,8 @@ public interface JdbcAggregateOperations {
* @param instances the aggregate roots to be saved. Must not be {@code null}.
* @param <T> 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
*/
<T> Iterable<T> saveAll(Iterable<T> instances);
@ -78,6 +83,11 @@ public interface JdbcAggregateOperations { @@ -78,6 +83,11 @@ public interface JdbcAggregateOperations {
/**
* Deletes a single Aggregate including all entities contained in that aggregate.
* <p>
* 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.
* </p>
*
* @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 { @@ -87,7 +97,12 @@ public interface JdbcAggregateOperations {
/**
* Deletes all aggregates identified by their aggregate root ids.
*
* <p>
* 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.
* </p>
*
* @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 <T> the type of the aggregate root.
@ -100,6 +115,9 @@ public interface JdbcAggregateOperations { @@ -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 <T> 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.
*/
<T> void delete(T aggregateRoot, Class<T> domainType);
@ -116,6 +134,9 @@ public interface JdbcAggregateOperations { @@ -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 <T> 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.
*/
<T> void deleteAll(Iterable<? extends T> aggregateRoots, Class<T> domainType);

12
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java

@ -866,11 +866,11 @@ class JdbcAggregateTemplateIntegrationTests { @@ -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 { @@ -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 { @@ -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) {

Loading…
Cancel
Save