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
4.0.x
Mark Paluch 3 weeks ago
parent
commit
2c580ce02f
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 22
      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. 13
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java
  4. 2
      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. 58
      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. 87
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/OptimisticLockingUtils.java

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

@ -15,14 +15,22 @@ @@ -15,14 +15,22 @@
*/
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.Function;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
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.Identifier;
import org.springframework.data.jdbc.core.convert.InsertSubject;
@ -35,6 +43,7 @@ import org.springframework.data.relational.core.conversion.DbAction; @@ -35,6 +43,7 @@ import org.springframework.data.relational.core.conversion.DbAction;
import org.springframework.data.relational.core.conversion.DbActionExecutionResult;
import org.springframework.data.relational.core.conversion.IdValueSource;
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.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@ -55,9 +64,6 @@ import org.springframework.util.Assert; @@ -55,9 +64,6 @@ import org.springframework.util.Assert;
@SuppressWarnings("rawtypes")
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 JdbcConverter converter;
private final DataAccessStrategy accessStrategy;
@ -357,8 +363,8 @@ class JdbcAggregateChangeExecutionContext { @@ -357,8 +363,8 @@ class JdbcAggregateChangeExecutionContext {
Assert.notNull(previousVersion, "The root aggregate cannot be updated because the version property is null");
if (!accessStrategy.updateWithVersion(update.entity(), update.getEntityType(), previousVersion)) {
throw new OptimisticLockingFailureException(String.format(UPDATE_FAILED_OPTIMISTIC_LOCKING, update.entity()));
throw OptimisticLockingUtils.updateFailed(update.entity(), previousVersion,
getRequiredPersistentEntity(update.getEntityType()));
}
}

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

@ -20,7 +20,6 @@ import java.util.Optional; @@ -20,7 +20,6 @@ import java.util.Optional;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@ -50,8 +49,8 @@ public interface JdbcAggregateOperations { @@ -50,8 +49,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.
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
* {@link org.springframework.data.annotation.Version} is defined.
*/
<T> T save(T instance);
@ -61,8 +60,8 @@ public interface JdbcAggregateOperations { @@ -61,8 +60,8 @@ public interface JdbcAggregateOperations {
* @param instances the aggregate roots to be saved. Must not be {@code null}.
* @param <T> the type of the aggregate root.
* @return the saved instances.
* @throws IncorrectUpdateSemanticsDataAccessException when at least one instance is determined to be not new and the
* resulting update does not update any rows.
* @throws org.springframework.dao.OptimisticLockingFailureException in case of version mismatch in case a
* {@link org.springframework.data.annotation.Version} is defined.
* @since 3.0
*/
<T> List<T> saveAll(Iterable<T> instances);
@ -99,6 +98,8 @@ public interface JdbcAggregateOperations { @@ -99,6 +98,8 @@ public interface JdbcAggregateOperations {
* @param instance the aggregate root of the aggregate to be inserted. Must not be {@code null}.
* @param <T> the type of the aggregate root.
* @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);
@ -108,6 +109,8 @@ public interface JdbcAggregateOperations { @@ -108,6 +109,8 @@ public interface JdbcAggregateOperations {
* @param instances the aggregate roots to be inserted. Must not be {@code null}.
* @param <T> the type of the aggregate root.
* @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
*/
<T> List<T> updateAll(Iterable<T> instances);
@ -319,6 +322,8 @@ public interface JdbcAggregateOperations { @@ -319,6 +322,8 @@ public interface JdbcAggregateOperations {
*
* @param aggregateRoot to delete. Must not be {@code null}.
* @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);
@ -334,6 +339,8 @@ public interface JdbcAggregateOperations { @@ -334,6 +339,8 @@ public interface JdbcAggregateOperations {
*
* @param aggregateRoots to delete. Must not be {@code null}.
* @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);

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

@ -25,8 +25,8 @@ import java.util.Optional; @@ -25,8 +25,8 @@ import java.util.Optional;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.PersistentPropertyPath;
@ -34,6 +34,7 @@ import org.springframework.data.relational.core.conversion.IdValueSource; @@ -34,6 +34,7 @@ import org.springframework.data.relational.core.conversion.IdValueSource;
import org.springframework.data.relational.core.dialect.Dialect;
import org.springframework.data.relational.core.mapping.AggregatePath;
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.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@ -164,8 +165,6 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { @@ -164,8 +165,6 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
@Override
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.
SqlIdentifierParameterSource parameterSource = sqlParametersFactory.forUpdate(instance, domainType);
parameterSource.addValue(VERSION_SQL_PARAMETER, previousVersion);
@ -173,9 +172,8 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { @@ -173,9 +172,8 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
int affectedRows = operations.update(sql(domainType).getUpdateWithVersion(), parameterSource);
if (affectedRows == 0) {
throw new OptimisticLockingFailureException(
String.format("Optimistic lock exception on saving entity of type %s", persistentEntity.getName()));
RelationalPersistentEntity<S> persistentEntity = getRequiredPersistentEntity(domainType);
throw OptimisticLockingUtils.updateFailed(instance, previousVersion, persistentEntity);
}
return true;
@ -211,8 +209,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { @@ -211,8 +209,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
int affectedRows = operations.update(sql(domainType).getDeleteByIdAndVersion(), parameterSource);
if (affectedRows == 0) {
throw new OptimisticLockingFailureException(
String.format("Optimistic lock exception deleting entity of type %s", persistentEntity.getName()));
throw OptimisticLockingUtils.deleteFailed(id, previousVersion, persistentEntity);
}
}

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

@ -24,6 +24,7 @@ import static org.springframework.data.jdbc.testing.TestDatabaseFeatures.Feature @@ -24,6 +24,7 @@ import static org.springframework.data.jdbc.testing.TestDatabaseFeatures.Feature
import java.time.LocalDateTime;
import java.util.*;
import java.util.ArrayList;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
@ -36,7 +37,6 @@ import org.springframework.context.annotation.Bean; @@ -36,7 +37,6 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.annotation.Id;
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; @@ -37,7 +37,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.jdbc.testing.TestClass;
@ -155,11 +154,6 @@ public class JdbcRepositoryConcurrencyIntegrationTests { @@ -155,11 +154,6 @@ public class JdbcRepositoryConcurrencyIntegrationTests {
try {
return repository.save(e);
} catch (Exception ex) {
// When the delete execution is complete, the Update execution throws an
// IncorrectUpdateSemanticsDataAccessException.
if (ex instanceof IncorrectUpdateSemanticsDataAccessException) {
return null;
}
throw ex;
}
};
@ -189,11 +183,6 @@ public class JdbcRepositoryConcurrencyIntegrationTests { @@ -189,11 +183,6 @@ public class JdbcRepositoryConcurrencyIntegrationTests {
try {
return repository.save(e);
} catch (Exception ex) {
// When the delete execution is complete, the Update execution throws an
// IncorrectUpdateSemanticsDataAccessException.
if (ex instanceof IncorrectUpdateSemanticsDataAccessException) {
return null;
}
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 { @@ -273,6 +273,8 @@ public interface R2dbcEntityOperations extends FluentR2dbcOperations {
* @return the updated entity.
* @throws DataAccessException if there is any problem issuing the execution.
* @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;
@ -282,6 +284,9 @@ public interface R2dbcEntityOperations extends FluentR2dbcOperations { @@ -282,6 +284,9 @@ public interface R2dbcEntityOperations extends FluentR2dbcOperations {
* @param entity must not be {@literal null}.
* @return the deleted entity.
* @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;
}

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

@ -35,6 +35,7 @@ import java.util.stream.Collectors; @@ -35,6 +35,7 @@ import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.Publisher;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
@ -42,8 +43,6 @@ import org.springframework.context.ApplicationContext; @@ -42,8 +43,6 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.convert.ConversionService;
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.MappingException;
import org.springframework.data.mapping.PersistentPropertyAccessor;
@ -60,6 +59,7 @@ import org.springframework.data.r2dbc.mapping.event.AfterSaveCallback; @@ -60,6 +59,7 @@ import org.springframework.data.r2dbc.mapping.event.AfterSaveCallback;
import org.springframework.data.r2dbc.mapping.event.BeforeConvertCallback;
import org.springframework.data.r2dbc.mapping.event.BeforeSaveCallback;
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.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@ -607,15 +607,22 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw @@ -607,15 +607,22 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw
return maybeCallBeforeConvert(entity, tableName).flatMap(onBeforeConvert -> {
T entityToUse;
Object version;
Criteria matchingVersionCriteria;
if (persistentEntity.hasVersionProperty()) {
PersistentPropertyAccessor<?> propertyAccessor = persistentEntity.getPropertyAccessor(entity);
RelationalPersistentProperty versionProperty = persistentEntity.getRequiredVersionProperty();
version = propertyAccessor.getProperty(versionProperty);
matchingVersionCriteria = createMatchingVersionCriteria(onBeforeConvert, persistentEntity);
entityToUse = incrementVersion(persistentEntity, onBeforeConvert);
} else {
entityToUse = onBeforeConvert;
version = null;
matchingVersionCriteria = null;
}
@ -653,17 +660,17 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw @@ -653,17 +660,17 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw
criteria = criteria.and(matchingVersionCriteria);
}
return doUpdate(onBeforeSave, tableName, persistentEntity, criteria, outboundRow);
return doUpdate(onBeforeSave, version, tableName, persistentEntity, criteria, outboundRow);
});
});
}
@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) {
Update update = Update.from((Map) outboundRow);
StatementMapper mapper = dataAccessStrategy.getStatementMapper();
StatementMapper.UpdateSpec updateSpec = mapper.createUpdate(tableName, update).withCriteria(criteria);
@ -680,27 +687,11 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw @@ -680,27 +687,11 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw
}
if (persistentEntity.hasVersionProperty()) {
sink.error(new OptimisticLockingFailureException(
formatOptimisticLockingExceptionMessage(entity, persistentEntity)));
} else {
sink.error(new TransientDataAccessResourceException(
formatTransientEntityExceptionMessage(entity, persistentEntity)));
sink.error(OptimisticLockingUtils.updateFailed(entity, version, persistentEntity));
}
}).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")
private <T> T incrementVersion(RelationalPersistentEntity<T> persistentEntity, T entity) {
@ -728,9 +719,7 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw @@ -728,9 +719,7 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw
private <T> Criteria createMatchingVersionCriteria(T entity, RelationalPersistentEntity<T> persistentEntity) {
PersistentPropertyAccessor<?> propertyAccessor = persistentEntity.getPropertyAccessor(entity);
RelationalPersistentProperty versionProperty = persistentEntity.getVersionProperty();
Assert.state(versionProperty != null, "Version property must not be null");
RelationalPersistentProperty versionProperty = persistentEntity.getRequiredVersionProperty();
Object version = propertyAccessor.getProperty(versionProperty);
Criteria.CriteriaStep versionColumn = Criteria.where(dataAccessStrategy.toSql(versionProperty.getColumnName()));
@ -748,7 +737,13 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw @@ -748,7 +737,13 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw
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) {
@ -795,8 +790,17 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw @@ -795,8 +790,17 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw
IdentifierAccessor identifierAccessor = persistentEntity.getIdentifierAccessor(entity);
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) {

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

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

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

@ -0,0 +1,87 @@ @@ -0,0 +1,87 @@
/*
* 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.jspecify.annotations.Nullable;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.mapping.IdentifierAccessor;
import org.springframework.data.mapping.PersistentProperty;
/**
* Utility methods to create {@link OptimisticLockingFailureException}s.
* <p>
* Strictly for internal use within the framework.
*
* @author Mark Paluch
* @since 4.0.1
*/
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