From 0aecfcee731948e4e3ded12966f1ed2220643c96 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 24 Apr 2025 09:42:10 +0200 Subject: [PATCH] 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 --- .../convert/IdGeneratingEntityCallback.java | 8 +- .../config/AbstractR2dbcConfiguration.java | 12 +- .../convert/IdGeneratingEntityCallback.java | 75 ++++++++++++ .../SequenceEntityCallbackDelegate.java | 108 ++++++++++++++++++ .../IdGeneratingBeforeSaveCallback.java | 104 ----------------- .../IdGeneratingEntityCallbackTest.java} | 62 +++++----- ...stgresR2dbcRepositoryIntegrationTests.java | 50 ++++---- src/main/antora/modules/ROOT/nav.adoc | 2 + .../modules/ROOT/pages/jdbc/sequences.adoc | 4 + .../modules/ROOT/pages/r2dbc/sequences.adoc | 46 +------- .../modules/ROOT/partials/sequences.adoc | 57 +++++++++ 11 files changed, 317 insertions(+), 211 deletions(-) create mode 100644 spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/IdGeneratingEntityCallback.java create mode 100644 spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/SequenceEntityCallbackDelegate.java delete mode 100644 spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/mapping/IdGeneratingBeforeSaveCallback.java rename spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/{core/mapping/IdGeneratingBeforeSaveCallbackTest.java => convert/IdGeneratingEntityCallbackTest.java} (57%) create mode 100644 src/main/antora/modules/ROOT/pages/jdbc/sequences.adoc create mode 100644 src/main/antora/modules/ROOT/partials/sequences.adoc diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/IdGeneratingEntityCallback.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/IdGeneratingEntityCallback.java index 878fd1b9..38d0338b 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/IdGeneratingEntityCallback.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/IdGeneratingEntityCallback.java @@ -48,7 +48,7 @@ public class IdGeneratingEntityCallback implements BeforeSaveCallback { @Override public Object onBeforeSave(Object aggregate, MutableAggregateChange 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 { return aggregate; } - RelationalPersistentProperty idProperty = entity.getRequiredIdProperty(); + RelationalPersistentProperty property = entity.getRequiredIdProperty(); PersistentPropertyAccessor 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(); } diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java index 58f80741..2b86b282 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java @@ -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 } /** - * 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); } diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/IdGeneratingEntityCallback.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/IdGeneratingEntityCallback.java new file mode 100644 index 00000000..d4d75a04 --- /dev/null +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/IdGeneratingEntityCallback.java @@ -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 { + + private final MappingContext, ? extends RelationalPersistentProperty> context; + private final SequenceEntityCallbackDelegate delegate; + + public IdGeneratingEntityCallback( + MappingContext, ? extends RelationalPersistentProperty> context, + R2dbcDialect dialect, + DatabaseClient databaseClient) { + + this.context = context; + this.delegate = new SequenceEntityCallbackDelegate(dialect, databaseClient); + } + + @Override + public Mono 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 accessor = persistentEntity.getPropertyAccessor(entity); + + if (!persistentEntity.isNew(entity) || delegate.hasValue(property, accessor) || !property.hasSequence()) { + return Mono.just(entity); + } + + Mono idGenerator = delegate.generateSequenceValue(property, row, accessor); + + return idGenerator.defaultIfEmpty(entity); + } + +} diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/SequenceEntityCallbackDelegate.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/SequenceEntityCallbackDelegate.java new file mode 100644 index 00000000..5c3f452d --- /dev/null +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/convert/SequenceEntityCallbackDelegate.java @@ -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 generateSequenceValue(RelationalPersistentProperty property, OutboundRow row, + PersistentPropertyAccessor 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) targetType); + } + + row.append(property.getColumnName(), Parameter.from(sequenceValue)); + accessor.setProperty(property, sequenceValue); + + return accessor.getBean(); + }); + } + + protected boolean hasValue(PersistentProperty property, PersistentPropertyAccessor 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 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(); + } + +} diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/mapping/IdGeneratingBeforeSaveCallback.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/mapping/IdGeneratingBeforeSaveCallback.java deleted file mode 100644 index 5dea28e0..00000000 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/mapping/IdGeneratingBeforeSaveCallback.java +++ /dev/null @@ -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 { - - 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 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 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())); - } -} diff --git a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/mapping/IdGeneratingBeforeSaveCallbackTest.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/convert/IdGeneratingEntityCallbackTest.java similarity index 57% rename from spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/mapping/IdGeneratingBeforeSaveCallbackTest.java rename to spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/convert/IdGeneratingEntityCallbackTest.java index 87029fec..37fd4140 100644 --- a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/mapping/IdGeneratingBeforeSaveCallbackTest.java +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/convert/IdGeneratingEntityCallbackTest.java @@ -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 @@ * 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; 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 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, + PostgresDialect dialect = PostgresDialect.INSTANCE; + + IdGeneratingEntityCallback callback = new IdGeneratingEntityCallback(r2dbcMappingContext, dialect, databaseClient); OutboundRow row = new OutboundRow("name", Parameter.from("my_name")); SimpleEntity entity = new SimpleEntity(); - Publisher 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 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 { diff --git a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java index 5c04f12e..9fa9fe03 100644 --- a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java @@ -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; 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; 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 }).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 }).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(); } diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc index e02181cc..b139edc8 100644 --- a/src/main/antora/modules/ROOT/nav.adoc +++ b/src/main/antora/modules/ROOT/nav.adoc @@ -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 @@ * 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[] diff --git a/src/main/antora/modules/ROOT/pages/jdbc/sequences.adoc b/src/main/antora/modules/ROOT/pages/jdbc/sequences.adoc new file mode 100644 index 00000000..bd9c0033 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/jdbc/sequences.adoc @@ -0,0 +1,4 @@ +[[jdbc.sequences]] += Sequence Support + +include::partial$sequences.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/r2dbc/sequences.adoc b/src/main/antora/modules/ROOT/pages/r2dbc/sequences.adoc index ea5e0fd3..df762cb8 100644 --- a/src/main/antora/modules/ROOT/pages/r2dbc/sequences.adoc +++ b/src/main/antora/modules/ROOT/pages/r2dbc/sequences.adoc @@ -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[] diff --git a/src/main/antora/modules/ROOT/partials/sequences.adoc b/src/main/antora/modules/ROOT/partials/sequences.adoc new file mode 100644 index 00000000..4415aac8 --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/sequences.adoc @@ -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 <>. +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.