Browse Source
Ids can be annotated with @Sequence to specify a sequence to pull id values from. Closes #1923 Original pull request #1955 Signed-off-by: mipo256 <mikhailpolivakha@gmail.com> Some accidential changes removed. Signed-off-by: schauder <jens.schauder@broadcom.com>pull/2001/head
33 changed files with 716 additions and 39 deletions
@ -0,0 +1,71 @@
@@ -0,0 +1,71 @@
|
||||
package org.springframework.data.jdbc.core.mapping; |
||||
|
||||
import java.util.Map; |
||||
import java.util.Optional; |
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration; |
||||
import org.springframework.data.mapping.PersistentPropertyAccessor; |
||||
import org.springframework.data.relational.core.conversion.MutableAggregateChange; |
||||
import org.springframework.data.relational.core.dialect.Dialect; |
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext; |
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; |
||||
import org.springframework.data.relational.core.mapping.event.BeforeSaveCallback; |
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* Callback for generating ID via the database sequence. By default, it is registered as a |
||||
* bean in {@link AbstractJdbcConfiguration} |
||||
* |
||||
* @author Mikhail Polivakha |
||||
*/ |
||||
public class IdGeneratingBeforeSaveCallback implements BeforeSaveCallback<Object> { |
||||
|
||||
private static final Log LOG = LogFactory.getLog(IdGeneratingBeforeSaveCallback.class); |
||||
|
||||
private final RelationalMappingContext relationalMappingContext; |
||||
private final Dialect dialect; |
||||
private final NamedParameterJdbcOperations operations; |
||||
|
||||
public IdGeneratingBeforeSaveCallback( |
||||
RelationalMappingContext relationalMappingContext, |
||||
Dialect dialect, |
||||
NamedParameterJdbcOperations namedParameterJdbcOperations |
||||
) { |
||||
this.relationalMappingContext = relationalMappingContext; |
||||
this.dialect = dialect; |
||||
this.operations = namedParameterJdbcOperations; |
||||
} |
||||
|
||||
@Override |
||||
public Object onBeforeSave(Object aggregate, MutableAggregateChange<Object> aggregateChange) { |
||||
Assert.notNull(aggregate, "The aggregate cannot be null at this point"); |
||||
RelationalPersistentEntity<?> persistentEntity = relationalMappingContext.getPersistentEntity(aggregate.getClass()); |
||||
Optional<String> idTargetSequence = persistentEntity.getIdTargetSequence(); |
||||
|
||||
if (dialect.getIdGeneration().sequencesSupported()) { |
||||
|
||||
if (persistentEntity.getIdProperty() != null) { |
||||
idTargetSequence |
||||
.map(s -> dialect.getIdGeneration().nextValueFromSequenceSelect(s)) |
||||
.ifPresent(sql -> { |
||||
Long idValue = operations.queryForObject(sql, Map.of(), (rs, rowNum) -> rs.getLong(1)); |
||||
PersistentPropertyAccessor<Object> propertyAccessor = persistentEntity.getPropertyAccessor(aggregate); |
||||
propertyAccessor.setProperty(persistentEntity.getRequiredIdProperty(), idValue); |
||||
}); |
||||
} |
||||
} else { |
||||
if (idTargetSequence.isPresent()) { |
||||
LOG.warn(""" |
||||
It seems you're trying to insert an aggregate of type '%s' annotated with @TargetSequence, but the problem is RDBMS you're |
||||
working with does not support sequences as such. Falling back to identity columns |
||||
""" |
||||
.formatted(aggregate.getClass().getName()) |
||||
); |
||||
} |
||||
} |
||||
|
||||
return aggregate; |
||||
} |
||||
} |
||||
@ -0,0 +1,121 @@
@@ -0,0 +1,121 @@
|
||||
package org.springframework.data.jdbc.core.mapping; |
||||
|
||||
import static org.mockito.ArgumentMatchers.anyMap; |
||||
import static org.mockito.ArgumentMatchers.anyString; |
||||
import static org.mockito.Mockito.any; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.when; |
||||
|
||||
import org.assertj.core.api.Assertions; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.springframework.data.annotation.Id; |
||||
import org.springframework.data.relational.core.conversion.MutableAggregateChange; |
||||
import org.springframework.data.relational.core.dialect.MySqlDialect; |
||||
import org.springframework.data.relational.core.dialect.PostgresDialect; |
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext; |
||||
import org.springframework.data.relational.core.mapping.Table; |
||||
import org.springframework.data.relational.core.mapping.TargetSequence; |
||||
import org.springframework.data.relational.core.sql.IdentifierProcessing; |
||||
import org.springframework.jdbc.core.RowMapper; |
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; |
||||
|
||||
/** |
||||
* Unit tests for {@link IdGeneratingBeforeSaveCallback} |
||||
* |
||||
* @author Mikhail Polivakha |
||||
*/ |
||||
class IdGeneratingBeforeSaveCallbackTest { |
||||
|
||||
@Test |
||||
void test_mySqlDialect_sequenceGenerationIsNotSupported() { |
||||
// given
|
||||
RelationalMappingContext relationalMappingContext = new RelationalMappingContext(); |
||||
MySqlDialect mySqlDialect = new MySqlDialect(IdentifierProcessing.NONE); |
||||
NamedParameterJdbcOperations operations = mock(NamedParameterJdbcOperations.class); |
||||
|
||||
// and
|
||||
IdGeneratingBeforeSaveCallback subject = new IdGeneratingBeforeSaveCallback(relationalMappingContext, mySqlDialect, operations); |
||||
|
||||
NoSequenceEntity entity = new NoSequenceEntity(); |
||||
|
||||
// when
|
||||
Object processed = subject.onBeforeSave(entity, MutableAggregateChange.forSave(entity)); |
||||
|
||||
// then
|
||||
Assertions.assertThat(processed).isSameAs(entity); |
||||
Assertions.assertThat(processed).usingRecursiveComparison().isEqualTo(entity); |
||||
} |
||||
|
||||
@Test |
||||
void test_EntityIsNotMarkedWithTargetSequence() { |
||||
// given
|
||||
RelationalMappingContext relationalMappingContext = new RelationalMappingContext(); |
||||
PostgresDialect mySqlDialect = PostgresDialect.INSTANCE; |
||||
NamedParameterJdbcOperations operations = mock(NamedParameterJdbcOperations.class); |
||||
|
||||
// and
|
||||
IdGeneratingBeforeSaveCallback subject = new IdGeneratingBeforeSaveCallback(relationalMappingContext, mySqlDialect, operations); |
||||
|
||||
NoSequenceEntity entity = new NoSequenceEntity(); |
||||
|
||||
// when
|
||||
Object processed = subject.onBeforeSave(entity, MutableAggregateChange.forSave(entity)); |
||||
|
||||
// then
|
||||
Assertions.assertThat(processed).isSameAs(entity); |
||||
Assertions.assertThat(processed).usingRecursiveComparison().isEqualTo(entity); |
||||
} |
||||
|
||||
@Test |
||||
void test_EntityIdIsPopulatedFromSequence() { |
||||
// given
|
||||
RelationalMappingContext relationalMappingContext = new RelationalMappingContext(); |
||||
relationalMappingContext.getRequiredPersistentEntity(EntityWithSequence.class); |
||||
|
||||
PostgresDialect mySqlDialect = PostgresDialect.INSTANCE; |
||||
NamedParameterJdbcOperations operations = mock(NamedParameterJdbcOperations.class); |
||||
|
||||
// and
|
||||
long generatedId = 112L; |
||||
when(operations.queryForObject(anyString(), anyMap(), any(RowMapper.class))).thenReturn(generatedId); |
||||
|
||||
// and
|
||||
IdGeneratingBeforeSaveCallback subject = new IdGeneratingBeforeSaveCallback(relationalMappingContext, mySqlDialect, operations); |
||||
|
||||
EntityWithSequence entity = new EntityWithSequence(); |
||||
|
||||
// when
|
||||
Object processed = subject.onBeforeSave(entity, MutableAggregateChange.forSave(entity)); |
||||
|
||||
// then
|
||||
Assertions.assertThat(processed).isSameAs(entity); |
||||
Assertions |
||||
.assertThat(processed) |
||||
.usingRecursiveComparison() |
||||
.ignoringFields("id") |
||||
.isEqualTo(entity); |
||||
Assertions.assertThat(entity.getId()).isEqualTo(generatedId); |
||||
} |
||||
|
||||
@Table |
||||
static class NoSequenceEntity { |
||||
|
||||
@Id |
||||
private Long id; |
||||
private Long name; |
||||
} |
||||
|
||||
@Table |
||||
static class EntityWithSequence { |
||||
|
||||
@Id |
||||
@TargetSequence(value = "id_seq", schema = "public") |
||||
private Long id; |
||||
|
||||
private Long name; |
||||
|
||||
public Long getId() { |
||||
return id; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
package org.springframework.data.jdbc.testing; |
||||
|
||||
import java.lang.annotation.Documented; |
||||
import java.lang.annotation.ElementType; |
||||
import java.lang.annotation.Retention; |
||||
import java.lang.annotation.RetentionPolicy; |
||||
import java.lang.annotation.Target; |
||||
|
||||
import org.junit.jupiter.api.extension.ExtendWith; |
||||
import org.springframework.test.context.junit.jupiter.EnabledIf; |
||||
|
||||
/** |
||||
* Annotation that allows to disable a particular test to be executed on a particular database |
||||
* |
||||
* @author Mikhail Polivakha |
||||
*/ |
||||
@Target({ElementType.TYPE, ElementType.METHOD}) |
||||
@Retention(RetentionPolicy.RUNTIME) |
||||
@Documented |
||||
@ExtendWith(DisabledOnDatabaseExecutionCondition.class) |
||||
public @interface DisabledOnDatabase { |
||||
|
||||
/** |
||||
* The database on which the test is not supposed to run on |
||||
*/ |
||||
DatabaseType database(); |
||||
} |
||||
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
package org.springframework.data.jdbc.testing; |
||||
|
||||
import org.apache.commons.lang3.ArrayUtils; |
||||
import org.junit.jupiter.api.extension.ConditionEvaluationResult; |
||||
import org.junit.jupiter.api.extension.ExecutionCondition; |
||||
import org.junit.jupiter.api.extension.ExtensionContext; |
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.core.annotation.MergedAnnotation; |
||||
import org.springframework.core.annotation.MergedAnnotations; |
||||
import org.springframework.test.context.junit.jupiter.SpringExtension; |
||||
|
||||
/** |
||||
* {@link ExecutionCondition} for the {@link DisabledOnDatabase} annotation |
||||
* |
||||
* @author Mikhail Polivakha |
||||
*/ |
||||
public class DisabledOnDatabaseExecutionCondition implements ExecutionCondition { |
||||
|
||||
@Override |
||||
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { |
||||
ApplicationContext applicationContext = SpringExtension.getApplicationContext(context); |
||||
|
||||
MergedAnnotation<DisabledOnDatabase> disabledOnDatabaseMergedAnnotation = MergedAnnotations |
||||
.from(context.getRequiredTestMethod(), MergedAnnotations.SearchStrategy.DIRECT) |
||||
.get(DisabledOnDatabase.class); |
||||
|
||||
DatabaseType database = disabledOnDatabaseMergedAnnotation.getEnum("database", DatabaseType.class); |
||||
|
||||
if (ArrayUtils.contains(applicationContext.getEnvironment().getActiveProfiles(), database.getProfile())) { |
||||
return ConditionEvaluationResult.disabled( |
||||
"The test method '%s' is disabled for '%s' because of the @DisabledOnDatabase annotation".formatted(context.getRequiredTestMethod().getName(), database) |
||||
); |
||||
} |
||||
return ConditionEvaluationResult.enabled("The test method '%s' is enabled".formatted(context.getRequiredTestMethod())); |
||||
} |
||||
} |
||||
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
package org.springframework.data.relational.core.mapping; |
||||
|
||||
import java.lang.annotation.Documented; |
||||
import java.lang.annotation.ElementType; |
||||
import java.lang.annotation.Retention; |
||||
import java.lang.annotation.RetentionPolicy; |
||||
import java.lang.annotation.Target; |
||||
|
||||
import org.springframework.core.annotation.AliasFor; |
||||
|
||||
/** |
||||
* Specify the sequence from which the value for the {@link org.springframework.data.annotation.Id} |
||||
* should be fetched |
||||
* |
||||
* @author Mikhail Polivakha |
||||
*/ |
||||
@Retention(RetentionPolicy.RUNTIME) |
||||
@Target(ElementType.FIELD) |
||||
@Documented |
||||
public @interface TargetSequence { |
||||
|
||||
/** |
||||
* The name of the sequence from which the id should be fetched |
||||
*/ |
||||
String value() default ""; |
||||
|
||||
/** |
||||
* Alias for {@link #value()} |
||||
*/ |
||||
@AliasFor("value") |
||||
String sequence() default ""; |
||||
|
||||
/** |
||||
* Schema where the sequence reside. |
||||
* Technically, this attribute is not necessarily the schema. It just represents the location/namespace, |
||||
* where the sequence resides. For instance, in Oracle databases the schema and user are often used |
||||
* interchangeably, so {@link #schema() schema} attribute may represent an Oracle user as well. |
||||
* <p> |
||||
* The final name of the sequence to be queried for the next value will be constructed by the concatenation |
||||
* of schema and sequence : <pre>schema().sequence()</pre> |
||||
*/ |
||||
String schema() default ""; |
||||
} |
||||
Loading…
Reference in new issue