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:
Tyler Van Gorder
2019-11-27 10:26:15 -08:00
committed by Jens Schauder
parent 8d0441c2e0
commit f7a04dcc2b
24 changed files with 772 additions and 88 deletions

View File

@@ -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());

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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}.
*

View File

@@ -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)

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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.
*/

View File

@@ -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)

View File

@@ -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();

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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

View File

@@ -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);

View File

@@ -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
);

View File

@@ -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
);

View File

@@ -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
);

View File

@@ -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
);

View File

@@ -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
);

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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();
}
}