Browse Source

Polishing.

Extract SequenceEntityCallbackDelegate from IdGeneratingBeforeSaveCallback. Renameto IdGeneratingEntityCallback and move callback to convert package.

Align return values and associate generated sequence value with the entity. Fix test. Add ticket references to tests.

Extract documentation partials.

See #1955
Original pull request: #2028
pull/2049/head
Mark Paluch 8 months ago
parent
commit
0aecfcee73
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 8
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/IdGeneratingEntityCallback.java
  2. 12
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java
  3. 75
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/IdGeneratingEntityCallback.java
  4. 108
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/SequenceEntityCallbackDelegate.java
  5. 104
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/mapping/IdGeneratingBeforeSaveCallback.java
  6. 60
      spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/convert/IdGeneratingEntityCallbackTest.java
  7. 50
      spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java
  8. 2
      src/main/antora/modules/ROOT/nav.adoc
  9. 4
      src/main/antora/modules/ROOT/pages/jdbc/sequences.adoc
  10. 46
      src/main/antora/modules/ROOT/pages/r2dbc/sequences.adoc
  11. 57
      src/main/antora/modules/ROOT/partials/sequences.adoc

8
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/IdGeneratingEntityCallback.java

@ -48,7 +48,7 @@ public class IdGeneratingEntityCallback implements BeforeSaveCallback<Object> { @@ -48,7 +48,7 @@ public class IdGeneratingEntityCallback implements BeforeSaveCallback<Object> {
@Override
public Object onBeforeSave(Object aggregate, MutableAggregateChange<Object> aggregateChange) {
Assert.notNull(aggregate, "aggregate must not be null");
Assert.notNull(aggregate, "Aggregate must not be null");
RelationalPersistentEntity<?> entity = context.getRequiredPersistentEntity(aggregate.getClass());
@ -56,14 +56,14 @@ public class IdGeneratingEntityCallback implements BeforeSaveCallback<Object> { @@ -56,14 +56,14 @@ public class IdGeneratingEntityCallback implements BeforeSaveCallback<Object> {
return aggregate;
}
RelationalPersistentProperty idProperty = entity.getRequiredIdProperty();
RelationalPersistentProperty property = entity.getRequiredIdProperty();
PersistentPropertyAccessor<Object> accessor = entity.getPropertyAccessor(aggregate);
if (!entity.isNew(aggregate) || delegate.hasValue(idProperty, accessor) || !idProperty.hasSequence()) {
if (!entity.isNew(aggregate) || delegate.hasValue(property, accessor) || !property.hasSequence()) {
return aggregate;
}
delegate.generateSequenceValue(idProperty, accessor);
delegate.generateSequenceValue(property, accessor);
return accessor.getBean();
}

12
spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java

@ -33,13 +33,13 @@ import org.springframework.context.annotation.Configuration; @@ -33,13 +33,13 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.CustomConversions.StoreConversions;
import org.springframework.data.r2dbc.convert.IdGeneratingEntityCallback;
import org.springframework.data.r2dbc.convert.MappingR2dbcConverter;
import org.springframework.data.r2dbc.convert.R2dbcConverter;
import org.springframework.data.r2dbc.convert.R2dbcCustomConversions;
import org.springframework.data.r2dbc.core.DefaultReactiveDataAccessStrategy;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
import org.springframework.data.r2dbc.core.mapping.IdGeneratingBeforeSaveCallback;
import org.springframework.data.r2dbc.dialect.DialectResolver;
import org.springframework.data.r2dbc.dialect.R2dbcDialect;
import org.springframework.data.r2dbc.mapping.R2dbcMappingContext;
@ -185,14 +185,16 @@ public abstract class AbstractR2dbcConfiguration implements ApplicationContextAw @@ -185,14 +185,16 @@ public abstract class AbstractR2dbcConfiguration implements ApplicationContextAw
}
/**
* Register a {@link IdGeneratingBeforeSaveCallback} using
* Register a {@link IdGeneratingEntityCallback} using
* {@link #r2dbcMappingContext(Optional, R2dbcCustomConversions, RelationalManagedTypes)} and
* {@link #databaseClient()}
* {@link #databaseClient()}.
*
* @since 3.5
*/
@Bean
public IdGeneratingBeforeSaveCallback idGeneratingBeforeSaveCallback(
public IdGeneratingEntityCallback idGeneratingBeforeSaveCallback(
RelationalMappingContext relationalMappingContext, DatabaseClient databaseClient) {
return new IdGeneratingBeforeSaveCallback(relationalMappingContext, getDialect(lookupConnectionFactory()),
return new IdGeneratingEntityCallback(relationalMappingContext, getDialect(lookupConnectionFactory()),
databaseClient);
}

75
spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/IdGeneratingEntityCallback.java

@ -0,0 +1,75 @@ @@ -0,0 +1,75 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.r2dbc.convert;
import reactor.core.publisher.Mono;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.r2dbc.dialect.R2dbcDialect;
import org.springframework.data.r2dbc.mapping.OutboundRow;
import org.springframework.data.r2dbc.mapping.event.BeforeSaveCallback;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.util.Assert;
/**
* Callback for generating identifier values through a database sequence.
*
* @author Mikhail Polivakha
* @author Mark Paluch
* @since 3.5
*/
public class IdGeneratingEntityCallback implements BeforeSaveCallback<Object> {
private final MappingContext<RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> context;
private final SequenceEntityCallbackDelegate delegate;
public IdGeneratingEntityCallback(
MappingContext<RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> context,
R2dbcDialect dialect,
DatabaseClient databaseClient) {
this.context = context;
this.delegate = new SequenceEntityCallbackDelegate(dialect, databaseClient);
}
@Override
public Mono<Object> onBeforeSave(Object entity, OutboundRow row, SqlIdentifier table) {
Assert.notNull(entity, "Entity must not be null");
RelationalPersistentEntity<?> persistentEntity = context.getRequiredPersistentEntity(entity.getClass());
if (!persistentEntity.hasIdProperty()) {
return Mono.just(entity);
}
RelationalPersistentProperty property = persistentEntity.getRequiredIdProperty();
PersistentPropertyAccessor<Object> accessor = persistentEntity.getPropertyAccessor(entity);
if (!persistentEntity.isNew(entity) || delegate.hasValue(property, accessor) || !property.hasSequence()) {
return Mono.just(entity);
}
Mono<Object> idGenerator = delegate.generateSequenceValue(property, row, accessor);
return idGenerator.defaultIfEmpty(entity);
}
}

108
spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/SequenceEntityCallbackDelegate.java

@ -0,0 +1,108 @@ @@ -0,0 +1,108 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.r2dbc.convert;
import reactor.core.publisher.Mono;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.r2dbc.mapping.OutboundRow;
import org.springframework.data.relational.core.dialect.Dialect;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.r2dbc.core.Parameter;
import org.springframework.util.ClassUtils;
import org.springframework.util.NumberUtils;
/**
* Support class for generating identifier values through a database sequence.
*
* @author Mikhail Polivakha
* @author Mark Paluch
* @since 3.5
* @see org.springframework.data.relational.core.mapping.Sequence
*/
class SequenceEntityCallbackDelegate {
private static final Log LOG = LogFactory.getLog(SequenceEntityCallbackDelegate.class);
private final Dialect dialect;
private final DatabaseClient databaseClient;
public SequenceEntityCallbackDelegate(Dialect dialect, DatabaseClient databaseClient) {
this.dialect = dialect;
this.databaseClient = databaseClient;
}
@SuppressWarnings("unchecked")
protected Mono<Object> generateSequenceValue(RelationalPersistentProperty property, OutboundRow row,
PersistentPropertyAccessor<Object> accessor) {
Class<?> targetType = ClassUtils.resolvePrimitiveIfNecessary(property.getType());
return getSequenceValue(property).map(it -> {
Object sequenceValue = it;
if (sequenceValue instanceof Number && Number.class.isAssignableFrom(targetType)) {
sequenceValue = NumberUtils.convertNumberToTargetClass((Number) sequenceValue,
(Class<? extends Number>) targetType);
}
row.append(property.getColumnName(), Parameter.from(sequenceValue));
accessor.setProperty(property, sequenceValue);
return accessor.getBean();
});
}
protected boolean hasValue(PersistentProperty<?> property, PersistentPropertyAccessor<Object> propertyAccessor) {
Object identifier = propertyAccessor.getProperty(property);
if (property.getType().isPrimitive()) {
Object primitiveDefault = ReflectionUtils.getPrimitiveDefault(property.getType());
return !primitiveDefault.equals(identifier);
}
return identifier != null;
}
private Mono<Object> getSequenceValue(RelationalPersistentProperty property) {
SqlIdentifier sequence = property.getSequence();
if (sequence != null && !dialect.getIdGeneration().sequencesSupported()) {
LOG.warn("""
Entity type '%s' is marked for sequence usage but configured dialect '%s'
does not support sequences. Falling back to identity columns.
""".formatted(property.getOwner().getType(), ClassUtils.getQualifiedName(dialect.getClass())));
return Mono.empty();
}
String sql = dialect.getIdGeneration().createSequenceQuery(sequence);
return databaseClient //
.sql(sql) //
.map((r, rowMetadata) -> r.get(0)) //
.one();
}
}

104
spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/mapping/IdGeneratingBeforeSaveCallback.java

@ -1,104 +0,0 @@ @@ -1,104 +0,0 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.r2dbc.core.mapping;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import org.springframework.data.r2dbc.dialect.R2dbcDialect;
import org.springframework.data.r2dbc.mapping.OutboundRow;
import org.springframework.data.r2dbc.mapping.event.BeforeSaveCallback;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.r2dbc.core.Parameter;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
/**
* R2DBC Callback for generating ID via the database sequence.
*
* @author Mikhail Polivakha
*/
public class IdGeneratingBeforeSaveCallback implements BeforeSaveCallback<Object> {
private static final Log LOG = LogFactory.getLog(IdGeneratingBeforeSaveCallback.class);
private final RelationalMappingContext relationalMappingContext;
private final R2dbcDialect dialect;
private final DatabaseClient databaseClient;
public IdGeneratingBeforeSaveCallback(RelationalMappingContext relationalMappingContext, R2dbcDialect dialect,
DatabaseClient databaseClient) {
this.relationalMappingContext = relationalMappingContext;
this.dialect = dialect;
this.databaseClient = databaseClient;
}
@Override
public Publisher<Object> onBeforeSave(Object entity, OutboundRow row, SqlIdentifier table) {
Assert.notNull(entity, "The aggregate cannot be null at this point");
RelationalPersistentEntity<?> persistentEntity = relationalMappingContext.getPersistentEntity(entity.getClass());
if (!persistentEntity.hasIdProperty() || //
!persistentEntity.getIdProperty().hasSequence() || //
!persistentEntity.isNew(entity) //
) {
return Mono.just(entity);
}
RelationalPersistentProperty property = persistentEntity.getIdProperty();
SqlIdentifier idSequence = property.getSequence();
if (dialect.getIdGeneration().sequencesSupported()) {
return fetchIdFromSeq(entity, row, persistentEntity, idSequence);
} else {
illegalSequenceUsageWarning(entity);
}
return Mono.just(entity);
}
private Mono<Object> fetchIdFromSeq(Object entity, OutboundRow row, RelationalPersistentEntity<?> persistentEntity,
SqlIdentifier idSequence) {
String sequenceQuery = dialect.getIdGeneration().createSequenceQuery(idSequence);
return databaseClient //
.sql(sequenceQuery) //
.map((r, rowMetadata) -> r.get(0)) //
.one() //
.map(fetchedId -> { //
row.put( //
persistentEntity.getIdColumn().toSql(dialect.getIdentifierProcessing()), //
Parameter.from(fetchedId) //
);
return entity;
});
}
private static void illegalSequenceUsageWarning(Object entity) {
LOG.warn("""
It seems you're trying to insert an aggregate of type '%s' annotated with @Sequence, but the problem is RDBMS you're
working with does not support sequences as such. Falling back to identity columns
""".stripIndent().formatted(entity.getClass().getName()));
}
}

60
spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/mapping/IdGeneratingBeforeSaveCallbackTest.java → spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/convert/IdGeneratingEntityCallbackTest.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2020-2025 the original author or authors.
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,18 +14,19 @@ @@ -14,18 +14,19 @@
* limitations under the License.
*/
package org.springframework.data.r2dbc.core.mapping;
package org.springframework.data.r2dbc.convert;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.function.BiFunction;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.reactivestreams.Publisher;
import org.springframework.data.annotation.Id;
import org.springframework.data.r2dbc.dialect.MySqlDialect;
import org.springframework.data.r2dbc.dialect.PostgresDialect;
@ -36,73 +37,70 @@ import org.springframework.data.relational.core.sql.SqlIdentifier; @@ -36,73 +37,70 @@ import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.r2dbc.core.Parameter;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
/**
* Unit tests for {@link IdGeneratingBeforeSaveCallback}.
* Unit tests for {@link IdGeneratingEntityCallback}.
*
* @author Mikhail Polivakha
* @author Mark Paluch
*/
class IdGeneratingBeforeSaveCallbackTest {
class IdGeneratingEntityCallbackTest {
R2dbcMappingContext r2dbcMappingContext = new R2dbcMappingContext();
DatabaseClient databaseClient = mock(DatabaseClient.class, RETURNS_DEEP_STUBS);
@Test
void testIdGenerationIsNotSupported() {
R2dbcMappingContext r2dbcMappingContext = new R2dbcMappingContext();
r2dbcMappingContext.getPersistentEntity(SimpleEntity.class);
MySqlDialect dialect = MySqlDialect.INSTANCE;
DatabaseClient databaseClient = mock(DatabaseClient.class);
IdGeneratingBeforeSaveCallback callback = new IdGeneratingBeforeSaveCallback(r2dbcMappingContext, dialect,
MySqlDialect dialect = MySqlDialect.INSTANCE;
IdGeneratingEntityCallback callback = new IdGeneratingEntityCallback(r2dbcMappingContext, dialect,
databaseClient);
OutboundRow row = new OutboundRow("name", Parameter.from("my_name"));
SimpleEntity entity = new SimpleEntity();
Publisher<Object> publisher = callback.onBeforeSave(entity, row, SqlIdentifier.unquoted("simple_entity"));
callback.onBeforeSave(entity, row, SqlIdentifier.unquoted("simple_entity")).as(StepVerifier::create)
.expectNext(entity).verifyComplete();
StepVerifier.create(publisher).expectNext(entity).expectComplete().verify();
assertThat(row).hasSize(1); // id is not added
}
@Test
void testEntityIsNotAnnotatedWithSequence() {
R2dbcMappingContext r2dbcMappingContext = new R2dbcMappingContext();
r2dbcMappingContext.getPersistentEntity(SimpleEntity.class);
PostgresDialect dialect = PostgresDialect.INSTANCE;
DatabaseClient databaseClient = mock(DatabaseClient.class);
IdGeneratingBeforeSaveCallback callback = new IdGeneratingBeforeSaveCallback(r2dbcMappingContext, dialect,
IdGeneratingEntityCallback callback = new IdGeneratingEntityCallback(r2dbcMappingContext, dialect,
databaseClient);
OutboundRow row = new OutboundRow("name", Parameter.from("my_name"));
SimpleEntity entity = new SimpleEntity();
Publisher<Object> publisher = callback.onBeforeSave(entity, row, SqlIdentifier.unquoted("simple_entity"));
StepVerifier.create(publisher).expectNext(entity).expectComplete().verify();
callback.onBeforeSave(entity, row, SqlIdentifier.unquoted("simple_entity")).as(StepVerifier::create)
.expectNext(entity).verifyComplete();
assertThat(row).hasSize(1); // id is not added
}
@Test
void testIdGeneratedFromSequenceHappyPath() {
R2dbcMappingContext r2dbcMappingContext = new R2dbcMappingContext();
r2dbcMappingContext.getPersistentEntity(WithSequence.class);
PostgresDialect dialect = PostgresDialect.INSTANCE;
DatabaseClient databaseClient = mock(DatabaseClient.class, RETURNS_DEEP_STUBS);
long generatedId = 1L;
when(databaseClient.sql(Mockito.anyString()).map(Mockito.any(BiFunction.class)).one()).thenReturn(
Mono.just(generatedId));
IdGeneratingBeforeSaveCallback callback = new IdGeneratingBeforeSaveCallback(r2dbcMappingContext, dialect,
IdGeneratingEntityCallback callback = new IdGeneratingEntityCallback(r2dbcMappingContext, dialect,
databaseClient);
OutboundRow row = new OutboundRow("name", Parameter.from("my_name"));
WithSequence entity = new WithSequence();
Publisher<Object> publisher = callback.onBeforeSave(entity, row, SqlIdentifier.unquoted("simple_entity"));
StepVerifier.create(publisher).expectNext(entity).expectComplete().verify();
callback.onBeforeSave(entity, row, SqlIdentifier.unquoted("simple_entity")).as(StepVerifier::create)
.expectNext(entity).verifyComplete();
assertThat(row).hasSize(2)
.containsEntry(SqlIdentifier.unquoted("id"), Parameter.from(generatedId));
assertThat(entity.id).isEqualTo(generatedId);
}
static class SimpleEntity {

50
spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java

@ -15,7 +15,13 @@ @@ -15,7 +15,13 @@
*/
package org.springframework.data.r2dbc.repository;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.*;
import io.r2dbc.postgresql.codec.Json;
import io.r2dbc.spi.ConnectionFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.Collections;
import java.util.Map;
@ -25,6 +31,7 @@ import javax.sql.DataSource; @@ -25,6 +31,7 @@ import javax.sql.DataSource;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan.Filter;
@ -45,12 +52,6 @@ import org.springframework.r2dbc.core.DatabaseClient; @@ -45,12 +52,6 @@ import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import io.r2dbc.postgresql.codec.Json;
import io.r2dbc.spi.ConnectionFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
/**
* Integration tests for {@link LegoSetRepository} using {@link R2dbcRepositoryFactory} against Postgres.
*
@ -156,17 +157,19 @@ public class PostgresR2dbcRepositoryIntegrationTests extends AbstractR2dbcReposi @@ -156,17 +157,19 @@ public class PostgresR2dbcRepositoryIntegrationTests extends AbstractR2dbcReposi
}).verifyComplete();
}
@Test
@Test // GH-1955
void shouldInsertWithAutoGeneratedId() {
JdbcTemplate template = new JdbcTemplate(createDataSource());
template.execute("DROP TABLE IF EXISTS with_id_from_sequence");
template.execute("CREATE SEQUENCE IF NOT EXISTS target_sequence START WITH 15");
template.execute("CREATE TABLE with_id_from_sequence(\n" //
+ " id BIGINT PRIMARY KEY,\n" //
+ " name TEXT NOT NULL" //
+ ");");
template.execute("DROP SEQUENCE IF EXISTS target_sequence");
template.execute("CREATE SEQUENCE target_sequence START WITH 15");
template.execute("""
CREATE TABLE with_id_from_sequence(
id BIGINT PRIMARY KEY,
name TEXT NOT NULL
);""");
WithIdFromSequence entity = new WithIdFromSequence(null, "Jordane");
withIdFromSequenceRepository.save(entity).as(StepVerifier::create).expectNextCount(1).verifyComplete();
@ -178,26 +181,29 @@ public class PostgresR2dbcRepositoryIntegrationTests extends AbstractR2dbcReposi @@ -178,26 +181,29 @@ public class PostgresR2dbcRepositoryIntegrationTests extends AbstractR2dbcReposi
}).verifyComplete();
}
@Test
@Test // GH-1955
void shouldUpdateNoIdGenerationHappens() {
JdbcTemplate template = new JdbcTemplate(createDataSource());
template.execute("DROP TABLE IF EXISTS with_id_from_sequence");
template.execute("CREATE SEQUENCE IF NOT EXISTS target_sequence");
template.execute("CREATE TABLE with_id_from_sequence(\n" //
+ " id BIGINT PRIMARY KEY,\n" //
+ " name TEXT NOT NULL" //
+ ");");
template.execute("DROP SEQUENCE IF EXISTS target_sequence");
template.execute("CREATE SEQUENCE target_sequence");
template.execute("""
CREATE TABLE with_id_from_sequence(
id BIGINT PRIMARY KEY,
name TEXT NOT NULL
);
""");
template.execute("INSERT INTO with_id_from_sequence VALUES(4, 'Alex');");
WithIdFromSequence entity = new WithIdFromSequence(4L, "NewName");
withIdFromSequenceRepository.save(entity).as(StepVerifier::create).expectNextCount(1).verifyComplete();
withJsonRepository.findAll().as(StepVerifier::create).consumeNextWith(actual -> {
withIdFromSequenceRepository.findAll().as(StepVerifier::create).consumeNextWith(actual -> {
assertThat(actual.jsonValue).isNotNull().isEqualTo(4);
assertThat(actual.jsonValue.asString()).isEqualTo("NewName");
assertThat(actual.id).isNotNull().isEqualTo(4);
assertThat(actual.name).isEqualTo("NewName");
}).verifyComplete();
}

2
src/main/antora/modules/ROOT/nav.adoc

@ -24,6 +24,7 @@ @@ -24,6 +24,7 @@
** xref:jdbc/domain-driven-design.adoc[]
** xref:jdbc/getting-started.adoc[]
** xref:jdbc/entity-persistence.adoc[]
** xref:jdbc/sequences.adoc[]
** xref:jdbc/mapping.adoc[]
** xref:jdbc/query-methods.adoc[]
** xref:jdbc/mybatis.adoc[]
@ -35,6 +36,7 @@ @@ -35,6 +36,7 @@
* xref:r2dbc.adoc[]
** xref:r2dbc/getting-started.adoc[]
** xref:r2dbc/entity-persistence.adoc[]
** xref:r2dbc/sequences.adoc[]
** xref:r2dbc/mapping.adoc[]
** xref:r2dbc/repositories.adoc[]
** xref:r2dbc/query-methods.adoc[]

4
src/main/antora/modules/ROOT/pages/jdbc/sequences.adoc

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
[[jdbc.sequences]]
= Sequence Support
include::partial$sequences.adoc[]

46
src/main/antora/modules/ROOT/pages/r2dbc/sequences.adoc

@ -1,46 +1,4 @@ @@ -1,46 +1,4 @@
[[r2dbc.sequences]]
= Sequences Support
Since Spring Data R2DBC 3.5, properties that are annotated with `@Id` and thus represent
an Id property can additionally be annotated with `@Sequence`. This signals, that the Id property
value would be fetched from the configured sequence during an `INSERT` statement. By default,
without `@Sequence`, the identity column is assumed. Consider the following entity.
.Entity with Id generation from sequence
[source,java]
----
@Table
class MyEntity {
@Id
@Sequence(
sequence = "my_seq",
schema = "public"
)
private Long id;
private String name;
}
----
When persisting this entity, before the SQL `INSERT` Spring Data will issue an additional `SELECT`
statement to fetch the next value from the sequence. For instance, for PostgreSQL the query, issued by
Spring Data, would look like this:
.Select for next sequence value in PostgreSQL
[source,sql]
----
SELECT nextval('public.my_seq');
----
The fetched Id would later be included in the `VALUES` list during an insert:
.Insert statement enriched with Id value
[source,sql]
----
INSERT INTO "my_entity"("id", "name") VALUES(?, ?);
----
For now, the sequence support is provided for almost every dialect supported by Spring Data R2DBC.
The only exception is MySQL, since MySQL does not have sequences as such.
= Sequence Support
include::partial$sequences.adoc[]

57
src/main/antora/modules/ROOT/partials/sequences.adoc

@ -0,0 +1,57 @@ @@ -0,0 +1,57 @@
Primary key properties (annotated with `@Id`) may also be annotated with `@Sequence`.
The presence of the `@Sequence` annotation indicates that the property's initial value should be obtained from a database sequence at the time of object insertion.
The ability of the database to generate a sequence is <<sequences.dialects,determined by the used database dialect>>.
In the absence of the `@Sequence` annotation, it is assumed that the value for the corresponding column is automatically generated by the database upon row insertion.
Consider the following entity:
.Entity with Id generation from a Sequence
[source,java]
----
@Table
class MyEntity {
@Id
@Sequence(
sequence = "my_seq",
schema = "public"
)
private Long id;
// …
}
----
When persisting this entity, before the SQL `INSERT`, Spring Data will issue an additional `SELECT` statement to fetch the next value from the sequence.
For instance, for PostgreSQL the query, issued by Spring Data, would look like this:
.Select for next sequence value in PostgreSQL
[source,sql]
----
SELECT nextval('public.my_seq');
----
The fetched identifier value is included in `VALUES` during the insert:
.Insert statement enriched with Id value
[source,sql]
----
INSERT INTO "my_entity"("id", "name") VALUES(?, ?);
----
NOTE: Obtaining a value from a sequence and inserting the object are two separate operations.
We highly recommend running these operations within a surrounding transaction to ensure atomicity.
[[sequences.dialects]]
== Supported Dialects
The following dialects support Sequences:
* H2
* HSQL
* PostgreSQL
* DB2
* Oracle
* Microsoft SQL Server
Note that MySQL does not support sequences.
Loading…
Cancel
Save