Browse Source
Signed-off-by: mipo256 <mikhailpolivakha@gmail.com> See #1955 Original pull request: #2028pull/2049/head
10 changed files with 382 additions and 28 deletions
@ -0,0 +1,104 @@
@@ -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())); |
||||
} |
||||
} |
||||
@ -0,0 +1,124 @@
@@ -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; |
||||
} |
||||
} |
||||
@ -0,0 +1,46 @@
@@ -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. |
||||
|
||||
Loading…
Reference in new issue