Browse Source

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
pull/2001/head
Jens Schauder 11 months ago
parent
commit
8c017fc56b
No known key found for this signature in database
GPG Key ID: 74F6C554AE971567
  1. 3
      pom.xml
  2. 11
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/IdGeneratingBeforeSaveCallback.java
  3. 32
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/mapping/IdGeneratingBeforeSaveCallbackTest.java
  4. 10
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java
  5. 27
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/DisabledOnDatabase.java
  6. 36
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/DisabledOnDatabaseExecutionCondition.java
  7. 2
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql
  8. 6
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql
  9. 2
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql
  10. 14
      spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/IdValueSource.java
  11. 21
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Db2Dialect.java
  12. 13
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/H2Dialect.java
  13. 11
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/HsqlDbDialect.java
  14. 46
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/IdGeneration.java
  15. 13
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MariaDbDialect.java
  16. 17
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java
  17. 16
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java
  18. 20
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java
  19. 11
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java
  20. 50
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntity.java
  21. 2
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentEntity.java
  22. 2
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java
  23. 46
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/Sequence.java
  24. 43
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/TargetSequence.java
  25. 46
      spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java
  26. 4
      src/main/antora/modules/ROOT/partials/id-generation.adoc

3
pom.xml

@ -1,5 +1,6 @@ @@ -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>

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

@ -11,6 +11,7 @@ import org.springframework.data.relational.core.dialect.Dialect; @@ -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 @@ -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 @@ -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

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

@ -14,7 +14,7 @@ import org.springframework.data.relational.core.dialect.MySqlDialect; @@ -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; @@ -26,68 +26,58 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
*/
class IdGeneratingBeforeSaveCallbackTest {
@Test
void test_mySqlDialect_sequenceGenerationIsNotSupported() {
// given
@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
@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 { @@ -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;

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

@ -65,7 +65,7 @@ import org.springframework.data.jdbc.testing.TestDatabaseFeatures; @@ -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 { @@ -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 { @@ -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 { @@ -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;

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

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

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

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

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

@ -47,4 +47,4 @@ CREATE TABLE ENTITY_WITH_SEQUENCE @@ -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;

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

@ -47,12 +47,12 @@ CREATE TABLE WITH_DELIMITED_COLUMN @@ -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;

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

@ -55,4 +55,4 @@ CREATE TABLE ENTITY_WITH_SEQUENCE @@ -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;

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

@ -15,8 +15,6 @@ @@ -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 { @@ -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 { @@ -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();

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

@ -18,8 +18,8 @@ package org.springframework.data.relational.core.dialect; @@ -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 { @@ -41,14 +41,20 @@ public class Db2Dialect extends AbstractDialect {
return false;
}
/**
@Override
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)
*/
@Override
public String nextValueFromSequenceSelect(String sequenceName) {
return "SELECT NEXT VALUE FOR %s FROM SYSCAT.SEQUENCES LIMIT 1".formatted(sequenceName);
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 { @@ -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);

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

@ -40,6 +40,9 @@ public class H2Dialect extends AbstractDialect { @@ -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 { @@ -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 { @@ -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;
}
}

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

@ -15,6 +15,8 @@ @@ -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 { @@ -70,16 +72,17 @@ public class HsqlDbDialect extends AbstractDialect {
public IdGeneration getIdGeneration() {
return new IdGeneration() {
/**
@Override
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>
*/
@Override
public String nextValueFromSequenceSelect(String sequenceName) {
return "SELECT NEXT VALUE FOR %s AS msq FROM INFORMATION_SCHEMA.SEQUENCES LIMIT 1".formatted(sequenceName);
return "SELECT NEXT VALUE FOR %s AS msq FROM INFORMATION_SCHEMA.SEQUENCES LIMIT 1"
.formatted(sequenceName.toSql(getIdentifierProcessing()));
}
};
}

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

@ -18,20 +18,30 @@ package org.springframework.data.relational.core.dialect; @@ -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 { @@ -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 { @@ -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;
}
}

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

@ -18,7 +18,6 @@ package org.springframework.data.relational.core.dialect; @@ -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 { @@ -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());
}
}

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

@ -18,10 +18,12 @@ package org.springframework.data.relational.core.dialect; @@ -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 { @@ -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 { @@ -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()));
}
};
}
}

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

@ -15,13 +15,14 @@ @@ -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 { @@ -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 { @@ -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

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

@ -55,6 +55,16 @@ public class PostgresDialect extends AbstractDialect { @@ -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() {
@ -145,7 +155,7 @@ public class PostgresDialect extends AbstractDialect { @@ -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 { @@ -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;
}
}

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

@ -17,6 +17,7 @@ package org.springframework.data.relational.core.dialect; @@ -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 { @@ -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 { @@ -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

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

@ -17,7 +17,8 @@ package org.springframework.data.relational.core.mapping; @@ -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 @@ -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 @@ -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 @@ -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 @@ -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());
}
}

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

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

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

@ -58,5 +58,5 @@ public interface RelationalPersistentEntity<T> extends MutablePersistentEntity<T @@ -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();
}

46
spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/Sequence.java

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

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

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

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

@ -58,32 +58,40 @@ class BasicRelationalPersistentEntityUnitTests { @@ -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,7 +211,8 @@ class BasicRelationalPersistentEntityUnitTests { @@ -203,7 +211,8 @@ class BasicRelationalPersistentEntityUnitTests {
@Id private Long id;
}
@Table(schema = "#{T(org.springframework.data.relational.core.mapping."
@Table(
schema = "#{T(org.springframework.data.relational.core.mapping."
+ "BasicRelationalPersistentEntityUnitTests$EntityWithSchemaAndTableSpelExpression).desiredSchemaName}",
name = "#{T(org.springframework.data.relational.core.mapping."
+ "BasicRelationalPersistentEntityUnitTests$EntityWithSchemaAndTableSpelExpression).desiredTableName}")
@ -213,7 +222,8 @@ class BasicRelationalPersistentEntityUnitTests { @@ -213,7 +222,8 @@ class BasicRelationalPersistentEntityUnitTests {
public static String desiredSchemaName = "HELP_ME_OBI_WON";
}
@Table(schema = "#{T(org.springframework.data.relational.core.mapping."
@Table(
schema = "#{T(org.springframework.data.relational.core.mapping."
+ "BasicRelationalPersistentEntityUnitTests$LittleBobbyTables).desiredSchemaName}",
name = "#{T(org.springframework.data.relational.core.mapping."
+ "BasicRelationalPersistentEntityUnitTests$LittleBobbyTables).desiredTableName}")
@ -232,19 +242,21 @@ class BasicRelationalPersistentEntityUnitTests { @@ -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()

4
src/main/antora/modules/ROOT/partials/id-generation.adoc

@ -6,7 +6,9 @@ The ID of an entity must be annotated with Spring Data's https://docs.spring.io/ @@ -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.

Loading…
Cancel
Save