Browse Source

DATAJDBC-493 - Polishing.

Using `execute` instead of `query` since we are not interested in the results.
Refactoring of the concurrency tests.
Make the concurrency tests run with all databases.
Added support for DB2.
Moved AnsiDialect to the main sources so it can be referenced in other Dialects.

Original pull request: #196.
pull/216/head
Jens Schauder 6 years ago
parent
commit
aa05b792c3
No known key found for this signature in database
GPG Key ID: 996B1389BA0721C3
  1. 1
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java
  2. 4
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java
  3. 24
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java
  4. 7
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java
  5. 3
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java
  6. 56
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java
  7. 210
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryConcurrencyIntegrationTests.java
  8. 1
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java
  9. 2
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryConcurrencyIntegrationTests-h2.sql
  10. 2
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryConcurrencyIntegrationTests-hsql.sql
  11. 2
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryConcurrencyIntegrationTests-mariadb.sql
  12. 2
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryConcurrencyIntegrationTests-mssql.sql
  13. 4
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryConcurrencyIntegrationTests-postgres.sql
  14. 9
      spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java
  15. 44
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/AbstractDialect.java
  16. 8
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/AnsiDialect.java
  17. 6
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Db2Dialect.java
  18. 24
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/H2Dialect.java
  19. 17
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/HsqlDbDialect.java
  20. 1
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java
  21. 3
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerSelectRenderContext.java
  22. 2
      spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelectBuilder.java
  23. 1
      spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/LockMode.java
  24. 2
      spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/LockOptions.java

1
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java

