DATAJDBC-493 - Avoids deadlocks by acquiring lock on aggregate root table.
Introduces infrastructure to obtain locks and uses them to acquire locks on the table of the aggregate root before deleting references. Without this lock deletes access non root entities before the aggregate root, which is the opposite order of updates and thus may cause deadlocks. Original pull request: #196.
This commit is contained in:
committed by
Jens Schauder
parent
fa8b95c5f3
commit
04c29f4004
@@ -27,6 +27,7 @@ import org.springframework.lang.Nullable;
|
||||
* Executes an {@link MutableAggregateChange}.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
* @author Myeonghyeon Lee
|
||||
* @since 2.0
|
||||
*/
|
||||
class AggregateChangeExecutor {
|
||||
@@ -77,6 +78,10 @@ class AggregateChangeExecutor {
|
||||
executionContext.executeDeleteRoot((DbAction.DeleteRoot<?>) action);
|
||||
} else if (action instanceof DbAction.DeleteAllRoot) {
|
||||
executionContext.executeDeleteAllRoot((DbAction.DeleteAllRoot<?>) action);
|
||||
} else if (action instanceof DbAction.AcquireLockRoot) {
|
||||
executionContext.executeAcquireLock((DbAction.AcquireLockRoot<?>) action);
|
||||
} else if (action instanceof DbAction.AcquireLockAllRoot) {
|
||||
executionContext.executeAcquireLockAllRoot((DbAction.AcquireLockAllRoot<?>) action);
|
||||
} else {
|
||||
throw new RuntimeException("unexpected action");
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.springframework.data.relational.core.conversion.RelationalEntityVersi
|
||||
import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
import org.springframework.data.relational.core.sql.LockMode;
|
||||
import org.springframework.data.util.Pair;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -49,6 +50,7 @@ import org.springframework.util.Assert;
|
||||
/**
|
||||
* @author Jens Schauder
|
||||
* @author Umut Erturk
|
||||
* @author Myeonghyeon Lee
|
||||
*/
|
||||
class JdbcAggregateChangeExecutionContext {
|
||||
|
||||
@@ -164,6 +166,14 @@ class JdbcAggregateChangeExecutionContext {
|
||||
}
|
||||
}
|
||||
|
||||
<T> void executeAcquireLock(DbAction.AcquireLockRoot<T> acquireLock) {
|
||||
accessStrategy.acquireLockById(acquireLock.getId(), LockMode.PESSIMISTIC_WRITE, acquireLock.getEntityType());
|
||||
}
|
||||
|
||||
<T> void executeAcquireLockAllRoot(DbAction.AcquireLockAllRoot<T> acquireLock) {
|
||||
accessStrategy.acquireLockAll(LockMode.PESSIMISTIC_WRITE, acquireLock.getEntityType());
|
||||
}
|
||||
|
||||
private void add(DbActionExecutionResult result) {
|
||||
results.put(result.getAction(), result);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ import org.springframework.util.Assert;
|
||||
* @author Thomas Lang
|
||||
* @author Christoph Strobl
|
||||
* @author Milan Milanov
|
||||
* @author Myeonghyeon Lee
|
||||
*/
|
||||
public class JdbcAggregateTemplate implements JdbcAggregateOperations {
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.mapping.PersistentPropertyPath;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
import org.springframework.data.relational.core.sql.LockMode;
|
||||
|
||||
/**
|
||||
* Delegates each methods to the {@link DataAccessStrategy}s passed to the constructor in turn until the first that does
|
||||
@@ -33,6 +34,7 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentProp
|
||||
* @author Mark Paluch
|
||||
* @author Tyler Van Gorder
|
||||
* @author Milan Milanov
|
||||
* @author Myeonghyeon Lee
|
||||
* @since 1.1
|
||||
*/
|
||||
public class CascadingDataAccessStrategy implements DataAccessStrategy {
|
||||
@@ -115,6 +117,24 @@ public class CascadingDataAccessStrategy implements DataAccessStrategy {
|
||||
collectVoid(das -> das.deleteAll(propertyPath));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#acquireLockById(java.lang.Object, org.springframework.data.relational.core.sql.LockMode, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> void acquireLockById(Object id, LockMode lockMode, Class<T> domainType) {
|
||||
collectVoid(das -> das.acquireLockById(id, lockMode, domainType));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#acquireLockAll(org.springframework.data.relational.core.sql.LockMode, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> void acquireLockAll(LockMode lockMode, Class<T> domainType) {
|
||||
collectVoid(das -> das.acquireLockAll(lockMode, domainType));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#count(java.lang.Class)
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jdbc.core.JdbcAggregateOperations;
|
||||
import org.springframework.data.mapping.PersistentPropertyPath;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
import org.springframework.data.relational.core.sql.LockMode;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
@@ -33,6 +34,7 @@ import org.springframework.lang.Nullable;
|
||||
* @author Jens Schauder
|
||||
* @author Tyler Van Gorder
|
||||
* @author Milan Milanov
|
||||
* @author Myeonghyeon Lee
|
||||
*/
|
||||
public interface DataAccessStrategy extends RelationResolver {
|
||||
|
||||
@@ -129,6 +131,23 @@ public interface DataAccessStrategy extends RelationResolver {
|
||||
*/
|
||||
void deleteAll(PersistentPropertyPath<RelationalPersistentProperty> propertyPath);
|
||||
|
||||
/**
|
||||
* Acquire Lock
|
||||
*
|
||||
* @param id the id of the entity to load. Must not be {@code null}.
|
||||
* @param lockMode the lock mode for select. Must not be {@code null}.
|
||||
* @param domainType the domain type of the entity. Must not be {@code null}.
|
||||
*/
|
||||
<T> void acquireLockById(Object id, LockMode lockMode, Class<T> domainType);
|
||||
|
||||
/**
|
||||
* Acquire Lock entities of the given domain type.
|
||||
*
|
||||
* @param lockMode the lock mode for select. Must not be {@code null}.
|
||||
* @param domainType the domain type of the entity. Must not be {@code null}.
|
||||
*/
|
||||
<T> void acquireLockAll(LockMode lockMode, Class<T> domainType);
|
||||
|
||||
/**
|
||||
* Counts the rows in the table representing the given domain type.
|
||||
*
|
||||
|
||||
@@ -18,6 +18,8 @@ package org.springframework.data.jdbc.core.convert;
|
||||
import static org.springframework.data.jdbc.core.convert.SqlGenerator.*;
|
||||
|
||||
import java.sql.JDBCType;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
@@ -25,10 +27,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
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.dao.*;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jdbc.support.JdbcUtil;
|
||||
@@ -41,7 +40,9 @@ 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.IdentifierProcessing;
|
||||
import org.springframework.data.relational.core.sql.LockMode;
|
||||
import org.springframework.data.relational.core.sql.SqlIdentifier;
|
||||
import org.springframework.jdbc.core.ResultSetExtractor;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
|
||||
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
|
||||
@@ -62,6 +63,7 @@ import org.springframework.util.Assert;
|
||||
* @author Tom Hombergs
|
||||
* @author Tyler Van Gorder
|
||||
* @author Milan Milanov
|
||||
* @author Myeonghyeon Lee
|
||||
* @since 1.1
|
||||
*/
|
||||
public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
@@ -237,6 +239,27 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
.update(sql(propertyPath.getBaseProperty().getOwner().getType()).createDeleteAllSql(propertyPath));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#acquireLockById(java.lang.Object, org.springframework.data.relational.core.sql.LockMode, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> void acquireLockById(Object id, LockMode lockMode, Class<T> domainType) {
|
||||
String acquireLockByIdSql = sql(domainType).getAcquireLockById(lockMode);
|
||||
SqlIdentifierParameterSource parameter = createIdParameterSource(id, domainType);
|
||||
operations.queryForObject(acquireLockByIdSql, parameter, Object.class);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#acquireLockAll(org.springframework.data.relational.core.sql.LockMode, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> void acquireLockAll(LockMode lockMode, Class<T> domainType) {
|
||||
String acquireLockAllSql = sql(domainType).getAcquireLockAll(lockMode);
|
||||
operations.query(acquireLockAllSql, Collections.emptyMap(), new NoMappingResultSetExtractor());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#count(java.lang.Class)
|
||||
@@ -582,4 +605,14 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The type No mapping result set extractor.
|
||||
*/
|
||||
static class NoMappingResultSetExtractor implements ResultSetExtractor<Object> {
|
||||
@Override
|
||||
public Object extractData(ResultSet resultSet) throws SQLException, DataAccessException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.mapping.PersistentPropertyPath;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
import org.springframework.data.relational.core.sql.LockMode;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -28,6 +29,7 @@ import org.springframework.util.Assert;
|
||||
* @author Jens Schauder
|
||||
* @author Tyler Van Gorder
|
||||
* @author Milan Milanov
|
||||
* @author Myeonghyeon Lee
|
||||
* @since 1.1
|
||||
*/
|
||||
public class DelegatingDataAccessStrategy implements DataAccessStrategy {
|
||||
@@ -107,6 +109,24 @@ public class DelegatingDataAccessStrategy implements DataAccessStrategy {
|
||||
delegate.deleteAll(propertyPath);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#acquireLockById(java.lang.Object, org.springframework.data.relational.core.sql.LockMode, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> void acquireLockById(Object id, LockMode lockMode, Class<T> domainType) {
|
||||
delegate.acquireLockById(id, lockMode, domainType);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#acquireLockAll(org.springframework.data.relational.core.sql.LockMode, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> void acquireLockAll(LockMode lockMode, Class<T> domainType) {
|
||||
delegate.acquireLockAll(lockMode, domainType);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#count(java.lang.Class)
|
||||
|
||||
@@ -258,6 +258,26 @@ class SqlGenerator {
|
||||
return findOneSql.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code SELECT count(id) FROM … WHERE :id = … (LOCK CLAUSE)} statement.
|
||||
*
|
||||
* @param lockMode Lock clause mode.
|
||||
* @return the statement as a {@link String}. Guaranteed to be not {@literal null}.
|
||||
*/
|
||||
String getAcquireLockById(LockMode lockMode) {
|
||||
return this.createAcquireLockById(lockMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code SELECT count(id) FROM … (LOCK CLAUSE)} statement.
|
||||
*
|
||||
* @param lockMode Lock clause mode.
|
||||
* @return the statement as a {@link String}. Guaranteed to be not {@literal null}.
|
||||
*/
|
||||
String getAcquireLockAll(LockMode lockMode) {
|
||||
return this.createAcquireLockAll(lockMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code INSERT INTO … (…) VALUES(…)} statement.
|
||||
*
|
||||
@@ -359,6 +379,33 @@ class SqlGenerator {
|
||||
return render(select);
|
||||
}
|
||||
|
||||
private String createAcquireLockById(LockMode lockMode) {
|
||||
|
||||
Table table = this.getTable();
|
||||
|
||||
Select select = StatementBuilder //
|
||||
.select(getIdColumn()) //
|
||||
.from(table) //
|
||||
.where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) //
|
||||
.lock(lockMode) //
|
||||
.build();
|
||||
|
||||
return render(select);
|
||||
}
|
||||
|
||||
private String createAcquireLockAll(LockMode lockMode) {
|
||||
|
||||
Table table = this.getTable();
|
||||
|
||||
Select select = StatementBuilder //
|
||||
.select(getIdColumn()) //
|
||||
.from(table) //
|
||||
.lock(lockMode) //
|
||||
.build();
|
||||
|
||||
return render(select);
|
||||
}
|
||||
|
||||
private String createFindAllSql() {
|
||||
return render(selectBuilder().build());
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.apache.ibatis.session.SqlSession;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jdbc.core.convert.CascadingDataAccessStrategy;
|
||||
@@ -41,6 +42,7 @@ import org.springframework.data.relational.core.dialect.Dialect;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
import org.springframework.data.relational.core.sql.IdentifierProcessing;
|
||||
import org.springframework.data.relational.core.sql.LockMode;
|
||||
import org.springframework.data.relational.core.sql.SqlIdentifier;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -60,6 +62,7 @@ import org.springframework.util.Assert;
|
||||
* @author Mark Paluch
|
||||
* @author Tyler Van Gorder
|
||||
* @author Milan Milanov
|
||||
* @author Myeonghyeon Lee
|
||||
*/
|
||||
public class MyBatisDataAccessStrategy implements DataAccessStrategy {
|
||||
|
||||
@@ -248,6 +251,34 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy {
|
||||
sqlSession().delete(statement, parameter);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#acquireLockById(java.lang.Object, org.springframework.data.relational.core.sql.LockMode, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> void acquireLockById(Object id, LockMode lockMode, Class<T> domainType) {
|
||||
String statement = namespace(domainType) + ".acquireLockById";
|
||||
MyBatisContext parameter = new MyBatisContext(id, null, domainType, Collections.emptyMap());
|
||||
|
||||
long result = sqlSession().selectOne(statement, parameter);
|
||||
if (result < 1) {
|
||||
throw new EmptyResultDataAccessException(
|
||||
String.format("The lock target does not exist. id: %s, statement: %s", id, statement), 1);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#acquireLockAll(org.springframework.data.relational.core.sql.LockMode, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> void acquireLockAll(LockMode lockMode, Class<T> domainType) {
|
||||
String statement = namespace(domainType) + ".acquireLockAll";
|
||||
MyBatisContext parameter = new MyBatisContext(null, null, domainType, Collections.emptyMap());
|
||||
|
||||
sqlSession().selectOne(statement, parameter);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.jdbc.core.DataAccessStrategy#findById(java.lang.Object, java.lang.Class)
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.springframework.data.relational.core.dialect.AbstractDialect;
|
||||
import org.springframework.data.relational.core.dialect.Dialect;
|
||||
import org.springframework.data.relational.core.dialect.HsqlDbDialect;
|
||||
import org.springframework.data.relational.core.dialect.LimitClause;
|
||||
import org.springframework.data.relational.core.dialect.LockClause;
|
||||
import org.springframework.data.relational.core.sql.IdentifierProcessing;
|
||||
|
||||
/**
|
||||
@@ -27,6 +28,7 @@ import org.springframework.data.relational.core.sql.IdentifierProcessing;
|
||||
* @author Mark Paluch
|
||||
* @author Milan Milanov
|
||||
* @author Jens Schauder
|
||||
* @author Myeonghyeon Lee
|
||||
*/
|
||||
public class NonQuotingDialect extends AbstractDialect implements Dialect {
|
||||
|
||||
@@ -39,6 +41,11 @@ public class NonQuotingDialect extends AbstractDialect implements Dialect {
|
||||
return HsqlDbDialect.INSTANCE.limit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LockClause lock() {
|
||||
return HsqlDbDialect.INSTANCE.lock();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentifierProcessing getIdentifierProcessing() {
|
||||
return IdentifierProcessing.create(new IdentifierProcessing.Quoting(""), IdentifierProcessing.LetterCasing.AS_IS);
|
||||
|
||||
@@ -46,6 +46,7 @@ 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.Aliased;
|
||||
import org.springframework.data.relational.core.sql.LockMode;
|
||||
import org.springframework.data.relational.core.sql.SqlIdentifier;
|
||||
import org.springframework.data.relational.core.sql.Table;
|
||||
|
||||
@@ -96,16 +97,45 @@ public class SqlGeneratorUnitTests {
|
||||
|
||||
SoftAssertions softAssertions = new SoftAssertions();
|
||||
softAssertions.assertThat(sql) //
|
||||
.startsWith("SELECT") //
|
||||
.contains("dummy_entity.id1 AS id1,") //
|
||||
.contains("dummy_entity.x_name AS x_name,") //
|
||||
.contains("dummy_entity.x_other AS x_other,") //
|
||||
.contains("ref.x_l1id AS ref_x_l1id") //
|
||||
.contains("ref.x_content AS ref_x_content").contains(" FROM dummy_entity") //
|
||||
.contains("ON ref.dummy_entity = dummy_entity.id1") //
|
||||
.contains("WHERE dummy_entity.id1 = :id") //
|
||||
// 1-N relationships do not get loaded via join
|
||||
.doesNotContain("Element AS elements");
|
||||
.startsWith("SELECT") //
|
||||
.contains("dummy_entity.id1 AS id1,") //
|
||||
.contains("dummy_entity.x_name AS x_name,") //
|
||||
.contains("dummy_entity.x_other AS x_other,") //
|
||||
.contains("ref.x_l1id AS ref_x_l1id") //
|
||||
.contains("ref.x_content AS ref_x_content").contains(" FROM dummy_entity") //
|
||||
.contains("ON ref.dummy_entity = dummy_entity.id1") //
|
||||
.contains("WHERE dummy_entity.id1 = :id") //
|
||||
// 1-N relationships do not get loaded via join
|
||||
.doesNotContain("Element AS elements");
|
||||
softAssertions.assertAll();
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-493
|
||||
public void getAcquireLockById() {
|
||||
|
||||
String sql = sqlGenerator.getAcquireLockById(LockMode.PESSIMISTIC_WRITE);
|
||||
|
||||
SoftAssertions softAssertions = new SoftAssertions();
|
||||
softAssertions.assertThat(sql) //
|
||||
.startsWith("SELECT") //
|
||||
.contains("dummy_entity.id1") //
|
||||
.contains("WHERE dummy_entity.id1 = :id") //
|
||||
.contains("FOR UPDATE") //
|
||||
.doesNotContain("Element AS elements");
|
||||
softAssertions.assertAll();
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-493
|
||||
public void getAcquireLockAll() {
|
||||
|
||||
String sql = sqlGenerator.getAcquireLockAll(LockMode.PESSIMISTIC_WRITE);
|
||||
|
||||
SoftAssertions softAssertions = new SoftAssertions();
|
||||
softAssertions.assertThat(sql) //
|
||||
.startsWith("SELECT") //
|
||||
.contains("dummy_entity.id1") //
|
||||
.contains("FOR UPDATE") //
|
||||
.doesNotContain("Element AS elements");
|
||||
softAssertions.assertAll();
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
|
||||
import org.springframework.data.jdbc.testing.DatabaseProfileValueSource;
|
||||
@@ -34,7 +35,6 @@ import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||
import org.springframework.test.annotation.IfProfileValue;
|
||||
import org.springframework.test.annotation.ProfileValueSourceConfiguration;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.rules.SpringClassRule;
|
||||
import org.springframework.test.context.junit4.rules.SpringMethodRule;
|
||||
@@ -123,6 +123,131 @@ public class JdbcRepositoryConcurrencyIntegrationTests {
|
||||
assertThat(exceptions).isEmpty();
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-493
|
||||
public void updateConcurrencyWithDelete() throws Exception {
|
||||
|
||||
DummyEntity entity = createDummyEntity();
|
||||
entity = repository.save(entity);
|
||||
|
||||
Long targetId = entity.getId();
|
||||
assertThat(targetId).isNotNull();
|
||||
|
||||
List<DummyEntity> concurrencyEntities = createEntityStates(entity);
|
||||
|
||||
TransactionTemplate transactionTemplate = new TransactionTemplate(this.transactionManager);
|
||||
|
||||
List<Exception> exceptions = new CopyOnWriteArrayList<>();
|
||||
CountDownLatch startLatch = new CountDownLatch(concurrencyEntities.size() + 1); // latch for all threads to wait on.
|
||||
CountDownLatch doneLatch = new CountDownLatch(concurrencyEntities.size() + 1); // latch for main thread to wait on until all threads are done.
|
||||
|
||||
// update
|
||||
concurrencyEntities.stream() //
|
||||
.map(e -> new Thread(() -> {
|
||||
|
||||
try {
|
||||
|
||||
startLatch.countDown();
|
||||
startLatch.await();
|
||||
|
||||
transactionTemplate.execute(status -> repository.save(e));
|
||||
} catch (Exception ex) {
|
||||
// When the delete execution is complete, the Update execution throws an IncorrectUpdateSemanticsDataAccessException.
|
||||
if (ex.getCause() instanceof IncorrectUpdateSemanticsDataAccessException) {
|
||||
return;
|
||||
}
|
||||
|
||||
exceptions.add(ex);
|
||||
} finally {
|
||||
doneLatch.countDown();
|
||||
}
|
||||
})) //
|
||||
.forEach(Thread::start);
|
||||
|
||||
// delete
|
||||
new Thread(() -> {
|
||||
try {
|
||||
|
||||
startLatch.countDown();
|
||||
startLatch.await();
|
||||
|
||||
transactionTemplate.execute(status -> {
|
||||
repository.deleteById(targetId);
|
||||
return null;
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
exceptions.add(ex);
|
||||
} finally {
|
||||
doneLatch.countDown();
|
||||
}
|
||||
}).start();
|
||||
|
||||
doneLatch.await();
|
||||
|
||||
assertThat(exceptions).isEmpty();
|
||||
assertThat(repository.findById(entity.id)).isEmpty();
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-493
|
||||
public void updateConcurrencyWithDeleteAll() throws Exception {
|
||||
|
||||
DummyEntity entity = createDummyEntity();
|
||||
entity = repository.save(entity);
|
||||
|
||||
List<DummyEntity> concurrencyEntities = createEntityStates(entity);
|
||||
|
||||
TransactionTemplate transactionTemplate = new TransactionTemplate(this.transactionManager);
|
||||
|
||||
List<Exception> exceptions = new CopyOnWriteArrayList<>();
|
||||
CountDownLatch startLatch = new CountDownLatch(concurrencyEntities.size() + 1); // latch for all threads to wait on.
|
||||
CountDownLatch doneLatch = new CountDownLatch(concurrencyEntities.size() + 1); // latch for main thread to wait on until all threads are done.
|
||||
|
||||
// update
|
||||
concurrencyEntities.stream() //
|
||||
.map(e -> new Thread(() -> {
|
||||
|
||||
try {
|
||||
|
||||
startLatch.countDown();
|
||||
startLatch.await();
|
||||
|
||||
transactionTemplate.execute(status -> repository.save(e));
|
||||
} catch (Exception ex) {
|
||||
// When the delete execution is complete, the Update execution throws an IncorrectUpdateSemanticsDataAccessException.
|
||||
if (ex.getCause() instanceof IncorrectUpdateSemanticsDataAccessException) {
|
||||
return;
|
||||
}
|
||||
|
||||
exceptions.add(ex);
|
||||
} finally {
|
||||
doneLatch.countDown();
|
||||
}
|
||||
})) //
|
||||
.forEach(Thread::start);
|
||||
|
||||
// delete
|
||||
new Thread(() -> {
|
||||
try {
|
||||
|
||||
startLatch.countDown();
|
||||
startLatch.await();
|
||||
|
||||
transactionTemplate.execute(status -> {
|
||||
repository.deleteAll();
|
||||
return null;
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
exceptions.add(ex);
|
||||
} finally {
|
||||
doneLatch.countDown();
|
||||
}
|
||||
}).start();
|
||||
|
||||
doneLatch.await();
|
||||
|
||||
assertThat(exceptions).isEmpty();
|
||||
assertThat(repository.count()).isEqualTo(0);
|
||||
}
|
||||
|
||||
private List<DummyEntity> createEntityStates(DummyEntity entity) {
|
||||
|
||||
List<DummyEntity> concurrencyEntities = new ArrayList<>();
|
||||
|
||||
@@ -20,7 +20,9 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.relational.core.dialect.AbstractDialect;
|
||||
import org.springframework.data.relational.core.dialect.ArrayColumns;
|
||||
import org.springframework.data.relational.core.dialect.LimitClause;
|
||||
import org.springframework.data.relational.core.dialect.LockClause;
|
||||
import org.springframework.data.relational.core.sql.IdentifierProcessing;
|
||||
import org.springframework.data.relational.core.sql.LockOptions;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
@@ -28,6 +30,7 @@ import org.springframework.util.ClassUtils;
|
||||
* An SQL dialect for the ANSI SQL standard.
|
||||
*
|
||||
* @author Milan Milanov
|
||||
* @author Myeonghyeon Lee
|
||||
* @since 2.0
|
||||
*/
|
||||
public class AnsiDialect extends AbstractDialect {
|
||||
@@ -78,6 +81,27 @@ public class AnsiDialect extends AbstractDialect {
|
||||
}
|
||||
};
|
||||
|
||||
private static final LockClause LOCK_CLAUSE = new LockClause() {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.relational.core.dialect.LockClause#getLock(LockOptions)
|
||||
*/
|
||||
@Override
|
||||
public String getLock(LockOptions lockOptions) {
|
||||
return "FOR UPDATE";
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.relational.core.dialect.LimitClause#getClausePosition()
|
||||
*/
|
||||
@Override
|
||||
public Position getClausePosition() {
|
||||
return Position.AFTER_ORDER_BY;
|
||||
}
|
||||
};
|
||||
|
||||
private final AnsiArrayColumns ARRAY_COLUMNS = new AnsiArrayColumns();
|
||||
|
||||
/*
|
||||
@@ -89,6 +113,15 @@ public class AnsiDialect extends AbstractDialect {
|
||||
return LIMIT_CLAUSE;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.relational.core.dialect.Dialect#lock()
|
||||
*/
|
||||
@Override
|
||||
public LockClause lock() {
|
||||
return LOCK_CLAUSE;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.relational.core.dialect.Dialect#getArraySupport()
|
||||
|
||||
Reference in New Issue
Block a user