Browse Source

Support for ID generation by sequence.

Ids can be annotated with @Sequence to specify a sequence to pull id values from.

Closes #1923
Original pull request #1955

Signed-off-by: mipo256 <mikhailpolivakha@gmail.com>

Some accidential changes removed.
Signed-off-by: schauder <jens.schauder@broadcom.com>
pull/2001/head
Mikhail2048 1 year ago committed by Jens Schauder
parent
commit
d1c996008c
No known key found for this signature in database
GPG Key ID: 74F6C554AE971567
  1. 16
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java
  2. 71
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/IdGeneratingBeforeSaveCallback.java
  3. 17
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java
  4. 2
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java
  5. 121
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/IdGeneratingBeforeSaveCallbackTest.java
  6. 69
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java
  7. 27
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/DisabledOnDatabase.java
  8. 36
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/DisabledOnDatabaseExecutionCondition.java
  9. 25
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java
  10. 6
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestDatabaseFeatures.java
  11. 12
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql
  12. 10
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql
  13. 10
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql
  14. 10
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql
  15. 12
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql
  16. 10
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql
  17. 12
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql
  18. 15
      spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/IdValueSource.java
  19. 11
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Db2Dialect.java
  20. 2
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java
  21. 12
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/H2Dialect.java
  22. 19
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/HsqlDbDialect.java
  23. 24
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/IdGeneration.java
  24. 13
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MariaDbDialect.java
  25. 11
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java
  26. 7
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java
  27. 27
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java
  28. 5
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java
  29. 40
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntity.java
  30. 7
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentEntity.java
  31. 7
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java
  32. 43
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/TargetSequence.java
  33. 46
      spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java

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

