Polishing.

Reference issues in tests comments.
Removed `DisabledOnDatabase`
IdGeneration default methods related to sequence generation are now internally consistent.
Formatting and naming.
IdGeneration offers simple support by default.
Fix exception in oracle integration test setup
Use SqlIdentifier for sequence names
Remove SEQUENCE id source
Added documentation

See #1923
Original pull request #1955
This commit is contained in:
Jens Schauder
2025-01-30 10:27:15 +01:00
parent d1c996008c
commit 8c017fc56b
26 changed files with 250 additions and 290 deletions

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

View File

@@ -11,6 +11,7 @@ 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.data.relational.core.sql.SqlIdentifier;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.util.Assert;
@@ -40,15 +41,17 @@ public class IdGeneratingBeforeSaveCallback implements BeforeSaveCallback<Object
@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();
Optional<SqlIdentifier> idSequence = persistentEntity.getIdSequence();
if (dialect.getIdGeneration().sequencesSupported()) {
if (persistentEntity.getIdProperty() != null) {
idTargetSequence
.map(s -> dialect.getIdGeneration().nextValueFromSequenceSelect(s))
idSequence
.map(s -> dialect.getIdGeneration().createSequenceQuery(s))
.ifPresent(sql -> {
Long idValue = operations.queryForObject(sql, Map.of(), (rs, rowNum) -> rs.getLong(1));
PersistentPropertyAccessor<Object> propertyAccessor = persistentEntity.getPropertyAccessor(aggregate);
@@ -56,7 +59,7 @@ public class IdGeneratingBeforeSaveCallback implements BeforeSaveCallback<Object
});
}
} else {
if (idTargetSequence.isPresent()) {
if (idSequence.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

View File

@@ -14,7 +14,7 @@ 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.mapping.Sequence;
import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
@@ -26,68 +26,58 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
*/
class IdGeneratingBeforeSaveCallbackTest {
@Test
void test_mySqlDialect_sequenceGenerationIsNotSupported() {
// given
RelationalMappingContext relationalMappingContext = new RelationalMappingContext();
@Test // GH-1923
void mySqlDialectsequenceGenerationIsNotSupported() {
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();
@Test // GH-1923
void entityIsNotMarkedWithTargetSequence() {
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
@Test // GH-1923
void entityIdIsPopulatedFromSequence() {
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)
@@ -109,7 +99,7 @@ class IdGeneratingBeforeSaveCallbackTest {
static class EntityWithSequence {
@Id
@TargetSequence(value = "id_seq", schema = "public")
@Sequence(value = "id_seq", schema = "public")
private Long id;
private Long name;

View File

@@ -65,7 +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.Sequence;
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;
@@ -126,9 +126,10 @@ public class JdbcRepositoryIntegrationTests {
"id_Prop = " + entity.getIdProp())).isEqualTo(1);
}
@Test
@Test // GH-1923
@EnabledOnFeature(value = TestDatabaseFeatures.Feature.SUPPORTS_SEQUENCES)
public void saveEntityWithTargetSequenceSpecified() {
EntityWithSequence first = entityWithSequenceRepository.save(new EntityWithSequence("first"));
EntityWithSequence second = entityWithSequenceRepository.save(new EntityWithSequence("second"));
@@ -139,9 +140,10 @@ public class JdbcRepositoryIntegrationTests {
assertThat(second.getName()).isEqualTo("second");
}
@Test
@Test // GH-1923
@EnabledOnFeature(value = TestDatabaseFeatures.Feature.SUPPORTS_SEQUENCES)
public void batchInsertEntityWithTargetSequenceSpecified() {
Iterable<EntityWithSequence> results = entityWithSequenceRepository
.saveAll(List.of(new EntityWithSequence("first"), new EntityWithSequence("second")));
@@ -1862,7 +1864,7 @@ public class JdbcRepositoryIntegrationTests {
static class EntityWithSequence {
@Id
@TargetSequence(sequence = "entity_sequence") private Long id;
@Sequence(sequence = "ENTITY_SEQUENCE") private Long id;
private String name;

View File

@@ -1,27 +0,0 @@
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();
}

View File

@@ -1,36 +0,0 @@
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()));
}
}

View File

@@ -47,4 +47,4 @@ CREATE TABLE ENTITY_WITH_SEQUENCE
NAME VARCHAR(100)
);
CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE;
CREATE SEQUENCE `ENTITY_SEQUENCE` START WITH 1 INCREMENT BY 1 NO MAXVALUE;

View File

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

View File

@@ -55,4 +55,4 @@ CREATE TABLE ENTITY_WITH_SEQUENCE
NAME VARCHAR(100)
);
CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE;
CREATE SEQUENCE "ENTITY_SEQUENCE" START WITH 1 INCREMENT BY 1 NO MAXVALUE;

View File

@@ -15,8 +15,6 @@
*/
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;
@@ -42,12 +40,7 @@ public enum IdValueSource {
/**
* There is no id property, and therefore no id value source.
*/
NONE,
/**
* The id should be dervied from the database sequence
*/
SEQUENCE;
NONE;
/**
* Returns the appropriate {@link IdValueSource} for the instance: {@link IdValueSource#NONE} when the entity has no
@@ -56,9 +49,8 @@ public enum IdValueSource {
*/
public static <T> IdValueSource forInstance(Object instance, RelationalPersistentEntity<T> persistentEntity) {
Optional<String> idTargetSequence = persistentEntity.getIdTargetSequence();
if (idTargetSequence.isPresent()) {
return IdValueSource.SEQUENCE;
if (persistentEntity.getIdSequence().isPresent()) {
return IdValueSource.PROVIDED;
}
Object idValue = persistentEntity.getIdentifierAccessor(instance).getIdentifier();

View File

@@ -18,8 +18,8 @@ package org.springframework.data.relational.core.dialect;
import java.util.Collection;
import java.util.Collections;
import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.data.relational.core.sql.LockOptions;
import org.springframework.data.relational.core.sql.SqlIdentifier;
/**
* An SQL dialect for DB2.
@@ -41,14 +41,20 @@ public class Db2Dialect extends AbstractDialect {
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);
public boolean sequencesSupported() {
return true;
}
@Override
public String createSequenceQuery(SqlIdentifier sequenceName) {
/*
* This workaround (non-ANSI SQL way of querying sequence) exists for the same reasons it exists for {@link HsqlDbDialect}
*
* @see HsqlDbDialect#getIdGeneration()#nextValueFromSequenceSelect(String)
*/
return "SELECT NEXT VALUE FOR %s FROM SYSCAT.SEQUENCES LIMIT 1"
.formatted(sequenceName.toSql(INSTANCE.getIdentifierProcessing()));
}
};
@@ -104,11 +110,6 @@ public class Db2Dialect extends AbstractDialect {
};
}
@Override
public IdentifierProcessing getIdentifierProcessing() {
return IdentifierProcessing.ANSI;
}
@Override
public Collection<Object> getConverters() {
return Collections.singletonList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE);

View File

@@ -40,6 +40,9 @@ public class H2Dialect extends AbstractDialect {
* Singleton instance.
*/
public static final H2Dialect INSTANCE = new H2Dialect();
private static final IdentifierProcessing IDENTIFIER_PROCESSING = IdentifierProcessing.create(Quoting.ANSI,
LetterCasing.UPPER_CASE);
private static final IdGeneration ID_GENERATION = IdGeneration.create(IDENTIFIER_PROCESSING);
protected H2Dialect() {}
@@ -101,7 +104,7 @@ public class H2Dialect extends AbstractDialect {
@Override
public IdentifierProcessing getIdentifierProcessing() {
return IdentifierProcessing.create(Quoting.ANSI, LetterCasing.UPPER_CASE);
return IDENTIFIER_PROCESSING;
}
@Override
@@ -117,12 +120,6 @@ public class H2Dialect extends AbstractDialect {
@Override
public IdGeneration getIdGeneration() {
return new IdGeneration() {
@Override
public String nextValueFromSequenceSelect(String sequenceName) {
return "SELECT NEXT VALUE FOR %s".formatted(sequenceName);
}
};
return ID_GENERATION;
}
}

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.relational.core.dialect;
import org.springframework.data.relational.core.sql.SqlIdentifier;
/**
* A {@link Dialect} for HsqlDb.
*
@@ -70,16 +72,17 @@ public class HsqlDbDialect extends AbstractDialect {
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);
public String createSequenceQuery(SqlIdentifier sequenceName) {
/*
* 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>
*/
return "SELECT NEXT VALUE FOR %s AS msq FROM INFORMATION_SCHEMA.SEQUENCES LIMIT 1"
.formatted(sequenceName.toSql(getIdentifierProcessing()));
}
};
}

View File

@@ -18,20 +18,30 @@ package org.springframework.data.relational.core.dialect;
import java.sql.Connection;
import java.sql.PreparedStatement;
import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.data.relational.core.sql.SqlIdentifier;
/**
* Encapsulates various properties that are related to ID generation process and specific to
* given {@link Dialect}
* 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 {
static IdGeneration create(IdentifierProcessing identifierProcessing) {
return new IdGeneration() {
@Override
public String createSequenceQuery(SqlIdentifier sequenceName) {
return IdGeneration.createSequenceQuery(sequenceName.toSql(identifierProcessing));
}
};
}
/**
* A default instance working for many databases and equivalent to Spring Data JDBCs behavior before version 2.1.
*/
@@ -62,13 +72,6 @@ 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>
@@ -82,15 +85,28 @@ public interface IdGeneration {
return true;
}
/**
* @return {@literal true} in case the sequences are supported by the underlying database, {@literal false} otherwise
* @since 3.5
*/
default boolean sequencesSupported() {
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
* @param sequenceName the sequence name to get the next value for
* @return SQL string
* @since 3.5
*/
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())
);
default String createSequenceQuery(SqlIdentifier sequenceName) {
String nameString = sequenceName.toString();
return createSequenceQuery(nameString);
}
static String createSequenceQuery(String nameString) {
return "SELECT NEXT VALUE FOR" + nameString;
}
}

View File

@@ -18,7 +18,6 @@ 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;
/**
@@ -36,19 +35,11 @@ public class MariaDbDialect extends MySqlDialect {
@Override
public Collection<Object> getConverters() {
return Arrays.asList(
TimestampAtUtcToOffsetDateTimeConverter.INSTANCE,
NumberToBooleanConverter.INSTANCE);
return Arrays.asList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE, NumberToBooleanConverter.INSTANCE);
}
@Override
public IdGeneration getIdGeneration() {
return new IdGeneration() {
@Override
public String nextValueFromSequenceSelect(String sequenceName) {
return "SELECT NEXTVAL(%s)".formatted(sequenceName);
}
};
return IdGeneration.create(getIdentifierProcessing());
}
}

View File

@@ -18,10 +18,12 @@ package org.springframework.data.relational.core.dialect;
import java.util.Arrays;
import java.util.Collection;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.data.relational.core.sql.LockOptions;
import org.springframework.data.relational.core.sql.IdentifierProcessing.LetterCasing;
import org.springframework.data.relational.core.sql.IdentifierProcessing.Quoting;
import org.springframework.data.relational.core.sql.LockOptions;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.util.Assert;
/**
@@ -131,10 +133,7 @@ public class MySqlDialect extends AbstractDialect {
@Override
public Collection<Object> getConverters() {
return Arrays.asList(
TimestampAtUtcToOffsetDateTimeConverter.INSTANCE,
NumberToBooleanConverter.INSTANCE
);
return Arrays.asList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE, NumberToBooleanConverter.INSTANCE);
}
@Override
@@ -144,12 +143,20 @@ public class MySqlDialect extends AbstractDialect {
@Override
public IdGeneration getIdGeneration() {
return new IdGeneration() {
@Override
public boolean sequencesSupported() {
return false;
}
@Override
public String createSequenceQuery(@NotNull SqlIdentifier 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()));
}
};
}
}

View File

@@ -15,13 +15,14 @@
*/
package org.springframework.data.relational.core.dialect;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import static java.util.Arrays.*;
import java.util.Collection;
import static java.util.Arrays.*;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.relational.core.sql.SqlIdentifier;
/**
* An SQL dialect for Oracle.
@@ -50,8 +51,8 @@ public class OracleDialect extends AnsiDialect {
}
@Override
public String nextValueFromSequenceSelect(String sequenceName) {
return "SELECT %s.nextval FROM DUAL".formatted(sequenceName);
public String createSequenceQuery(@NotNull SqlIdentifier sequenceName) {
return "SELECT %s.nextval FROM DUAL".formatted(sequenceName.toSql(INSTANCE.getIdentifierProcessing()));
}
};
@@ -64,7 +65,8 @@ public class OracleDialect extends AnsiDialect {
@Override
public Collection<Object> getConverters() {
return asList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE, NumberToBooleanConverter.INSTANCE, BooleanToIntegerConverter.INSTANCE);
return asList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE, NumberToBooleanConverter.INSTANCE,
BooleanToIntegerConverter.INSTANCE);
}
@WritingConverter

View File

@@ -55,6 +55,16 @@ public class PostgresDialect extends AbstractDialect {
private static final Set<Class<?>> POSTGRES_SIMPLE_TYPES = Set.of(UUID.class, URL.class, URI.class, InetAddress.class,
Map.class);
private IdentifierProcessing identifierProcessing = IdentifierProcessing.create(Quoting.ANSI,
LetterCasing.LOWER_CASE);
private IdGeneration idGeneration = new IdGeneration() {
@Override
public String createSequenceQuery(SqlIdentifier sequenceName) {
return "SELECT nextval('%s')".formatted(sequenceName.toSql(getIdentifierProcessing()));
}
};
protected PostgresDialect() {}
private static final LimitClause LIMIT_CLAUSE = new LimitClause() {
@@ -131,10 +141,10 @@ public class PostgresDialect extends AbstractDialect {
// without schema
String tableName = last.toSql(this.identifierProcessing);
return switch (lockOptions.getLockMode()) {
case PESSIMISTIC_WRITE -> "FOR UPDATE OF " + tableName;
case PESSIMISTIC_READ -> "FOR SHARE OF " + tableName;
};
return switch (lockOptions.getLockMode()) {
case PESSIMISTIC_WRITE -> "FOR UPDATE OF " + tableName;
case PESSIMISTIC_READ -> "FOR SHARE OF " + tableName;
};
}
@Override
@@ -145,7 +155,7 @@ public class PostgresDialect extends AbstractDialect {
@Override
public IdentifierProcessing getIdentifierProcessing() {
return IdentifierProcessing.create(Quoting.ANSI, LetterCasing.LOWER_CASE);
return identifierProcessing;
}
@Override
@@ -160,12 +170,6 @@ public class PostgresDialect extends AbstractDialect {
@Override
public IdGeneration getIdGeneration() {
return new IdGeneration() {
@Override
public String nextValueFromSequenceSelect(String sequenceName) {
return "SELECT nextval('%s')".formatted(sequenceName);
}
};
return idGeneration;
}
}

View File

@@ -17,6 +17,7 @@ package org.springframework.data.relational.core.dialect;
import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.data.relational.core.sql.LockOptions;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.relational.core.sql.render.SelectRenderContext;
import org.springframework.data.util.Lazy;
@@ -36,6 +37,9 @@ public class SqlServerDialect extends AbstractDialect {
*/
public static final SqlServerDialect INSTANCE = new SqlServerDialect();
private static final IdentifierProcessing IDENTIFIER_PROCESSING = IdentifierProcessing
.create(IdentifierProcessing.Quoting.ANSI, IdentifierProcessing.LetterCasing.AS_IS);
private static final IdGeneration ID_GENERATION = new IdGeneration() {
@Override
@@ -44,14 +48,11 @@ public class SqlServerDialect extends AbstractDialect {
}
@Override
public String nextValueFromSequenceSelect(String sequenceName) {
return "SELECT NEXT VALUE FOR %s".formatted(sequenceName);
public String createSequenceQuery(SqlIdentifier sequenceName) {
return IdGeneration.createSequenceQuery(sequenceName.toSql(IDENTIFIER_PROCESSING));
}
};
private static final IdentifierProcessing IDENTIFIER_PROCESSING = IdentifierProcessing
.create(IdentifierProcessing.Quoting.ANSI, IdentifierProcessing.LetterCasing.AS_IS);
protected SqlServerDialect() {}
@Override
@@ -86,7 +87,7 @@ public class SqlServerDialect extends AbstractDialect {
@Override
public String getLock(LockOptions lockOptions) {
return switch (lockOptions.getLockMode()) {
case PESSIMISTIC_WRITE -> "WITH (UPDLOCK, ROWLOCK)";
case PESSIMISTIC_READ -> "WITH (HOLDLOCK, ROWLOCK)";

View File

@@ -17,7 +17,8 @@ package org.springframework.data.relational.core.mapping;
import java.util.Optional;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.util.Lazy;
@@ -47,7 +48,7 @@ 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<SqlIdentifier> idSequenceName;
private final Lazy<Optional<SqlIdentifier>> schemaName;
private final @Nullable Expression schemaNameExpression;
@@ -91,7 +92,7 @@ class BasicRelationalPersistentEntity<T> extends BasicPersistentEntity<T, Relati
this.schemaNameExpression = null;
}
this.idTargetSequenceName = Lazy.of(this::determineTargetSequenceName);
this.idSequenceName = Lazy.of(this::determineSequenceName);
}
/**
@@ -165,8 +166,8 @@ class BasicRelationalPersistentEntity<T> extends BasicPersistentEntity<T, Relati
}
@Override
public Optional<String> getIdTargetSequence() {
return idTargetSequenceName.getOptional();
public Optional<SqlIdentifier> getIdSequence() {
return idSequenceName.getOptional();
}
@Override
@@ -174,33 +175,28 @@ class BasicRelationalPersistentEntity<T> extends BasicPersistentEntity<T, Relati
return String.format("BasicRelationalPersistentEntity<%s>", getType());
}
private @Nullable String determineTargetSequenceName() {
private @Nullable SqlIdentifier determineSequenceName() {
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;
if (idProperty != null && idProperty.isAnnotationPresent(Sequence.class)) {
Sequence requiredAnnotation = idProperty.getRequiredAnnotation(Sequence.class);
MergedAnnotation<Sequence> targetSequence = MergedAnnotations.from(requiredAnnotation)
.get(Sequence.class);
String sequence = targetSequence.getString("sequence");
String schema = targetSequence.getString("schema");
SqlIdentifier sequenceIdentifier = SqlIdentifier.quoted(sequence);
if (StringUtils.hasText(schema)) {
sequenceIdentifier = SqlIdentifier.from(SqlIdentifier.quoted(schema), sequenceIdentifier);
}
return sequenceIdentifier;
} else {
return null;
}
}
@NotNull
private static String getSequenceName(TargetSequence requiredAnnotation) {
return Optional.of(requiredAnnotation.sequence())
.filter(s -> !s.isBlank())
.orElse(requiredAnnotation.value());
}
}

View File

@@ -57,7 +57,7 @@ class EmbeddedRelationalPersistentEntity<T> implements RelationalPersistentEntit
}
@Override
public Optional<String> getIdTargetSequence() {
public Optional<SqlIdentifier> getIdSequence() {
return Optional.empty();
}

View File

@@ -58,5 +58,5 @@ public interface RelationalPersistentEntity<T> extends MutablePersistentEntity<T
/**
* @return the target sequence that should be used for id generation
*/
Optional<String> getIdTargetSequence();
Optional<SqlIdentifier> getIdSequence();
}

View File

@@ -0,0 +1,46 @@
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 Sequence {
/**
* The name of the sequence from which the id should be fetched
*/
@AliasFor("sequence")
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 "";
}

View File

@@ -1,43 +0,0 @@
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 "";
}

View File

@@ -58,32 +58,40 @@ class BasicRelationalPersistentEntityUnitTests {
assertThat(entity.getTableName()).isEqualTo(quoted("dummy_sub_entity"));
}
@Test
void entityWithNotargetSequence() {
@Test // GH-1923
void entityWithNoSequence() {
RelationalPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(DummySubEntity.class);
assertThat(entity.getIdTargetSequence()).isEmpty();
assertThat(entity.getIdSequence()).isEmpty();
}
@Test
@Test // GH-1923
void determineSequenceName() {
RelationalPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(EntityWithSequence.class);
assertThat(persistentEntity.getIdTargetSequence()).isPresent().hasValue("my_seq");
RelationalPersistentEntity<?> persistentEntity = mappingContext
.getRequiredPersistentEntity(EntityWithSequence.class);
assertThat(persistentEntity.getIdSequence()).contains(SqlIdentifier.quoted("my_seq"));
}
@Test
@Test // GH-1923
void determineSequenceNameFromValue() {
RelationalPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(EntityWithSequenceValueAlias.class);
assertThat(persistentEntity.getIdTargetSequence()).isPresent().hasValue("my_seq");
RelationalPersistentEntity<?> persistentEntity = mappingContext
.getRequiredPersistentEntity(EntityWithSequenceValueAlias.class);
assertThat(persistentEntity.getIdSequence()).contains(SqlIdentifier.quoted("my_seq"));
}
@Test
@Test // GH-1923
void determineSequenceNameWithSchemaSpecified() {
RelationalPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(EntityWithSequenceAndSchema.class);
assertThat(persistentEntity.getIdTargetSequence()).isPresent().hasValue("public.my_seq");
RelationalPersistentEntity<?> persistentEntity = mappingContext
.getRequiredPersistentEntity(EntityWithSequenceAndSchema.class);
assertThat(persistentEntity.getIdSequence())
.contains(SqlIdentifier.from(SqlIdentifier.quoted("public"), SqlIdentifier.quoted("my_seq")));
}
@Test // DATAJDBC-294
@@ -203,8 +211,9 @@ class BasicRelationalPersistentEntityUnitTests {
@Id private Long id;
}
@Table(schema = "#{T(org.springframework.data.relational.core.mapping."
+ "BasicRelationalPersistentEntityUnitTests$EntityWithSchemaAndTableSpelExpression).desiredSchemaName}",
@Table(
schema = "#{T(org.springframework.data.relational.core.mapping."
+ "BasicRelationalPersistentEntityUnitTests$EntityWithSchemaAndTableSpelExpression).desiredSchemaName}",
name = "#{T(org.springframework.data.relational.core.mapping."
+ "BasicRelationalPersistentEntityUnitTests$EntityWithSchemaAndTableSpelExpression).desiredTableName}")
private static class EntityWithSchemaAndTableSpelExpression {
@@ -213,10 +222,11 @@ class BasicRelationalPersistentEntityUnitTests {
public static String desiredSchemaName = "HELP_ME_OBI_WON";
}
@Table(schema = "#{T(org.springframework.data.relational.core.mapping."
+ "BasicRelationalPersistentEntityUnitTests$LittleBobbyTables).desiredSchemaName}",
@Table(
schema = "#{T(org.springframework.data.relational.core.mapping."
+ "BasicRelationalPersistentEntityUnitTests$LittleBobbyTables).desiredSchemaName}",
name = "#{T(org.springframework.data.relational.core.mapping."
+ "BasicRelationalPersistentEntityUnitTests$LittleBobbyTables).desiredTableName}")
+ "BasicRelationalPersistentEntityUnitTests$LittleBobbyTables).desiredTableName}")
private static class LittleBobbyTables {
@Id private Long id;
public static String desiredTableName = "Robert'); DROP TABLE students;--";
@@ -232,19 +242,21 @@ class BasicRelationalPersistentEntityUnitTests {
@Table("entity_with_sequence")
static class EntityWithSequence {
@Id
@TargetSequence(sequence = "my_seq") Long id;
@Sequence(sequence = "my_seq") Long id;
}
@Table("entity_with_sequence_value_alias")
static class EntityWithSequenceValueAlias {
@Id
@Column("myId") @TargetSequence(value = "my_seq") Long id;
@Column("myId")
@Sequence(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;
@Column("myId")
@Sequence(sequence = "my_seq", schema = "public") Long id;
}
@Table()

View File

@@ -6,7 +6,9 @@ The ID of an entity must be annotated with Spring Data's https://docs.spring.io/
When your database has an auto-increment column for the ID column, the generated value gets set in the entity after inserting it into the database.
Spring Data does not attempt to insert values of identifier columns when the entity is new and the identifier value defaults to its initial value.
If you annotate the id additionally with `@Sequence` a database sequence will be used to obtain values for the id.
Otherwise Spring Data does not attempt to insert values of identifier columns when the entity is new and the identifier value defaults to its initial value.
That is `0` for primitive types and `null` if the identifier property uses a numeric wrapper type such as `Long`.
xref:repositories/core-concepts.adoc#is-new-state-detection[Entity State Detection] explains in detail the strategies to detect whether an entity is new or whether it is expected to exist in your database.