Browse Source

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
3.5.x
Mark Paluch 3 weeks ago
parent
commit
0958a99331
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 23
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java
  2. 17
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java
  3. 12
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java
  4. 1
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java
  5. 11
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryConcurrencyIntegrationTests.java
  6. 5
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityOperations.java
  7. 55
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java
  8. 15
      spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/support/H2SimpleR2dbcRepositoryIntegrationTests.java
  9. 86
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/OptimisticLockingUtils.java

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

@ -15,12 +15,19 @@
*/ */
package org.springframework.data.jdbc.core; package org.springframework.data.jdbc.core;
import java.util.*; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.DataAccessStrategy;
import org.springframework.data.jdbc.core.convert.Identifier; import org.springframework.data.jdbc.core.convert.Identifier;
import org.springframework.data.jdbc.core.convert.InsertSubject; import org.springframework.data.jdbc.core.convert.InsertSubject;
@ -33,6 +40,7 @@ import org.springframework.data.relational.core.conversion.DbAction;
import org.springframework.data.relational.core.conversion.DbActionExecutionResult; import org.springframework.data.relational.core.conversion.DbActionExecutionResult;
import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.conversion.IdValueSource;
import org.springframework.data.relational.core.mapping.AggregatePath; import org.springframework.data.relational.core.mapping.AggregatePath;
import org.springframework.data.relational.core.mapping.OptimisticLockingUtils;
import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@ -54,9 +62,6 @@ import org.springframework.util.Assert;
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
class JdbcAggregateChangeExecutionContext { class JdbcAggregateChangeExecutionContext {
private static final String UPDATE_FAILED = "Failed to update entity [%s]; Id [%s] not found in database";
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";
private final RelationalMappingContext context; private final RelationalMappingContext context;
private final JdbcConverter converter; private final JdbcConverter converter;
private final DataAccessStrategy accessStrategy; private final DataAccessStrategy accessStrategy;
@ -334,7 +339,7 @@ class JdbcAggregateChangeExecutionContext {
} }
private <T> void updateWithoutVersion(DbAction.UpdateRoot<T> update) { private <T> void updateWithoutVersion(DbAction.UpdateRoot<T> update) {
accessStrategy.update(update.entity(), update.getEntityType()); accessStrategy.update(update.getEntity(), update.getEntityType());
} }
private <T> void updateWithVersion(DbAction.UpdateRoot<T> update) { private <T> void updateWithVersion(DbAction.UpdateRoot<T> update) {
@ -343,8 +348,8 @@ class JdbcAggregateChangeExecutionContext {
Assert.notNull(previousVersion, "The root aggregate cannot be updated because the version property is null"); Assert.notNull(previousVersion, "The root aggregate cannot be updated because the version property is null");
if (!accessStrategy.updateWithVersion(update.getEntity(), update.getEntityType(), previousVersion)) { if (!accessStrategy.updateWithVersion(update.getEntity(), update.getEntityType(), previousVersion)) {
throw OptimisticLockingUtils.updateFailed(update.getEntity(), previousVersion,
throw new OptimisticLockingFailureException(String.format(UPDATE_FAILED_OPTIMISTIC_LOCKING, update.getEntity())); getRequiredPersistentEntity(update.getEntityType()));
} }
} }

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

@ -19,7 +19,6 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
import org.springframework.data.domain.Example; import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
@ -46,8 +45,8 @@ public interface JdbcAggregateOperations {
* @param instance the aggregate root of the aggregate to be saved. Must not be {@code null}. * @param instance the aggregate root of the aggregate to be saved. Must not be {@code null}.
* @param <T> the type of the aggregate root. * @param <T> the type of the aggregate root.
* @return the saved instance. * @return the saved instance.
* @throws IncorrectUpdateSemanticsDataAccessException when the instance is determined to be not new and the resulting * @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
* update does not update any rows. * {@link org.springframework.data.annotation.Version} is defined.
*/ */
<T> T save(T instance); <T> T save(T instance);
@ -57,8 +56,8 @@ public interface JdbcAggregateOperations {
* @param instances the aggregate roots to be saved. Must not be {@code null}. * @param instances the aggregate roots to be saved. Must not be {@code null}.
* @param <T> the type of the aggregate root. * @param <T> the type of the aggregate root.
* @return the saved instances. * @return the saved instances.
* @throws IncorrectUpdateSemanticsDataAccessException when at least one instance is determined to be not new and the * @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
* resulting update does not update any rows. * {@link org.springframework.data.annotation.Version} is defined.
* @since 3.0 * @since 3.0
*/ */
<T> List<T> saveAll(Iterable<T> instances); <T> List<T> saveAll(Iterable<T> instances);
@ -95,6 +94,8 @@ public interface JdbcAggregateOperations {
* @param instance the aggregate root of the aggregate to be inserted. Must not be {@code null}. * @param instance the aggregate root of the aggregate to be inserted. Must not be {@code null}.
* @param <T> the type of the aggregate root. * @param <T> the type of the aggregate root.
* @return the saved instance. * @return the saved instance.
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
* {@link org.springframework.data.annotation.Version} is defined.
*/ */
<T> T update(T instance); <T> T update(T instance);
@ -104,6 +105,8 @@ public interface JdbcAggregateOperations {
* @param instances the aggregate roots to be inserted. Must not be {@code null}. * @param instances the aggregate roots to be inserted. Must not be {@code null}.
* @param <T> the type of the aggregate root. * @param <T> the type of the aggregate root.
* @return the saved instances. * @return the saved instances.
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
* {@link org.springframework.data.annotation.Version} is defined.
* @since 3.1 * @since 3.1
*/ */
<T> List<T> updateAll(Iterable<T> instances); <T> List<T> updateAll(Iterable<T> instances);
@ -315,6 +318,8 @@ public interface JdbcAggregateOperations {
* *
* @param aggregateRoot to delete. Must not be {@code null}. * @param aggregateRoot to delete. Must not be {@code null}.
* @param <T> the type of the aggregate root. * @param <T> the type of the aggregate root.
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
* {@link org.springframework.data.annotation.Version} is defined.
*/ */
<T> void delete(T aggregateRoot); <T> void delete(T aggregateRoot);
@ -330,6 +335,8 @@ public interface JdbcAggregateOperations {
* *
* @param aggregateRoots to delete. Must not be {@code null}. * @param aggregateRoots to delete. Must not be {@code null}.
* @param <T> the type of the aggregate roots. * @param <T> the type of the aggregate roots.
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
* {@link org.springframework.data.annotation.Version} is defined.
*/ */
<T> void deleteAll(Iterable<? extends T> aggregateRoots); <T> void deleteAll(Iterable<? extends T> aggregateRoots);
} }

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

@ -25,13 +25,13 @@ import java.util.Optional;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.conversion.IdValueSource;
import org.springframework.data.relational.core.mapping.AggregatePath; import org.springframework.data.relational.core.mapping.AggregatePath;
import org.springframework.data.relational.core.mapping.AggregatePath.TableInfo; import org.springframework.data.relational.core.mapping.AggregatePath.TableInfo;
import org.springframework.data.relational.core.mapping.OptimisticLockingUtils;
import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@ -146,8 +146,6 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
@Override @Override
public <S> boolean updateWithVersion(S instance, Class<S> domainType, Number previousVersion) { public <S> boolean updateWithVersion(S instance, Class<S> domainType, Number previousVersion) {
RelationalPersistentEntity<S> persistentEntity = getRequiredPersistentEntity(domainType);
// Adjust update statement to set the new version and use the old version in where clause. // Adjust update statement to set the new version and use the old version in where clause.
SqlIdentifierParameterSource parameterSource = sqlParametersFactory.forUpdate(instance, domainType); SqlIdentifierParameterSource parameterSource = sqlParametersFactory.forUpdate(instance, domainType);
parameterSource.addValue(VERSION_SQL_PARAMETER, previousVersion); parameterSource.addValue(VERSION_SQL_PARAMETER, previousVersion);
@ -155,9 +153,8 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
int affectedRows = operations.update(sql(domainType).getUpdateWithVersion(), parameterSource); int affectedRows = operations.update(sql(domainType).getUpdateWithVersion(), parameterSource);
if (affectedRows == 0) { if (affectedRows == 0) {
RelationalPersistentEntity<S> persistentEntity = getRequiredPersistentEntity(domainType);
throw new OptimisticLockingFailureException( throw OptimisticLockingUtils.updateFailed(instance, previousVersion, persistentEntity);
String.format("Optimistic lock exception on saving entity of type %s", persistentEntity.getName()));
} }
return true; return true;
@ -193,8 +190,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
int affectedRows = operations.update(sql(domainType).getDeleteByIdAndVersion(), parameterSource); int affectedRows = operations.update(sql(domainType).getDeleteByIdAndVersion(), parameterSource);
if (affectedRows == 0) { if (affectedRows == 0) {
throw new OptimisticLockingFailureException( throw OptimisticLockingUtils.deleteFailed(id, previousVersion, persistentEntity);
String.format("Optimistic lock exception deleting entity of type %s", persistentEntity.getName()));
} }
} }

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

@ -37,7 +37,6 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.annotation.PersistenceCreator;

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

@ -37,7 +37,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.jdbc.testing.TestClass; import org.springframework.data.jdbc.testing.TestClass;
@ -157,11 +156,6 @@ public class JdbcRepositoryConcurrencyIntegrationTests {
try { try {
return repository.save(e); return repository.save(e);
} catch (Exception ex) { } catch (Exception ex) {
// When the delete execution is complete, the Update execution throws an
// IncorrectUpdateSemanticsDataAccessException.
if (ex.getCause() instanceof IncorrectUpdateSemanticsDataAccessException) {
return null;
}
throw ex; throw ex;
} }
}; };
@ -191,11 +185,6 @@ public class JdbcRepositoryConcurrencyIntegrationTests {
try { try {
return repository.save(e); return repository.save(e);
} catch (Exception ex) { } catch (Exception ex) {
// When the delete execution is complete, the Update execution throws an
// IncorrectUpdateSemanticsDataAccessException.
if (ex.getCause() instanceof IncorrectUpdateSemanticsDataAccessException) {
return null;
}
throw ex; throw ex;
} }
}; };

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

@ -273,6 +273,8 @@ public interface R2dbcEntityOperations extends FluentR2dbcOperations {
* @return the updated entity. * @return the updated entity.
* @throws DataAccessException if there is any problem issuing the execution. * @throws DataAccessException if there is any problem issuing the execution.
* @throws TransientDataAccessResourceException if the update did not affect any rows. * @throws TransientDataAccessResourceException if the update did not affect any rows.
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
* {@link org.springframework.data.annotation.Version} is defined.
*/ */
<T> Mono<T> update(T entity) throws DataAccessException; <T> Mono<T> update(T entity) throws DataAccessException;
@ -282,6 +284,9 @@ public interface R2dbcEntityOperations extends FluentR2dbcOperations {
* @param entity must not be {@literal null}. * @param entity must not be {@literal null}.
* @return the deleted entity. * @return the deleted entity.
* @throws DataAccessException if there is any problem issuing the execution. * @throws DataAccessException if there is any problem issuing the execution.
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
* {@link org.springframework.data.annotation.Version} is defined.
*/ */
<T> Mono<T> delete(T entity) throws DataAccessException; <T> Mono<T> delete(T entity) throws DataAccessException;
} }

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

@ -41,8 +41,6 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationContextAware;
import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConversionService;
import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.dao.TransientDataAccessResourceException;
import org.springframework.data.mapping.IdentifierAccessor; import org.springframework.data.mapping.IdentifierAccessor;
import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PersistentPropertyAccessor;
@ -59,6 +57,7 @@ import org.springframework.data.r2dbc.mapping.event.AfterSaveCallback;
import org.springframework.data.r2dbc.mapping.event.BeforeConvertCallback; import org.springframework.data.r2dbc.mapping.event.BeforeConvertCallback;
import org.springframework.data.r2dbc.mapping.event.BeforeSaveCallback; import org.springframework.data.r2dbc.mapping.event.BeforeSaveCallback;
import org.springframework.data.relational.core.conversion.AbstractRelationalConverter; import org.springframework.data.relational.core.conversion.AbstractRelationalConverter;
import org.springframework.data.relational.core.mapping.OptimisticLockingUtils;
import org.springframework.data.relational.core.mapping.PersistentPropertyTranslator; import org.springframework.data.relational.core.mapping.PersistentPropertyTranslator;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@ -605,15 +604,22 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw
return maybeCallBeforeConvert(entity, tableName).flatMap(onBeforeConvert -> { return maybeCallBeforeConvert(entity, tableName).flatMap(onBeforeConvert -> {
T entityToUse; T entityToUse;
Object version;
Criteria matchingVersionCriteria; Criteria matchingVersionCriteria;
if (persistentEntity.hasVersionProperty()) { if (persistentEntity.hasVersionProperty()) {
PersistentPropertyAccessor<?> propertyAccessor = persistentEntity.getPropertyAccessor(entity);
RelationalPersistentProperty versionProperty = persistentEntity.getRequiredVersionProperty();
version = propertyAccessor.getProperty(versionProperty);
matchingVersionCriteria = createMatchingVersionCriteria(onBeforeConvert, persistentEntity); matchingVersionCriteria = createMatchingVersionCriteria(onBeforeConvert, persistentEntity);
entityToUse = incrementVersion(persistentEntity, onBeforeConvert); entityToUse = incrementVersion(persistentEntity, onBeforeConvert);
} else { } else {
entityToUse = onBeforeConvert; entityToUse = onBeforeConvert;
version = null;
matchingVersionCriteria = null; matchingVersionCriteria = null;
} }
@ -637,17 +643,17 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw
criteria = criteria.and(matchingVersionCriteria); criteria = criteria.and(matchingVersionCriteria);
} }
return doUpdate(onBeforeSave, tableName, persistentEntity, criteria, outboundRow); return doUpdate(onBeforeSave, version, tableName, persistentEntity, criteria, outboundRow);
}); });
}); });
} }
@SuppressWarnings({ "unchecked", "rawtypes" }) @SuppressWarnings({ "unchecked", "rawtypes" })
private <T> Mono<T> doUpdate(T entity, SqlIdentifier tableName, RelationalPersistentEntity<T> persistentEntity, private <T> Mono<T> doUpdate(T entity, @Nullable Object version, SqlIdentifier tableName,
RelationalPersistentEntity<T> persistentEntity,
Criteria criteria, OutboundRow outboundRow) { Criteria criteria, OutboundRow outboundRow) {
Update update = Update.from((Map) outboundRow); Update update = Update.from((Map) outboundRow);
StatementMapper mapper = dataAccessStrategy.getStatementMapper(); StatementMapper mapper = dataAccessStrategy.getStatementMapper();
StatementMapper.UpdateSpec updateSpec = mapper.createUpdate(tableName, update).withCriteria(criteria); StatementMapper.UpdateSpec updateSpec = mapper.createUpdate(tableName, update).withCriteria(criteria);
@ -664,27 +670,11 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw
} }
if (persistentEntity.hasVersionProperty()) { if (persistentEntity.hasVersionProperty()) {
sink.error(new OptimisticLockingFailureException( sink.error(OptimisticLockingUtils.updateFailed(entity, version, persistentEntity));
formatOptimisticLockingExceptionMessage(entity, persistentEntity)));
} else {
sink.error(new TransientDataAccessResourceException(
formatTransientEntityExceptionMessage(entity, persistentEntity)));
} }
}).then(maybeCallAfterSave(entity, outboundRow, tableName)); }).then(maybeCallAfterSave(entity, outboundRow, tableName));
} }
private <T> String formatOptimisticLockingExceptionMessage(T entity, RelationalPersistentEntity<T> persistentEntity) {
return String.format("Failed to update table [%s]; Version does not match for row with Id [%s]",
persistentEntity.getQualifiedTableName(), persistentEntity.getIdentifierAccessor(entity).getIdentifier());
}
private <T> String formatTransientEntityExceptionMessage(T entity, RelationalPersistentEntity<T> persistentEntity) {
return String.format("Failed to update table [%s]; Row with Id [%s] does not exist",
persistentEntity.getQualifiedTableName(), persistentEntity.getIdentifierAccessor(entity).getIdentifier());
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T> T incrementVersion(RelationalPersistentEntity<T> persistentEntity, T entity) { private <T> T incrementVersion(RelationalPersistentEntity<T> persistentEntity, T entity) {
@ -706,7 +696,7 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw
private <T> Criteria createMatchingVersionCriteria(T entity, RelationalPersistentEntity<T> persistentEntity) { private <T> Criteria createMatchingVersionCriteria(T entity, RelationalPersistentEntity<T> persistentEntity) {
PersistentPropertyAccessor<?> propertyAccessor = persistentEntity.getPropertyAccessor(entity); PersistentPropertyAccessor<?> propertyAccessor = persistentEntity.getPropertyAccessor(entity);
RelationalPersistentProperty versionProperty = persistentEntity.getVersionProperty(); RelationalPersistentProperty versionProperty = persistentEntity.getRequiredVersionProperty();
Object version = propertyAccessor.getProperty(versionProperty); Object version = propertyAccessor.getProperty(versionProperty);
Criteria.CriteriaStep versionColumn = Criteria.where(dataAccessStrategy.toSql(versionProperty.getColumnName())); Criteria.CriteriaStep versionColumn = Criteria.where(dataAccessStrategy.toSql(versionProperty.getColumnName()));
@ -724,7 +714,13 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw
RelationalPersistentEntity<?> persistentEntity = getRequiredEntity(entity); RelationalPersistentEntity<?> persistentEntity = getRequiredEntity(entity);
return delete(getByIdQuery(entity, persistentEntity), persistentEntity.getType()).thenReturn(entity); Mono<Long> delete = delete(getByIdQuery(entity, persistentEntity), persistentEntity.getType());
if (persistentEntity.hasVersionProperty()) {
delete = delete.flatMap(
it -> it == 0 ? Mono.error(OptimisticLockingUtils.deleteFailed(entity, persistentEntity)) : Mono.just(it));
}
return delete.thenReturn(entity);
} }
protected <T> Mono<T> maybeCallBeforeConvert(T object, SqlIdentifier table) { protected <T> Mono<T> maybeCallBeforeConvert(T object, SqlIdentifier table) {
@ -771,8 +767,17 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw
IdentifierAccessor identifierAccessor = persistentEntity.getIdentifierAccessor(entity); IdentifierAccessor identifierAccessor = persistentEntity.getIdentifierAccessor(entity);
Object id = identifierAccessor.getRequiredIdentifier(); Object id = identifierAccessor.getRequiredIdentifier();
Criteria criteria = Criteria.where(persistentEntity.getRequiredIdProperty().getName()).is(id);
if (persistentEntity.hasVersionProperty()) {
RelationalPersistentProperty versionProperty = persistentEntity.getRequiredVersionProperty();
Object version = persistentEntity.getPropertyAccessor(entity).getProperty(versionProperty);
Criteria.CriteriaStep versionColumn = Criteria.where(versionProperty.getName());
criteria = version == null ? criteria.and(versionColumn.isNull()) : criteria.and(versionColumn.is(version));
}
return Query.query(Criteria.where(persistentEntity.getRequiredIdProperty().getName()).is(id)); return Query.query(criteria);
} }
SqlIdentifier getTableName(Class<?> entityClass) { SqlIdentifier getTableName(Class<?> entityClass) {

15
spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/support/H2SimpleR2dbcRepositoryIntegrationTests.java

@ -18,10 +18,10 @@ package org.springframework.data.r2dbc.repository.support;
import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactory;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessException;
import org.springframework.dao.TransientDataAccessException;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Persistable; import org.springframework.data.domain.Persistable;
import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration; import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration;
@ -73,7 +73,7 @@ public class H2SimpleR2dbcRepositoryIntegrationTests extends AbstractSimpleR2dbc
return H2TestSupport.CREATE_TABLE_LEGOSET_WITH_ID_GENERATION; return H2TestSupport.CREATE_TABLE_LEGOSET_WITH_ID_GENERATION;
} }
@Test // gh-90 @Test // GH-90
void shouldInsertNewObjectWithGivenId() { void shouldInsertNewObjectWithGivenId() {
try { try {
@ -103,18 +103,15 @@ public class H2SimpleR2dbcRepositoryIntegrationTests extends AbstractSimpleR2dbc
assertThat(map).containsEntry("name", "SCHAUFELRADBAGGER").containsKey("id"); assertThat(map).containsEntry("name", "SCHAUFELRADBAGGER").containsKey("id");
} }
@Test // gh-232 @Test // GH-232, GH-2176
void updateShouldFailIfRowDoesNotExist() { void updateShouldNotFailIfRowDoesNotExist() {
LegoSet legoSet = new LegoSet(9999, "SCHAUFELRADBAGGER", 12); LegoSet legoSet = new LegoSet(9999, "SCHAUFELRADBAGGER", 12);
repository.save(legoSet) // repository.save(legoSet) //
.as(StepVerifier::create) // .as(StepVerifier::create) //
.verifyErrorSatisfies(actual -> { .expectNextCount(1) //
.verifyComplete();
assertThat(actual).isInstanceOf(TransientDataAccessException.class)
.hasMessage("Failed to update table [legoset]; Row with Id [9999] does not exist");
});
} }
static class AlwaysNew implements Persistable<Long> { static class AlwaysNew implements Persistable<Long> {

86
spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/OptimisticLockingUtils.java

@ -0,0 +1,86 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.relational.core.mapping;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.mapping.IdentifierAccessor;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.lang.Nullable;
/**
* Utility methods to create {@link OptimisticLockingFailureException}s.
* <p>
* Strictly for internal use within the framework.
*
* @author Mark Paluch
* @since 3.5.7
*/
public class OptimisticLockingUtils {
/**
* Create an {@link OptimisticLockingFailureException} for an update failure.
*
* @param entity the object.
* @param version the object version.
* @param persistentEntity the {@link RelationalPersistentEntity} metadata.
* @return the exception.
*/
public static OptimisticLockingFailureException updateFailed(Object entity, @Nullable Object version,
RelationalPersistentEntity<?> persistentEntity) {
IdentifierAccessor identifierAccessor = persistentEntity.getIdentifierAccessor(entity);
Object id = identifierAccessor.getRequiredIdentifier();
return new OptimisticLockingFailureException(String.format(
"Failed to update versioned entity with id '%s' (version '%s') in table [%s]; Was the entity updated or deleted concurrently?",
id, version, persistentEntity.getTableName()));
}
/**
* Create an {@link OptimisticLockingFailureException} for a delete failure.
*
* @param entity actual entity to be deleted.
* @param persistentEntity the {@link RelationalPersistentEntity} metadata.
* @return the exception.
*/
public static OptimisticLockingFailureException deleteFailed(Object entity,
RelationalPersistentEntity<?> persistentEntity) {
IdentifierAccessor identifierAccessor = persistentEntity.getIdentifierAccessor(entity);
Object id = identifierAccessor.getRequiredIdentifier();
PersistentProperty<?> versionProperty = persistentEntity.getRequiredVersionProperty();
Object version = persistentEntity.getPropertyAccessor(entity).getProperty(versionProperty);
return deleteFailed(id, version, persistentEntity);
}
/**
* Create an {@link OptimisticLockingFailureException} for a delete failure.
*
* @param id the object identifier.
* @param version the object version.
* @param persistentEntity the {@link RelationalPersistentEntity} metadata.
* @return the exception.
*/
public static OptimisticLockingFailureException deleteFailed(@Nullable Object id, @Nullable Object version,
RelationalPersistentEntity<?> persistentEntity) {
return new OptimisticLockingFailureException(String.format(
"Failed to delete versioned entity with id '%s' (version '%s') in table [%s]; Was the entity updated or deleted concurrently?",
id, version, persistentEntity.getTableName()));
}
}
Loading…
Cancel
Save