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
This commit is contained in:
Jens Schauder
2022-08-25 16:15:27 +02:00
committed by Mark Paluch
parent d95b5592a7
commit 2d76885fc6
3 changed files with 22 additions and 6 deletions

View File

@@ -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;
@@ -87,6 +88,10 @@ class AggregateChangeExecutor {
throw new RuntimeException("unexpected action");
}
} catch (Exception e) {
if (e instanceof OptimisticLockingFailureException) {
throw e;
}
throw new DbActionExecutionException(action, e);
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.jdbc.core;
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
@@ -35,6 +36,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);
@@ -62,6 +65,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.
@@ -75,6 +83,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);

View File

@@ -828,11 +828,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
@@ -877,12 +877,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);
@@ -1047,12 +1047,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) {