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>
This commit is contained in:
committed by
Jens Schauder
parent
b51c77b50a
commit
d1c996008c
@@ -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 {
|
||||
|
||||
@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 {
|
||||
|
||||
@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 {
|
||||
|
||||
return baseProperty.getOwner().getType();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
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)}.
|
||||
|
||||
@@ -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 {
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
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;
|
||||
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 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 {
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
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 {
|
||||
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());
|
||||
|
||||
@@ -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 {
|
||||
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 {
|
||||
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;
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
);
|
||||
|
||||
CREATE TABLE ENTITY_WITH_SEQUENCE
|
||||
(
|
||||
ID BIGINT,
|
||||
NAME VARCHAR(100)
|
||||
);
|
||||
|
||||
CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
);
|
||||
);
|
||||
|
||||
CREATE TABLE ENTITY_WITH_SEQUENCE
|
||||
(
|
||||
ID BIGINT,
|
||||
NAME VARCHAR(100)
|
||||
);
|
||||
|
||||
CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE;
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
);
|
||||
);
|
||||
|
||||
CREATE TABLE ENTITY_WITH_SEQUENCE
|
||||
(
|
||||
ID BIGINT,
|
||||
NAME VARCHAR(100)
|
||||
);
|
||||
|
||||
CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE;
|
||||
@@ -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
|
||||
* 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 <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) {
|
||||
|
||||
@@ -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 {
|
||||
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() {}
|
||||
|
||||
@@ -146,5 +146,5 @@ public interface Dialect {
|
||||
|
||||
default boolean supportsSingleQueryLoading() {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
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 {
|
||||
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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
TimestampAtUtcToOffsetDateTimeConverter.INSTANCE,
|
||||
NumberToBooleanConverter.INSTANCE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdGeneration getIdGeneration() {
|
||||
return new IdGeneration() {
|
||||
|
||||
@Override
|
||||
public String nextValueFromSequenceSelect(String sequenceName) {
|
||||
return "SELECT NEXTVAL(%s)".formatted(sequenceName);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
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 {
|
||||
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() {}
|
||||
|
||||
@@ -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 {
|
||||
// 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 {
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
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
|
||||
this.schemaName = defaultSchema;
|
||||
this.schemaNameExpression = null;
|
||||
}
|
||||
|
||||
this.idTargetSequenceName = Lazy.of(this::determineTargetSequenceName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
* 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
|
||||
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();
|
||||
|
||||
@@ -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;
|
||||
* @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
|
||||
*/
|
||||
SqlIdentifier getIdColumn();
|
||||
|
||||
/**
|
||||
* @return the target sequence that should be used for id generation
|
||||
*/
|
||||
Optional<String> getIdTargetSequence();
|
||||
}
|
||||
|
||||
@@ -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 "";
|
||||
}
|
||||
@@ -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 {
|
||||
@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
|
||||
|
||||
Reference in New Issue
Block a user