Skip to content

Commit 0958a99

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 8c30022 commit 0958a99

File tree

9 files changed

+157
-68
lines changed

9 files changed

+157
-68
lines changed

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,19 @@
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.stream.Collectors;
2130

22-
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
23-
import org.springframework.dao.OptimisticLockingFailureException;
2431
import org.springframework.data.jdbc.core.convert.DataAccessStrategy;
2532
import org.springframework.data.jdbc.core.convert.Identifier;
2633
import org.springframework.data.jdbc.core.convert.InsertSubject;
@@ -33,6 +40,7 @@
3340
import org.springframework.data.relational.core.conversion.DbActionExecutionResult;
3441
import org.springframework.data.relational.core.conversion.IdValueSource;
3542
import org.springframework.data.relational.core.mapping.AggregatePath;
43+
import org.springframework.data.relational.core.mapping.OptimisticLockingUtils;
3644
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
3745
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
3846
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@@ -54,9 +62,6 @@
5462
@SuppressWarnings("rawtypes")
5563
class JdbcAggregateChangeExecutionContext {
5664

57-
private static final String UPDATE_FAILED = "Failed to update entity [%s]; Id [%s] not found in database";
58-
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";
59-
6065
private final RelationalMappingContext context;
6166
private final JdbcConverter converter;
6267
private final DataAccessStrategy accessStrategy;
@@ -334,7 +339,7 @@ private <T> RelationalPersistentEntity<T> getRequiredPersistentEntity(Class<T> t
334339
}
335340

336341
private <T> void updateWithoutVersion(DbAction.UpdateRoot<T> update) {
337-
accessStrategy.update(update.entity(), update.getEntityType());
342+
accessStrategy.update(update.getEntity(), update.getEntityType());
338343
}
339344

340345
private <T> void updateWithVersion(DbAction.UpdateRoot<T> update) {
@@ -343,8 +348,8 @@ private <T> void updateWithVersion(DbAction.UpdateRoot<T> update) {
343348
Assert.notNull(previousVersion, "The root aggregate cannot be updated because the version property is null");
344349

345350
if (!accessStrategy.updateWithVersion(update.getEntity(), update.getEntityType(), previousVersion)) {
346-
347-
throw new OptimisticLockingFailureException(String.format(UPDATE_FAILED_OPTIMISTIC_LOCKING, update.getEntity()));
351+
throw OptimisticLockingUtils.updateFailed(update.getEntity(), previousVersion,
352+
getRequiredPersistentEntity(update.getEntityType()));
348353
}
349354
}
350355

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
@@ -19,7 +19,6 @@
1919
import java.util.Optional;
2020
import java.util.stream.Stream;
2121

22-
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
2322
import org.springframework.data.domain.Example;
2423
import org.springframework.data.domain.Page;
2524
import org.springframework.data.domain.Pageable;
@@ -46,8 +45,8 @@ public interface JdbcAggregateOperations {
4645
* @param instance the aggregate root of the aggregate to be saved. Must not be {@code null}.
4746
* @param <T> the type of the aggregate root.
4847
* @return the saved instance.
49-
* @throws IncorrectUpdateSemanticsDataAccessException when the instance is determined to be not new and the resulting
50-
* update does not update any rows.
48+
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
49+
* {@link org.springframework.data.annotation.Version} is defined.
5150
*/
5251
<T> T save(T instance);
5352

@@ -57,8 +56,8 @@ public interface JdbcAggregateOperations {
5756
* @param instances the aggregate roots to be saved. Must not be {@code null}.
5857
* @param <T> the type of the aggregate root.
5958
* @return the saved instances.
60-
* @throws IncorrectUpdateSemanticsDataAccessException when at least one instance is determined to be not new and the
61-
* resulting update does not update any rows.
59+
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
60+
* {@link org.springframework.data.annotation.Version} is defined.
6261
* @since 3.0
6362
*/
6463
<T> List<T> saveAll(Iterable<T> instances);
@@ -95,6 +94,8 @@ public interface JdbcAggregateOperations {
9594
* @param instance the aggregate root of the aggregate to be inserted. Must not be {@code null}.
9695
* @param <T> the type of the aggregate root.
9796
* @return the saved instance.
97+
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
98+
* {@link org.springframework.data.annotation.Version} is defined.
9899
*/
99100
<T> T update(T instance);
100101

@@ -104,6 +105,8 @@ public interface JdbcAggregateOperations {
104105
* @param instances the aggregate roots to be inserted. Must not be {@code null}.
105106
* @param <T> the type of the aggregate root.
106107
* @return the saved instances.
108+
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
109+
* {@link org.springframework.data.annotation.Version} is defined.
107110
* @since 3.1
108111
*/
109112
<T> List<T> updateAll(Iterable<T> instances);
@@ -315,6 +318,8 @@ public interface JdbcAggregateOperations {
315318
*
316319
* @param aggregateRoot to delete. Must not be {@code null}.
317320
* @param <T> the type of the aggregate root.
321+
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
322+
* {@link org.springframework.data.annotation.Version} is defined.
318323
*/
319324
<T> void delete(T aggregateRoot);
320325

@@ -330,6 +335,8 @@ public interface JdbcAggregateOperations {
330335
*
331336
* @param aggregateRoots to delete. Must not be {@code null}.
332337
* @param <T> the type of the aggregate roots.
338+
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
339+
* {@link org.springframework.data.annotation.Version} is defined.
333340
*/
334341
<T> void deleteAll(Iterable<? extends T> aggregateRoots);
335342
}

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

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

2727
import org.springframework.dao.EmptyResultDataAccessException;
28-
import org.springframework.dao.OptimisticLockingFailureException;
2928
import org.springframework.data.domain.Pageable;
3029
import org.springframework.data.domain.Sort;
3130
import org.springframework.data.mapping.PersistentPropertyPath;
3231
import org.springframework.data.relational.core.conversion.IdValueSource;
3332
import org.springframework.data.relational.core.mapping.AggregatePath;
3433
import org.springframework.data.relational.core.mapping.AggregatePath.TableInfo;
34+
import org.springframework.data.relational.core.mapping.OptimisticLockingUtils;
3535
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
3636
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
3737
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@@ -146,18 +146,15 @@ public <S> boolean update(S instance, Class<S> domainType) {
146146
@Override
147147
public <S> boolean updateWithVersion(S instance, Class<S> domainType, Number previousVersion) {
148148

149-
RelationalPersistentEntity<S> persistentEntity = getRequiredPersistentEntity(domainType);
150-
151149
// Adjust update statement to set the new version and use the old version in where clause.
152150
SqlIdentifierParameterSource parameterSource = sqlParametersFactory.forUpdate(instance, domainType);
153151
parameterSource.addValue(VERSION_SQL_PARAMETER, previousVersion);
154152

155153
int affectedRows = operations.update(sql(domainType).getUpdateWithVersion(), parameterSource);
156154

157155
if (affectedRows == 0) {
158-
159-
throw new OptimisticLockingFailureException(
160-
String.format("Optimistic lock exception on saving entity of type %s", persistentEntity.getName()));
156+
RelationalPersistentEntity<S> persistentEntity = getRequiredPersistentEntity(domainType);
157+
throw OptimisticLockingUtils.updateFailed(instance, previousVersion, persistentEntity);
161158
}
162159

163160
return true;
@@ -193,8 +190,7 @@ public <T> void deleteWithVersion(Object id, Class<T> domainType, Number previou
193190
int affectedRows = operations.update(sql(domainType).getDeleteByIdAndVersion(), parameterSource);
194191

195192
if (affectedRows == 0) {
196-
throw new OptimisticLockingFailureException(
197-
String.format("Optimistic lock exception deleting entity of type %s", persistentEntity.getName()));
193+
throw OptimisticLockingUtils.deleteFailed(id, previousVersion, persistentEntity);
198194
}
199195
}
200196

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import org.springframework.context.annotation.Configuration;
3838
import org.springframework.context.annotation.Import;
3939
import org.springframework.dao.IncorrectResultSizeDataAccessException;
40-
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
4140
import org.springframework.dao.OptimisticLockingFailureException;
4241
import org.springframework.data.annotation.Id;
4342
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;
@@ -157,11 +156,6 @@ public void concurrentUpdateAndDelete() throws Exception {
157156
try {
158157
return repository.save(e);
159158
} catch (Exception ex) {
160-
// When the delete execution is complete, the Update execution throws an
161-
// IncorrectUpdateSemanticsDataAccessException.
162-
if (ex.getCause() instanceof IncorrectUpdateSemanticsDataAccessException) {
163-
return null;
164-
}
165159
throw ex;
166160
}
167161
};
@@ -191,11 +185,6 @@ public void concurrentUpdateAndDeleteAll() throws Exception {
191185
try {
192186
return repository.save(e);
193187
} catch (Exception ex) {
194-
// When the delete execution is complete, the Update execution throws an
195-
// IncorrectUpdateSemanticsDataAccessException.
196-
if (ex.getCause() instanceof IncorrectUpdateSemanticsDataAccessException) {
197-
return null;
198-
}
199188
throw ex;
200189
}
201190
};

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: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,6 @@
4141
import org.springframework.context.ApplicationContextAware;
4242
import org.springframework.core.convert.ConversionService;
4343
import org.springframework.dao.DataAccessException;
44-
import org.springframework.dao.OptimisticLockingFailureException;
45-
import org.springframework.dao.TransientDataAccessResourceException;
4644
import org.springframework.data.mapping.IdentifierAccessor;
4745
import org.springframework.data.mapping.MappingException;
4846
import org.springframework.data.mapping.PersistentPropertyAccessor;
@@ -59,6 +57,7 @@
5957
import org.springframework.data.r2dbc.mapping.event.BeforeConvertCallback;
6058
import org.springframework.data.r2dbc.mapping.event.BeforeSaveCallback;
6159
import org.springframework.data.relational.core.conversion.AbstractRelationalConverter;
60+
import org.springframework.data.relational.core.mapping.OptimisticLockingUtils;
6261
import org.springframework.data.relational.core.mapping.PersistentPropertyTranslator;
6362
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
6463
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@@ -605,15 +604,22 @@ private <T> Mono<T> doUpdate(T entity, SqlIdentifier tableName) {
605604
return maybeCallBeforeConvert(entity, tableName).flatMap(onBeforeConvert -> {
606605

607606
T entityToUse;
607+
Object version;
608608
Criteria matchingVersionCriteria;
609609

610610
if (persistentEntity.hasVersionProperty()) {
611611

612+
PersistentPropertyAccessor<?> propertyAccessor = persistentEntity.getPropertyAccessor(entity);
613+
RelationalPersistentProperty versionProperty = persistentEntity.getRequiredVersionProperty();
614+
615+
version = propertyAccessor.getProperty(versionProperty);
612616
matchingVersionCriteria = createMatchingVersionCriteria(onBeforeConvert, persistentEntity);
617+
613618
entityToUse = incrementVersion(persistentEntity, onBeforeConvert);
614619
} else {
615620

616621
entityToUse = onBeforeConvert;
622+
version = null;
617623
matchingVersionCriteria = null;
618624
}
619625

@@ -637,17 +643,17 @@ private <T> Mono<T> doUpdate(T entity, SqlIdentifier tableName) {
637643
criteria = criteria.and(matchingVersionCriteria);
638644
}
639645

640-
return doUpdate(onBeforeSave, tableName, persistentEntity, criteria, outboundRow);
646+
return doUpdate(onBeforeSave, version, tableName, persistentEntity, criteria, outboundRow);
641647
});
642648
});
643649
}
644650

645651
@SuppressWarnings({ "unchecked", "rawtypes" })
646-
private <T> Mono<T> doUpdate(T entity, SqlIdentifier tableName, RelationalPersistentEntity<T> persistentEntity,
652+
private <T> Mono<T> doUpdate(T entity, @Nullable Object version, SqlIdentifier tableName,
653+
RelationalPersistentEntity<T> persistentEntity,
647654
Criteria criteria, OutboundRow outboundRow) {
648655

649656
Update update = Update.from((Map) outboundRow);
650-
651657
StatementMapper mapper = dataAccessStrategy.getStatementMapper();
652658
StatementMapper.UpdateSpec updateSpec = mapper.createUpdate(tableName, update).withCriteria(criteria);
653659

@@ -664,27 +670,11 @@ private <T> Mono<T> doUpdate(T entity, SqlIdentifier tableName, RelationalPersis
664670
}
665671

666672
if (persistentEntity.hasVersionProperty()) {
667-
sink.error(new OptimisticLockingFailureException(
668-
formatOptimisticLockingExceptionMessage(entity, persistentEntity)));
669-
} else {
670-
sink.error(new TransientDataAccessResourceException(
671-
formatTransientEntityExceptionMessage(entity, persistentEntity)));
673+
sink.error(OptimisticLockingUtils.updateFailed(entity, version, persistentEntity));
672674
}
673675
}).then(maybeCallAfterSave(entity, outboundRow, tableName));
674676
}
675677

676-
private <T> String formatOptimisticLockingExceptionMessage(T entity, RelationalPersistentEntity<T> persistentEntity) {
677-
678-
return String.format("Failed to update table [%s]; Version does not match for row with Id [%s]",
679-
persistentEntity.getQualifiedTableName(), persistentEntity.getIdentifierAccessor(entity).getIdentifier());
680-
}
681-
682-
private <T> String formatTransientEntityExceptionMessage(T entity, RelationalPersistentEntity<T> persistentEntity) {
683-
684-
return String.format("Failed to update table [%s]; Row with Id [%s] does not exist",
685-
persistentEntity.getQualifiedTableName(), persistentEntity.getIdentifierAccessor(entity).getIdentifier());
686-
}
687-
688678
@SuppressWarnings("unchecked")
689679
private <T> T incrementVersion(RelationalPersistentEntity<T> persistentEntity, T entity) {
690680

@@ -706,7 +696,7 @@ private <T> T incrementVersion(RelationalPersistentEntity<T> persistentEntity, T
706696
private <T> Criteria createMatchingVersionCriteria(T entity, RelationalPersistentEntity<T> persistentEntity) {
707697

708698
PersistentPropertyAccessor<?> propertyAccessor = persistentEntity.getPropertyAccessor(entity);
709-
RelationalPersistentProperty versionProperty = persistentEntity.getVersionProperty();
699+
RelationalPersistentProperty versionProperty = persistentEntity.getRequiredVersionProperty();
710700

711701
Object version = propertyAccessor.getProperty(versionProperty);
712702
Criteria.CriteriaStep versionColumn = Criteria.where(dataAccessStrategy.toSql(versionProperty.getColumnName()));
@@ -724,7 +714,13 @@ public <T> Mono<T> delete(T entity) throws DataAccessException {
724714

725715
RelationalPersistentEntity<?> persistentEntity = getRequiredEntity(entity);
726716

727-
return delete(getByIdQuery(entity, persistentEntity), persistentEntity.getType()).thenReturn(entity);
717+
Mono<Long> delete = delete(getByIdQuery(entity, persistentEntity), persistentEntity.getType());
718+
if (persistentEntity.hasVersionProperty()) {
719+
delete = delete.flatMap(
720+
it -> it == 0 ? Mono.error(OptimisticLockingUtils.deleteFailed(entity, persistentEntity)) : Mono.just(it));
721+
}
722+
723+
return delete.thenReturn(entity);
728724
}
729725

730726
protected <T> Mono<T> maybeCallBeforeConvert(T object, SqlIdentifier table) {
@@ -771,8 +767,17 @@ private <T> Query getByIdQuery(T entity, RelationalPersistentEntity<?> persisten
771767

772768
IdentifierAccessor identifierAccessor = persistentEntity.getIdentifierAccessor(entity);
773769
Object id = identifierAccessor.getRequiredIdentifier();
770+
Criteria criteria = Criteria.where(persistentEntity.getRequiredIdProperty().getName()).is(id);
771+
772+
if (persistentEntity.hasVersionProperty()) {
773+
774+
RelationalPersistentProperty versionProperty = persistentEntity.getRequiredVersionProperty();
775+
Object version = persistentEntity.getPropertyAccessor(entity).getProperty(versionProperty);
776+
Criteria.CriteriaStep versionColumn = Criteria.where(versionProperty.getName());
777+
criteria = version == null ? criteria.and(versionColumn.isNull()) : criteria.and(versionColumn.is(version));
778+
}
774779

775-
return Query.query(Criteria.where(persistentEntity.getRequiredIdProperty().getName()).is(id));
780+
return Query.query(criteria);
776781
}
777782

778783
SqlIdentifier getTableName(Class<?> entityClass) {

0 commit comments

Comments
 (0)