@ -118,8 +118,13 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { @@ -118,8 +118,13 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
Assert.notEmpty(insertSubjects, "Batch insert must contain at least one InsertSubject");
SqlIdentifierParameterSource[] sqlParameterSources = insertSubjects.stream()
.map(insertSubject -> sqlParametersFactory.forInsert(insertSubject.getInstance(), domainType,
insertSubject.getIdentifier(), idValueSource))
.map(insertSubject -> sqlParametersFactory.forInsert( //
insertSubject.getInstance(), //
domainType, //
insertSubject.getIdentifier(), //
idValueSource //
) //
) //
.toArray(SqlIdentifierParameterSource[]::new);
String insertSql = sql(domainType).getInsert(sqlParameterSources[0].getIdentifiers());
@ -280,7 +285,8 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { @@ -280,7 +285,8 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
@Override
public <T> Stream<T> streamAll(Class<T> domainType) {
return operations.queryForStream(sql(domainType).getFindAll(), new MapSqlParameterSource(), getEntityRowMapper(domainType));
return operations.queryForStream(sql(domainType).getFindAll(), new MapSqlParameterSource(),
getEntityRowMapper(domainType));
}
@Override
@ -364,7 +370,8 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { @@ -364,7 +370,8 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
@Override
public <T> Stream<T> streamAll(Class<T> domainType, Sort sort) {
return operations.queryForStream(sql(domainType).getFindAll(sort), new MapSqlParameterSource(), getEntityRowMapper(domainType));
return operations.queryForStream(sql(domainType).getFindAll(sort), new MapSqlParameterSource(),
getEntityRowMapper(domainType));
}
@Override
@ -479,5 +486,4 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { @@ -479,5 +486,4 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
return baseProperty.getOwner().getType();
}
}

71
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/IdGeneratingBeforeSaveCallback.java

@ -0,0 +1,71 @@ @@ -0,0 +1,71 @@
package org.springframework.data.jdbc.core.mapping;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.relational.core.conversion.MutableAggregateChange;
import org.springframework.data.relational.core.dialect.Dialect;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.event.BeforeSaveCallback;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.util.Assert;
/**
* Callback for generating ID via the database sequence. By default, it is registered as a
* bean in {@link AbstractJdbcConfiguration}
*
* @author Mikhail Polivakha
*/
public class IdGeneratingBeforeSaveCallback implements BeforeSaveCallback<Object> {
private static final Log LOG = LogFactory.getLog(IdGeneratingBeforeSaveCallback.class);
private final RelationalMappingContext relationalMappingContext;
private final Dialect dialect;
private final NamedParameterJdbcOperations operations;
public IdGeneratingBeforeSaveCallback(
RelationalMappingContext relationalMappingContext,
Dialect dialect,
NamedParameterJdbcOperations namedParameterJdbcOperations
) {
this.relationalMappingContext = relationalMappingContext;
this.dialect = dialect;
this.operations = namedParameterJdbcOperations;
}
@Override
public Object onBeforeSave(Object aggregate, MutableAggregateChange<Object> aggregateChange) {
Assert.notNull(aggregate, "The aggregate cannot be null at this point");
RelationalPersistentEntity<?> persistentEntity = relationalMappingContext.getPersistentEntity(aggregate.getClass());
Optional<String> idTargetSequence = persistentEntity.getIdTargetSequence();
if (dialect.getIdGeneration().sequencesSupported()) {
if (persistentEntity.getIdProperty() != null) {
idTargetSequence
.map(s -> dialect.getIdGeneration().nextValueFromSequenceSelect(s))
.ifPresent(sql -> {
Long idValue = operations.queryForObject(sql, Map.of(), (rs, rowNum) -> rs.getLong(1));
PersistentPropertyAccessor<Object> propertyAccessor = persistentEntity.getPropertyAccessor(aggregate);
propertyAccessor.setProperty(persistentEntity.getRequiredIdProperty(), idValue);
});
}
} else {
if (idTargetSequence.isPresent()) {
LOG.warn("""
It seems you're trying to insert an aggregate of type '%s' annotated with @TargetSequence, but the problem is RDBMS you're
working with does not support sequences as such. Falling back to identity columns
"""
.formatted(aggregate.getClass().getName())
);
}
}
return aggregate;
}
}

17
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java

@ -38,6 +38,7 @@ import org.springframework.data.jdbc.core.JdbcAggregateOperations; @@ -38,6 +38,7 @@ import org.springframework.data.jdbc.core.JdbcAggregateOperations;
import org.springframework.data.jdbc.core.JdbcAggregateTemplate;
import org.springframework.data.jdbc.core.convert.*;
import org.springframework.data.jdbc.core.dialect.JdbcDialect;
import org.springframework.data.jdbc.core.mapping.IdGeneratingBeforeSaveCallback;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes;
import org.springframework.data.mapping.model.SimpleTypeHolder;
@ -119,6 +120,22 @@ public class AbstractJdbcConfiguration implements ApplicationContextAware { @@ -119,6 +120,22 @@ public class AbstractJdbcConfiguration implements ApplicationContextAware {
return mappingContext;
}
/**
* Creates a {@link IdGeneratingBeforeSaveCallback} bean using the configured
* {@link #jdbcMappingContext(Optional, JdbcCustomConversions, RelationalManagedTypes)} and
* {@link #jdbcDialect(NamedParameterJdbcOperations)}.
*
* @return must not be {@literal null}.
*/
@Bean
public IdGeneratingBeforeSaveCallback idGeneratingBeforeSaveCallback(
JdbcMappingContext mappingContext,
NamedParameterJdbcOperations operations,
Dialect dialect
) {
return new IdGeneratingBeforeSaveCallback(mappingContext, dialect, operations);
}
/**
* Creates a {@link RelationalConverter} using the configured
* {@link #jdbcMappingContext(Optional, JdbcCustomConversions, RelationalManagedTypes)}.

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

@ -33,7 +33,6 @@ import org.springframework.data.convert.ReadingConverter; @@ -33,7 +33,6 @@ import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.relational.core.conversion.IdValueSource;
import org.springframework.data.relational.core.dialect.AnsiDialect;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.sql.SqlIdentifier;
@ -49,7 +48,6 @@ class SqlParametersFactoryTest { @@ -49,7 +48,6 @@ class SqlParametersFactoryTest {
RelationalMappingContext context = new JdbcMappingContext();
RelationResolver relationResolver = mock(RelationResolver.class);
MappingJdbcConverter converter = new MappingJdbcConverter(context, relationResolver);
AnsiDialect dialect = AnsiDialect.INSTANCE;
SqlParametersFactory sqlParametersFactory = new SqlParametersFactory(context, converter);
@Test // DATAJDBC-412

121
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/IdGeneratingBeforeSaveCallbackTest.java

@ -0,0 +1,121 @@ @@ -0,0 +1,121 @@
package org.springframework.data.jdbc.core.mapping;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.conversion.MutableAggregateChange;
import org.springframework.data.relational.core.dialect.MySqlDialect;
import org.springframework.data.relational.core.dialect.PostgresDialect;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.Table;
import org.springframework.data.relational.core.mapping.TargetSequence;
import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
/**
* Unit tests for {@link IdGeneratingBeforeSaveCallback}
*
* @author Mikhail Polivakha
*/
class IdGeneratingBeforeSaveCallbackTest {
@Test
void test_mySqlDialect_sequenceGenerationIsNotSupported() {
// given
RelationalMappingContext relationalMappingContext = new RelationalMappingContext();
MySqlDialect mySqlDialect = new MySqlDialect(IdentifierProcessing.NONE);
NamedParameterJdbcOperations operations = mock(NamedParameterJdbcOperations.class);
// and
IdGeneratingBeforeSaveCallback subject = new IdGeneratingBeforeSaveCallback(relationalMappingContext, mySqlDialect, operations);
NoSequenceEntity entity = new NoSequenceEntity();
// when
Object processed = subject.onBeforeSave(entity, MutableAggregateChange.forSave(entity));
// then
Assertions.assertThat(processed).isSameAs(entity);
Assertions.assertThat(processed).usingRecursiveComparison().isEqualTo(entity);
}
@Test
void test_EntityIsNotMarkedWithTargetSequence() {
// given
RelationalMappingContext relationalMappingContext = new RelationalMappingContext();
PostgresDialect mySqlDialect = PostgresDialect.INSTANCE;
NamedParameterJdbcOperations operations = mock(NamedParameterJdbcOperations.class);
// and
IdGeneratingBeforeSaveCallback subject = new IdGeneratingBeforeSaveCallback(relationalMappingContext, mySqlDialect, operations);
NoSequenceEntity entity = new NoSequenceEntity();
// when
Object processed = subject.onBeforeSave(entity, MutableAggregateChange.forSave(entity));
// then
Assertions.assertThat(processed).isSameAs(entity);
Assertions.assertThat(processed).usingRecursiveComparison().isEqualTo(entity);
}
@Test
void test_EntityIdIsPopulatedFromSequence() {
// given
RelationalMappingContext relationalMappingContext = new RelationalMappingContext();
relationalMappingContext.getRequiredPersistentEntity(EntityWithSequence.class);
PostgresDialect mySqlDialect = PostgresDialect.INSTANCE;
NamedParameterJdbcOperations operations = mock(NamedParameterJdbcOperations.class);
// and
long generatedId = 112L;
when(operations.queryForObject(anyString(), anyMap(), any(RowMapper.class))).thenReturn(generatedId);
// and
IdGeneratingBeforeSaveCallback subject = new IdGeneratingBeforeSaveCallback(relationalMappingContext, mySqlDialect, operations);
EntityWithSequence entity = new EntityWithSequence();
// when
Object processed = subject.onBeforeSave(entity, MutableAggregateChange.forSave(entity));
// then
Assertions.assertThat(processed).isSameAs(entity);
Assertions
.assertThat(processed)
.usingRecursiveComparison()
.ignoringFields("id")
.isEqualTo(entity);
Assertions.assertThat(entity.getId()).isEqualTo(generatedId);
}
@Table
static class NoSequenceEntity {
@Id
private Long id;
private Long name;
}
@Table
static class EntityWithSequence {
@Id
@TargetSequence(value = "id_seq", schema = "public")
private Long id;
private Long name;
public Long getId() {
return id;
}
}
}

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

@ -42,7 +42,6 @@ import org.junit.jupiter.api.Test; @@ -42,7 +42,6 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.context.ApplicationListener;
@ -52,16 +51,7 @@ import org.springframework.context.annotation.Import; @@ -52,16 +51,7 @@ import org.springframework.context.annotation.Import;
import org.springframework.core.io.ClassPathResource;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Window;
import org.springframework.data.domain.*;
import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.jdbc.repository.query.Modifying;
import org.springframework.data.jdbc.repository.query.Query;
@ -75,6 +65,7 @@ import org.springframework.data.jdbc.testing.TestDatabaseFeatures; @@ -75,6 +65,7 @@ import org.springframework.data.jdbc.testing.TestDatabaseFeatures;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.MappedCollection;
import org.springframework.data.relational.core.mapping.Table;
import org.springframework.data.relational.core.mapping.TargetSequence;
import org.springframework.data.relational.core.mapping.event.AbstractRelationalEvent;
import org.springframework.data.relational.core.mapping.event.AfterConvertEvent;
import org.springframework.data.relational.core.sql.LockMode;
@ -115,8 +106,8 @@ public class JdbcRepositoryIntegrationTests { @@ -115,8 +106,8 @@ public class JdbcRepositoryIntegrationTests {
@Autowired DummyEntityRepository repository;
@Autowired MyEventListener eventListener;
@Autowired RootRepository rootRepository;
@Autowired WithDelimitedColumnRepository withDelimitedColumnRepository;
@Autowired EntityWithSequenceRepository entityWithSequenceRepository;
@BeforeEach
public void before() {
@ -135,6 +126,28 @@ public class JdbcRepositoryIntegrationTests { @@ -135,6 +126,28 @@ public class JdbcRepositoryIntegrationTests {
"id_Prop = " + entity.getIdProp())).isEqualTo(1);
}
@Test
@EnabledOnFeature(value = TestDatabaseFeatures.Feature.SUPPORTS_SEQUENCES)
public void saveEntityWithTargetSequenceSpecified() {
EntityWithSequence first = entityWithSequenceRepository.save(new EntityWithSequence("first"));
EntityWithSequence second = entityWithSequenceRepository.save(new EntityWithSequence("second"));
assertThat(first.getId()).isNotNull();
assertThat(second.getId()).isNotNull();
assertThat(first.getId()).isLessThan(second.getId());
assertThat(first.getName()).isEqualTo("first");
assertThat(second.getName()).isEqualTo("second");
}
@Test
@EnabledOnFeature(value = TestDatabaseFeatures.Feature.SUPPORTS_SEQUENCES)
public void batchInsertEntityWithTargetSequenceSpecified() {
Iterable<EntityWithSequence> results = entityWithSequenceRepository
.saveAll(List.of(new EntityWithSequence("first"), new EntityWithSequence("second")));
assertThat(results).hasSize(2).extracting(EntityWithSequence::getId).containsExactly(1L, 2L);
}
@Test // DATAJDBC-95
public void saveAndLoadAnEntity() {
@ -1515,6 +1528,8 @@ public class JdbcRepositoryIntegrationTests { @@ -1515,6 +1528,8 @@ public class JdbcRepositoryIntegrationTests {
interface WithDelimitedColumnRepository extends CrudRepository<WithDelimitedColumn, Long> {}
interface EntityWithSequenceRepository extends CrudRepository<EntityWithSequence, Long> {}
@Configuration
@Import(TestConfiguration.class)
static class Config {
@ -1536,6 +1551,11 @@ public class JdbcRepositoryIntegrationTests { @@ -1536,6 +1551,11 @@ public class JdbcRepositoryIntegrationTests {
return factory.getRepository(WithDelimitedColumnRepository.class);
}
@Bean
EntityWithSequenceRepository entityWithSequenceRepository() {
return factory.getRepository(EntityWithSequenceRepository.class);
}
@Bean
NamedQueries namedQueries() throws IOException {
@ -1839,6 +1859,31 @@ public class JdbcRepositoryIntegrationTests { @@ -1839,6 +1859,31 @@ public class JdbcRepositoryIntegrationTests {
return entity;
}
static class EntityWithSequence {
@Id
@TargetSequence(sequence = "entity_sequence") private Long id;
private String name;
public EntityWithSequence(Long id, String name) {
this.id = id;
this.name = name;
}
public EntityWithSequence(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
static class DummyEntity {
String name;

27
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/DisabledOnDatabase.java

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
package org.springframework.data.jdbc.testing;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.test.context.junit.jupiter.EnabledIf;
/**
* Annotation that allows to disable a particular test to be executed on a particular database
*
* @author Mikhail Polivakha
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ExtendWith(DisabledOnDatabaseExecutionCondition.class)
public @interface DisabledOnDatabase {
/**
* The database on which the test is not supposed to run on
*/
DatabaseType database();
}

36
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/DisabledOnDatabaseExecutionCondition.java

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
package org.springframework.data.jdbc.testing;
import org.apache.commons.lang3.ArrayUtils;
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* {@link ExecutionCondition} for the {@link DisabledOnDatabase} annotation
*
* @author Mikhail Polivakha
*/
public class DisabledOnDatabaseExecutionCondition implements ExecutionCondition {
@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
ApplicationContext applicationContext = SpringExtension.getApplicationContext(context);
MergedAnnotation<DisabledOnDatabase> disabledOnDatabaseMergedAnnotation = MergedAnnotations
.from(context.getRequiredTestMethod(), MergedAnnotations.SearchStrategy.DIRECT)
.get(DisabledOnDatabase.class);
DatabaseType database = disabledOnDatabaseMergedAnnotation.getEnum("database", DatabaseType.class);
if (ArrayUtils.contains(applicationContext.getEnvironment().getActiveProfiles(), database.getProfile())) {
return ConditionEvaluationResult.disabled(
"The test method '%s' is disabled for '%s' because of the @DisabledOnDatabase annotation".formatted(context.getRequiredTestMethod().getName(), database)
);
}
return ConditionEvaluationResult.enabled("The test method '%s' is enabled".formatted(context.getRequiredTestMethod()));
}
}

25
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java

@ -36,11 +36,15 @@ import org.springframework.context.annotation.Profile; @@ -36,11 +36,15 @@ import org.springframework.context.annotation.Profile;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.jdbc.core.convert.*;
import org.springframework.data.jdbc.core.dialect.JdbcDialect;
import org.springframework.data.jdbc.core.mapping.IdGeneratingBeforeSaveCallback;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes;
import org.springframework.data.jdbc.repository.config.DialectResolver;
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.mapping.callback.EntityCallback;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.relational.RelationalManagedTypes;
import org.springframework.data.relational.core.dialect.Dialect;
import org.springframework.data.relational.core.mapping.DefaultNamingStrategy;
import org.springframework.data.relational.core.mapping.NamingStrategy;
@ -81,10 +85,16 @@ public class TestConfiguration { @@ -81,10 +85,16 @@ public class TestConfiguration {
JdbcRepositoryFactory jdbcRepositoryFactory(
@Qualifier("defaultDataAccessStrategy") DataAccessStrategy dataAccessStrategy, RelationalMappingContext context,
Dialect dialect, JdbcConverter converter, Optional<List<NamedQueries>> namedQueries,
List<EntityCallback<?>> callbacks,
List<EvaluationContextExtension> evaulationContextExtensions) {
JdbcRepositoryFactory factory = new JdbcRepositoryFactory(dataAccessStrategy, context, converter, dialect,
publisher, namedParameterJdbcTemplate());
factory.setEntityCallbacks(
EntityCallbacks.create(callbacks.toArray(new EntityCallback[0]))
);
namedQueries.map(it -> it.iterator().next()).ifPresent(factory::setNamedQueries);
factory.setEvaluationContextProvider(
@ -164,6 +174,21 @@ public class TestConfiguration { @@ -164,6 +174,21 @@ public class TestConfiguration {
new DefaultJdbcTypeFactory(template.getJdbcOperations(), arrayColumns));
}
/**
* Creates a {@link IdGeneratingBeforeSaveCallback} bean using the configured
* {@link #jdbcDialect(NamedParameterJdbcOperations)}.
*
* @return must not be {@literal null}.
*/
@Bean
public IdGeneratingBeforeSaveCallback idGeneratingBeforeSaveCallback(
JdbcMappingContext mappingContext,
NamedParameterJdbcOperations operations,
Dialect dialect
) {
return new IdGeneratingBeforeSaveCallback(mappingContext, dialect, operations);
}
@Bean
Dialect jdbcDialect(NamedParameterJdbcOperations operations) {
return DialectResolver.getDialect(operations.getJdbcOperations());

6
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestDatabaseFeatures.java

@ -30,6 +30,7 @@ import org.springframework.jdbc.core.JdbcOperations; @@ -30,6 +30,7 @@ import org.springframework.jdbc.core.JdbcOperations;
*
* @author Jens Schauder
* @author Chirag Tailor
* @author Mikhail Polivakha
*/
public class TestDatabaseFeatures {
@ -79,6 +80,10 @@ public class TestDatabaseFeatures { @@ -79,6 +80,10 @@ public class TestDatabaseFeatures {
assumeThat(database).isNotIn(Database.MySql, Database.MariaDb, Database.SqlServer);
}
private void supportsSequences() {
assumeThat(database).isNotIn(Database.MySql);
}
private void supportsWhereInTuples() {
assumeThat(database).isIn(Database.MySql, Database.PostgreSql);
}
@ -117,6 +122,7 @@ public class TestDatabaseFeatures { @@ -117,6 +122,7 @@ public class TestDatabaseFeatures {
SUPPORTS_NULL_PRECEDENCE(TestDatabaseFeatures::supportsNullPrecedence),
IS_POSTGRES(f -> f.databaseIs(Database.PostgreSql)), //
WHERE_IN_TUPLE(TestDatabaseFeatures::supportsWhereInTuples), //
SUPPORTS_SEQUENCES(TestDatabaseFeatures::supportsSequences), //
IS_HSQL(f -> f.databaseIs(Database.Hsql));
private final Consumer<TestDatabaseFeatures> featureMethod;

12
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql

@ -3,6 +3,8 @@ DROP TABLE ROOT; @@ -3,6 +3,8 @@ DROP TABLE ROOT;
DROP TABLE INTERMEDIATE;
DROP TABLE LEAF;
DROP TABLE WITH_DELIMITED_COLUMN;
DROP TABLE ENTITY_WITH_SEQUENCE;
DROP SEQUENCE ENTITY_SEQUENCE;
CREATE TABLE dummy_entity
(
@ -45,4 +47,12 @@ CREATE TABLE WITH_DELIMITED_COLUMN @@ -45,4 +47,12 @@ CREATE TABLE WITH_DELIMITED_COLUMN
ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
"ORG.XTUNIT.IDENTIFIER" VARCHAR(100),
STYPE VARCHAR(100)
);
);
CREATE TABLE ENTITY_WITH_SEQUENCE
(
ID BIGINT,
NAME VARCHAR(100)
);
CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE;

10
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql

@ -39,4 +39,12 @@ CREATE TABLE WITH_DELIMITED_COLUMN @@ -39,4 +39,12 @@ CREATE TABLE WITH_DELIMITED_COLUMN
ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
"ORG.XTUNIT.IDENTIFIER" VARCHAR(100),
STYPE VARCHAR(100)
);
);
CREATE TABLE ENTITY_WITH_SEQUENCE
(
ID BIGINT,
NAME VARCHAR(100)
);
CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE;

10
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql

@ -39,4 +39,12 @@ CREATE TABLE WITH_DELIMITED_COLUMN @@ -39,4 +39,12 @@ CREATE TABLE WITH_DELIMITED_COLUMN
ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
"ORG.XTUNIT.IDENTIFIER" VARCHAR(100),
STYPE VARCHAR(100)
);
);
CREATE TABLE ENTITY_WITH_SEQUENCE
(
ID BIGINT,
NAME VARCHAR(100)
);
CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE;

10
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql

@ -39,4 +39,12 @@ CREATE TABLE WITH_DELIMITED_COLUMN @@ -39,4 +39,12 @@ CREATE TABLE WITH_DELIMITED_COLUMN
ID BIGINT AUTO_INCREMENT PRIMARY KEY,
`ORG.XTUNIT.IDENTIFIER` VARCHAR(100),
STYPE VARCHAR(100)
);
);
CREATE TABLE ENTITY_WITH_SEQUENCE
(
ID BIGINT,
NAME VARCHAR(100)
);
CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE;

12
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql

@ -3,6 +3,8 @@ DROP TABLE IF EXISTS ROOT; @@ -3,6 +3,8 @@ DROP TABLE IF EXISTS ROOT;
DROP TABLE IF EXISTS INTERMEDIATE;
DROP TABLE IF EXISTS LEAF;
DROP TABLE IF EXISTS WITH_DELIMITED_COLUMN;
DROP TABLE IF EXISTS ENTITY_WITH_SEQUENCE;
DROP SEQUENCE IF EXISTS ENTITY_SEQUENCE;
CREATE TABLE dummy_entity
(
@ -45,4 +47,12 @@ CREATE TABLE WITH_DELIMITED_COLUMN @@ -45,4 +47,12 @@ CREATE TABLE WITH_DELIMITED_COLUMN
ID BIGINT IDENTITY PRIMARY KEY,
"ORG.XTUNIT.IDENTIFIER" VARCHAR(100),
STYPE VARCHAR(100)
);
);
CREATE TABLE ENTITY_WITH_SEQUENCE
(
ID BIGINT,
NAME VARCHAR(100)
);
CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE;

10
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql

@ -3,6 +3,8 @@ DROP TABLE ROOT CASCADE CONSTRAINTS PURGE; @@ -3,6 +3,8 @@ DROP TABLE ROOT CASCADE CONSTRAINTS PURGE;
DROP TABLE INTERMEDIATE CASCADE CONSTRAINTS PURGE;
DROP TABLE LEAF CASCADE CONSTRAINTS PURGE;
DROP TABLE WITH_DELIMITED_COLUMN CASCADE CONSTRAINTS PURGE;
DROP TABLE ENTITY_WITH_SEQUENCE CASCADE CONSTRAINTS PURGE;
DROP SEQUENCE ENTITY_SEQUENCE;
CREATE TABLE DUMMY_ENTITY
(
@ -46,3 +48,11 @@ CREATE TABLE WITH_DELIMITED_COLUMN @@ -46,3 +48,11 @@ CREATE TABLE WITH_DELIMITED_COLUMN
"ORG.XTUNIT.IDENTIFIER" VARCHAR(100),
STYPE VARCHAR(100)
)
CREATE TABLE ENTITY_WITH_SEQUENCE
(
ID BIGINT,
NAME VARCHAR(100)
);
CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE;

12
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql

@ -3,6 +3,8 @@ DROP TABLE ROOT; @@ -3,6 +3,8 @@ DROP TABLE ROOT;
DROP TABLE INTERMEDIATE;
DROP TABLE LEAF;
DROP TABLE WITH_DELIMITED_COLUMN;
DROP TABLE ENTITY_WITH_SEQUENCE;
DROP SEQUENCE ENTITY_SEQUENCE;
CREATE TABLE dummy_entity
(
@ -45,4 +47,12 @@ CREATE TABLE "WITH_DELIMITED_COLUMN" @@ -45,4 +47,12 @@ CREATE TABLE "WITH_DELIMITED_COLUMN"
ID SERIAL PRIMARY KEY,
"ORG.XTUNIT.IDENTIFIER" VARCHAR(100),
"STYPE" VARCHAR(100)
);
);
CREATE TABLE ENTITY_WITH_SEQUENCE
(
ID BIGINT,
NAME VARCHAR(100)
);
CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE;

15
spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/IdValueSource.java

@ -15,6 +15,8 @@ @@ -15,6 +15,8 @@
*/
package org.springframework.data.relational.core.conversion;
import java.util.Optional;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@ -22,6 +24,7 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentProp @@ -22,6 +24,7 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentProp
* Enumeration describing the source of a value for an id property.
*
* @author Chirag Tailor
* @author Mikhail Polivakha
* @since 2.4
*/
public enum IdValueSource {
@ -39,7 +42,12 @@ public enum IdValueSource { @@ -39,7 +42,12 @@ public enum IdValueSource {
/**
* There is no id property, and therefore no id value source.
*/
NONE;
NONE,
/**
* The id should be dervied from the database sequence
*/
SEQUENCE;
/**
* Returns the appropriate {@link IdValueSource} for the instance: {@link IdValueSource#NONE} when the entity has no
@ -48,6 +56,11 @@ public enum IdValueSource { @@ -48,6 +56,11 @@ public enum IdValueSource {
*/
public static <T> IdValueSource forInstance(Object instance, RelationalPersistentEntity<T> persistentEntity) {
Optional<String> idTargetSequence = persistentEntity.getIdTargetSequence();
if (idTargetSequence.isPresent()) {
return IdValueSource.SEQUENCE;
}
Object idValue = persistentEntity.getIdentifierAccessor(instance).getIdentifier();
RelationalPersistentProperty idProperty = persistentEntity.getIdProperty();
if (idProperty == null) {

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

@ -25,6 +25,7 @@ import org.springframework.data.relational.core.sql.LockOptions; @@ -25,6 +25,7 @@ import org.springframework.data.relational.core.sql.LockOptions;
* An SQL dialect for DB2.
*
* @author Jens Schauder
* @author Mikhail Polivakha
* @since 2.0
*/
public class Db2Dialect extends AbstractDialect {
@ -39,6 +40,16 @@ public class Db2Dialect extends AbstractDialect { @@ -39,6 +40,16 @@ public class Db2Dialect extends AbstractDialect {
public boolean supportedForBatchOperations() {
return false;
}
/**
* This workaround (non-ANSI SQL way of querying sequence) exists for the same reasons it exists for {@link HsqlDbDialect}
*
* @see HsqlDbDialect#getIdGeneration()#nextValueFromSequenceSelect(String)
*/
@Override
public String nextValueFromSequenceSelect(String sequenceName) {
return "SELECT NEXT VALUE FOR %s FROM SYSCAT.SEQUENCES LIMIT 1".formatted(sequenceName);
}
};
protected Db2Dialect() {}

2
spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java

@ -146,5 +146,5 @@ public interface Dialect { @@ -146,5 +146,5 @@ public interface Dialect {
default boolean supportsSingleQueryLoading() {
return true;
};
}
}

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

@ -31,6 +31,7 @@ import org.springframework.util.ClassUtils; @@ -31,6 +31,7 @@ import org.springframework.util.ClassUtils;
* @author Myeonghyeon Lee
* @author Christph Strobl
* @author Jens Schauder
* @author Mikhail Polivakha
* @since 2.0
*/
public class H2Dialect extends AbstractDialect {
@ -113,4 +114,15 @@ public class H2Dialect extends AbstractDialect { @@ -113,4 +114,15 @@ public class H2Dialect extends AbstractDialect {
public boolean supportsSingleQueryLoading() {
return false;
}
@Override
public IdGeneration getIdGeneration() {
return new IdGeneration() {
@Override
public String nextValueFromSequenceSelect(String sequenceName) {
return "SELECT NEXT VALUE FOR %s".formatted(sequenceName);
}
};
}
}

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

@ -20,6 +20,7 @@ package org.springframework.data.relational.core.dialect; @@ -20,6 +20,7 @@ package org.springframework.data.relational.core.dialect;
*
* @author Jens Schauder
* @author Myeonghyeon Lee
* @author Mikhail Polivakha
*/
public class HsqlDbDialect extends AbstractDialect {
@ -64,4 +65,22 @@ public class HsqlDbDialect extends AbstractDialect { @@ -64,4 +65,22 @@ public class HsqlDbDialect extends AbstractDialect {
return Position.AFTER_ORDER_BY;
}
};
@Override
public IdGeneration getIdGeneration() {
return new IdGeneration() {
/**
* One may think that this is an over-complication, but it is actually not.
* There is no a direct way to query the next value for the sequence, only to use it as an expression
* inside other queries (SELECT/INSERT). Therefore, such a workaround is required
*
* @see <a href="https://github.com/jOOQ/jOOQ/issues/3762">The way JOOQ solves this problem</a>
*/
@Override
public String nextValueFromSequenceSelect(String sequenceName) {
return "SELECT NEXT VALUE FOR %s AS msq FROM INFORMATION_SCHEMA.SEQUENCES LIMIT 1".formatted(sequenceName);
}
};
}
}

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

@ -21,10 +21,13 @@ import java.sql.PreparedStatement; @@ -21,10 +21,13 @@ import java.sql.PreparedStatement;
import org.springframework.data.relational.core.sql.SqlIdentifier;
/**
* Describes how obtaining generated ids after an insert works for a given JDBC driver.
* Encapsulates various properties that are related to ID generation process and specific to
* given {@link Dialect}
*
* @author Jens Schauder
* @author Chirag Tailor
* @author Mikhail Polivakha
*
* @since 2.1
*/
public interface IdGeneration {
@ -59,6 +62,13 @@ public interface IdGeneration { @@ -59,6 +62,13 @@ public interface IdGeneration {
return id.getReference();
}
/**
* @return {@literal true} in case the sequences are supported by the underlying database, {@literal false} otherwise
*/
default boolean sequencesSupported() {
return true;
}
/**
* Does the driver support id generation for batch operations.
* <p>
@ -71,4 +81,16 @@ public interface IdGeneration { @@ -71,4 +81,16 @@ public interface IdGeneration {
default boolean supportedForBatchOperations() {
return true;
}
/**
* The SQL statement that allows retrieving the next value from the passed sequence
*
* @param sequenceName the sequence name to get the enxt value for
* @return SQL string
*/
default String nextValueFromSequenceSelect(String sequenceName) {
throw new UnsupportedOperationException(
"Currently, there is no support for sequence generation for %s dialect. If you need it, please, submit a ticket".formatted(this.getClass().getSimpleName())
);
}
}

13
spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MariaDbDialect.java

@ -18,12 +18,14 @@ package org.springframework.data.relational.core.dialect; @@ -18,12 +18,14 @@ package org.springframework.data.relational.core.dialect;
import java.util.Arrays;
import java.util.Collection;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.sql.IdentifierProcessing;
/**
* A SQL dialect for MariaDb.
*
* @author Jens Schauder
* @author Mikhail Polivakha
* @since 2.3
*/
public class MariaDbDialect extends MySqlDialect {
@ -38,4 +40,15 @@ public class MariaDbDialect extends MySqlDialect { @@ -38,4 +40,15 @@ public class MariaDbDialect extends MySqlDialect {
TimestampAtUtcToOffsetDateTimeConverter.INSTANCE,
NumberToBooleanConverter.INSTANCE);
}
@Override
public IdGeneration getIdGeneration() {
return new IdGeneration() {
@Override
public String nextValueFromSequenceSelect(String sequenceName) {
return "SELECT NEXTVAL(%s)".formatted(sequenceName);
}
};
}
}

11
spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java

@ -141,4 +141,15 @@ public class MySqlDialect extends AbstractDialect { @@ -141,4 +141,15 @@ public class MySqlDialect extends AbstractDialect {
public OrderByNullPrecedence orderByNullHandling() {
return OrderByNullPrecedence.NONE;
}
@Override
public IdGeneration getIdGeneration() {
return new IdGeneration() {
@Override
public boolean sequencesSupported() {
return false;
}
};
}
}

7
spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java

@ -27,6 +27,7 @@ import static java.util.Arrays.*; @@ -27,6 +27,7 @@ import static java.util.Arrays.*;
* An SQL dialect for Oracle.
*
* @author Jens Schauder
* @author Mikahil Polivakha
* @since 2.1
*/
public class OracleDialect extends AnsiDialect {
@ -37,6 +38,7 @@ public class OracleDialect extends AnsiDialect { @@ -37,6 +38,7 @@ public class OracleDialect extends AnsiDialect {
public static final OracleDialect INSTANCE = new OracleDialect();
private static final IdGeneration ID_GENERATION = new IdGeneration() {
@Override
public boolean driverRequiresKeyColumnNames() {
return true;
@ -46,6 +48,11 @@ public class OracleDialect extends AnsiDialect { @@ -46,6 +48,11 @@ public class OracleDialect extends AnsiDialect {
public String getKeyColumnName(SqlIdentifier id) {
return id.toSql(INSTANCE.getIdentifierProcessing());
}
@Override
public String nextValueFromSequenceSelect(String sequenceName) {
return "SELECT %s.nextval FROM DUAL".formatted(sequenceName);
}
};
protected OracleDialect() {}

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

@ -42,6 +42,7 @@ import org.springframework.data.relational.core.sql.TableLike; @@ -42,6 +42,7 @@ import org.springframework.data.relational.core.sql.TableLike;
* @author Myeonghyeon Lee
* @author Jens Schauder
* @author Nikita Konev
* @author Mikhail Polivakha
* @since 1.1
*/
public class PostgresDialect extends AbstractDialect {
@ -130,17 +131,10 @@ public class PostgresDialect extends AbstractDialect { @@ -130,17 +131,10 @@ public class PostgresDialect extends AbstractDialect {
// without schema
String tableName = last.toSql(this.identifierProcessing);
switch (lockOptions.getLockMode()) {
case PESSIMISTIC_WRITE:
return "FOR UPDATE OF " + tableName;
case PESSIMISTIC_READ:
return "FOR SHARE OF " + tableName;
default:
return "";
}
return switch (lockOptions.getLockMode()) {
case PESSIMISTIC_WRITE -> "FOR UPDATE OF " + tableName;
case PESSIMISTIC_READ -> "FOR SHARE OF " + tableName;
};
}
@Override
@ -163,4 +157,15 @@ public class PostgresDialect extends AbstractDialect { @@ -163,4 +157,15 @@ public class PostgresDialect extends AbstractDialect {
public SimpleFunction getExistsFunction() {
return Functions.least(Functions.count(SQL.literalOf(1)), SQL.literalOf(1));
}
@Override
public IdGeneration getIdGeneration() {
return new IdGeneration() {
@Override
public String nextValueFromSequenceSelect(String sequenceName) {
return "SELECT nextval('%s')".formatted(sequenceName);
}
};
}
}

5
spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java

@ -42,6 +42,11 @@ public class SqlServerDialect extends AbstractDialect { @@ -42,6 +42,11 @@ public class SqlServerDialect extends AbstractDialect {
public boolean supportedForBatchOperations() {
return false;
}
@Override
public String nextValueFromSequenceSelect(String sequenceName) {
return "SELECT NEXT VALUE FOR %s".formatted(sequenceName);
}
};
private static final IdentifierProcessing IDENTIFIER_PROCESSING = IdentifierProcessing

40
spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntity.java

@ -17,6 +17,7 @@ package org.springframework.data.relational.core.mapping; @@ -17,6 +17,7 @@ package org.springframework.data.relational.core.mapping;
import java.util.Optional;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.util.Lazy;
@ -46,6 +47,8 @@ class BasicRelationalPersistentEntity<T> extends BasicPersistentEntity<T, Relati @@ -46,6 +47,8 @@ class BasicRelationalPersistentEntity<T> extends BasicPersistentEntity<T, Relati
private final Lazy<SqlIdentifier> tableName;
private final @Nullable Expression tableNameExpression;
private final Lazy<String> idTargetSequenceName;
private final Lazy<Optional<SqlIdentifier>> schemaName;
private final @Nullable Expression schemaNameExpression;
private final ExpressionEvaluator expressionEvaluator;
@ -87,6 +90,8 @@ class BasicRelationalPersistentEntity<T> extends BasicPersistentEntity<T, Relati @@ -87,6 +90,8 @@ class BasicRelationalPersistentEntity<T> extends BasicPersistentEntity<T, Relati
this.schemaName = defaultSchema;
this.schemaNameExpression = null;
}
this.idTargetSequenceName = Lazy.of(this::determineTargetSequenceName);
}
/**
@ -159,8 +164,43 @@ class BasicRelationalPersistentEntity<T> extends BasicPersistentEntity<T, Relati @@ -159,8 +164,43 @@ class BasicRelationalPersistentEntity<T> extends BasicPersistentEntity<T, Relati
return getRequiredIdProperty().getColumnName();
}
@Override
public Optional<String> getIdTargetSequence() {
return idTargetSequenceName.getOptional();
}
@Override
public String toString() {
return String.format("BasicRelationalPersistentEntity<%s>", getType());
}
private @Nullable String determineTargetSequenceName() {
RelationalPersistentProperty idProperty = getIdProperty();
if (idProperty != null && idProperty.isAnnotationPresent(TargetSequence.class)) {
TargetSequence requiredAnnotation = idProperty.getRequiredAnnotation(TargetSequence.class);
if (!StringUtils.hasText(requiredAnnotation.sequence()) && !StringUtils.hasText(requiredAnnotation.value())) {
throw new IllegalStateException("""
For the persistent entity '%s' the @TargetSequence annotation was specified for the @Id, however, neither
the value() nor the sequence() attributes are specified
"""
);
} else {
String sequenceFullyQualifiedName = getSequenceName(requiredAnnotation);
if (StringUtils.hasText(requiredAnnotation.schema())) {
return String.join(".", requiredAnnotation.schema(), sequenceFullyQualifiedName);
}
return sequenceFullyQualifiedName;
}
} else {
return null;
}
}
@NotNull
private static String getSequenceName(TargetSequence requiredAnnotation) {
return Optional.of(requiredAnnotation.sequence())
.filter(s -> !s.isBlank())
.orElse(requiredAnnotation.value());
}
}

7
spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentEntity.java

@ -17,6 +17,7 @@ package org.springframework.data.relational.core.mapping; @@ -17,6 +17,7 @@ package org.springframework.data.relational.core.mapping;
import java.lang.annotation.Annotation;
import java.util.Iterator;
import java.util.Optional;
import org.springframework.core.env.Environment;
import org.springframework.data.mapping.*;
@ -31,6 +32,7 @@ import org.springframework.lang.Nullable; @@ -31,6 +32,7 @@ import org.springframework.lang.Nullable;
* Embedded entity extension for a {@link Embedded entity}.
*
* @author Mark Paluch
* @author Mikhail Polivakha
* @since 3.2
*/
class EmbeddedRelationalPersistentEntity<T> implements RelationalPersistentEntity<T> {
@ -54,6 +56,11 @@ class EmbeddedRelationalPersistentEntity<T> implements RelationalPersistentEntit @@ -54,6 +56,11 @@ class EmbeddedRelationalPersistentEntity<T> implements RelationalPersistentEntit
throw new MappingException("Embedded entity does not have an id column");
}
@Override
public Optional<String> getIdTargetSequence() {
return Optional.empty();
}
@Override
public void addPersistentProperty(RelationalPersistentProperty property) {
throw new UnsupportedOperationException();

7
spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java

@ -15,6 +15,8 @@ @@ -15,6 +15,8 @@
*/
package org.springframework.data.relational.core.mapping;
import java.util.Optional;
import org.springframework.data.mapping.model.MutablePersistentEntity;
import org.springframework.data.relational.core.sql.SqlIdentifier;
@ -25,6 +27,7 @@ import org.springframework.data.relational.core.sql.SqlIdentifier; @@ -25,6 +27,7 @@ import org.springframework.data.relational.core.sql.SqlIdentifier;
* @author Jens Schauder
* @author Oliver Gierke
* @author Mark Paluch
* @author Mikhail Polivakha
*/
public interface RelationalPersistentEntity<T> extends MutablePersistentEntity<T, RelationalPersistentProperty> {
@ -52,4 +55,8 @@ public interface RelationalPersistentEntity<T> extends MutablePersistentEntity<T @@ -52,4 +55,8 @@ public interface RelationalPersistentEntity<T> extends MutablePersistentEntity<T
*/
SqlIdentifier getIdColumn();
/**
* @return the target sequence that should be used for id generation
*/
Optional<String> getIdTargetSequence();
}

43
spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/TargetSequence.java

@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
package org.springframework.data.relational.core.mapping;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
/**
* Specify the sequence from which the value for the {@link org.springframework.data.annotation.Id}
* should be fetched
*
* @author Mikhail Polivakha
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface TargetSequence {
/**
* The name of the sequence from which the id should be fetched
*/
String value() default "";
/**
* Alias for {@link #value()}
*/
@AliasFor("value")
String sequence() default "";
/**
* Schema where the sequence reside.
* Technically, this attribute is not necessarily the schema. It just represents the location/namespace,
* where the sequence resides. For instance, in Oracle databases the schema and user are often used
* interchangeably, so {@link #schema() schema} attribute may represent an Oracle user as well.
* <p>
* The final name of the sequence to be queried for the next value will be constructed by the concatenation
* of schema and sequence : <pre>schema().sequence()</pre>
*/
String schema() default "";
}

46
spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java

@ -58,6 +58,34 @@ class BasicRelationalPersistentEntityUnitTests { @@ -58,6 +58,34 @@ class BasicRelationalPersistentEntityUnitTests {
assertThat(entity.getTableName()).isEqualTo(quoted("dummy_sub_entity"));
}
@Test
void entityWithNotargetSequence() {
RelationalPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(DummySubEntity.class);
assertThat(entity.getIdTargetSequence()).isEmpty();
}
@Test
void determineSequenceName() {
RelationalPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(EntityWithSequence.class);
assertThat(persistentEntity.getIdTargetSequence()).isPresent().hasValue("my_seq");
}
@Test
void determineSequenceNameFromValue() {
RelationalPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(EntityWithSequenceValueAlias.class);
assertThat(persistentEntity.getIdTargetSequence()).isPresent().hasValue("my_seq");
}
@Test
void determineSequenceNameWithSchemaSpecified() {
RelationalPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(EntityWithSequenceAndSchema.class);
assertThat(persistentEntity.getIdTargetSequence()).isPresent().hasValue("public.my_seq");
}
@Test // DATAJDBC-294
void considerIdColumnName() {
@ -201,6 +229,24 @@ class BasicRelationalPersistentEntityUnitTests { @@ -201,6 +229,24 @@ class BasicRelationalPersistentEntityUnitTests {
@Column("renamedId") Long id;
}
@Table("entity_with_sequence")
static class EntityWithSequence {
@Id
@TargetSequence(sequence = "my_seq") Long id;
}
@Table("entity_with_sequence_value_alias")
static class EntityWithSequenceValueAlias {
@Id
@Column("myId") @TargetSequence(value = "my_seq") Long id;
}
@Table("entity_with_sequence_and_schema")
static class EntityWithSequenceAndSchema {
@Id
@Column("myId") @TargetSequence(sequence = "my_seq", schema = "public") Long id;
}
@Table()
static class DummyEntityWithEmptyAnnotation {
@Id

Loading…
Cancel
Save