Browse Source
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: #2028pull/2049/head
11 changed files with 316 additions and 210 deletions
@ -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); |
||||
} |
||||
|
||||
} |
||||
@ -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(); |
||||
} |
||||
|
||||
} |
||||
@ -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())); |
||||
} |
||||
} |
||||
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
[[jdbc.sequences]] |
||||
= Sequence Support |
||||
|
||||
include::partial$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[] |
||||
|
||||
@ -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…
Reference in new issue