Skip to content

Commit e21cdd4

Browse files
committed
Polishing.
No longer throw TransientDataAccessResourceException if R2DBC update does not yield any updated rows. Remove mentions of IncorrectUpdateSemanticsDataAccessException, add mention of OptimisticLockingFailureException to affected methods. Consistent OptimisticLockingFailureException exception message. See #2176 Original pull request: #2185
1 parent b06d348 commit e21cdd4

File tree

9 files changed

+161
-69
lines changed

9 files changed

+161
-69
lines changed

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

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

18-
import java.util.*;
18+
import java.util.ArrayList;
19+
import java.util.Arrays;
20+
import java.util.Collections;
21+
import java.util.HashMap;
22+
import java.util.HashSet;
23+
import java.util.LinkedHashMap;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Optional;
27+
import java.util.Set;
1928
import java.util.function.BiConsumer;
2029
import java.util.function.Function;
2130
import java.util.stream.Collectors;
2231

2332
import org.jspecify.annotations.Nullable;
24-
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
25-
import org.springframework.dao.OptimisticLockingFailureException;
33+
2634
import org.springframework.data.jdbc.core.convert.DataAccessStrategy;
2735
import org.springframework.data.jdbc.core.convert.Identifier;
2836
import org.springframework.data.jdbc.core.convert.InsertSubject;
@@ -35,6 +43,7 @@
3543
import org.springframework.data.relational.core.conversion.DbActionExecutionResult;
3644
import org.springframework.data.relational.core.conversion.IdValueSource;
3745
import org.springframework.data.relational.core.mapping.AggregatePath;
46+
import org.springframework.data.relational.core.mapping.OptimisticLockingUtils;
3847
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
3948
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
4049
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@@ -55,9 +64,6 @@
5564
@SuppressWarnings("rawtypes")
5665
class JdbcAggregateChangeExecutionContext {
5766

58-
private static final String UPDATE_FAILED = "Failed to update entity [%s]; Id [%s] not found in database";
59-
private static final String UPDATE_FAILED_OPTIMISTIC_LOCKING = "Failed to update entity [%s]; The entity was updated since it was read or it isn't in the database at all";
60-
6167
private final RelationalMappingContext context;
6268
private final JdbcConverter converter;
6369
private final DataAccessStrategy accessStrategy;
@@ -357,8 +363,8 @@ private <T> void updateWithVersion(DbAction.UpdateRoot<T> update) {
357363
Assert.notNull(previousVersion, "The root aggregate cannot be updated because the version property is null");
358364

359365
if (!accessStrategy.updateWithVersion(update.entity(), update.getEntityType(), previousVersion)) {
360-
361-
throw new OptimisticLockingFailureException(String.format(UPDATE_FAILED_OPTIMISTIC_LOCKING, update.entity()));
366+
throw OptimisticLockingUtils.updateFailed(update.entity(), previousVersion,
367+
getRequiredPersistentEntity(update.getEntityType()));
362368
}
363369
}
364370

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.util.stream.Stream;
2121

2222
import org.jspecify.annotations.Nullable;
23-
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
2423
import org.springframework.data.domain.Example;
2524
import org.springframework.data.domain.Page;
2625
import org.springframework.data.domain.Pageable;
@@ -50,8 +49,8 @@ public interface JdbcAggregateOperations {
5049
* @param instance the aggregate root of the aggregate to be saved. Must not be {@code null}.
5150
* @param <T> the type of the aggregate root.
5251
* @return the saved instance.
53-
* @throws IncorrectUpdateSemanticsDataAccessException when the instance is determined to be not new and the resulting
54-
* update does not update any rows.
52+
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
53+
* {@link org.springframework.data.annotation.Version} is defined.
5554
*/
5655
<T> T save(T instance);
5756

@@ -61,8 +60,8 @@ public interface JdbcAggregateOperations {
6160
* @param instances the aggregate roots to be saved. Must not be {@code null}.
6261
* @param <T> the type of the aggregate root.
6362
* @return the saved instances.
64-
* @throws IncorrectUpdateSemanticsDataAccessException when at least one instance is determined to be not new and the
65-
* resulting update does not update any rows.
63+
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
64+
* {@link org.springframework.data.annotation.Version} is defined.
6665
* @since 3.0
6766
*/
6867
<T> List<T> saveAll(Iterable<T> instances);
@@ -99,6 +98,8 @@ public interface JdbcAggregateOperations {
9998
* @param instance the aggregate root of the aggregate to be inserted. Must not be {@code null}.
10099
* @param <T> the type of the aggregate root.
101100
* @return the saved instance.
101+
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
102+
* {@link org.springframework.data.annotation.Version} is defined.
102103
*/
103104
<T> T update(T instance);
104105

@@ -108,6 +109,8 @@ public interface JdbcAggregateOperations {
108109
* @param instances the aggregate roots to be inserted. Must not be {@code null}.
109110
* @param <T> the type of the aggregate root.
110111
* @return the saved instances.
112+
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
113+
* {@link org.springframework.data.annotation.Version} is defined.
111114
* @since 3.1
112115
*/
113116
<T> List<T> updateAll(Iterable<T> instances);
@@ -319,6 +322,8 @@ public interface JdbcAggregateOperations {
319322
*
320323
* @param aggregateRoot to delete. Must not be {@code null}.
321324
* @param <T> the type of the aggregate root.
325+
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
326+
* {@link org.springframework.data.annotation.Version} is defined.
322327
*/
323328
<T> void delete(T aggregateRoot);
324329

@@ -334,6 +339,8 @@ public interface JdbcAggregateOperations {
334339
*
335340
* @param aggregateRoots to delete. Must not be {@code null}.
336341
* @param <T> the type of the aggregate roots.
342+
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
343+
* {@link org.springframework.data.annotation.Version} is defined.
337344
*/
338345
<T> void deleteAll(Iterable<? extends T> aggregateRoots);
339346

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,16 @@
2525
import java.util.stream.Stream;
2626

2727
import org.jspecify.annotations.Nullable;
28+
2829
import org.springframework.dao.EmptyResultDataAccessException;
29-
import org.springframework.dao.OptimisticLockingFailureException;
3030
import org.springframework.data.domain.Pageable;
3131
import org.springframework.data.domain.Sort;
3232
import org.springframework.data.mapping.PersistentPropertyPath;
3333
import org.springframework.data.relational.core.conversion.IdValueSource;
3434
import org.springframework.data.relational.core.dialect.Dialect;
3535
import org.springframework.data.relational.core.mapping.AggregatePath;
3636
import org.springframework.data.relational.core.mapping.AggregatePath.TableInfo;
37+
import org.springframework.data.relational.core.mapping.OptimisticLockingUtils;
3738
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
3839
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
3940
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@@ -164,18 +165,15 @@ public <S> boolean update(S instance, Class<S> domainType) {
164165
@Override
165166
public <S> boolean updateWithVersion(S instance, Class<S> domainType, Number previousVersion) {
166167

167-
RelationalPersistentEntity<S> persistentEntity = getRequiredPersistentEntity(domainType);
168-
169168
// Adjust update statement to set the new version and use the old version in where clause.
170169
SqlIdentifierParameterSource parameterSource = sqlParametersFactory.forUpdate(instance, domainType);
171170
parameterSource.addValue(VERSION_SQL_PARAMETER, previousVersion);
172171

173172
int affectedRows = operations.update(sql(domainType).getUpdateWithVersion(), parameterSource);
174173

175174
if (affectedRows == 0) {
176-
177-
throw new OptimisticLockingFailureException(
178-
String.format("Optimistic lock exception on saving entity of type %s", persistentEntity.getName()));
175+
RelationalPersistentEntity<S> persistentEntity = getRequiredPersistentEntity(domainType);
176+
throw OptimisticLockingUtils.updateFailed(instance, previousVersion, persistentEntity);
179177
}
180178

181179
return true;
@@ -211,8 +209,7 @@ public <T> void deleteWithVersion(Object id, Class<T> domainType, Number previou
211209
int affectedRows = operations.update(sql(domainType).getDeleteByIdAndVersion(), parameterSource);
212210

213211
if (affectedRows == 0) {
214-
throw new OptimisticLockingFailureException(
215-
String.format("Optimistic lock exception deleting entity of type %s", persistentEntity.getName()));
212+
throw OptimisticLockingUtils.deleteFailed(id, previousVersion, persistentEntity);
216213
}
217214
}
218215

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import java.time.LocalDateTime;
2626
import java.util.*;
27+
import java.util.ArrayList;
2728
import java.util.function.Function;
2829
import java.util.stream.IntStream;
2930
import java.util.stream.Stream;
@@ -36,7 +37,6 @@
3637
import org.springframework.context.annotation.Configuration;
3738
import org.springframework.context.annotation.Import;
3839
import org.springframework.dao.IncorrectResultSizeDataAccessException;
39-
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
4040
import org.springframework.dao.OptimisticLockingFailureException;
4141
import org.springframework.data.annotation.Id;
4242
import org.springframework.data.annotation.PersistenceCreator;

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryConcurrencyIntegrationTests.java

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import org.springframework.context.annotation.Bean;
3838
import org.springframework.context.annotation.Configuration;
3939
import org.springframework.context.annotation.Import;
40-
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
4140
import org.springframework.data.annotation.Id;
4241
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
4342
import org.springframework.data.jdbc.testing.TestClass;
@@ -155,11 +154,6 @@ public void concurrentUpdateAndDelete() throws Exception {
155154
try {
156155
return repository.save(e);
157156
} catch (Exception ex) {
158-
// When the delete execution is complete, the Update execution throws an
159-
// IncorrectUpdateSemanticsDataAccessException.
160-
if (ex instanceof IncorrectUpdateSemanticsDataAccessException) {
161-
return null;
162-
}
163157
throw ex;
164158
}
165159
};
@@ -189,11 +183,6 @@ public void concurrentUpdateAndDeleteAll() throws Exception {
189183
try {
190184
return repository.save(e);
191185
} catch (Exception ex) {
192-
// When the delete execution is complete, the Update execution throws an
193-
// IncorrectUpdateSemanticsDataAccessException.
194-
if (ex instanceof IncorrectUpdateSemanticsDataAccessException) {
195-
return null;
196-
}
197186
throw ex;
198187
}
199188
};

spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityOperations.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,8 @@ <T> RowsFetchSpec<T> getRowsFetchSpec(DatabaseClient.GenericExecuteSpec executeS
273273
* @return the updated entity.
274274
* @throws DataAccessException if there is any problem issuing the execution.
275275
* @throws TransientDataAccessResourceException if the update did not affect any rows.
276+
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
277+
* {@link org.springframework.data.annotation.Version} is defined.
276278
*/
277279
<T> Mono<T> update(T entity) throws DataAccessException;
278280

@@ -282,6 +284,9 @@ <T> RowsFetchSpec<T> getRowsFetchSpec(DatabaseClient.GenericExecuteSpec executeS
282284
* @param entity must not be {@literal null}.
283285
* @return the deleted entity.
284286
* @throws DataAccessException if there is any problem issuing the execution.
287+
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
288+
* {@link org.springframework.data.annotation.Version} is defined.
285289
*/
286290
<T> Mono<T> delete(T entity) throws DataAccessException;
291+
287292
}

spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,14 @@
3535

3636
import org.jspecify.annotations.Nullable;
3737
import org.reactivestreams.Publisher;
38+
3839
import org.springframework.beans.BeansException;
3940
import org.springframework.beans.factory.BeanFactory;
4041
import org.springframework.beans.factory.BeanFactoryAware;
4142
import org.springframework.context.ApplicationContext;
4243
import org.springframework.context.ApplicationContextAware;
4344
import org.springframework.core.convert.ConversionService;
4445
import org.springframework.dao.DataAccessException;
45-
import org.springframework.dao.OptimisticLockingFailureException;
46-
import org.springframework.dao.TransientDataAccessResourceException;
4746
import org.springframework.data.mapping.IdentifierAccessor;
4847
import org.springframework.data.mapping.MappingException;
4948
import org.springframework.data.mapping.PersistentPropertyAccessor;
@@ -60,6 +59,7 @@
6059
import org.springframework.data.r2dbc.mapping.event.BeforeConvertCallback;
6160
import org.springframework.data.r2dbc.mapping.event.BeforeSaveCallback;
6261
import org.springframework.data.relational.core.conversion.AbstractRelationalConverter;
62+
import org.springframework.data.relational.core.mapping.OptimisticLockingUtils;
6363
import org.springframework.data.relational.core.mapping.PersistentPropertyTranslator;
6464
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
6565
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@@ -607,15 +607,22 @@ private <T> Mono<T> doUpdate(T entity, SqlIdentifier tableName) {
607607
return maybeCallBeforeConvert(entity, tableName).flatMap(onBeforeConvert -> {
608608

609609
T entityToUse;
610+
Object version;
610611
Criteria matchingVersionCriteria;
611612

612613
if (persistentEntity.hasVersionProperty()) {
613614

615+
PersistentPropertyAccessor<?> propertyAccessor = persistentEntity.getPropertyAccessor(entity);
616+
RelationalPersistentProperty versionProperty = persistentEntity.getRequiredVersionProperty();
617+
618+
version = propertyAccessor.getProperty(versionProperty);
614619
matchingVersionCriteria = createMatchingVersionCriteria(onBeforeConvert, persistentEntity);
620+
615621
entityToUse = incrementVersion(persistentEntity, onBeforeConvert);
616622
} else {
617623

618624
entityToUse = onBeforeConvert;
625+
version = null;
619626
matchingVersionCriteria = null;
620627
}
621628

@@ -653,17 +660,17 @@ private <T> Mono<T> doUpdate(T entity, SqlIdentifier tableName) {
653660
criteria = criteria.and(matchingVersionCriteria);
654661
}
655662

656-
return doUpdate(onBeforeSave, tableName, persistentEntity, criteria, outboundRow);
663+
return doUpdate(onBeforeSave, version, tableName, persistentEntity, criteria, outboundRow);
657664
});
658665
});
659666
}
660667

661668
@SuppressWarnings({ "unchecked", "rawtypes" })
662-
private <T> Mono<T> doUpdate(T entity, SqlIdentifier tableName, RelationalPersistentEntity<T> persistentEntity,
669+
private <T> Mono<T> doUpdate(T entity, @Nullable Object version, SqlIdentifier tableName,
670+
RelationalPersistentEntity<T> persistentEntity,
663671
Criteria criteria, OutboundRow outboundRow) {
664672

665673
Update update = Update.from((Map) outboundRow);
666-
667674
StatementMapper mapper = dataAccessStrategy.getStatementMapper();
668675
StatementMapper.UpdateSpec updateSpec = mapper.createUpdate(tableName, update).withCriteria(criteria);
669676

@@ -680,27 +687,11 @@ private <T> Mono<T> doUpdate(T entity, SqlIdentifier tableName, RelationalPersis
680687
}
681688

682689
if (persistentEntity.hasVersionProperty()) {
683-
sink.error(new OptimisticLockingFailureException(
684-
formatOptimisticLockingExceptionMessage(entity, persistentEntity)));
685-
} else {
686-
sink.error(new TransientDataAccessResourceException(
687-
formatTransientEntityExceptionMessage(entity, persistentEntity)));
690+
sink.error(OptimisticLockingUtils.updateFailed(entity, version, persistentEntity));
688691
}
689692
}).then(maybeCallAfterSave(entity, outboundRow, tableName));
690693
}
691694

692-
private <T> String formatOptimisticLockingExceptionMessage(T entity, RelationalPersistentEntity<T> persistentEntity) {
693-
694-
return String.format("Failed to update table [%s]; Version does not match for row with Id [%s]",
695-
persistentEntity.getQualifiedTableName(), persistentEntity.getIdentifierAccessor(entity).getIdentifier());
696-
}
697-
698-
private <T> String formatTransientEntityExceptionMessage(T entity, RelationalPersistentEntity<T> persistentEntity) {
699-
700-
return String.format("Failed to update table [%s]; Row with Id [%s] does not exist",
701-
persistentEntity.getQualifiedTableName(), persistentEntity.getIdentifierAccessor(entity).getIdentifier());
702-
}
703-
704695
@SuppressWarnings("unchecked")
705696
private <T> T incrementVersion(RelationalPersistentEntity<T> persistentEntity, T entity) {
706697

@@ -728,9 +719,7 @@ private <T> T incrementVersion(RelationalPersistentEntity<T> persistentEntity, T
728719
private <T> Criteria createMatchingVersionCriteria(T entity, RelationalPersistentEntity<T> persistentEntity) {
729720

730721
PersistentPropertyAccessor<?> propertyAccessor = persistentEntity.getPropertyAccessor(entity);
731-
RelationalPersistentProperty versionProperty = persistentEntity.getVersionProperty();
732-
733-
Assert.state(versionProperty != null, "Version property must not be null");
722+
RelationalPersistentProperty versionProperty = persistentEntity.getRequiredVersionProperty();
734723

735724
Object version = propertyAccessor.getProperty(versionProperty);
736725
Criteria.CriteriaStep versionColumn = Criteria.where(dataAccessStrategy.toSql(versionProperty.getColumnName()));
@@ -748,7 +737,13 @@ public <T> Mono<T> delete(T entity) throws DataAccessException {
748737

749738
RelationalPersistentEntity<?> persistentEntity = getRequiredEntity(entity);
750739

751-
return delete(getByIdQuery(entity, persistentEntity), persistentEntity.getType()).thenReturn(entity);
740+
Mono<Long> delete = delete(getByIdQuery(entity, persistentEntity), persistentEntity.getType());
741+
if (persistentEntity.hasVersionProperty()) {
742+
delete = delete.flatMap(
743+
it -> it == 0 ? Mono.error(OptimisticLockingUtils.deleteFailed(entity, persistentEntity)) : Mono.just(it));
744+
}
745+
746+
return delete.thenReturn(entity);
752747
}
753748

754749
protected <T> Mono<T> maybeCallBeforeConvert(T object, SqlIdentifier table) {
@@ -795,8 +790,17 @@ private <T> Query getByIdQuery(T entity, RelationalPersistentEntity<?> persisten
795790

796791
IdentifierAccessor identifierAccessor = persistentEntity.getIdentifierAccessor(entity);
797792
Object id = identifierAccessor.getRequiredIdentifier();
793+
Criteria criteria = Criteria.where(persistentEntity.getRequiredIdProperty().getName()).is(id);
794+
795+
if (persistentEntity.hasVersionProperty()) {
796+
797+
RelationalPersistentProperty versionProperty = persistentEntity.getRequiredVersionProperty();
798+
Object version = persistentEntity.getPropertyAccessor(entity).getProperty(versionProperty);
799+
Criteria.CriteriaStep versionColumn = Criteria.where(versionProperty.getName());
800+
criteria = version == null ? criteria.and(versionColumn.isNull()) : criteria.and(versionColumn.is(version));
801+
}
798802

799-
return Query.query(Criteria.where(persistentEntity.getRequiredIdProperty().getName()).is(id));
803+
return Query.query(criteria);
800804
}
801805

802806
SqlIdentifier getTableName(Class<?> entityClass) {

0 commit comments

Comments
 (0)