R2DBC @Sequence annotation support.
Signed-off-by: mipo256 <mikhailpolivakha@gmail.com> See #1955 Original pull request: #2028
This commit is contained in:
@@ -56,14 +56,14 @@ public class IdGeneratingEntityCallback implements BeforeSaveCallback<Object> {
|
||||
return aggregate;
|
||||
}
|
||||
|
||||
RelationalPersistentProperty property = entity.getRequiredIdProperty();
|
||||
RelationalPersistentProperty idProperty = entity.getRequiredIdProperty();
|
||||
PersistentPropertyAccessor<Object> accessor = entity.getPropertyAccessor(aggregate);
|
||||
|
||||
if (!entity.isNew(aggregate) || delegate.hasValue(property, accessor) || !property.hasSequence()) {
|
||||
if (!entity.isNew(aggregate) || delegate.hasValue(idProperty, accessor) || !idProperty.hasSequence()) {
|
||||
return aggregate;
|
||||
}
|
||||
|
||||
delegate.generateSequenceValue(property, accessor);
|
||||
delegate.generateSequenceValue(idProperty, accessor);
|
||||
|
||||
return accessor.getBean();
|
||||
}
|
||||
|
||||
@@ -39,12 +39,14 @@ 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;
|
||||
import org.springframework.data.relational.RelationalManagedTypes;
|
||||
import org.springframework.data.relational.core.mapping.DefaultNamingStrategy;
|
||||
import org.springframework.data.relational.core.mapping.NamingStrategy;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
import org.springframework.data.util.TypeScanner;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -182,6 +184,18 @@ public abstract class AbstractR2dbcConfiguration implements ApplicationContextAw
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a {@link IdGeneratingBeforeSaveCallback} using
|
||||
* {@link #r2dbcMappingContext(Optional, R2dbcCustomConversions, RelationalManagedTypes)} and
|
||||
* {@link #databaseClient()}
|
||||
*/
|
||||
@Bean
|
||||
public IdGeneratingBeforeSaveCallback idGeneratingBeforeSaveCallback(
|
||||
RelationalMappingContext relationalMappingContext, DatabaseClient databaseClient) {
|
||||
return new IdGeneratingBeforeSaveCallback(relationalMappingContext, getDialect(lookupConnectionFactory()),
|
||||
databaseClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link ReactiveDataAccessStrategy} using the configured
|
||||
* {@link #r2dbcConverter(R2dbcMappingContext, R2dbcCustomConversions) R2dbcConverter}.
|
||||
|
||||
@@ -186,11 +186,11 @@ public class MappingR2dbcConverter extends MappingRelationalConverter implements
|
||||
RelationalPersistentEntity<?> entity = getRequiredPersistentEntity(userClass);
|
||||
PersistentPropertyAccessor<?> propertyAccessor = entity.getPropertyAccessor(source);
|
||||
|
||||
writeProperties(sink, entity, propertyAccessor, entity.isNew(source));
|
||||
writeProperties(sink, entity, propertyAccessor);
|
||||
}
|
||||
|
||||
private void writeProperties(OutboundRow sink, RelationalPersistentEntity<?> entity,
|
||||
PersistentPropertyAccessor<?> accessor, boolean isNew) {
|
||||
PersistentPropertyAccessor<?> accessor) {
|
||||
|
||||
for (RelationalPersistentProperty property : entity) {
|
||||
|
||||
@@ -213,15 +213,14 @@ public class MappingR2dbcConverter extends MappingRelationalConverter implements
|
||||
}
|
||||
|
||||
if (getConversions().isSimpleType(value.getClass())) {
|
||||
writeSimpleInternal(sink, value, isNew, property);
|
||||
writeSimpleInternal(sink, value, property);
|
||||
} else {
|
||||
writePropertyInternal(sink, value, isNew, property);
|
||||
writePropertyInternal(sink, value, property);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeSimpleInternal(OutboundRow sink, Object value, boolean isNew,
|
||||
RelationalPersistentProperty property) {
|
||||
private void writeSimpleInternal(OutboundRow sink, Object value, RelationalPersistentProperty property) {
|
||||
|
||||
Object result = getPotentiallyConvertedSimpleWrite(value);
|
||||
|
||||
@@ -229,8 +228,7 @@ public class MappingR2dbcConverter extends MappingRelationalConverter implements
|
||||
Parameter.fromOrEmpty(result, getPotentiallyConvertedSimpleNullType(property.getType())));
|
||||
}
|
||||
|
||||
private void writePropertyInternal(OutboundRow sink, Object value, boolean isNew,
|
||||
RelationalPersistentProperty property) {
|
||||
private void writePropertyInternal(OutboundRow sink, Object value, RelationalPersistentProperty property) {
|
||||
|
||||
TypeInformation<?> valueType = TypeInformation.of(value.getClass());
|
||||
|
||||
@@ -239,7 +237,7 @@ public class MappingR2dbcConverter extends MappingRelationalConverter implements
|
||||
if (valueType.getActualType() != null && valueType.getRequiredActualType().isCollectionLike()) {
|
||||
|
||||
// pass-thru nested collections
|
||||
writeSimpleInternal(sink, value, isNew, property);
|
||||
writeSimpleInternal(sink, value, property);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -310,7 +308,6 @@ public class MappingR2dbcConverter extends MappingRelationalConverter implements
|
||||
|
||||
if (customTarget.isPresent()) {
|
||||
return customTarget.get();
|
||||
|
||||
}
|
||||
|
||||
if (type.isEnum()) {
|
||||
|
||||
@@ -521,15 +521,15 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw
|
||||
return;
|
||||
}
|
||||
|
||||
SqlIdentifier columnName = idProperty.getColumnName();
|
||||
Parameter parameter = outboundRow.get(columnName);
|
||||
SqlIdentifier idColumnName = idProperty.getColumnName();
|
||||
Parameter parameter = outboundRow.get(idColumnName);
|
||||
|
||||
if (shouldSkipIdValue(parameter, idProperty)) {
|
||||
outboundRow.remove(columnName);
|
||||
if (shouldSkipIdValue(parameter)) {
|
||||
outboundRow.remove(idColumnName);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldSkipIdValue(@Nullable Parameter value, RelationalPersistentProperty property) {
|
||||
private boolean shouldSkipIdValue(@Nullable Parameter value) {
|
||||
|
||||
if (value == null || value.getValue() == null) {
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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()));
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ public class UpdateMapper extends QueryMapper {
|
||||
* @param entity related {@link RelationalPersistentEntity}, can be {@literal null}.
|
||||
* @return the mapped {@link BoundAssignments}.
|
||||
*/
|
||||
public BoundAssignments getMappedObject(BindMarkers markers, Map<SqlIdentifier, ? extends Object> assignments,
|
||||
public BoundAssignments getMappedObject(BindMarkers markers, Map<SqlIdentifier, ?> assignments,
|
||||
Table table, @Nullable RelationalPersistentEntity<?> entity) {
|
||||
|
||||
Assert.notNull(markers, "BindMarkers must not be null");
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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 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 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;
|
||||
import org.springframework.data.r2dbc.mapping.OutboundRow;
|
||||
import org.springframework.data.r2dbc.mapping.R2dbcMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.Sequence;
|
||||
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}.
|
||||
*
|
||||
* @author Mikhail Polivakha
|
||||
*/
|
||||
class IdGeneratingBeforeSaveCallbackTest {
|
||||
|
||||
@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,
|
||||
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();
|
||||
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,
|
||||
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();
|
||||
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,
|
||||
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();
|
||||
assertThat(row).hasSize(2)
|
||||
.containsEntry(SqlIdentifier.unquoted("id"), Parameter.from(generatedId));
|
||||
}
|
||||
|
||||
static class SimpleEntity {
|
||||
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
}
|
||||
|
||||
static class WithSequence {
|
||||
|
||||
@Id
|
||||
@Sequence(sequence = "seq_name")
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,13 @@
|
||||
*/
|
||||
package org.springframework.data.r2dbc.repository;
|
||||
|
||||
import io.r2dbc.postgresql.codec.Json;
|
||||
import io.r2dbc.spi.ConnectionFactory;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
@@ -32,22 +37,20 @@ import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
|
||||
import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory;
|
||||
import org.springframework.data.r2dbc.testing.ExternalDatabase;
|
||||
import org.springframework.data.r2dbc.testing.PostgresTestSupport;
|
||||
import org.springframework.data.relational.core.mapping.Sequence;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
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;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link LegoSetRepository} using {@link R2dbcRepositoryFactory} against Postgres.
|
||||
*
|
||||
@@ -62,12 +65,14 @@ public class PostgresR2dbcRepositoryIntegrationTests extends AbstractR2dbcReposi
|
||||
|
||||
@Autowired WithJsonRepository withJsonRepository;
|
||||
|
||||
@Autowired WithIdFromSequenceRepository withIdFromSequenceRepository;
|
||||
|
||||
@Autowired WithHStoreRepository hstoreRepositoryWith;
|
||||
|
||||
@Configuration
|
||||
@EnableR2dbcRepositories(considerNestedRepositories = true,
|
||||
includeFilters = @Filter(
|
||||
classes = { PostgresLegoSetRepository.class, WithJsonRepository.class, WithHStoreRepository.class },
|
||||
classes = { PostgresLegoSetRepository.class, WithJsonRepository.class, WithHStoreRepository.class, WithIdFromSequenceRepository.class },
|
||||
type = FilterType.ASSIGNABLE_TYPE))
|
||||
static class IntegrationTestConfiguration extends AbstractR2dbcConfiguration {
|
||||
|
||||
@@ -151,6 +156,51 @@ public class PostgresR2dbcRepositoryIntegrationTests extends AbstractR2dbcReposi
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
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" //
|
||||
+ ");");
|
||||
|
||||
WithIdFromSequence entity = new WithIdFromSequence(null, "Jordane");
|
||||
withIdFromSequenceRepository.save(entity).as(StepVerifier::create).expectNextCount(1).verifyComplete();
|
||||
|
||||
withIdFromSequenceRepository.findAll().as(StepVerifier::create).consumeNextWith(actual -> {
|
||||
|
||||
assertThat(actual.id).isNotNull().isEqualTo(15);
|
||||
assertThat(actual.name).isEqualTo("Jordane");
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
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("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 -> {
|
||||
|
||||
assertThat(actual.jsonValue).isNotNull().isEqualTo(4);
|
||||
assertThat(actual.jsonValue.asString()).isEqualTo("NewName");
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
@Test // gh-492
|
||||
void shouldSaveAndLoadHStore() {
|
||||
|
||||
@@ -188,6 +238,24 @@ public class PostgresR2dbcRepositoryIntegrationTests extends AbstractR2dbcReposi
|
||||
|
||||
}
|
||||
|
||||
static class WithIdFromSequence {
|
||||
|
||||
@Id
|
||||
@Sequence(sequence = "target_sequence")
|
||||
Long id;
|
||||
|
||||
String name;
|
||||
|
||||
public WithIdFromSequence(Long id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
interface WithIdFromSequenceRepository extends ReactiveCrudRepository<WithIdFromSequence, Long> {
|
||||
|
||||
}
|
||||
|
||||
@Table("with_hstore")
|
||||
static class WithHStore {
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ public class PostgresDialect extends AbstractDialect {
|
||||
|
||||
private IdentifierProcessing identifierProcessing = IdentifierProcessing.create(Quoting.ANSI,
|
||||
LetterCasing.LOWER_CASE);
|
||||
|
||||
private IdGeneration idGeneration = new IdGeneration() {
|
||||
|
||||
@Override
|
||||
|
||||
46
src/main/antora/modules/ROOT/pages/r2dbc/sequences.adoc
Normal file
46
src/main/antora/modules/ROOT/pages/r2dbc/sequences.adoc
Normal file
@@ -0,0 +1,46 @@
|
||||
[[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.
|
||||
|
||||
Reference in New Issue
Block a user