diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java index 997931cd9f..c638a3e763 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java @@ -118,8 +118,13 @@ public Object[] insert(List> insertSubjects, Class domai 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 List findAll(Class domainType) { @Override public Stream streamAll(Class 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 List findAll(Class domainType, Sort sort) { @Override public Stream streamAll(Class 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 @@ private Class getBaseType(PersistentPropertyPath { + + 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 aggregateChange) { + Assert.notNull(aggregate, "The aggregate cannot be null at this point"); + RelationalPersistentEntity persistentEntity = relationalMappingContext.getPersistentEntity(aggregate.getClass()); + Optional 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 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; + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java index 2b62c96ef7..3abef09dcf 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java @@ -38,6 +38,7 @@ 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 JdbcMappingContext jdbcMappingContext(Optional namingStra 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)}. diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java index 7a177a525b..9efdb3aeab 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java @@ -33,7 +33,6 @@ 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 { 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 diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/IdGeneratingBeforeSaveCallbackTest.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/IdGeneratingBeforeSaveCallbackTest.java new file mode 100644 index 0000000000..65b2222bab --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/IdGeneratingBeforeSaveCallbackTest.java @@ -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; + } + } +} \ No newline at end of file diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index f2d61cadcd..8a52fcafc4 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -42,7 +42,6 @@ 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.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.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 { @Autowired DummyEntityRepository repository; @Autowired MyEventListener eventListener; @Autowired RootRepository rootRepository; - @Autowired WithDelimitedColumnRepository withDelimitedColumnRepository; + @Autowired EntityWithSequenceRepository entityWithSequenceRepository; @BeforeEach public void before() { @@ -135,6 +126,28 @@ public void savesAnEntity() { "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 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 @@ interface RootRepository extends ListCrudRepository { interface WithDelimitedColumnRepository extends CrudRepository {} + interface EntityWithSequenceRepository extends CrudRepository {} + @Configuration @Import(TestConfiguration.class) static class Config { @@ -1536,6 +1551,11 @@ WithDelimitedColumnRepository withDelimitedColumnRepository() { return factory.getRepository(WithDelimitedColumnRepository.class); } + @Bean + EntityWithSequenceRepository entityWithSequenceRepository() { + return factory.getRepository(EntityWithSequenceRepository.class); + } + @Bean NamedQueries namedQueries() throws IOException { @@ -1839,6 +1859,31 @@ private static DummyEntity createEntity(String entityName, Consumer 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; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/DisabledOnDatabase.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/DisabledOnDatabase.java new file mode 100644 index 0000000000..c83ec900f6 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/DisabledOnDatabase.java @@ -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(); +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/DisabledOnDatabaseExecutionCondition.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/DisabledOnDatabaseExecutionCondition.java new file mode 100644 index 0000000000..17f9bfdf20 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/DisabledOnDatabaseExecutionCondition.java @@ -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 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())); + } +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java index 2b1c8a843c..63db08a0cc 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java @@ -36,11 +36,15 @@ 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 { JdbcRepositoryFactory jdbcRepositoryFactory( @Qualifier("defaultDataAccessStrategy") DataAccessStrategy dataAccessStrategy, RelationalMappingContext context, Dialect dialect, JdbcConverter converter, Optional> namedQueries, + List> callbacks, List 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 @@ JdbcConverter relationalConverter(RelationalMappingContext mappingContext, @Lazy 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()); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestDatabaseFeatures.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestDatabaseFeatures.java index aa812923fc..0a985bd5ad 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestDatabaseFeatures.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestDatabaseFeatures.java @@ -30,6 +30,7 @@ * * @author Jens Schauder * @author Chirag Tailor + * @author Mikhail Polivakha */ public class TestDatabaseFeatures { @@ -79,6 +80,10 @@ private void supportsNullPrecedence() { 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 enum Feature { 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 featureMethod; diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql index 2c66f226e1..1c00e779a6 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql @@ -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 ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, "ORG.XTUNIT.IDENTIFIER" VARCHAR(100), STYPE VARCHAR(100) -); \ No newline at end of file +); + +CREATE TABLE ENTITY_WITH_SEQUENCE +( + ID BIGINT, + NAME VARCHAR(100) +); + +CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql index b72f664535..6f9087b69d 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql @@ -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) -); \ No newline at end of file +); + +CREATE TABLE ENTITY_WITH_SEQUENCE +( + ID BIGINT, + NAME VARCHAR(100) +); + +CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql index b72f664535..6f9087b69d 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql @@ -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) -); \ No newline at end of file +); + +CREATE TABLE ENTITY_WITH_SEQUENCE +( + ID BIGINT, + NAME VARCHAR(100) +); + +CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql index 75b4663989..19ebad8bc3 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql @@ -39,4 +39,12 @@ CREATE TABLE WITH_DELIMITED_COLUMN ID BIGINT AUTO_INCREMENT PRIMARY KEY, `ORG.XTUNIT.IDENTIFIER` VARCHAR(100), STYPE VARCHAR(100) -); \ No newline at end of file +); + +CREATE TABLE ENTITY_WITH_SEQUENCE +( + ID BIGINT, + NAME VARCHAR(100) +); + +CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql index 9959dea4a8..69f191f65d 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql @@ -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 ID BIGINT IDENTITY PRIMARY KEY, "ORG.XTUNIT.IDENTIFIER" VARCHAR(100), STYPE VARCHAR(100) -); \ No newline at end of file +); + +CREATE TABLE ENTITY_WITH_SEQUENCE +( + ID BIGINT, + NAME VARCHAR(100) +); + +CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql index 0a08dfbf9e..179ac5abb9 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql @@ -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 "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; \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql index 37ad6914de..14dff05925 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql @@ -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" ID SERIAL PRIMARY KEY, "ORG.XTUNIT.IDENTIFIER" VARCHAR(100), "STYPE" VARCHAR(100) -); \ No newline at end of file +); + +CREATE TABLE ENTITY_WITH_SEQUENCE +( + ID BIGINT, + NAME VARCHAR(100) +); + +CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; \ No newline at end of file diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/IdValueSource.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/IdValueSource.java index 8f174b7b1e..0ee5384839 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/IdValueSource.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/IdValueSource.java @@ -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 @@ * 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 { /** * 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 { */ public static IdValueSource forInstance(Object instance, RelationalPersistentEntity persistentEntity) { + Optional idTargetSequence = persistentEntity.getIdTargetSequence(); + if (idTargetSequence.isPresent()) { + return IdValueSource.SEQUENCE; + } + Object idValue = persistentEntity.getIdentifierAccessor(instance).getIdentifier(); RelationalPersistentProperty idProperty = persistentEntity.getIdProperty(); if (idProperty == null) { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Db2Dialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Db2Dialect.java index e88bf30a0e..2658cf5c7f 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Db2Dialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Db2Dialect.java @@ -25,6 +25,7 @@ * 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 { 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() {} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java index 32e1b4fae4..9214389d58 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java @@ -146,5 +146,5 @@ default SimpleFunction getExistsFunction() { default boolean supportsSingleQueryLoading() { return true; - }; + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/H2Dialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/H2Dialect.java index 2fa5b40191..cf8f69d44d 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/H2Dialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/H2Dialect.java @@ -31,6 +31,7 @@ * @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 Set> simpleTypes() { 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); + } + }; + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/HsqlDbDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/HsqlDbDialect.java index 0fad643cb9..51e7079fba 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/HsqlDbDialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/HsqlDbDialect.java @@ -20,6 +20,7 @@ * * @author Jens Schauder * @author Myeonghyeon Lee + * @author Mikhail Polivakha */ public class HsqlDbDialect extends AbstractDialect { @@ -64,4 +65,22 @@ public Position getClausePosition() { 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 The way JOOQ solves this problem + */ + @Override + public String nextValueFromSequenceSelect(String sequenceName) { + return "SELECT NEXT VALUE FOR %s AS msq FROM INFORMATION_SCHEMA.SEQUENCES LIMIT 1".formatted(sequenceName); + } + }; + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/IdGeneration.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/IdGeneration.java index ba405272d9..738f3ec591 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/IdGeneration.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/IdGeneration.java @@ -21,10 +21,13 @@ 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 @@ default String getKeyColumnName(SqlIdentifier id) { 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. *

@@ -71,4 +81,16 @@ default String getKeyColumnName(SqlIdentifier id) { 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()) + ); + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MariaDbDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MariaDbDialect.java index 3c3a36f558..93c4261d8d 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MariaDbDialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MariaDbDialect.java @@ -18,12 +18,14 @@ 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 Collection getConverters() { TimestampAtUtcToOffsetDateTimeConverter.INSTANCE, NumberToBooleanConverter.INSTANCE); } + + @Override + public IdGeneration getIdGeneration() { + return new IdGeneration() { + + @Override + public String nextValueFromSequenceSelect(String sequenceName) { + return "SELECT NEXTVAL(%s)".formatted(sequenceName); + } + }; + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java index 425480331b..323e472346 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java @@ -141,4 +141,15 @@ public Collection getConverters() { public OrderByNullPrecedence orderByNullHandling() { return OrderByNullPrecedence.NONE; } + + @Override + public IdGeneration getIdGeneration() { + return new IdGeneration() { + + @Override + public boolean sequencesSupported() { + return false; + } + }; + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java index e7ab812f28..eafd8cf506 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java @@ -27,6 +27,7 @@ * 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 { 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 boolean driverRequiresKeyColumnNames() { 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() {} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java index 38762d7b70..a5ba6b672b 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java @@ -42,6 +42,7 @@ * @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 String getLock(LockOptions lockOptions) { // 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 Set> simpleTypes() { 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); + } + }; + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java index de1f74551a..2a8a1e2ed8 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java @@ -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 diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntity.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntity.java index 180f1b6340..75a501a3ed 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntity.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntity.java @@ -17,6 +17,7 @@ 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 extends BasicPersistentEntity tableName; private final @Nullable Expression tableNameExpression; + private final Lazy idTargetSequenceName; + private final Lazy> schemaName; private final @Nullable Expression schemaNameExpression; private final ExpressionEvaluator expressionEvaluator; @@ -87,6 +90,8 @@ class BasicRelationalPersistentEntity extends BasicPersistentEntity 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()); + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentEntity.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentEntity.java index 9f06fb7f7d..433e9e25c5 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentEntity.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentEntity.java @@ -17,6 +17,7 @@ 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 @@ * Embedded entity extension for a {@link Embedded entity}. * * @author Mark Paluch + * @author Mikhail Polivakha * @since 3.2 */ class EmbeddedRelationalPersistentEntity implements RelationalPersistentEntity { @@ -54,6 +56,11 @@ public SqlIdentifier getIdColumn() { throw new MappingException("Embedded entity does not have an id column"); } + @Override + public Optional getIdTargetSequence() { + return Optional.empty(); + } + @Override public void addPersistentProperty(RelationalPersistentProperty property) { throw new UnsupportedOperationException(); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java index 7c732db44f..3334451302 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java @@ -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 @@ * @author Jens Schauder * @author Oliver Gierke * @author Mark Paluch + * @author Mikhail Polivakha */ public interface RelationalPersistentEntity extends MutablePersistentEntity { @@ -52,4 +55,8 @@ default SqlIdentifier getQualifiedTableName() { */ SqlIdentifier getIdColumn(); + /** + * @return the target sequence that should be used for id generation + */ + Optional getIdTargetSequence(); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/TargetSequence.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/TargetSequence.java new file mode 100644 index 0000000000..be16bcfc7f --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/TargetSequence.java @@ -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. + *

+ * The final name of the sequence to be queried for the next value will be constructed by the concatenation + * of schema and sequence :

schema().sequence()
+ */ + String schema() default ""; +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java index e116ff2d0a..a63c2d1125 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java @@ -58,6 +58,34 @@ void discoversAnnotatedTableName() { 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 @@ static class DummySubEntity { @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