Move Sequence to RelationalPersistentProperty.

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: #2005
This commit is contained in:
Mark Paluch
2025-04-09 10:30:32 +02:00
parent d0e43be314
commit eb25cc3ed4
16 changed files with 396 additions and 303 deletions

View File

@@ -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();
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
});
}
}

View File

@@ -25,6 +25,7 @@ import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
@@ -38,7 +39,6 @@ import org.springframework.data.jdbc.core.JdbcAggregateOperations;
import org.springframework.data.jdbc.core.JdbcAggregateTemplate;
import org.springframework.data.jdbc.core.convert.*;
import org.springframework.data.jdbc.core.dialect.JdbcDialect;
import org.springframework.data.jdbc.core.mapping.IdGeneratingBeforeSaveCallback;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes;
import org.springframework.data.mapping.model.SimpleTypeHolder;
@@ -121,7 +121,7 @@ public class AbstractJdbcConfiguration implements ApplicationContextAware {
}
/**
* Creates a {@link IdGeneratingBeforeSaveCallback} bean using the configured
* Creates a {@link IdGeneratingEntityCallback} bean using the configured
* {@link #jdbcMappingContext(Optional, JdbcCustomConversions, RelationalManagedTypes)} and
* {@link #jdbcDialect(NamedParameterJdbcOperations)}.
*
@@ -129,9 +129,9 @@ public class AbstractJdbcConfiguration implements ApplicationContextAware {
* @since 3.5
*/
@Bean
public IdGeneratingBeforeSaveCallback idGeneratingBeforeSaveCallback(JdbcMappingContext mappingContext,
public IdGeneratingEntityCallback idGeneratingBeforeSaveCallback(JdbcMappingContext mappingContext,
NamedParameterJdbcOperations operations, Dialect dialect) {
return new IdGeneratingBeforeSaveCallback(mappingContext, dialect, operations);
return new IdGeneratingEntityCallback(mappingContext, dialect, operations);
}
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024-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.
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.core.mapping;
package org.springframework.data.jdbc.core.convert;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
@@ -41,13 +41,13 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
/**
* Unit tests for {@link IdGeneratingBeforeSaveCallback}
* Unit tests for {@link IdGeneratingEntityCallback}
*
* @author Mikhail Polivakha
* @author Mark Paluch
*/
@MockitoSettings(strictness = Strictness.LENIENT)
class IdGeneratingBeforeSaveCallbackTest {
class IdGeneratingEntityCallbackTest {
@Mock NamedParameterJdbcOperations operations;
RelationalMappingContext relationalMappingContext;
@@ -64,7 +64,7 @@ class IdGeneratingBeforeSaveCallbackTest {
NamedParameterJdbcOperations operations = mock(NamedParameterJdbcOperations.class);
IdGeneratingBeforeSaveCallback subject = new IdGeneratingBeforeSaveCallback(relationalMappingContext,
IdGeneratingEntityCallback subject = new IdGeneratingEntityCallback(relationalMappingContext,
MySqlDialect.INSTANCE, operations);
EntityWithSequence processed = (EntityWithSequence) subject.onBeforeSave(new EntityWithSequence(),
@@ -76,7 +76,7 @@ class IdGeneratingBeforeSaveCallbackTest {
@Test // GH-1923
void entityIsNotMarkedWithTargetSequence() {
IdGeneratingBeforeSaveCallback subject = new IdGeneratingBeforeSaveCallback(relationalMappingContext,
IdGeneratingEntityCallback subject = new IdGeneratingEntityCallback(relationalMappingContext,
MySqlDialect.INSTANCE, operations);
NoSequenceEntity processed = (NoSequenceEntity) subject.onBeforeSave(new NoSequenceEntity(),
@@ -92,7 +92,7 @@ class IdGeneratingBeforeSaveCallbackTest {
when(operations.queryForObject(anyString(), any(SqlParameterSource.class), any(RowMapper.class)))
.thenReturn(generatedId);
IdGeneratingBeforeSaveCallback subject = new IdGeneratingBeforeSaveCallback(relationalMappingContext,
IdGeneratingEntityCallback subject = new IdGeneratingEntityCallback(relationalMappingContext,
PostgresDialect.INSTANCE, operations);
EntityWithSequence processed = (EntityWithSequence) subject.onBeforeSave(new EntityWithSequence(),
@@ -108,7 +108,7 @@ class IdGeneratingBeforeSaveCallbackTest {
when(operations.queryForObject(anyString(), any(SqlParameterSource.class), any(RowMapper.class)))
.thenReturn(generatedId);
IdGeneratingBeforeSaveCallback subject = new IdGeneratingBeforeSaveCallback(relationalMappingContext,
IdGeneratingEntityCallback subject = new IdGeneratingEntityCallback(relationalMappingContext,
PostgresDialect.INSTANCE, operations);
EntityWithIntSequence processed = (EntityWithIntSequence) subject.onBeforeSave(new EntityWithIntSequence(),
@@ -124,7 +124,7 @@ class IdGeneratingBeforeSaveCallbackTest {
when(operations.queryForObject(anyString(), any(SqlParameterSource.class), any(RowMapper.class)))
.thenReturn(generatedId);
IdGeneratingBeforeSaveCallback subject = new IdGeneratingBeforeSaveCallback(relationalMappingContext,
IdGeneratingEntityCallback subject = new IdGeneratingEntityCallback(relationalMappingContext,
PostgresDialect.INSTANCE, operations);
EntityWithUuidSequence processed = (EntityWithUuidSequence) subject.onBeforeSave(new EntityWithUuidSequence(),

View File

@@ -35,7 +35,7 @@ import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceCreator;
import org.springframework.data.annotation.Transient;
import org.springframework.data.domain.Persistable;
import org.springframework.data.jdbc.core.mapping.IdGeneratingBeforeSaveCallback;
import org.springframework.data.jdbc.core.convert.IdGeneratingEntityCallback;
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
import org.springframework.data.jdbc.repository.support.SimpleJdbcRepository;
import org.springframework.data.jdbc.testing.IntegrationTest;
@@ -66,7 +66,7 @@ class JdbcRepositoryIdGenerationIntegrationTests {
@Autowired SimpleSeqRepository simpleSeqRepository;
@Autowired PersistableSeqRepository persistableSeqRepository;
@Autowired PrimitiveIdSeqRepository primitiveIdSeqRepository;
@Autowired IdGeneratingBeforeSaveCallback idGeneratingCallback;
@Autowired IdGeneratingEntityCallback idGeneratingCallback;
@Test // DATAJDBC-98
void idWithoutSetterGetsSet() {

View File

@@ -24,6 +24,7 @@ import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mockito.Mockito;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -37,7 +38,6 @@ import org.springframework.context.annotation.Profile;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.jdbc.core.convert.*;
import org.springframework.data.jdbc.core.dialect.JdbcDialect;
import org.springframework.data.jdbc.core.mapping.IdGeneratingBeforeSaveCallback;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes;
import org.springframework.data.jdbc.repository.config.DialectResolver;
@@ -45,7 +45,6 @@ import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.mapping.callback.EntityCallback;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.relational.RelationalManagedTypes;
import org.springframework.data.relational.core.dialect.Dialect;
import org.springframework.data.relational.core.mapping.DefaultNamingStrategy;
import org.springframework.data.relational.core.mapping.NamingStrategy;
@@ -182,15 +181,15 @@ public class TestConfiguration {
}
/**
* Creates a {@link IdGeneratingBeforeSaveCallback} bean using the configured
* Creates a {@link IdGeneratingEntityCallback} bean using the configured
* {@link #jdbcDialect(NamedParameterJdbcOperations)}.
*
* @return must not be {@literal null}.
*/
@Bean
public IdGeneratingBeforeSaveCallback idGeneratingBeforeSaveCallback(JdbcMappingContext mappingContext,
public IdGeneratingEntityCallback idGeneratingBeforeSaveCallback(JdbcMappingContext mappingContext,
NamedParameterJdbcOperations operations, Dialect dialect) {
return Mockito.spy(new IdGeneratingBeforeSaveCallback(mappingContext, dialect, operations));
return Mockito.spy(new IdGeneratingEntityCallback(mappingContext, dialect, operations));
}
@Bean