@ -50,7 +50,6 @@ import org.springframework.util.Assert;
* @author Thomas Lang * @author Thomas Lang
* @author Christoph Strobl * @author Christoph Strobl
* @author Milan Milanov * @author Milan Milanov
* @author Myeonghyeon Lee
*/ */
public class JdbcAggregateTemplate implements JdbcAggregateOperations { public class JdbcAggregateTemplate implements JdbcAggregateOperations {

4
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java

@ -132,7 +132,7 @@ public interface DataAccessStrategy extends RelationResolver {
void deleteAll(PersistentPropertyPath<RelationalPersistentProperty> propertyPath); void deleteAll(PersistentPropertyPath<RelationalPersistentProperty> propertyPath);
/** /**
* Acquire Lock * Acquire a lock on the aggregate specified by id.
* *
* @param id the id of the entity to load. Must not be {@code null}. * @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 lockMode the lock mode for select. Must not be {@code null}.
@ -141,7 +141,7 @@ public interface DataAccessStrategy extends RelationResolver {
<T> void acquireLockById(Object id, LockMode lockMode, Class<T> domainType); <T> void acquireLockById(Object id, LockMode lockMode, Class<T> domainType);
/** /**
* Acquire Lock entities of the given domain type. * Acquire a lock on all aggregates of the given domain type.
* *
* @param lockMode the lock mode for select. 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}. * @param domainType the domain type of the entity. Must not be {@code null}.

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

@ -18,6 +18,7 @@ package org.springframework.data.jdbc.core.convert;
import static org.springframework.data.jdbc.core.convert.SqlGenerator.*; import static org.springframework.data.jdbc.core.convert.SqlGenerator.*;
import java.sql.JDBCType; import java.sql.JDBCType;
import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
@ -27,7 +28,11 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.springframework.dao.*; import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.jdbc.support.JdbcUtil; import org.springframework.data.jdbc.support.JdbcUtil;
@ -42,6 +47,7 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentProp
import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.data.relational.core.sql.LockMode; import org.springframework.data.relational.core.sql.LockMode;
import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.jdbc.core.PreparedStatementCallback;
import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
@ -245,9 +251,10 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
*/ */
@Override @Override
public <T> void acquireLockById(Object id, LockMode lockMode, Class<T> domainType) { public <T> void acquireLockById(Object id, LockMode lockMode, Class<T> domainType) {
String acquireLockByIdSql = sql(domainType).getAcquireLockById(lockMode); String acquireLockByIdSql = sql(domainType).getAcquireLockById(lockMode);
SqlIdentifierParameterSource parameter = createIdParameterSource(id, domainType); SqlIdentifierParameterSource parameter = createIdParameterSource(id, domainType);
operations.queryForObject(acquireLockByIdSql, parameter, Object.class); operations.execute(acquireLockByIdSql, parameter, ps -> {ps.execute(); return null;});
} }
/* /*
@ -256,8 +263,9 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
*/ */
@Override @Override
public <T> void acquireLockAll(LockMode lockMode, Class<T> domainType) { public <T> void acquireLockAll(LockMode lockMode, Class<T> domainType) {
String acquireLockAllSql = sql(domainType).getAcquireLockAll(lockMode); String acquireLockAllSql = sql(domainType).getAcquireLockAll(lockMode);
operations.query(acquireLockAllSql, Collections.emptyMap(), new NoMappingResultSetExtractor()); operations.getJdbcOperations().execute(acquireLockAllSql);
} }
/* /*
@ -605,14 +613,4 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
return null; 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;
}
}
} }

7
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java

@ -257,13 +257,15 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy {
*/ */
@Override @Override
public <T> void acquireLockById(Object id, LockMode lockMode, Class<T> domainType) { public <T> void acquireLockById(Object id, LockMode lockMode, Class<T> domainType) {
String statement = namespace(domainType) + ".acquireLockById"; String statement = namespace(domainType) + ".acquireLockById";
MyBatisContext parameter = new MyBatisContext(id, null, domainType, Collections.emptyMap()); MyBatisContext parameter = new MyBatisContext(id, null, domainType, Collections.emptyMap());
long result = sqlSession().selectOne(statement, parameter); long result = sqlSession().selectOne(statement, parameter);
if (result < 1) { if (result < 1) {
throw new EmptyResultDataAccessException(
String.format("The lock target does not exist. id: %s, statement: %s", id, statement), 1); String message = String.format("The lock target does not exist. id: %s, statement: %s", id, statement);
throw new EmptyResultDataAccessException(message, 1);
} }
} }
@ -273,6 +275,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy {
*/ */
@Override @Override
public <T> void acquireLockAll(LockMode lockMode, Class<T> domainType) { public <T> void acquireLockAll(LockMode lockMode, Class<T> domainType) {
String statement = namespace(domainType) + ".acquireLockAll"; String statement = namespace(domainType) + ".acquireLockAll";
MyBatisContext parameter = new MyBatisContext(null, null, domainType, Collections.emptyMap()); MyBatisContext parameter = new MyBatisContext(null, null, domainType, Collections.emptyMap());

3
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java

@ -23,9 +23,8 @@ import org.junit.Test;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.jdbc.core.mapping.PersistentPropertyPathTestUtils; import org.springframework.data.jdbc.core.mapping.PersistentPropertyPathTestUtils;
import org.springframework.data.jdbc.testing.AnsiDialect; import org.springframework.data.relational.core.dialect.AnsiDialect;
import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.relational.core.dialect.HsqlDbDialect;
import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.NamingStrategy;
import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;

56
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java

@ -25,7 +25,6 @@ import java.util.Set;
import org.assertj.core.api.SoftAssertions; import org.assertj.core.api.SoftAssertions;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.annotation.ReadOnlyProperty;
import org.springframework.data.annotation.Version; import org.springframework.data.annotation.Version;
@ -36,7 +35,7 @@ import org.springframework.data.jdbc.core.PropertyPathTestingUtils;
import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.jdbc.core.mapping.PersistentPropertyPathTestUtils; import org.springframework.data.jdbc.core.mapping.PersistentPropertyPathTestUtils;
import org.springframework.data.jdbc.testing.AnsiDialect; import org.springframework.data.relational.core.dialect.AnsiDialect;
import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.dialect.Dialect;
import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Column;
@ -95,19 +94,18 @@ public class SqlGeneratorUnitTests {
String sql = sqlGenerator.getFindOne(); String sql = sqlGenerator.getFindOne();
SoftAssertions softAssertions = new SoftAssertions(); SoftAssertions.assertSoftly(softly -> softly //
softAssertions.assertThat(sql) // .assertThat(sql) //
.startsWith("SELECT") // .startsWith("SELECT") //
.contains("dummy_entity.id1 AS id1,") // .contains("dummy_entity.id1 AS id1,") //
.contains("dummy_entity.x_name AS x_name,") // .contains("dummy_entity.x_name AS x_name,") //
.contains("dummy_entity.x_other AS x_other,") // .contains("dummy_entity.x_other AS x_other,") //
.contains("ref.x_l1id AS ref_x_l1id") // .contains("ref.x_l1id AS ref_x_l1id") //
.contains("ref.x_content AS ref_x_content").contains(" FROM dummy_entity") // .contains("ref.x_content AS ref_x_content").contains(" FROM dummy_entity") //
.contains("ON ref.dummy_entity = dummy_entity.id1") // .contains("ON ref.dummy_entity = dummy_entity.id1") //
.contains("WHERE dummy_entity.id1 = :id") // .contains("WHERE dummy_entity.id1 = :id") //
// 1-N relationships do not get loaded via join // 1-N relationships do not get loaded via join
.doesNotContain("Element AS elements"); .doesNotContain("Element AS elements"));
softAssertions.assertAll();
} }
@Test // DATAJDBC-493 @Test // DATAJDBC-493
@ -115,14 +113,13 @@ public class SqlGeneratorUnitTests {
String sql = sqlGenerator.getAcquireLockById(LockMode.PESSIMISTIC_WRITE); String sql = sqlGenerator.getAcquireLockById(LockMode.PESSIMISTIC_WRITE);
SoftAssertions softAssertions = new SoftAssertions(); SoftAssertions.assertSoftly(softly -> softly //
softAssertions.assertThat(sql) // .assertThat(sql) //
.startsWith("SELECT") // .startsWith("SELECT") //
.contains("dummy_entity.id1") // .contains("dummy_entity.id1") //
.contains("WHERE dummy_entity.id1 = :id") // .contains("WHERE dummy_entity.id1 = :id") //
.contains("FOR UPDATE") // .contains("FOR UPDATE") //
.doesNotContain("Element AS elements"); .doesNotContain("Element AS elements"));
softAssertions.assertAll();
} }
@Test // DATAJDBC-493 @Test // DATAJDBC-493
@ -130,13 +127,12 @@ public class SqlGeneratorUnitTests {
String sql = sqlGenerator.getAcquireLockAll(LockMode.PESSIMISTIC_WRITE); String sql = sqlGenerator.getAcquireLockAll(LockMode.PESSIMISTIC_WRITE);
SoftAssertions softAssertions = new SoftAssertions(); SoftAssertions.assertSoftly(softly -> softly //
softAssertions.assertThat(sql) // .assertThat(sql) //
.startsWith("SELECT") // .startsWith("SELECT") //
.contains("dummy_entity.id1") // .contains("dummy_entity.id1") //
.contains("FOR UPDATE") // .contains("FOR UPDATE") //
.doesNotContain("Element AS elements"); .doesNotContain("Element AS elements"));
softAssertions.assertAll();
} }
@Test // DATAJDBC-112 @Test // DATAJDBC-112

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

@ -15,10 +15,21 @@
*/ */
package org.springframework.data.jdbc.repository; package org.springframework.data.jdbc.repository;
import static org.assertj.core.api.Assertions.*;
import junit.framework.AssertionFailedError; import junit.framework.AssertionFailedError;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.With; import lombok.With;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.function.UnaryOperator;
import org.junit.Before;
import org.junit.ClassRule; import org.junit.ClassRule;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
@ -29,34 +40,20 @@ import org.springframework.context.annotation.Import;
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException; import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.jdbc.testing.DatabaseProfileValueSource;
import org.springframework.data.jdbc.testing.TestConfiguration; import org.springframework.data.jdbc.testing.TestConfiguration;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.test.annotation.IfProfileValue;
import org.springframework.test.annotation.ProfileValueSourceConfiguration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.rules.SpringClassRule; import org.springframework.test.context.junit4.rules.SpringClassRule;
import org.springframework.test.context.junit4.rules.SpringMethodRule; import org.springframework.test.context.junit4.rules.SpringMethodRule;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import java.util.ArrayList; /**
import java.util.Arrays; * Tests that highly concurrent update operations of an entity don't cause deadlocks.
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import static org.assertj.core.api.Assertions.assertThat;
/** Tests that highly concurrent update operations of an entity don't cause deadlocks.
* *
* @author Myeonghyeon Lee * @author Myeonghyeon Lee
* @author Jens Schauder * @author Jens Schauder
*/ */
@ContextConfiguration
@ProfileValueSourceConfiguration(DatabaseProfileValueSource.class)
@IfProfileValue(name = "current.database.is.not.mysql", value = "false")
public class JdbcRepositoryConcurrencyIntegrationTests { public class JdbcRepositoryConcurrencyIntegrationTests {
@Configuration @Configuration
@ -83,38 +80,37 @@ public class JdbcRepositoryConcurrencyIntegrationTests {
@Autowired DummyEntityRepository repository; @Autowired DummyEntityRepository repository;
@Autowired PlatformTransactionManager transactionManager; @Autowired PlatformTransactionManager transactionManager;
@Test // DATAJDBC-488 List<DummyEntity> concurrencyEntities;
public void updateConcurrencyWithEmptyReferences() throws Exception { DummyEntity entity;
DummyEntity entity = createDummyEntity(); TransactionTemplate transactionTemplate;
entity = repository.save(entity); List<Exception> exceptions;
@Before
public void before() {
entity = repository.save(createDummyEntity());
assertThat(entity.getId()).isNotNull(); assertThat(entity.getId()).isNotNull();
List<DummyEntity> concurrencyEntities = createEntityStates(entity); concurrencyEntities = createEntityStates(entity);
TransactionTemplate transactionTemplate = new TransactionTemplate(this.transactionManager); transactionTemplate = new TransactionTemplate(this.transactionManager);
List<Exception> exceptions = new CopyOnWriteArrayList<>(); exceptions = new CopyOnWriteArrayList<>();
CountDownLatch startLatch = new CountDownLatch(concurrencyEntities.size()); // latch for all threads to wait on. }
CountDownLatch doneLatch = new CountDownLatch(concurrencyEntities.size()); // latch for main thread to wait on until all threads are done.
concurrencyEntities.stream() // @Test // DATAJDBC-488
.map(e -> new Thread(() -> { public void updateConcurrencyWithEmptyReferences() throws Exception {
try { // latch for all threads to wait on.
CountDownLatch startLatch = new CountDownLatch(concurrencyEntities.size());
// latch for main thread to wait on until all threads are done.
CountDownLatch doneLatch = new CountDownLatch(concurrencyEntities.size());
startLatch.countDown(); UnaryOperator<DummyEntity> action = e -> repository.save(e);
startLatch.await();
transactionTemplate.execute(status -> repository.save(e)); concurrencyEntities.forEach(e -> executeInParallel(startLatch, doneLatch, action, e));
} catch (Exception ex) {
exceptions.add(ex);
} finally {
doneLatch.countDown();
}
})) //
.forEach(Thread::start);
doneLatch.await(); doneLatch.await();
@ -124,62 +120,31 @@ public class JdbcRepositoryConcurrencyIntegrationTests {
} }
@Test // DATAJDBC-493 @Test // DATAJDBC-493
public void updateConcurrencyWithDelete() throws Exception { public void concurrentUpdateAndDelete() 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 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. CountDownLatch doneLatch = new CountDownLatch(concurrencyEntities.size() + 1); // latch for main thread to wait on
// until all threads are done.
// update UnaryOperator<DummyEntity> updateAction = e -> {
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 { try {
return repository.save(e);
startLatch.countDown();
startLatch.await();
transactionTemplate.execute(status -> {
repository.deleteById(targetId);
return null;
});
} catch (Exception ex) { } catch (Exception ex) {
exceptions.add(ex); // When the delete execution is complete, the Update execution throws an
} finally { // IncorrectUpdateSemanticsDataAccessException.
doneLatch.countDown(); if (ex.getCause() instanceof IncorrectUpdateSemanticsDataAccessException) {
return null;
}
throw ex;
} }
}).start(); };
UnaryOperator<DummyEntity> deleteAction = e -> {
repository.deleteById(entity.id);
return null;
};
concurrencyEntities.forEach(e -> executeInParallel(startLatch, doneLatch, updateAction, e));
executeInParallel(startLatch, doneLatch, deleteAction, entity);
doneLatch.await(); doneLatch.await();
@ -188,42 +153,41 @@ public class JdbcRepositoryConcurrencyIntegrationTests {
} }
@Test // DATAJDBC-493 @Test // DATAJDBC-493
public void updateConcurrencyWithDeleteAll() throws Exception { public void concurrentUpdateAndDeleteAll() 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 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. CountDownLatch doneLatch = new CountDownLatch(concurrencyEntities.size() + 1); // latch for main thread to wait on
// until all threads are done.
// update UnaryOperator<DummyEntity> updateAction = e -> {
concurrencyEntities.stream() // try {
.map(e -> new Thread(() -> { return repository.save(e);
} catch (Exception ex) {
// When the delete execution is complete, the Update execution throws an
// IncorrectUpdateSemanticsDataAccessException.
if (ex.getCause() instanceof IncorrectUpdateSemanticsDataAccessException) {
return null;
}
throw ex;
}
};
try { UnaryOperator<DummyEntity> deleteAction = e -> {
repository.deleteAll();
return null;
};
startLatch.countDown(); concurrencyEntities.forEach(e -> executeInParallel(startLatch, doneLatch, updateAction, e));
startLatch.await(); executeInParallel(startLatch, doneLatch, deleteAction, entity);
transactionTemplate.execute(status -> repository.save(e)); doneLatch.await();
} catch (Exception ex) {
// When the delete execution is complete, the Update execution throws an IncorrectUpdateSemanticsDataAccessException.
if (ex.getCause() instanceof IncorrectUpdateSemanticsDataAccessException) {
return;
}
exceptions.add(ex); assertThat(exceptions).isEmpty();
} finally { assertThat(repository.count()).isEqualTo(0);
doneLatch.countDown(); }
}
})) //
.forEach(Thread::start);
private void executeInParallel(CountDownLatch startLatch, CountDownLatch doneLatch,
UnaryOperator<DummyEntity> deleteAction, DummyEntity entity) {
// delete // delete
new Thread(() -> { new Thread(() -> {
try { try {
@ -231,21 +195,13 @@ public class JdbcRepositoryConcurrencyIntegrationTests {
startLatch.countDown(); startLatch.countDown();
startLatch.await(); startLatch.await();
transactionTemplate.execute(status -> { transactionTemplate.execute(status -> deleteAction.apply(entity));
repository.deleteAll();
return null;
});
} catch (Exception ex) { } catch (Exception ex) {
exceptions.add(ex); exceptions.add(ex);
} finally { } finally {
doneLatch.countDown(); doneLatch.countDown();
} }
}).start(); }).start();
doneLatch.await();
assertThat(exceptions).isEmpty();
assertThat(repository.count()).isEqualTo(0);
} }
private List<DummyEntity> createEntityStates(DummyEntity entity) { private List<DummyEntity> createEntityStates(DummyEntity entity) {
@ -254,7 +210,7 @@ public class JdbcRepositoryConcurrencyIntegrationTests {
Element element1 = new Element(null, 1L); Element element1 = new Element(null, 1L);
Element element2 = new Element(null, 2L); Element element2 = new Element(null, 2L);
for (int i = 0; i < 100; i++) { for (int i = 0; i < 50; i++) {
List<Element> newContent = Arrays.asList(element1.withContent(element1.content + i + 2), List<Element> newContent = Arrays.asList(element1.withContent(element1.content + i + 2),
element2.withContent(element2.content + i + 2)); element2.withContent(element2.content + i + 2));

1
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java

@ -64,7 +64,6 @@ import org.springframework.transaction.annotation.Transactional;
* @author Jens Schauder * @author Jens Schauder
* @author Mark Paluch * @author Mark Paluch
*/ */
@ContextConfiguration
@Transactional @Transactional
public class JdbcRepositoryIntegrationTests { public class JdbcRepositoryIntegrationTests {

2
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryConcurrencyIntegrationTests-h2.sql

@ -0,0 +1,2 @@
CREATE TABLE dummy_entity ( id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100));
CREATE TABLE element (id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, content BIGINT, Dummy_Entity_key BIGINT,dummy_entity BIGINT);

2
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryConcurrencyIntegrationTests-hsql.sql

@ -0,0 +1,2 @@
CREATE TABLE dummy_entity ( id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100));
CREATE TABLE element (id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, content BIGINT, Dummy_Entity_key BIGINT,dummy_entity BIGINT);

2
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryConcurrencyIntegrationTests-mariadb.sql

@ -0,0 +1,2 @@
CREATE TABLE dummy_entity ( id BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100));
CREATE TABLE element (id BIGINT AUTO_INCREMENT PRIMARY KEY, content BIGINT, Dummy_Entity_key BIGINT,dummy_entity BIGINT);

2
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryConcurrencyIntegrationTests-mssql.sql

@ -0,0 +1,2 @@
CREATE TABLE dummy_entity ( id BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100));
CREATE TABLE element (id BIGINT AUTO_INCREMENT PRIMARY KEY, content BIGINT, Dummy_Entity_key BIGINT,dummy_entity BIGINT);

4
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryConcurrencyIntegrationTests-postgres.sql

@ -0,0 +1,4 @@
DROP TABLE dummy_entity;
DROP TABLE element;
CREATE TABLE dummy_entity ( id SERIAL PRIMARY KEY, NAME VARCHAR(100));
CREATE TABLE element (id SERIAL PRIMARY KEY, content BIGINT, Dummy_Entity_key BIGINT,dummy_entity BIGINT);

9
spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java

@ -327,10 +327,11 @@ public interface DbAction<T> {
* @param <T> type of the entity for which this represents a database interaction. * @param <T> type of the entity for which this represents a database interaction.
*/ */
final class AcquireLockRoot<T> implements DbAction<T> { final class AcquireLockRoot<T> implements DbAction<T> {
private final Object id; private final Object id;
private final Class<T> entityType; private final Class<T> entityType;
public AcquireLockRoot(Object id, Class<T> entityType) { AcquireLockRoot(Object id, Class<T> entityType) {
this.id = id; this.id = id;
this.entityType = entityType; this.entityType = entityType;
} }
@ -349,15 +350,15 @@ public interface DbAction<T> {
} }
/** /**
* Represents an acquire lock statement for all entities that that a reachable via a give path from any aggregate root of a * Represents an acquire lock statement for all aggregate roots of a given type.
* given type.
* *
* @param <T> type of the entity for which this represents a database interaction. * @param <T> type of the entity for which this represents a database interaction.
*/ */
final class AcquireLockAllRoot<T> implements DbAction<T> { final class AcquireLockAllRoot<T> implements DbAction<T> {
private final Class<T> entityType; private final Class<T> entityType;
public AcquireLockAllRoot(Class<T> entityType) { AcquireLockAllRoot(Class<T> entityType) {
this.entityType = entityType; this.entityType = entityType;
} }

44
spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/AbstractDialect.java

@ -57,12 +57,8 @@ public abstract class AbstractDialect implements Dialect {
Function<Select, ? extends CharSequence> afterFromTable = select -> ""; Function<Select, ? extends CharSequence> afterFromTable = select -> "";
LockClause lockClause = lock(); LockClause lockClause = lock();
switch (lockClause.getClausePosition()) { if (lockClause.getClausePosition() == LockClause.Position.AFTER_FROM_TABLE) {
afterFromTable = new LockRenderFunction(lockClause);
case AFTER_FROM_TABLE:
afterFromTable = new LockRenderFunction(lockClause);
default:
} }
return afterFromTable.andThen(PrependWithLeadingWhitespace.INSTANCE); return afterFromTable.andThen(PrependWithLeadingWhitespace.INSTANCE);
@ -78,31 +74,19 @@ public abstract class AbstractDialect implements Dialect {
Function<Select, ? extends CharSequence> afterOrderByLimit = getAfterOrderByLimit(); Function<Select, ? extends CharSequence> afterOrderByLimit = getAfterOrderByLimit();
Function<Select, ? extends CharSequence> afterOrderByLock = getAfterOrderByLock(); Function<Select, ? extends CharSequence> afterOrderByLock = getAfterOrderByLock();
return select -> { return select -> String.valueOf(afterOrderByLimit.apply(select)) +
afterOrderByLock.apply(select);
StringBuilder afterOrderByBuilder = new StringBuilder();
afterOrderByBuilder.append(afterOrderByLimit.apply(select));
afterOrderByBuilder.append(afterOrderByLock.apply(select));
return afterOrderByBuilder.toString();
};
} }
private Function<Select, ? extends CharSequence> getAfterOrderByLimit() { private Function<Select, ? extends CharSequence> getAfterOrderByLimit() {
LimitClause limit = limit(); LimitClause limit = limit();
Function<Select, ? extends CharSequence> afterOrderByLimit = select -> ""; if (limit.getClausePosition() == LimitClause.Position.AFTER_ORDER_BY) {
return new AfterOrderByLimitRenderFunction(limit) //
switch (limit.getClausePosition()) { .andThen(PrependWithLeadingWhitespace.INSTANCE);
} else {
case AFTER_ORDER_BY: throw new UnsupportedOperationException(String.format("Clause position %s not supported!", limit));
afterOrderByLimit = new AfterOrderByLimitRenderFunction(limit);
break;
default:
throw new UnsupportedOperationException(String.format("Clause position %s not supported!", limit));
} }
return afterOrderByLimit.andThen(PrependWithLeadingWhitespace.INSTANCE);
} }
private Function<Select, ? extends CharSequence> getAfterOrderByLock() { private Function<Select, ? extends CharSequence> getAfterOrderByLock() {
@ -110,12 +94,8 @@ public abstract class AbstractDialect implements Dialect {
Function<Select, ? extends CharSequence> afterOrderByLock = select -> ""; Function<Select, ? extends CharSequence> afterOrderByLock = select -> "";
switch (lock.getClausePosition()) { if (lock.getClausePosition() == LockClause.Position.AFTER_ORDER_BY) {
afterOrderByLock = new LockRenderFunction(lock);
case AFTER_ORDER_BY:
afterOrderByLock = new LockRenderFunction(lock);
default:
} }
return afterOrderByLock.andThen(PrependWithLeadingWhitespace.INSTANCE); return afterOrderByLock.andThen(PrependWithLeadingWhitespace.INSTANCE);
@ -124,7 +104,7 @@ public abstract class AbstractDialect implements Dialect {
/** /**
* {@link SelectRenderContext} derived from {@link Dialect} specifics. * {@link SelectRenderContext} derived from {@link Dialect} specifics.
*/ */
class DialectSelectRenderContext implements SelectRenderContext { static class DialectSelectRenderContext implements SelectRenderContext {
private final Function<Select, ? extends CharSequence> afterFromTable; private final Function<Select, ? extends CharSequence> afterFromTable;
private final Function<Select, ? extends CharSequence> afterOrderBy; private final Function<Select, ? extends CharSequence> afterOrderBy;

8
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/AnsiDialect.java → spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/AnsiDialect.java

@ -13,14 +13,10 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.data.jdbc.testing; package org.springframework.data.relational.core.dialect;
import lombok.RequiredArgsConstructor; 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.IdentifierProcessing;
import org.springframework.data.relational.core.sql.LockOptions; import org.springframework.data.relational.core.sql.LockOptions;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -81,7 +77,7 @@ public class AnsiDialect extends AbstractDialect {
} }
}; };
private static final LockClause LOCK_CLAUSE = new LockClause() { static final LockClause LOCK_CLAUSE = new LockClause() {
/* /*
* (non-Javadoc) * (non-Javadoc)

6
spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Db2Dialect.java

@ -16,6 +16,7 @@
package org.springframework.data.relational.core.dialect; package org.springframework.data.relational.core.dialect;
import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.data.relational.core.sql.LockOptions;
/** /**
* An SQL dialect for DB2. * An SQL dialect for DB2.
@ -80,6 +81,11 @@ public class Db2Dialect extends AbstractDialect {
return LIMIT_CLAUSE; return LIMIT_CLAUSE;
} }
@Override
public LockClause lock() {
return AnsiDialect.LOCK_CLAUSE;
}
/* /*
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.data.relational.core.dialect.Dialect#getIdentifierProcessing() * @see org.springframework.data.relational.core.dialect.Dialect#getIdentifierProcessing()

24
spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/H2Dialect.java

@ -20,7 +20,6 @@ import lombok.RequiredArgsConstructor;
import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.data.relational.core.sql.IdentifierProcessing.LetterCasing; import org.springframework.data.relational.core.sql.IdentifierProcessing.LetterCasing;
import org.springframework.data.relational.core.sql.IdentifierProcessing.Quoting; import org.springframework.data.relational.core.sql.IdentifierProcessing.Quoting;
import org.springframework.data.relational.core.sql.LockOptions;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
@ -79,27 +78,6 @@ public class H2Dialect 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.LockClause#getClausePosition()
*/
@Override
public Position getClausePosition() {
return Position.AFTER_ORDER_BY;
}
};
private final H2ArrayColumns ARRAY_COLUMNS = new H2ArrayColumns(); private final H2ArrayColumns ARRAY_COLUMNS = new H2ArrayColumns();
/* /*
@ -117,7 +95,7 @@ public class H2Dialect extends AbstractDialect {
*/ */
@Override @Override
public LockClause lock() { public LockClause lock() {
return LOCK_CLAUSE; return AnsiDialect.LOCK_CLAUSE;
} }
/* /*

17
spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/HsqlDbDialect.java

@ -15,8 +15,6 @@
*/ */
package org.springframework.data.relational.core.dialect; package org.springframework.data.relational.core.dialect;
import org.springframework.data.relational.core.sql.LockOptions;
/** /**
* A {@link Dialect} for HsqlDb. * A {@link Dialect} for HsqlDb.
* *
@ -36,7 +34,7 @@ public class HsqlDbDialect extends AbstractDialect {
@Override @Override
public LockClause lock() { public LockClause lock() {
return LOCK_CLAUSE; return AnsiDialect.LOCK_CLAUSE;
} }
private static final LimitClause LIMIT_CLAUSE = new LimitClause() { private static final LimitClause LIMIT_CLAUSE = new LimitClause() {
@ -61,17 +59,4 @@ public class HsqlDbDialect extends AbstractDialect {
return Position.AFTER_ORDER_BY; return Position.AFTER_ORDER_BY;
} }
}; };
private static final LockClause LOCK_CLAUSE = new LockClause() {
@Override
public String getLock(LockOptions lockOptions) {
return "FOR UPDATE";
}
@Override
public Position getClausePosition() {
return Position.AFTER_ORDER_BY;
}
};
} }

1
spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java

@ -115,6 +115,7 @@ public class PostgresDialect extends AbstractDialect {
} }
static class PostgresLockClause implements LockClause { static class PostgresLockClause implements LockClause {
private final IdentifierProcessing identifierProcessing; private final IdentifierProcessing identifierProcessing;
PostgresLockClause(IdentifierProcessing identifierProcessing) { PostgresLockClause(IdentifierProcessing identifierProcessing) {

3
spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerSelectRenderContext.java

@ -46,8 +46,7 @@ public class SqlServerSelectRenderContext implements SelectRenderContext {
* @param afterFromTable the delegate {@code afterFromTable} function. * @param afterFromTable the delegate {@code afterFromTable} function.
* @param afterOrderBy the delegate {@code afterOrderBy} function. * @param afterOrderBy the delegate {@code afterOrderBy} function.
*/ */
protected SqlServerSelectRenderContext( protected SqlServerSelectRenderContext(Function<Select, CharSequence> afterFromTable,
Function<Select, CharSequence> afterFromTable,
Function<Select, CharSequence> afterOrderBy) { Function<Select, CharSequence> afterOrderBy) {
this.afterFromTable = afterFromTable; this.afterFromTable = afterFromTable;

2
spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelectBuilder.java

@ -273,6 +273,7 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
*/ */
@Override @Override
public SelectLock lock(LockMode lockMode) { public SelectLock lock(LockMode lockMode) {
this.lockMode = lockMode; this.lockMode = lockMode;
return this; return this;
} }
@ -283,6 +284,7 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
*/ */
@Override @Override
public Select build() { public Select build() {
DefaultSelect select = new DefaultSelect(distinct, selectList, from, limit, offset, joins, where, orderBy, lockMode); DefaultSelect select = new DefaultSelect(distinct, selectList, from, limit, offset, joins, where, orderBy, lockMode);
SelectValidator.validate(select); SelectValidator.validate(select);
return select; return select;

1
spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/LockMode.java

@ -22,6 +22,7 @@ package org.springframework.data.relational.core.sql;
* @since 2.0 * @since 2.0
*/ */
public enum LockMode { public enum LockMode {
PESSIMISTIC_READ, PESSIMISTIC_READ,
PESSIMISTIC_WRITE PESSIMISTIC_WRITE
} }

2
spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/LockOptions.java

@ -22,10 +22,12 @@ package org.springframework.data.relational.core.sql;
* @since 2.0 * @since 2.0
*/ */
public class LockOptions { public class LockOptions {
private final LockMode lockMode; private final LockMode lockMode;
private final From from; private final From from;
public LockOptions(LockMode lockMode, From from) { public LockOptions(LockMode lockMode, From from) {
this.lockMode = lockMode; this.lockMode = lockMode;
this.from = from; this.from = from;
} }

Loading…
Cancel
Save