Skip to content

Commit 7b1f680

Browse files
schaudermp911de
authored andcommitted
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 spring-projects/spring-data-commons#2651
1 parent f5989e1 commit 7b1f680

File tree

3 files changed

+33
-7
lines changed

3 files changed

+33
-7
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.jdbc.core;
1717

18+
import org.springframework.dao.OptimisticLockingFailureException;
1819
import org.springframework.data.jdbc.core.convert.DataAccessStrategy;
1920
import org.springframework.data.jdbc.core.convert.JdbcConverter;
2021
import org.springframework.data.relational.core.conversion.AggregateChange;
@@ -110,6 +111,10 @@ private void execute(DbAction<?> action, JdbcAggregateChangeExecutionContext exe
110111
throw new RuntimeException("unexpected action");
111112
}
112113
} catch (Exception e) {
114+
115+
if (e instanceof OptimisticLockingFailureException) {
116+
throw e;
117+
}
113118
throw new DbActionExecutionException(action, e);
114119
}
115120
}

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.util.Optional;
1919

20+
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
2021
import org.springframework.data.domain.Example;
2122
import org.springframework.data.domain.Page;
2223
import org.springframework.data.domain.Pageable;
@@ -41,6 +42,8 @@ public interface JdbcAggregateOperations {
4142
* @param instance the aggregate root of the aggregate to be saved. Must not be {@code null}.
4243
* @param <T> the type of the aggregate root.
4344
* @return the saved instance.
45+
* @throws IncorrectUpdateSemanticsDataAccessException when the instance is determined to be not new and the resulting
46+
* update does not update any rows.
4447
*/
4548
<T> T save(T instance);
4649

@@ -50,6 +53,8 @@ public interface JdbcAggregateOperations {
5053
* @param instances the aggregate roots to be saved. Must not be {@code null}.
5154
* @param <T> the type of the aggregate root.
5255
* @return the saved instances.
56+
* @throws IncorrectUpdateSemanticsDataAccessException when at least one instance is determined to be not new and the
57+
* resulting update does not update any rows.
5358
* @since 3.0
5459
*/
5560
<T> Iterable<T> saveAll(Iterable<T> instances);
@@ -78,6 +83,11 @@ public interface JdbcAggregateOperations {
7883

7984
/**
8085
* Deletes a single Aggregate including all entities contained in that aggregate.
86+
* <p>
87+
* Since no version attribute is provided this method will never throw a
88+
* {@link org.springframework.dao.OptimisticLockingFailureException}. If no rows match the generated delete operation
89+
* this fact will be silently ignored.
90+
* </p>
8191
*
8292
* @param id the id of the aggregate root of the aggregate to be deleted. Must not be {@code null}.
8393
* @param domainType the type of the aggregate root.
@@ -87,7 +97,12 @@ public interface JdbcAggregateOperations {
8797

8898
/**
8999
* Deletes all aggregates identified by their aggregate root ids.
90-
*
100+
* <p>
101+
* Since no version attribute is provided this method will never throw a
102+
* {@link org.springframework.dao.OptimisticLockingFailureException}. If no rows match the generated delete operation
103+
* this fact will be silently ignored.
104+
* </p>
105+
*
91106
* @param ids the ids of the aggregate roots of the aggregates to be deleted. Must not be {@code null}.
92107
* @param domainType the type of the aggregate root.
93108
* @param <T> the type of the aggregate root.
@@ -100,6 +115,9 @@ public interface JdbcAggregateOperations {
100115
* @param aggregateRoot to delete. Must not be {@code null}.
101116
* @param domainType the type of the aggregate root. Must not be {@code null}.
102117
* @param <T> the type of the aggregate root.
118+
* @throws org.springframework.dao.OptimisticLockingFailureException when {@literal T} has a version attribute and the
119+
* version attribute of the provided entity does not match the version attribute in the database, or when
120+
* there is no aggregate root with matching id. In other cases a NOOP delete is silently ignored.
103121
*/
104122
<T> void delete(T aggregateRoot, Class<T> domainType);
105123

@@ -116,6 +134,9 @@ public interface JdbcAggregateOperations {
116134
* @param aggregateRoots to delete. Must not be {@code null}.
117135
* @param domainType type of the aggregate roots to be deleted. Must not be {@code null}.
118136
* @param <T> the type of the aggregate roots.
137+
* @throws org.springframework.dao.OptimisticLockingFailureException when {@literal T} has a version attribute and for at least on entity the
138+
* version attribute of the entity does not match the version attribute in the database, or when
139+
* there is no aggregate root with matching id. In other cases a NOOP delete is silently ignored.
119140
*/
120141
<T> void deleteAll(Iterable<? extends T> aggregateRoots, Class<T> domainType);
121142

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -866,11 +866,11 @@ void saveAndUpdateAggregateWithImmutableVersion() {
866866

867867
assertThatThrownBy(() -> template.save(new AggregateWithImmutableVersion(id, 0L)))
868868
.describedAs("saving an aggregate with an outdated version should raise an exception")
869-
.hasRootCauseInstanceOf(OptimisticLockingFailureException.class);
869+
.isInstanceOf(OptimisticLockingFailureException.class);
870870

871871
assertThatThrownBy(() -> template.save(new AggregateWithImmutableVersion(id, 2L)))
872872
.describedAs("saving an aggregate with a future version should raise an exception")
873-
.hasRootCauseInstanceOf(OptimisticLockingFailureException.class);
873+
.isInstanceOf(OptimisticLockingFailureException.class);
874874
}
875875

876876
@Test // GH-1137
@@ -915,12 +915,12 @@ void deleteAggregateWithVersion() {
915915
assertThatThrownBy(
916916
() -> template.delete(new AggregateWithImmutableVersion(id, 0L), AggregateWithImmutableVersion.class))
917917
.describedAs("deleting an aggregate with an outdated version should raise an exception")
918-
.hasRootCauseInstanceOf(OptimisticLockingFailureException.class);
918+
.isInstanceOf(OptimisticLockingFailureException.class);
919919

920920
assertThatThrownBy(
921921
() -> template.delete(new AggregateWithImmutableVersion(id, 2L), AggregateWithImmutableVersion.class))
922922
.describedAs("deleting an aggregate with a future version should raise an exception")
923-
.hasRootCauseInstanceOf(OptimisticLockingFailureException.class);
923+
.isInstanceOf(OptimisticLockingFailureException.class);
924924

925925
// This should succeed
926926
template.delete(aggregate, AggregateWithImmutableVersion.class);
@@ -1060,12 +1060,12 @@ private <T extends Number> void saveAndUpdateAggregateWithVersion(VersionedAggre
10601060
reloadedAggregate.setVersion(toConcreteNumber.apply(initialId));
10611061
assertThatThrownBy(() -> template.save(reloadedAggregate))
10621062
.withFailMessage("saving an aggregate with an outdated version should raise an exception")
1063-
.hasRootCauseInstanceOf(OptimisticLockingFailureException.class);
1063+
.isInstanceOf(OptimisticLockingFailureException.class);
10641064

10651065
reloadedAggregate.setVersion(toConcreteNumber.apply(initialId + 2));
10661066
assertThatThrownBy(() -> template.save(reloadedAggregate))
10671067
.withFailMessage("saving an aggregate with a future version should raise an exception")
1068-
.hasRootCauseInstanceOf(OptimisticLockingFailureException.class);
1068+
.isInstanceOf(OptimisticLockingFailureException.class);
10691069
}
10701070

10711071
private Long count(String tableName) {

0 commit comments

Comments
 (0)