Browse Source
Sequence details are now maintained on the property level instead of using the entity level. This is a more accurate representation of the underlying model and that properties are annotated and not entities. It also allows future extension of expanding sequence support to general properties. Extract delegate for sequence generation. Move types to org.springframework.data.jdbc.core.convert to resolve package cycles. See #2003 Original pull request: #2005pull/2034/head
16 changed files with 395 additions and 302 deletions
@ -0,0 +1,71 @@ |
|||||||
|
/* |
||||||
|
* 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.jdbc.core.convert; |
||||||
|
|
||||||
|
import org.springframework.data.mapping.PersistentPropertyAccessor; |
||||||
|
import org.springframework.data.mapping.context.MappingContext; |
||||||
|
import org.springframework.data.relational.core.conversion.MutableAggregateChange; |
||||||
|
import org.springframework.data.relational.core.dialect.Dialect; |
||||||
|
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; |
||||||
|
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; |
||||||
|
import org.springframework.data.relational.core.mapping.event.BeforeSaveCallback; |
||||||
|
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; |
||||||
|
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, Dialect dialect, |
||||||
|
NamedParameterJdbcOperations operations) { |
||||||
|
|
||||||
|
this.context = context; |
||||||
|
this.delegate = new SequenceEntityCallbackDelegate(dialect, operations); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Object onBeforeSave(Object aggregate, MutableAggregateChange<Object> aggregateChange) { |
||||||
|
|
||||||
|
Assert.notNull(aggregate, "aggregate must not be null"); |
||||||
|
|
||||||
|
RelationalPersistentEntity<?> entity = context.getRequiredPersistentEntity(aggregate.getClass()); |
||||||
|
|
||||||
|
if (!entity.hasIdProperty()) { |
||||||
|
return aggregate; |
||||||
|
} |
||||||
|
|
||||||
|
RelationalPersistentProperty property = entity.getRequiredIdProperty(); |
||||||
|
PersistentPropertyAccessor<Object> accessor = entity.getPropertyAccessor(aggregate); |
||||||
|
|
||||||
|
if (!entity.isNew(aggregate) || delegate.hasValue(property, accessor) || !property.hasSequence()) { |
||||||
|
return aggregate; |
||||||
|
} |
||||||
|
|
||||||
|
delegate.generateSequenceValue(property, accessor); |
||||||
|
|
||||||
|
return accessor.getBean(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,102 @@ |
|||||||
|
/* |
||||||
|
* 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.jdbc.core.convert; |
||||||
|
|
||||||
|
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.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.jdbc.core.namedparam.MapSqlParameterSource; |
||||||
|
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
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 static MapSqlParameterSource EMPTY_PARAMETERS = new MapSqlParameterSource(); |
||||||
|
|
||||||
|
private final Dialect dialect; |
||||||
|
private final NamedParameterJdbcOperations operations; |
||||||
|
|
||||||
|
public SequenceEntityCallbackDelegate(Dialect dialect, NamedParameterJdbcOperations operations) { |
||||||
|
this.dialect = dialect; |
||||||
|
this.operations = operations; |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
protected void generateSequenceValue(RelationalPersistentProperty property, |
||||||
|
PersistentPropertyAccessor<Object> accessor) { |
||||||
|
|
||||||
|
Object sequenceValue = getSequenceValue(property); |
||||||
|
|
||||||
|
if (sequenceValue == null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
Class<?> targetType = ClassUtils.resolvePrimitiveIfNecessary(property.getType()); |
||||||
|
if (sequenceValue instanceof Number && Number.class.isAssignableFrom(targetType)) { |
||||||
|
sequenceValue = NumberUtils.convertNumberToTargetClass((Number) sequenceValue, |
||||||
|
(Class<? extends Number>) targetType); |
||||||
|
} |
||||||
|
|
||||||
|
accessor.setProperty(property, sequenceValue); |
||||||
|
} |
||||||
|
|
||||||
|
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 @Nullable Object getSequenceValue(RelationalPersistentProperty property) { |
||||||
|
|
||||||
|
SqlIdentifier sequence = property.getSequence(); |
||||||
|
|
||||||
|
if (sequence != null && !dialect.getIdGeneration().sequencesSupported()) { |
||||||
|
LOG.warn(""" |
||||||
|
Aggregate 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 null; |
||||||
|
} |
||||||
|
|
||||||
|
String sql = dialect.getIdGeneration().createSequenceQuery(sequence); |
||||||
|
return operations.queryForObject(sql, EMPTY_PARAMETERS, (rs, rowNum) -> rs.getObject(1)); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -1,112 +0,0 @@ |
|||||||
package org.springframework.data.jdbc.core.mapping; |
|
||||||
|
|
||||||
import java.util.Optional; |
|
||||||
|
|
||||||
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.mapping.context.MappingContext; |
|
||||||
import org.springframework.data.relational.core.conversion.MutableAggregateChange; |
|
||||||
import org.springframework.data.relational.core.dialect.Dialect; |
|
||||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; |
|
||||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; |
|
||||||
import org.springframework.data.relational.core.mapping.event.BeforeSaveCallback; |
|
||||||
import org.springframework.data.relational.core.sql.SqlIdentifier; |
|
||||||
import org.springframework.data.util.ReflectionUtils; |
|
||||||
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; |
|
||||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; |
|
||||||
import org.springframework.util.Assert; |
|
||||||
import org.springframework.util.ClassUtils; |
|
||||||
import org.springframework.util.NumberUtils; |
|
||||||
|
|
||||||
/** |
|
||||||
* Callback 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 |
|
||||||
*/ |
|
||||||
public class IdGeneratingBeforeSaveCallback implements BeforeSaveCallback<Object> { |
|
||||||
|
|
||||||
private static final Log LOG = LogFactory.getLog(IdGeneratingBeforeSaveCallback.class); |
|
||||||
private final static MapSqlParameterSource EMPTY_PARAMETERS = new MapSqlParameterSource(); |
|
||||||
|
|
||||||
private final MappingContext<RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext; |
|
||||||
private final Dialect dialect; |
|
||||||
private final NamedParameterJdbcOperations operations; |
|
||||||
|
|
||||||
public IdGeneratingBeforeSaveCallback( |
|
||||||
MappingContext<RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext, |
|
||||||
Dialect dialect, NamedParameterJdbcOperations operations) { |
|
||||||
this.mappingContext = mappingContext; |
|
||||||
this.dialect = dialect; |
|
||||||
this.operations = operations; |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public Object onBeforeSave(Object aggregate, MutableAggregateChange<Object> aggregateChange) { |
|
||||||
|
|
||||||
Assert.notNull(aggregate, "aggregate must not be null"); |
|
||||||
|
|
||||||
RelationalPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(aggregate.getClass()); |
|
||||||
|
|
||||||
if (!entity.hasIdProperty()) { |
|
||||||
return aggregate; |
|
||||||
} |
|
||||||
|
|
||||||
RelationalPersistentProperty idProperty = entity.getRequiredIdProperty(); |
|
||||||
PersistentPropertyAccessor<Object> accessor = entity.getPropertyAccessor(aggregate); |
|
||||||
|
|
||||||
if (!entity.isNew(aggregate) || hasIdentifierValue(idProperty, accessor)) { |
|
||||||
return aggregate; |
|
||||||
} |
|
||||||
|
|
||||||
potentiallyFetchIdFromSequence(idProperty, entity, accessor); |
|
||||||
return accessor.getBean(); |
|
||||||
} |
|
||||||
|
|
||||||
private boolean hasIdentifierValue(PersistentProperty<?> idProperty, |
|
||||||
PersistentPropertyAccessor<Object> propertyAccessor) { |
|
||||||
|
|
||||||
Object identifier = propertyAccessor.getProperty(idProperty); |
|
||||||
|
|
||||||
if (idProperty.getType().isPrimitive()) { |
|
||||||
|
|
||||||
Object primitiveDefault = ReflectionUtils.getPrimitiveDefault(idProperty.getType()); |
|
||||||
return !primitiveDefault.equals(identifier); |
|
||||||
} |
|
||||||
|
|
||||||
return identifier != null; |
|
||||||
} |
|
||||||
|
|
||||||
@SuppressWarnings("unchecked") |
|
||||||
private void potentiallyFetchIdFromSequence(PersistentProperty<?> idProperty, |
|
||||||
RelationalPersistentEntity<?> persistentEntity, PersistentPropertyAccessor<Object> accessor) { |
|
||||||
|
|
||||||
Optional<SqlIdentifier> idSequence = persistentEntity.getIdSequence(); |
|
||||||
|
|
||||||
if (idSequence.isPresent() && !dialect.getIdGeneration().sequencesSupported()) { |
|
||||||
LOG.warn(""" |
|
||||||
Aggregate type '%s' is marked for sequence usage but configured dialect '%s' |
|
||||||
does not support sequences. Falling back to identity columns. |
|
||||||
""".formatted(persistentEntity.getType(), ClassUtils.getQualifiedName(dialect.getClass()))); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
idSequence.map(s -> dialect.getIdGeneration().createSequenceQuery(s)).ifPresent(sql -> { |
|
||||||
|
|
||||||
Object idValue = operations.queryForObject(sql, EMPTY_PARAMETERS, (rs, rowNum) -> rs.getObject(1)); |
|
||||||
|
|
||||||
Class<?> targetType = ClassUtils.resolvePrimitiveIfNecessary(idProperty.getType()); |
|
||||||
if (idValue instanceof Number && Number.class.isAssignableFrom(targetType)) { |
|
||||||
accessor.setProperty(idProperty, |
|
||||||
NumberUtils.convertNumberToTargetClass((Number) idValue, (Class<? extends Number>) targetType)); |
|
||||||
} else { |
|
||||||
accessor.setProperty(idProperty, idValue); |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
} |
|
||||||
Loading…
Reference in new issue