DATAJDBC-219 - Implements optimistic record locking.
Optimistic locking is based on a numeric attribute annotated with `@Version` on the aggregate root. That attribute is increased before any save operation and checked during updates to ensure that the database state hasn't changed since loading the aggregate. Original pull request: #166.
This commit is contained in:
committed by
Jens Schauder
parent
8d0441c2e0
commit
f7a04dcc2b
@@ -22,6 +22,7 @@ import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
@@ -33,6 +34,7 @@ import org.springframework.data.relational.core.conversion.AggregateChange;
|
||||
import org.springframework.data.relational.core.conversion.DbAction;
|
||||
import org.springframework.data.relational.core.conversion.Interpreter;
|
||||
import org.springframework.data.relational.core.conversion.RelationalConverter;
|
||||
import org.springframework.data.relational.core.conversion.RelationalEntityVersionUtils;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
import org.springframework.data.util.Pair;
|
||||
@@ -45,6 +47,7 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Jens Schauder
|
||||
* @author Mark Paluch
|
||||
* @author Tyler Van Gorder
|
||||
* @since 1.2
|
||||
*/
|
||||
class AggregateChangeExecutor {
|
||||
@@ -60,7 +63,6 @@ class AggregateChangeExecutor {
|
||||
this.context = converter.getMappingContext();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
<T> void execute(AggregateChange<T> aggregateChange) {
|
||||
|
||||
List<DbAction<?>> actions = new ArrayList<>();
|
||||
@@ -70,17 +72,42 @@ class AggregateChangeExecutor {
|
||||
actions.add(action);
|
||||
});
|
||||
|
||||
T newRoot = (T) populateIdsIfNecessary(actions);
|
||||
|
||||
T newRoot = populateIdsIfNecessary(actions);
|
||||
if (newRoot != null) {
|
||||
newRoot = populateRootVersionIfNecessary(newRoot, actions);
|
||||
aggregateChange.setEntity(newRoot);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
private Object populateIdsIfNecessary(List<DbAction<?>> actions) {
|
||||
private <T> T populateRootVersionIfNecessary(T newRoot, List<DbAction<?>> actions) {
|
||||
|
||||
Object newRoot = null;
|
||||
// Does the root entity have a version attribute?
|
||||
RelationalPersistentEntity<T> persistentEntity = (RelationalPersistentEntity<T>) context
|
||||
.getRequiredPersistentEntity(newRoot.getClass());
|
||||
if (!persistentEntity.hasVersionProperty()) {
|
||||
return newRoot;
|
||||
}
|
||||
|
||||
// Find the root action
|
||||
Optional<DbAction<?>> rootAction = actions.parallelStream().filter(action -> action instanceof DbAction.WithVersion)
|
||||
.findFirst();
|
||||
|
||||
if (!rootAction.isPresent()) {
|
||||
// This really should never happen.
|
||||
return newRoot;
|
||||
}
|
||||
DbAction.WithVersion<T> versionAction = (DbAction.WithVersion<T>) rootAction.get();
|
||||
|
||||
return RelationalEntityVersionUtils.setVersionNumberOnEntity(newRoot,
|
||||
versionAction.getNextVersion(), persistentEntity, converter);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private <T> T populateIdsIfNecessary(List<DbAction<?>> actions) {
|
||||
|
||||
T newRoot = null;
|
||||
|
||||
// have the actions so that the inserts on the leaves come first.
|
||||
List<DbAction<?>> reverseActions = new ArrayList<>(actions);
|
||||
@@ -102,15 +129,15 @@ class AggregateChangeExecutor {
|
||||
if (newEntity != ((DbAction.WithGeneratedId<?>) action).getEntity()) {
|
||||
|
||||
if (action instanceof DbAction.Insert) {
|
||||
DbAction.Insert insert = (DbAction.Insert) action;
|
||||
DbAction.Insert<?> insert = (DbAction.Insert<?>) action;
|
||||
|
||||
Pair qualifier = insert.getQualifier();
|
||||
Pair<?, ?> qualifier = insert.getQualifier();
|
||||
|
||||
cascadingValues.stage(insert.getDependingOn(), insert.getPropertyPath(),
|
||||
qualifier == null ? null : qualifier.getSecond(), newEntity);
|
||||
|
||||
} else if (action instanceof DbAction.InsertRoot) {
|
||||
newRoot = newEntity;
|
||||
newRoot = (T) newEntity;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,7 +167,7 @@ class AggregateChangeExecutor {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private PersistentPropertyPath getRelativePath(DbAction action, PersistentPropertyPath pathToValue) {
|
||||
private PersistentPropertyPath<?> getRelativePath(DbAction<?> action, PersistentPropertyPath<?> pathToValue) {
|
||||
|
||||
if (action instanceof DbAction.Insert) {
|
||||
return pathToValue.getExtensionForBaseOf(((DbAction.Insert) action).getPropertyPath());
|
||||
|
||||
@@ -17,7 +17,6 @@ package org.springframework.data.jdbc.core;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
|
||||
@@ -34,6 +33,8 @@ import org.springframework.data.relational.core.conversion.DbAction.Merge;
|
||||
import org.springframework.data.relational.core.conversion.DbAction.Update;
|
||||
import org.springframework.data.relational.core.conversion.DbAction.UpdateRoot;
|
||||
import org.springframework.data.relational.core.conversion.Interpreter;
|
||||
import org.springframework.data.relational.core.conversion.RelationalConverter;
|
||||
import org.springframework.data.relational.core.conversion.RelationalEntityVersionUtils;
|
||||
import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
@@ -48,11 +49,13 @@ import org.springframework.util.Assert;
|
||||
* @author Jens Schauder
|
||||
* @author Mark Paluch
|
||||
* @author Myeonghyeon Lee
|
||||
* @author Tyler Van Gorder
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
class DefaultJdbcInterpreter implements Interpreter {
|
||||
|
||||
public static final String UPDATE_FAILED = "Failed to update entity [%s]. Id [%s] not found in database.";
|
||||
private final RelationalConverter converter;
|
||||
private final RelationalMappingContext context;
|
||||
private final DataAccessStrategy accessStrategy;
|
||||
|
||||
@@ -62,12 +65,15 @@ class DefaultJdbcInterpreter implements Interpreter {
|
||||
*/
|
||||
@Override
|
||||
public <T> void interpret(Insert<T> insert) {
|
||||
|
||||
Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), getParentKeys(insert));
|
||||
|
||||
insert.setGeneratedId(id);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> RelationalPersistentEntity<T> getRequiredPersistentEntity(Class<T> type) {
|
||||
return (RelationalPersistentEntity<T>) context.getRequiredPersistentEntity(type);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.relational.core.conversion.Interpreter#interpret(org.springframework.data.relational.core.conversion.DbAction.InsertRoot)
|
||||
@@ -75,8 +81,24 @@ class DefaultJdbcInterpreter implements Interpreter {
|
||||
@Override
|
||||
public <T> void interpret(InsertRoot<T> insert) {
|
||||
|
||||
Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), Collections.emptyMap());
|
||||
insert.setGeneratedId(id);
|
||||
RelationalPersistentEntity<T> persistentEntity = getRequiredPersistentEntity(insert.getEntityType());
|
||||
|
||||
if (persistentEntity.hasVersionProperty()) {
|
||||
// The interpreter is responsible for setting the initial version on the entity prior to calling insert.
|
||||
Number version = RelationalEntityVersionUtils.getVersionNumberFromEntity(insert.getEntity(), persistentEntity,
|
||||
converter);
|
||||
if (version != null && version.longValue() > 0) {
|
||||
throw new IllegalArgumentException("The entity cannot be inserted because it already has a version.");
|
||||
}
|
||||
T rootEntity = RelationalEntityVersionUtils.setVersionNumberOnEntity(insert.getEntity(), 1, persistentEntity,
|
||||
converter);
|
||||
Object id = accessStrategy.insert(rootEntity, insert.getEntityType(), Identifier.empty());
|
||||
insert.setNextVersion(1);
|
||||
insert.setGeneratedId(id);
|
||||
} else {
|
||||
Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), Identifier.empty());
|
||||
insert.setGeneratedId(id);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -100,10 +122,31 @@ class DefaultJdbcInterpreter implements Interpreter {
|
||||
@Override
|
||||
public <T> void interpret(UpdateRoot<T> update) {
|
||||
|
||||
if (!accessStrategy.update(update.getEntity(), update.getEntityType())) {
|
||||
RelationalPersistentEntity<T> persistentEntity = getRequiredPersistentEntity(update.getEntityType());
|
||||
|
||||
throw new IncorrectUpdateSemanticsDataAccessException(
|
||||
String.format(UPDATE_FAILED, update.getEntity(), getIdFrom(update)));
|
||||
if (persistentEntity.hasVersionProperty()) {
|
||||
// If the root aggregate has a version property, increment it.
|
||||
Number previousVersion = RelationalEntityVersionUtils.getVersionNumberFromEntity(update.getEntity(),
|
||||
persistentEntity, converter);
|
||||
Assert.notNull(previousVersion, "The root aggregate cannot be updated because the version property is null.");
|
||||
|
||||
T rootEntity = RelationalEntityVersionUtils.setVersionNumberOnEntity(update.getEntity(),
|
||||
previousVersion.longValue() + 1, persistentEntity,
|
||||
converter);
|
||||
|
||||
if (accessStrategy.updateWithVersion(rootEntity, update.getEntityType(), previousVersion)) {
|
||||
// Successful update, set the in-memory version on the action.
|
||||
update.setNextVersion(previousVersion);
|
||||
} else {
|
||||
throw new IncorrectUpdateSemanticsDataAccessException(
|
||||
String.format(UPDATE_FAILED, update.getEntity(), getIdFrom(update)));
|
||||
}
|
||||
} else {
|
||||
if (!accessStrategy.update(update.getEntity(), update.getEntityType())) {
|
||||
|
||||
throw new IncorrectUpdateSemanticsDataAccessException(
|
||||
String.format(UPDATE_FAILED, update.getEntity(), getIdFrom(update)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +178,16 @@ class DefaultJdbcInterpreter implements Interpreter {
|
||||
*/
|
||||
@Override
|
||||
public <T> void interpret(DeleteRoot<T> delete) {
|
||||
accessStrategy.delete(delete.getRootId(), delete.getEntityType());
|
||||
|
||||
if (delete.getEntity() != null) {
|
||||
RelationalPersistentEntity<T> persistentEntity = getRequiredPersistentEntity(delete.getEntityType());
|
||||
if (persistentEntity.hasVersionProperty()) {
|
||||
accessStrategy.deleteWithVersion(delete.getEntity(), delete.getEntityType());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
accessStrategy.delete(delete.getId(), delete.getEntityType());
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -177,13 +229,13 @@ class DefaultJdbcInterpreter implements Interpreter {
|
||||
PersistentPropertyPathExtension path = new PersistentPropertyPathExtension(context, action.getPropertyPath());
|
||||
PersistentPropertyPathExtension idPath = path.getIdDefiningParentPath();
|
||||
|
||||
DbAction.WithEntity idOwningAction = getIdOwningAction(action, idPath);
|
||||
DbAction.WithEntity<?> idOwningAction = getIdOwningAction(action, idPath);
|
||||
|
||||
return getIdFrom(idOwningAction);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private DbAction.WithEntity getIdOwningAction(DbAction.WithEntity action, PersistentPropertyPathExtension idPath) {
|
||||
private DbAction.WithEntity<?> getIdOwningAction(DbAction.WithEntity<?> action,
|
||||
PersistentPropertyPathExtension idPath) {
|
||||
|
||||
if (!(action instanceof DbAction.WithDependingOn)) {
|
||||
|
||||
@@ -193,7 +245,7 @@ class DefaultJdbcInterpreter implements Interpreter {
|
||||
return action;
|
||||
}
|
||||
|
||||
DbAction.WithDependingOn withDependingOn = (DbAction.WithDependingOn) action;
|
||||
DbAction.WithDependingOn<?> withDependingOn = (DbAction.WithDependingOn<?>) action;
|
||||
|
||||
if (idPath.matches(withDependingOn.getPropertyPath())) {
|
||||
return action;
|
||||
@@ -202,7 +254,7 @@ class DefaultJdbcInterpreter implements Interpreter {
|
||||
return getIdOwningAction(withDependingOn.getDependingOn(), idPath);
|
||||
}
|
||||
|
||||
private Object getIdFrom(DbAction.WithEntity idOwningAction) {
|
||||
private Object getIdFrom(DbAction.WithEntity<?> idOwningAction) {
|
||||
|
||||
if (idOwningAction instanceof DbAction.WithGeneratedId) {
|
||||
|
||||
@@ -221,4 +273,5 @@ class DefaultJdbcInterpreter implements Interpreter {
|
||||
|
||||
return identifier;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -31,10 +31,20 @@ import org.springframework.data.relational.core.conversion.RelationalConverter;
|
||||
import org.springframework.data.relational.core.conversion.RelationalEntityDeleteWriter;
|
||||
import org.springframework.data.relational.core.conversion.RelationalEntityInsertWriter;
|
||||
import org.springframework.data.relational.core.conversion.RelationalEntityUpdateWriter;
|
||||
import org.springframework.data.relational.core.conversion.RelationalEntityWriter;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.mapping.event.*;
|
||||
import org.springframework.data.relational.core.mapping.event.AfterDeleteCallback;
|
||||
import org.springframework.data.relational.core.mapping.event.AfterDeleteEvent;
|
||||
import org.springframework.data.relational.core.mapping.event.AfterLoadCallback;
|
||||
import org.springframework.data.relational.core.mapping.event.AfterLoadEvent;
|
||||
import org.springframework.data.relational.core.mapping.event.AfterSaveCallback;
|
||||
import org.springframework.data.relational.core.mapping.event.AfterSaveEvent;
|
||||
import org.springframework.data.relational.core.mapping.event.BeforeConvertCallback;
|
||||
import org.springframework.data.relational.core.mapping.event.BeforeDeleteCallback;
|
||||
import org.springframework.data.relational.core.mapping.event.BeforeDeleteEvent;
|
||||
import org.springframework.data.relational.core.mapping.event.BeforeSaveCallback;
|
||||
import org.springframework.data.relational.core.mapping.event.BeforeSaveEvent;
|
||||
import org.springframework.data.relational.core.mapping.event.Identifier;
|
||||
import org.springframework.data.relational.core.mapping.event.Identifier.Specified;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -51,10 +61,8 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
|
||||
|
||||
private final ApplicationEventPublisher publisher;
|
||||
private final RelationalMappingContext context;
|
||||
private final RelationalConverter converter;
|
||||
private final Interpreter interpreter;
|
||||
|
||||
private final RelationalEntityWriter jdbcEntityWriter;
|
||||
private final RelationalEntityDeleteWriter jdbcEntityDeleteWriter;
|
||||
private final RelationalEntityInsertWriter jdbcEntityInsertWriter;
|
||||
private final RelationalEntityUpdateWriter jdbcEntityUpdateWriter;
|
||||
@@ -83,14 +91,12 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
|
||||
|
||||
this.publisher = publisher;
|
||||
this.context = context;
|
||||
this.converter = converter;
|
||||
this.accessStrategy = dataAccessStrategy;
|
||||
|
||||
this.jdbcEntityWriter = new RelationalEntityWriter(context);
|
||||
this.jdbcEntityInsertWriter = new RelationalEntityInsertWriter(context);
|
||||
this.jdbcEntityUpdateWriter = new RelationalEntityUpdateWriter(context);
|
||||
this.jdbcEntityDeleteWriter = new RelationalEntityDeleteWriter(context);
|
||||
this.interpreter = new DefaultJdbcInterpreter(context, accessStrategy);
|
||||
this.interpreter = new DefaultJdbcInterpreter(converter, context, accessStrategy);
|
||||
|
||||
this.executor = new AggregateChangeExecutor(interpreter, converter);
|
||||
|
||||
@@ -115,15 +121,12 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
|
||||
|
||||
this.publisher = publisher;
|
||||
this.context = context;
|
||||
this.converter = converter;
|
||||
this.accessStrategy = dataAccessStrategy;
|
||||
|
||||
this.jdbcEntityWriter = new RelationalEntityWriter(context);
|
||||
this.jdbcEntityInsertWriter = new RelationalEntityInsertWriter(context);
|
||||
this.jdbcEntityUpdateWriter = new RelationalEntityUpdateWriter(context);
|
||||
this.jdbcEntityDeleteWriter = new RelationalEntityDeleteWriter(context);
|
||||
this.interpreter = new DefaultJdbcInterpreter(context, accessStrategy);
|
||||
|
||||
this.interpreter = new DefaultJdbcInterpreter(converter, context, accessStrategy);
|
||||
this.executor = new AggregateChangeExecutor(interpreter, converter);
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,15 @@ public class CascadingDataAccessStrategy implements DataAccessStrategy {
|
||||
return collect(das -> das.update(instance, domainType));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#updateWithVersion(java.lang.Object, java.lang.Class, java.lang.Number)
|
||||
*/
|
||||
@Override
|
||||
public <S> boolean updateWithVersion(S instance, Class<S> domainType, Number previousVersion) {
|
||||
return collect(das -> das.updateWithVersion(instance, domainType, previousVersion));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#delete(java.lang.Object, java.lang.Class)
|
||||
@@ -77,6 +86,15 @@ public class CascadingDataAccessStrategy implements DataAccessStrategy {
|
||||
collectVoid(das -> das.delete(id, domainType));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#deleteInstance(java.lang.Object, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> void deleteWithVersion(T instance, Class<T> domainType) {
|
||||
collectVoid(das -> das.deleteWithVersion(instance, domainType));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#delete(java.lang.Object, org.springframework.data.mapping.PersistentPropertyPath)
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.springframework.lang.Nullable;
|
||||
* complete aggregates.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
* @author Tyler Van Gorder
|
||||
*/
|
||||
public interface DataAccessStrategy extends RelationResolver {
|
||||
|
||||
@@ -74,8 +75,26 @@ public interface DataAccessStrategy extends RelationResolver {
|
||||
<T> boolean update(T instance, Class<T> domainType);
|
||||
|
||||
/**
|
||||
* deletes a single row identified by the id, from the table identified by the domainType. Does not handle cascading
|
||||
* Updates the data of a single entity in the database and enforce optimistic record locking using the previousVersion
|
||||
* property. Referenced entities don't get handled.
|
||||
* <P>
|
||||
* The statement will be of the form : {@code UPDATE … SET … WHERE ID = :id and VERSION_COLUMN = :previousVersion }
|
||||
* and throw an optimistic record locking exception if no rows have been updated.
|
||||
*
|
||||
* @param instance the instance to save. Must not be {@code null}.
|
||||
* @param domainType the type of the instance to save. Must not be {@code null}.
|
||||
* @param previousVersion The previous version assigned to the instance being saved.
|
||||
* @param <T> the type of the instance to save.
|
||||
* @return whether the update actually updated a row.
|
||||
*/
|
||||
<T> boolean updateWithVersion(T instance, Class<T> domainType, Number previousVersion);
|
||||
|
||||
/**
|
||||
* Deletes a single row identified by the id, from the table identified by the domainType. Does not handle cascading
|
||||
* deletes.
|
||||
* <P>
|
||||
* The statement will be of the form : {@code DELETE FROM … WHERE ID = :id and VERSION_COLUMN = :version } and throw
|
||||
* an optimistic record locking exception if no rows have been updated.
|
||||
*
|
||||
* @param id the id of the row to be deleted. Must not be {@code null}.
|
||||
* @param domainType the type of entity to be deleted. Implicitly determines the table to operate on. Must not be
|
||||
@@ -83,6 +102,16 @@ public interface DataAccessStrategy extends RelationResolver {
|
||||
*/
|
||||
void delete(Object id, Class<?> domainType);
|
||||
|
||||
/**
|
||||
* Deletes a single entity from the database and enforce optimistic record locking using the version property. Does
|
||||
* not handle cascading deletes.
|
||||
*
|
||||
* @param id the id of the row to be deleted. Must not be {@code null}.
|
||||
* @param domainType the type of entity to be deleted. Implicitly determines the table to operate on. Must not be
|
||||
* {@code null}.
|
||||
*/
|
||||
<T> void deleteWithVersion(T instance, Class<T> domainType);
|
||||
|
||||
/**
|
||||
* Deletes all entities reachable via {@literal propertyPath} from the instance identified by {@literal rootId}.
|
||||
*
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.data.jdbc.core.convert;
|
||||
|
||||
import static org.springframework.data.jdbc.core.convert.SqlGenerator.VERSION_SQL_PARAMETER_NAME;
|
||||
|
||||
import java.sql.JDBCType;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -28,11 +30,13 @@ import java.util.function.Predicate;
|
||||
import org.springframework.dao.DataRetrievalFailureException;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.data.jdbc.support.JdbcUtil;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.PersistentPropertyPath;
|
||||
import org.springframework.data.mapping.PropertyHandler;
|
||||
import org.springframework.data.relational.core.conversion.RelationalEntityVersionUtils;
|
||||
import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
@@ -55,6 +59,8 @@ import org.springframework.util.Assert;
|
||||
* @author Thomas Lang
|
||||
* @author Bastian Wilhelm
|
||||
* @author Christoph Strobl
|
||||
* @author Tom Hombergs
|
||||
* @author Tyler Van Gorder
|
||||
* @since 1.1
|
||||
*/
|
||||
public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
@@ -135,11 +141,34 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
public <S> boolean update(S instance, Class<S> domainType) {
|
||||
|
||||
RelationalPersistentEntity<S> persistentEntity = getRequiredPersistentEntity(domainType);
|
||||
|
||||
return operations.update(sql(domainType).getUpdate(),
|
||||
getParameterSource(instance, persistentEntity, "", Predicates.includeAll())) != 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#updateWithVersion(java.lang.Object, java.lang.Class, java.lang.Number)
|
||||
*/
|
||||
@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.
|
||||
MapSqlParameterSource parameterSource = getParameterSource(instance, persistentEntity, "",
|
||||
Predicates.includeAll());
|
||||
parameterSource.addValue(VERSION_SQL_PARAMETER_NAME, previousVersion);
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#delete(java.lang.Object, java.lang.Class)
|
||||
@@ -153,6 +182,33 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
operations.update(deleteByIdSql, parameter);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#deleteInstance(java.lang.Object, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> void deleteWithVersion(T instance, Class<T> domainType) {
|
||||
|
||||
RelationalPersistentEntity<T> persistentEntity = getRequiredPersistentEntity(domainType);
|
||||
Object id = getIdValueOrNull(instance, persistentEntity);
|
||||
Assert.notNull(id, "Cannot delete an instance without it's ID being populated.");
|
||||
|
||||
if (!persistentEntity.hasVersionProperty()) {
|
||||
delete(id, domainType);
|
||||
return;
|
||||
}
|
||||
|
||||
Number oldVersion = RelationalEntityVersionUtils.getVersionNumberFromEntity(instance, persistentEntity, converter);
|
||||
MapSqlParameterSource parameterSource = createIdParameterSource(id, domainType);
|
||||
parameterSource.addValue(VERSION_SQL_PARAMETER_NAME, oldVersion);
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#delete(java.lang.Object, org.springframework.data.mapping.PropertyPath)
|
||||
|
||||
@@ -60,6 +60,15 @@ public class DelegatingDataAccessStrategy implements DataAccessStrategy {
|
||||
return delegate.update(instance, domainType);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#updateWithVersion(java.lang.Object, java.lang.Class, java.lang.Number)
|
||||
*/
|
||||
@Override
|
||||
public <S> boolean updateWithVersion(S instance, Class<S> domainType, Number nextVersion) {
|
||||
return delegate.updateWithVersion(instance, domainType, nextVersion);
|
||||
|
||||
}
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#delete(java.lang.Object, org.springframework.data.mapping.PersistentPropertyPath)
|
||||
@@ -78,6 +87,15 @@ public class DelegatingDataAccessStrategy implements DataAccessStrategy {
|
||||
delegate.delete(id, domainType);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#deleteWithVersion(java.lang.Object, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> void deleteWithVersion(T instance, Class<T> domainType) {
|
||||
delegate.deleteWithVersion(instance, domainType);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#deleteAll(java.lang.Class)
|
||||
|
||||
@@ -42,6 +42,13 @@ class SqlContext {
|
||||
return table.column(entity.getIdColumn());
|
||||
}
|
||||
|
||||
Column getVersionColumn() {
|
||||
if (!entity.hasVersionProperty()) {
|
||||
return null;
|
||||
}
|
||||
return table.column(entity.getVersionProperty().getColumnName());
|
||||
}
|
||||
|
||||
Table getTable() {
|
||||
return table;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,24 @@ import org.springframework.data.relational.core.mapping.PersistentPropertyPathEx
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
import org.springframework.data.relational.core.sql.*;
|
||||
import org.springframework.data.relational.core.sql.AssignValue;
|
||||
import org.springframework.data.relational.core.sql.Assignments;
|
||||
import org.springframework.data.relational.core.sql.BindMarker;
|
||||
import org.springframework.data.relational.core.sql.Column;
|
||||
import org.springframework.data.relational.core.sql.Condition;
|
||||
import org.springframework.data.relational.core.sql.Delete;
|
||||
import org.springframework.data.relational.core.sql.DeleteBuilder;
|
||||
import org.springframework.data.relational.core.sql.Expression;
|
||||
import org.springframework.data.relational.core.sql.Expressions;
|
||||
import org.springframework.data.relational.core.sql.Functions;
|
||||
import org.springframework.data.relational.core.sql.Insert;
|
||||
import org.springframework.data.relational.core.sql.InsertBuilder;
|
||||
import org.springframework.data.relational.core.sql.SQL;
|
||||
import org.springframework.data.relational.core.sql.Select;
|
||||
import org.springframework.data.relational.core.sql.SelectBuilder;
|
||||
import org.springframework.data.relational.core.sql.StatementBuilder;
|
||||
import org.springframework.data.relational.core.sql.Table;
|
||||
import org.springframework.data.relational.core.sql.Update;
|
||||
import org.springframework.data.relational.core.sql.render.SqlRenderer;
|
||||
import org.springframework.data.relational.domain.Identifier;
|
||||
import org.springframework.data.util.Lazy;
|
||||
@@ -52,11 +69,14 @@ import org.springframework.util.Assert;
|
||||
* @author Bastian Wilhelm
|
||||
* @author Oleksandr Kucher
|
||||
* @author Mark Paluch
|
||||
* @author Tom Hombergs
|
||||
* @author Tyler Van Gorder
|
||||
*/
|
||||
class SqlGenerator {
|
||||
|
||||
private static final Pattern parameterPattern = Pattern.compile("\\W");
|
||||
static final String VERSION_SQL_PARAMETER_NAME = "___oldOptimisticLockingVersion";
|
||||
|
||||
private static final Pattern parameterPattern = Pattern.compile("\\W");
|
||||
private final RelationalPersistentEntity<?> entity;
|
||||
private final MappingContext<RelationalPersistentEntity<?>, RelationalPersistentProperty> mappingContext;
|
||||
|
||||
@@ -71,8 +91,10 @@ class SqlGenerator {
|
||||
private final Lazy<String> countSql = Lazy.of(this::createCountSql);
|
||||
|
||||
private final Lazy<String> updateSql = Lazy.of(this::createUpdateSql);
|
||||
private final Lazy<String> updateWithVersionSql = Lazy.of(this::createUpdateWithVersionSql);
|
||||
|
||||
private final Lazy<String> deleteByIdSql = Lazy.of(this::createDeleteSql);
|
||||
private final Lazy<String> deleteByIdAndVersionSql = Lazy.of(this::createDeleteByIdAndVersionSql);
|
||||
private final Lazy<String> deleteByListSql = Lazy.of(this::createDeleteByListSql);
|
||||
|
||||
/**
|
||||
@@ -242,6 +264,15 @@ class SqlGenerator {
|
||||
return updateSql.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code UPDATE … SET … WHERE ID = :id and VERSION_COLUMN = :___oldOptimisticLockingVersion } statement.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String getUpdateWithVersion() {
|
||||
return updateWithVersionSql.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code SELECT COUNT(*) FROM …} statement.
|
||||
*
|
||||
@@ -260,6 +291,15 @@ class SqlGenerator {
|
||||
return deleteByIdSql.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code DELETE FROM … WHERE :id = … and :___oldOptimisticLockingVersion = ...} statement.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String getDeleteByIdAndVersion() {
|
||||
return deleteByIdAndVersionSql.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code DELETE FROM … WHERE :ids in (…)} statement.
|
||||
*
|
||||
@@ -461,7 +501,6 @@ class SqlGenerator {
|
||||
}
|
||||
|
||||
private String createUpdateSql() {
|
||||
|
||||
Table table = getTable();
|
||||
|
||||
List<AssignValue> assignments = columns.getUpdateableColumns() //
|
||||
@@ -472,9 +511,30 @@ class SqlGenerator {
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Update update = Update.builder() //
|
||||
.table(table) //
|
||||
.set(assignments) //
|
||||
.where(getIdColumn().isEqualTo(getBindMarker(entity.getIdColumn()))).build();
|
||||
|
||||
return render(update);
|
||||
}
|
||||
|
||||
private String createUpdateWithVersionSql() {
|
||||
|
||||
Table table = getTable();
|
||||
|
||||
List<AssignValue> assignments = columns.getUpdateableColumns() //
|
||||
.stream() //
|
||||
.map(columnName -> Assignments.value( //
|
||||
table.column(columnName), //
|
||||
getBindMarker(columnName))) //
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Update update = null;
|
||||
update = Update.builder() //
|
||||
.table(table) //
|
||||
.set(assignments) //
|
||||
.where(getIdColumn().isEqualTo(getBindMarker(entity.getIdColumn()))) //
|
||||
.and(getVersionColumn().isEqualTo(SQL.bindMarker(":" + VERSION_SQL_PARAMETER_NAME))) //
|
||||
.build();
|
||||
|
||||
return render(update);
|
||||
@@ -490,6 +550,18 @@ class SqlGenerator {
|
||||
return render(delete);
|
||||
}
|
||||
|
||||
private String createDeleteByIdAndVersionSql() {
|
||||
Table table = getTable();
|
||||
|
||||
Delete delete = Delete.builder() //
|
||||
.from(table) //
|
||||
.where(getIdColumn().isEqualTo(SQL.bindMarker(":id"))) //
|
||||
.and(getVersionColumn().isEqualTo(SQL.bindMarker(":" + VERSION_SQL_PARAMETER_NAME))) //
|
||||
.build();
|
||||
|
||||
return render(delete);
|
||||
}
|
||||
|
||||
private String createDeleteByPathAndCriteria(PersistentPropertyPathExtension path,
|
||||
Function<Column, Condition> rootCondition) {
|
||||
|
||||
@@ -551,6 +623,10 @@ class SqlGenerator {
|
||||
return sqlContext.getIdColumn();
|
||||
}
|
||||
|
||||
private Column getVersionColumn() {
|
||||
return sqlContext.getVersionColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Value object representing a {@code JOIN} association.
|
||||
*/
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
*/
|
||||
package org.springframework.data.jdbc.mybatis;
|
||||
|
||||
import static java.util.Arrays.*;
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.ibatis.exceptions.PersistenceException;
|
||||
@@ -52,10 +53,12 @@ import org.springframework.util.Assert;
|
||||
* @author Kazuki Shimizu
|
||||
* @author Oliver Gierke
|
||||
* @author Mark Paluch
|
||||
* @author Tyler Van Gorder
|
||||
*/
|
||||
public class MyBatisDataAccessStrategy implements DataAccessStrategy {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MyBatisDataAccessStrategy.class);
|
||||
private static final String VERSION_SQL_PARAMETER_NAME_OLD = "___oldOptimisticLockingVersion";
|
||||
|
||||
private final SqlSession sqlSession;
|
||||
private NamespaceStrategy namespaceStrategy = NamespaceStrategy.DEFAULT_INSTANCE;
|
||||
@@ -81,7 +84,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy {
|
||||
// cycle. In order to create it, we need something that allows to defer closing the cycle until all the elements are
|
||||
// created. That is the purpose of the DelegatingAccessStrategy.
|
||||
DelegatingDataAccessStrategy delegatingDataAccessStrategy = new DelegatingDataAccessStrategy();
|
||||
MyBatisDataAccessStrategy myBatisDataAccessStrategy = new MyBatisDataAccessStrategy(sqlSession);
|
||||
MyBatisDataAccessStrategy myBatisDataAccessStrategy = new MyBatisDataAccessStrategy(sqlSession, context, converter);
|
||||
myBatisDataAccessStrategy.setNamespaceStrategy(namespaceStrategy);
|
||||
|
||||
CascadingDataAccessStrategy cascadingDataAccessStrategy = new CascadingDataAccessStrategy(
|
||||
@@ -111,7 +114,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy {
|
||||
*
|
||||
* @param sqlSession Must be non {@literal null}.
|
||||
*/
|
||||
public MyBatisDataAccessStrategy(SqlSession sqlSession) {
|
||||
public MyBatisDataAccessStrategy(SqlSession sqlSession, RelationalMappingContext context, JdbcConverter converter) {
|
||||
this.sqlSession = sqlSession;
|
||||
}
|
||||
|
||||
@@ -164,6 +167,20 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy {
|
||||
new MyBatisContext(null, instance, domainType, Collections.emptyMap())) != 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#updateWithVersion(java.lang.Object, java.lang.Class, java.lang.Number)
|
||||
*/
|
||||
@Override
|
||||
public <S> boolean updateWithVersion(S instance, Class<S> domainType, Number previousVersion) {
|
||||
|
||||
Map<String, Object> additionalValues = new HashMap<>();
|
||||
additionalValues.put(VERSION_SQL_PARAMETER_NAME_OLD, previousVersion);
|
||||
|
||||
return sqlSession().update(namespace(domainType) + ".updateWithVersion",
|
||||
new MyBatisContext(null, instance, domainType, additionalValues)) != 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#delete(java.lang.Object, java.lang.Class)
|
||||
@@ -175,6 +192,17 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy {
|
||||
new MyBatisContext(id, null, domainType, Collections.emptyMap()));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#deleteInstance(java.lang.Object, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> void deleteWithVersion(T instance, Class<T> domainType) {
|
||||
|
||||
sqlSession().delete(namespace(domainType) + ".deleteWithVersion",
|
||||
new MyBatisContext(null, instance, domainType, Collections.emptyMap()));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#delete(java.lang.Object, org.springframework.data.mapping.PersistentPropertyPath)
|
||||
|
||||
@@ -15,19 +15,24 @@
|
||||
*/
|
||||
package org.springframework.data.jdbc.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.data.jdbc.core.PropertyPathTestingUtils.*;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.tuple;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.data.jdbc.core.PropertyPathTestingUtils.toPath;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
|
||||
import org.springframework.dao.TransientDataAccessResourceException;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.jdbc.core.convert.BasicJdbcConverter;
|
||||
import org.springframework.data.jdbc.core.convert.DataAccessStrategy;
|
||||
import org.springframework.data.jdbc.core.convert.JdbcConverter;
|
||||
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
|
||||
import org.springframework.data.mapping.PersistentPropertyPath;
|
||||
import org.springframework.data.relational.core.conversion.DbAction.Insert;
|
||||
@@ -50,9 +55,9 @@ public class DefaultJdbcInterpreterUnitTests {
|
||||
static final String BACK_REFERENCE = "container";
|
||||
|
||||
RelationalMappingContext context = new JdbcMappingContext();
|
||||
|
||||
JdbcConverter converter = new BasicJdbcConverter(context, (Identifier, path) -> null);
|
||||
DataAccessStrategy dataAccessStrategy = mock(DataAccessStrategy.class);
|
||||
DefaultJdbcInterpreter interpreter = new DefaultJdbcInterpreter(context, dataAccessStrategy);
|
||||
DefaultJdbcInterpreter interpreter = new DefaultJdbcInterpreter(converter, context, dataAccessStrategy);
|
||||
|
||||
Container container = new Container();
|
||||
Element element = new Element();
|
||||
|
||||
@@ -15,11 +15,17 @@
|
||||
*/
|
||||
package org.springframework.data.jdbc.core;
|
||||
|
||||
import static java.util.Collections.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static java.util.Collections.emptyMap;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.assertj.core.api.Assertions.tuple;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -29,6 +35,7 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.assertj.core.api.SoftAssertions;
|
||||
@@ -42,8 +49,10 @@ 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.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.annotation.ReadOnlyProperty;
|
||||
import org.springframework.data.annotation.Version;
|
||||
import org.springframework.data.jdbc.core.convert.DataAccessStrategy;
|
||||
import org.springframework.data.jdbc.testing.DatabaseProfileValueSource;
|
||||
import org.springframework.data.jdbc.testing.HsqlDbOnly;
|
||||
@@ -69,6 +78,8 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
* @author Thomas Lang
|
||||
* @author Mark Paluch
|
||||
* @author Myeonghyeon Lee
|
||||
* @author Tom Hombergs
|
||||
* @author Tyler Van Gorder
|
||||
*/
|
||||
@ContextConfiguration
|
||||
@Transactional
|
||||
@@ -629,6 +640,116 @@ public class JdbcAggregateTemplateIntegrationTests {
|
||||
.isEqualTo("from-db");
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-219 Test that immutable version attribute works as expected.
|
||||
public void saveAndUpdateAggregateWithImmutableVersion() {
|
||||
AggregateWithImmutableVersion aggregate = new AggregateWithImmutableVersion(null, null);
|
||||
aggregate = template.save(aggregate);
|
||||
|
||||
Long id = aggregate.getId();
|
||||
|
||||
AggregateWithImmutableVersion reloadedAggregate = template.findById(id, aggregate.getClass());
|
||||
assertThat(reloadedAggregate.getVersion()).isEqualTo(1L)
|
||||
.withFailMessage("version field should initially have the value 1");
|
||||
reloadedAggregate = template.save(reloadedAggregate);
|
||||
|
||||
AggregateWithImmutableVersion updatedAggregate = template.findById(id, aggregate.getClass());
|
||||
assertThat(updatedAggregate.getVersion()).isEqualTo(2L)
|
||||
.withFailMessage("version field should increment by one with each save");
|
||||
|
||||
assertThatThrownBy(() -> template.save(new AggregateWithImmutableVersion(id, 1L)))
|
||||
.hasRootCauseInstanceOf(OptimisticLockingFailureException.class)
|
||||
.withFailMessage("saving an aggregate with an outdated version should raise an exception");
|
||||
|
||||
assertThatThrownBy(() -> template.save(new AggregateWithImmutableVersion(id, 3L)))
|
||||
.hasRootCauseInstanceOf(OptimisticLockingFailureException.class)
|
||||
.withFailMessage("saving an aggregate with a future version should raise an exception");
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-219 Test that a delete with a version attribute works as expected.
|
||||
public void deleteAggregateWithVersion() {
|
||||
|
||||
AggregateWithImmutableVersion aggregate = new AggregateWithImmutableVersion(null, null);
|
||||
aggregate = template.save(aggregate);
|
||||
|
||||
//Should have an ID and a version of 1.
|
||||
final Long id = aggregate.getId();
|
||||
|
||||
assertThatThrownBy(() -> template.delete(new AggregateWithImmutableVersion(id, 0L), AggregateWithImmutableVersion.class))
|
||||
.hasRootCauseInstanceOf(OptimisticLockingFailureException.class)
|
||||
.withFailMessage("deleting an aggregate with an outdated version should raise an exception");
|
||||
|
||||
assertThatThrownBy(() -> template.delete(new AggregateWithImmutableVersion(id, 3L), AggregateWithImmutableVersion.class))
|
||||
.hasRootCauseInstanceOf(OptimisticLockingFailureException.class)
|
||||
.withFailMessage("deleting an aggregate with a future version should raise an exception");
|
||||
|
||||
|
||||
//This should succeed
|
||||
template.delete(aggregate, AggregateWithImmutableVersion.class);
|
||||
|
||||
|
||||
aggregate = new AggregateWithImmutableVersion(null, null);
|
||||
aggregate = template.save(aggregate);
|
||||
|
||||
//This should succeed, as version will not be used.
|
||||
template.deleteById(aggregate.getId(), AggregateWithImmutableVersion.class);
|
||||
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-219
|
||||
public void saveAndUpdateAggregateWithLongVersion() {
|
||||
saveAndUpdateAggregateWithVersion(new AggregateWithLongVersion(), Number::longValue);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-219
|
||||
public void saveAndUpdateAggregateWithPrimitiveLongVersion() {
|
||||
saveAndUpdateAggregateWithVersion(new AggregateWithPrimitiveLongVersion(), Number::longValue);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-219
|
||||
public void saveAndUpdateAggregateWithIntegerVersion() {
|
||||
saveAndUpdateAggregateWithVersion(new AggregateWithIntegerVersion(), Number::intValue);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-219
|
||||
public void saveAndUpdateAggregateWithPrimitiveIntegerVersion() {
|
||||
saveAndUpdateAggregateWithVersion(new AggregateWithPrimitiveIntegerVersion(), Number::intValue);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-219
|
||||
public void saveAndUpdateAggregateWithShortVersion() {
|
||||
saveAndUpdateAggregateWithVersion(new AggregateWithShortVersion(), Number::shortValue);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-219
|
||||
public void saveAndUpdateAggregateWithPrimitiveShortVersion() {
|
||||
saveAndUpdateAggregateWithVersion(new AggregateWithPrimitiveShortVersion(), Number::shortValue);
|
||||
}
|
||||
|
||||
private <T extends Number> void saveAndUpdateAggregateWithVersion(VersionedAggregate aggregate,
|
||||
Function<Number, T> toConcreteNumber) {
|
||||
|
||||
template.save(aggregate);
|
||||
|
||||
VersionedAggregate reloadedAggregate = template.findById(aggregate.getId(), aggregate.getClass());
|
||||
assertThat(reloadedAggregate.getVersion()).isEqualTo(toConcreteNumber.apply(1))
|
||||
.withFailMessage("version field should initially have the value 1");
|
||||
template.save(reloadedAggregate);
|
||||
|
||||
VersionedAggregate updatedAggregate = template.findById(aggregate.getId(), aggregate.getClass());
|
||||
assertThat(updatedAggregate.getVersion()).isEqualTo(toConcreteNumber.apply(2))
|
||||
.withFailMessage("version field should increment by one with each save");
|
||||
|
||||
reloadedAggregate.setVersion(toConcreteNumber.apply(1));
|
||||
assertThatThrownBy(() -> template.save(reloadedAggregate))
|
||||
.hasRootCauseInstanceOf(OptimisticLockingFailureException.class)
|
||||
.withFailMessage("saving an aggregate with an outdated version should raise an exception");
|
||||
|
||||
reloadedAggregate.setVersion(toConcreteNumber.apply(3));
|
||||
assertThatThrownBy(() -> template.save(reloadedAggregate))
|
||||
.hasRootCauseInstanceOf(OptimisticLockingFailureException.class)
|
||||
.withFailMessage("saving an aggregate with a future version should raise an exception");
|
||||
}
|
||||
|
||||
private static NoIdMapChain4 createNoIdMapTree() {
|
||||
|
||||
NoIdMapChain4 chain4 = new NoIdMapChain4();
|
||||
@@ -892,6 +1013,109 @@ public class JdbcAggregateTemplateIntegrationTests {
|
||||
String name;
|
||||
@ReadOnlyProperty String readOnly;
|
||||
}
|
||||
@Data
|
||||
static abstract class VersionedAggregate {
|
||||
|
||||
@Id private Long id;
|
||||
|
||||
abstract Number getVersion();
|
||||
|
||||
abstract void setVersion(Number newVersion);
|
||||
}
|
||||
|
||||
@Value
|
||||
@Wither
|
||||
@Table("VERSIONED_AGGREGATE")
|
||||
static class AggregateWithImmutableVersion {
|
||||
|
||||
@Id private Long id;
|
||||
@Version private final Long version;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
@Table("VERSIONED_AGGREGATE")
|
||||
static class AggregateWithLongVersion extends VersionedAggregate {
|
||||
|
||||
@Version private Long version;
|
||||
|
||||
@Override
|
||||
void setVersion(Number newVersion) {
|
||||
this.version = (Long) newVersion;
|
||||
}
|
||||
}
|
||||
|
||||
@Table("VERSIONED_AGGREGATE")
|
||||
static class AggregateWithPrimitiveLongVersion extends VersionedAggregate {
|
||||
|
||||
@Version private long version;
|
||||
|
||||
@Override
|
||||
void setVersion(Number newVersion) {
|
||||
this.version = (long) newVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
Number getVersion() {
|
||||
return this.version;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@Table("VERSIONED_AGGREGATE")
|
||||
static class AggregateWithIntegerVersion extends VersionedAggregate {
|
||||
|
||||
@Version private Integer version;
|
||||
|
||||
@Override
|
||||
void setVersion(Number newVersion) {
|
||||
this.version = (Integer) newVersion;
|
||||
}
|
||||
}
|
||||
|
||||
@Table("VERSIONED_AGGREGATE")
|
||||
static class AggregateWithPrimitiveIntegerVersion extends VersionedAggregate {
|
||||
|
||||
@Version private int version;
|
||||
|
||||
@Override
|
||||
void setVersion(Number newVersion) {
|
||||
this.version = (int) newVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
Number getVersion() {
|
||||
return this.version;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@Table("VERSIONED_AGGREGATE")
|
||||
static class AggregateWithShortVersion extends VersionedAggregate {
|
||||
|
||||
@Version private Short version;
|
||||
|
||||
@Override
|
||||
void setVersion(Number newVersion) {
|
||||
this.version = (Short) newVersion;
|
||||
}
|
||||
}
|
||||
|
||||
@Table("VERSIONED_AGGREGATE")
|
||||
static class AggregateWithPrimitiveShortVersion extends VersionedAggregate {
|
||||
|
||||
@Version private short version;
|
||||
|
||||
@Override
|
||||
void setVersion(Number newVersion) {
|
||||
this.version = (short) newVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
Number getVersion() {
|
||||
return this.version;
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(TestConfiguration.class)
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
*/
|
||||
package org.springframework.data.jdbc.core.convert;
|
||||
|
||||
import static java.util.Collections.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static java.util.Collections.emptySet;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -26,6 +26,7 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.annotation.ReadOnlyProperty;
|
||||
import org.springframework.data.annotation.Version;
|
||||
import org.springframework.data.jdbc.core.PropertyPathTestingUtils;
|
||||
import org.springframework.data.jdbc.core.mapping.AggregateReference;
|
||||
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
|
||||
@@ -49,6 +50,7 @@ import org.springframework.data.relational.domain.Identifier;
|
||||
* @author Oleksandr Kucher
|
||||
* @author Bastian Wilhelm
|
||||
* @author Mark Paluch
|
||||
* @author Tom Hombergs
|
||||
*/
|
||||
public class SqlGeneratorUnitTests {
|
||||
|
||||
@@ -172,7 +174,9 @@ public class SqlGeneratorUnitTests {
|
||||
public void findAllByPropertyWithMultipartIdentifier() {
|
||||
|
||||
// this would get called when ListParent is the element type of a Set
|
||||
String sql = sqlGenerator.getFindAllByProperty(Identifier.of("backref", "some-value", String.class).withPart("backref_key", "key-value", Object.class), null, false);
|
||||
String sql = sqlGenerator.getFindAllByProperty(
|
||||
Identifier.of("backref", "some-value", String.class).withPart("backref_key", "key-value", Object.class), null,
|
||||
false);
|
||||
|
||||
assertThat(sql).contains("SELECT", //
|
||||
"dummy_entity.id1 AS id1", //
|
||||
@@ -185,9 +189,7 @@ public class SqlGeneratorUnitTests {
|
||||
"FROM dummy_entity ", //
|
||||
"LEFT OUTER JOIN referenced_entity AS ref ON ref.dummy_entity = dummy_entity.id1", //
|
||||
"LEFT OUTER JOIN second_level_referenced_entity AS ref_further ON ref_further.referenced_entity = ref.x_l1id", //
|
||||
"dummy_entity.backref = :backref",
|
||||
"dummy_entity.backref_key = :backref_key"
|
||||
);
|
||||
"dummy_entity.backref = :backref", "dummy_entity.backref_key = :backref_key");
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-131, DATAJDBC-111
|
||||
@@ -229,6 +231,21 @@ public class SqlGeneratorUnitTests {
|
||||
+ "WHERE dummy_entity.backref = :backref " + "ORDER BY key-column");
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-219
|
||||
public void updateWithVersion() {
|
||||
|
||||
SqlGenerator sqlGenerator = createSqlGenerator(VersionedEntity.class);
|
||||
|
||||
assertThat(sqlGenerator.getUpdateWithVersion()).containsSequence( //
|
||||
"UPDATE", //
|
||||
"versioned_entity", //
|
||||
"SET", //
|
||||
"WHERE", //
|
||||
"id1 = :id", //
|
||||
"AND", //
|
||||
"version = :___oldOptimisticLockingVersion");
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-264
|
||||
public void getInsertForEmptyColumnList() {
|
||||
|
||||
@@ -540,6 +557,10 @@ public class SqlGeneratorUnitTests {
|
||||
AggregateReference<OtherAggregate, Long> other;
|
||||
}
|
||||
|
||||
static class VersionedEntity extends DummyEntity {
|
||||
@Version Integer version;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
static class ReferencedEntity {
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.jdbc.mybatis;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import junit.framework.AssertionFailedError;
|
||||
|
||||
@@ -34,8 +34,12 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.data.jdbc.core.convert.BasicJdbcConverter;
|
||||
import org.springframework.data.jdbc.core.convert.JdbcConverter;
|
||||
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
|
||||
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
|
||||
import org.springframework.data.jdbc.testing.TestConfiguration;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
@@ -109,7 +113,10 @@ public class MyBatisCustomizingNamespaceHsqlIntegrationTests {
|
||||
@Primary
|
||||
MyBatisDataAccessStrategy dataAccessStrategy(SqlSession sqlSession) {
|
||||
|
||||
MyBatisDataAccessStrategy strategy = new MyBatisDataAccessStrategy(sqlSession);
|
||||
RelationalMappingContext context = new JdbcMappingContext();
|
||||
JdbcConverter converter = new BasicJdbcConverter(context, (Identifier, path) -> null);
|
||||
|
||||
MyBatisDataAccessStrategy strategy = new MyBatisDataAccessStrategy(sqlSession, context, converter);
|
||||
|
||||
strategy.setNamespaceStrategy(new NamespaceStrategy() {
|
||||
@Override
|
||||
|
||||
@@ -15,10 +15,18 @@
|
||||
*/
|
||||
package org.springframework.data.jdbc.mybatis;
|
||||
|
||||
import static java.util.Arrays.*;
|
||||
import static java.util.Collections.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
@@ -29,6 +37,8 @@ import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.data.jdbc.core.PropertyPathTestingUtils;
|
||||
import org.springframework.data.jdbc.core.convert.BasicJdbcConverter;
|
||||
import org.springframework.data.jdbc.core.convert.JdbcConverter;
|
||||
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
|
||||
import org.springframework.data.mapping.PersistentPropertyPath;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
@@ -44,11 +54,12 @@ import org.springframework.data.relational.domain.Identifier;
|
||||
public class MyBatisDataAccessStrategyUnitTests {
|
||||
|
||||
RelationalMappingContext context = new JdbcMappingContext();
|
||||
JdbcConverter converter = new BasicJdbcConverter(context, (Identifier, path) -> null);
|
||||
|
||||
SqlSession session = mock(SqlSession.class);
|
||||
ArgumentCaptor<MyBatisContext> captor = ArgumentCaptor.forClass(MyBatisContext.class);
|
||||
|
||||
MyBatisDataAccessStrategy accessStrategy = new MyBatisDataAccessStrategy(session);
|
||||
MyBatisDataAccessStrategy accessStrategy = new MyBatisDataAccessStrategy(session, context, converter);
|
||||
|
||||
PersistentPropertyPath<RelationalPersistentProperty> path = PropertyPathTestingUtils.toPath("one.two",
|
||||
DummyEntity.class, context);
|
||||
|
||||
@@ -298,4 +298,10 @@ CREATE TABLE WITH_READ_ONLY (
|
||||
ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY,
|
||||
NAME VARCHAR(200),
|
||||
READ_ONLY VARCHAR(200) DEFAULT 'from-db'
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TABLE VERSIONED_AGGREGATE
|
||||
(
|
||||
ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY,
|
||||
VERSION BIGINT
|
||||
);
|
||||
|
||||
@@ -283,4 +283,10 @@ CREATE TABLE NO_ID_MAP_CHAIN0
|
||||
NO_ID_MAP_CHAIN3_KEY,
|
||||
NO_ID_MAP_CHAIN2_KEY
|
||||
)
|
||||
);
|
||||
);
|
||||
|
||||
CREATE TABLE VERSIONED_AGGREGATE
|
||||
(
|
||||
ID BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
VERSION BIGINT
|
||||
);
|
||||
|
||||
@@ -291,4 +291,10 @@ CREATE TABLE NO_ID_MAP_CHAIN0
|
||||
NO_ID_MAP_CHAIN3_KEY,
|
||||
NO_ID_MAP_CHAIN2_KEY
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TABLE VERSIONED_AGGREGATE
|
||||
(
|
||||
ID BIGINT IDENTITY PRIMARY KEY,
|
||||
VERSION BIGINT
|
||||
);
|
||||
@@ -282,4 +282,11 @@ CREATE TABLE NO_ID_MAP_CHAIN0
|
||||
NO_ID_MAP_CHAIN3_KEY,
|
||||
NO_ID_MAP_CHAIN2_KEY
|
||||
)
|
||||
);
|
||||
);
|
||||
|
||||
CREATE TABLE VERSIONED_AGGREGATE
|
||||
(
|
||||
ID BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
VERSION BIGINT
|
||||
);
|
||||
|
||||
|
||||
@@ -303,4 +303,10 @@ CREATE TABLE NO_ID_MAP_CHAIN0
|
||||
NO_ID_MAP_CHAIN3_KEY,
|
||||
NO_ID_MAP_CHAIN2_KEY
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TABLE VERSIONED_AGGREGATE
|
||||
(
|
||||
ID SERIAL PRIMARY KEY,
|
||||
VERSION BIGINT
|
||||
);
|
||||
@@ -35,6 +35,7 @@ import org.springframework.lang.Nullable;
|
||||
* @param <T> the type of the entity that is affected by this action.
|
||||
* @author Jens Schauder
|
||||
* @author Mark Paluch
|
||||
* @author Tyler Van Gorder
|
||||
*/
|
||||
public interface DbAction<T> {
|
||||
|
||||
@@ -92,16 +93,16 @@ public interface DbAction<T> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an insert statement for the root of an aggregate.
|
||||
* Represents an insert statement for the root of an aggregate. Upon a successful insert, the initial version and generated ids are populated.
|
||||
*
|
||||
* @param <T> type of the entity for which this represents a database interaction.
|
||||
*/
|
||||
@Data
|
||||
@RequiredArgsConstructor
|
||||
class InsertRoot<T> implements WithEntity<T>, WithGeneratedId<T> {
|
||||
|
||||
@NonNull private final T entity;
|
||||
class InsertRoot<T> implements WithVersion<T>, WithGeneratedId<T> {
|
||||
|
||||
@NonNull private T entity;
|
||||
private Number nextVersion;
|
||||
private Object generatedId;
|
||||
|
||||
@Override
|
||||
@@ -128,14 +129,15 @@ public interface DbAction<T> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an insert statement for the root of an aggregate.
|
||||
* Represents an update statement for the aggregate root.
|
||||
*
|
||||
* @param <T> type of the entity for which this represents a database interaction.
|
||||
*/
|
||||
@Value
|
||||
class UpdateRoot<T> implements WithEntity<T> {
|
||||
@Data
|
||||
class UpdateRoot<T> implements WithVersion<T> {
|
||||
|
||||
@NonNull private final T entity;
|
||||
@NonNull private T entity;
|
||||
@Nullable Number nextVersion;
|
||||
|
||||
@Override
|
||||
public void doExecuteWith(Interpreter interpreter) {
|
||||
@@ -148,7 +150,7 @@ public interface DbAction<T> {
|
||||
*
|
||||
* @param <T> type of the entity for which this represents a database interaction.
|
||||
*/
|
||||
@Value
|
||||
@Data
|
||||
class Merge<T> implements WithDependingOn<T>, WithPropertyPath<T> {
|
||||
|
||||
@NonNull T entity;
|
||||
@@ -181,7 +183,7 @@ public interface DbAction<T> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a delete statement for a aggregate root.
|
||||
* Represents a delete statement for a aggregate root when only the ID is known.
|
||||
* <p>
|
||||
* Note that deletes for contained entities that reference the root are to be represented by separate
|
||||
* {@link DbAction}s.
|
||||
@@ -189,10 +191,11 @@ public interface DbAction<T> {
|
||||
* @param <T> type of the entity for which this represents a database interaction.
|
||||
*/
|
||||
@Value
|
||||
class DeleteRoot<T> implements DbAction<T> {
|
||||
class DeleteRoot<T> implements WithEntity<T> {
|
||||
|
||||
@NonNull Object id;
|
||||
@Nullable T entity;
|
||||
@NonNull Class<T> entityType;
|
||||
@NonNull Object rootId;
|
||||
|
||||
@Override
|
||||
public void doExecuteWith(Interpreter interpreter) {
|
||||
@@ -347,4 +350,8 @@ public interface DbAction<T> {
|
||||
return (Class<T>) getPropertyPath().getRequiredLeafProperty().getActualType();
|
||||
}
|
||||
}
|
||||
interface WithVersion<T> extends WithEntity<T> {
|
||||
Number getNextVersion();
|
||||
void setNextVersion(Number nextVersion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,5 +57,5 @@ public interface Interpreter {
|
||||
|
||||
<T> void interpret(DeleteAll<T> delete);
|
||||
|
||||
<T> void interpret(DeleteAllRoot<T> DeleteAllRoot);
|
||||
<T> void interpret(DeleteAllRoot<T> deleteAllRoot);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Converts an entity that is about to be deleted into {@link DbAction}s inside a {@link AggregateChange} that need to
|
||||
* be executed against the database to recreate the appropriate state in the database.
|
||||
* be executed against the database to recreate the appropriate state in the database. If the {@link AggregateChange}
|
||||
* has a reference to the entity and the entity has a version attribute, the delete will include an optimistic record
|
||||
* locking check.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
* @author Mark Paluch
|
||||
@@ -58,7 +60,7 @@ public class RelationalEntityDeleteWriter implements EntityWriter<Object, Aggreg
|
||||
if (id == null) {
|
||||
deleteAll(aggregateChange.getEntityType()).forEach(aggregateChange::addAction);
|
||||
} else {
|
||||
deleteById(id, aggregateChange).forEach(aggregateChange::addAction);
|
||||
deleteRoot(id, aggregateChange).forEach(aggregateChange::addAction);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,8 +69,7 @@ public class RelationalEntityDeleteWriter implements EntityWriter<Object, Aggreg
|
||||
List<DbAction<?>> actions = new ArrayList<>();
|
||||
|
||||
context.findPersistentPropertyPaths(entityType, PersistentProperty::isEntity)
|
||||
.filter(p -> !p.getRequiredLeafProperty().isEmbedded())
|
||||
.forEach(p -> actions.add(new DbAction.DeleteAll<>(p)));
|
||||
.filter(p -> !p.getRequiredLeafProperty().isEmbedded()).forEach(p -> actions.add(new DbAction.DeleteAll<>(p)));
|
||||
|
||||
Collections.reverse(actions);
|
||||
|
||||
@@ -78,10 +79,10 @@ public class RelationalEntityDeleteWriter implements EntityWriter<Object, Aggreg
|
||||
return actions;
|
||||
}
|
||||
|
||||
private <T> List<DbAction<?>> deleteById(Object id, AggregateChange<T> aggregateChange) {
|
||||
private <T> List<DbAction<?>> deleteRoot(Object id, AggregateChange<T> aggregateChange) {
|
||||
|
||||
List<DbAction<?>> actions = new ArrayList<>(deleteReferencedEntities(id, aggregateChange));
|
||||
actions.add(new DbAction.DeleteRoot<>(aggregateChange.getEntityType(), id));
|
||||
actions.add(new DbAction.DeleteRoot<>(id, aggregateChange.getEntity(), aggregateChange.getEntityType()));
|
||||
|
||||
return actions;
|
||||
}
|
||||
@@ -97,8 +98,7 @@ public class RelationalEntityDeleteWriter implements EntityWriter<Object, Aggreg
|
||||
List<DbAction<?>> actions = new ArrayList<>();
|
||||
|
||||
context.findPersistentPropertyPaths(aggregateChange.getEntityType(), PersistentProperty::isEntity)
|
||||
.filter(p -> !p.getRequiredLeafProperty().isEmbedded())
|
||||
.forEach(p -> actions.add(new DbAction.Delete<>(id, p)));
|
||||
.filter(p -> !p.getRequiredLeafProperty().isEmbedded()).forEach(p -> actions.add(new DbAction.Delete<>(id, p)));
|
||||
|
||||
Collections.reverse(actions);
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.springframework.data.relational.core.conversion;
|
||||
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Utilities commonly used to set/get properties for instances of RelationalPersistentEntities.
|
||||
*
|
||||
* @author Tyler Van Gorder
|
||||
*/
|
||||
public class RelationalEntityVersionUtils {
|
||||
|
||||
private RelationalEntityVersionUtils() {}
|
||||
|
||||
/**
|
||||
* Get the current value of the version property for an instance of a relational persistent entity.
|
||||
*
|
||||
* @param instance must not be {@literal null}.
|
||||
* @param persistentEntity must not be {@literal null}.
|
||||
* @param converter must not be {@literal null}.
|
||||
* @return Current value of the version property
|
||||
* @throws IllegalArgumentException if the entity does not have a version property.
|
||||
*/
|
||||
@Nullable
|
||||
public static <S> Number getVersionNumberFromEntity(S instance, RelationalPersistentEntity<S> persistentEntity,
|
||||
RelationalConverter converter) {
|
||||
if (!persistentEntity.hasVersionProperty()) {
|
||||
throw new IllegalArgumentException("The entity does not have a version property.");
|
||||
}
|
||||
|
||||
ConvertingPropertyAccessor<S> convertingPropertyAccessor = new ConvertingPropertyAccessor<>(persistentEntity.getPropertyAccessor(instance),
|
||||
converter.getConversionService());
|
||||
return convertingPropertyAccessor.getProperty(persistentEntity.getRequiredVersionProperty(), Number.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the version property on an instance of a relational persistent entity. This method returns an instance of the
|
||||
* same type with the updated version property and will correctly handle the case where the version is immutable.
|
||||
*
|
||||
* @param instance must not be {@literal null}.
|
||||
* @param version The value to be set on the version property.
|
||||
* @param persistentEntity must not be {@literal null}.
|
||||
* @param converter must not be {@literal null}.
|
||||
* @return An instance of the entity with an updated version property.
|
||||
* @throws IllegalArgumentException if the entity does not have a version property.
|
||||
*/
|
||||
public static <S> S setVersionNumberOnEntity(S instance, @Nullable Number version,
|
||||
RelationalPersistentEntity<S> persistentEntity, RelationalConverter converter) {
|
||||
|
||||
if (!persistentEntity.hasVersionProperty()) {
|
||||
throw new IllegalArgumentException("The entity does not have a version property.");
|
||||
}
|
||||
|
||||
PersistentPropertyAccessor<S> propertyAccessor = converter.getPropertyAccessor(persistentEntity, instance);
|
||||
RelationalPersistentProperty versionProperty = persistentEntity.getRequiredVersionProperty();
|
||||
propertyAccessor.setProperty(versionProperty, version);
|
||||
|
||||
return propertyAccessor.getBean();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user