Skip to content

Commit eb25cc3

Browse files
committed
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
1 parent d0e43be commit eb25cc3

16 files changed

+395
-302
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jdbc.core.convert;
17+
18+
import org.springframework.data.mapping.PersistentPropertyAccessor;
19+
import org.springframework.data.mapping.context.MappingContext;
20+
import org.springframework.data.relational.core.conversion.MutableAggregateChange;
21+
import org.springframework.data.relational.core.dialect.Dialect;
22+
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
23+
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
24+
import org.springframework.data.relational.core.mapping.event.BeforeSaveCallback;
25+
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
26+
import org.springframework.util.Assert;
27+
28+
/**
29+
* Callback for generating identifier values through a database sequence.
30+
*
31+
* @author Mikhail Polivakha
32+
* @author Mark Paluch
33+
* @since 3.5
34+
*/
35+
public class IdGeneratingEntityCallback implements BeforeSaveCallback<Object> {
36+
37+
private final MappingContext<RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> context;
38+
private final SequenceEntityCallbackDelegate delegate;
39+
40+
public IdGeneratingEntityCallback(
41+
MappingContext<RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> context, Dialect dialect,
42+
NamedParameterJdbcOperations operations) {
43+
44+
this.context = context;
45+
this.delegate = new SequenceEntityCallbackDelegate(dialect, operations);
46+
}
47+
48+
@Override
49+
public Object onBeforeSave(Object aggregate, MutableAggregateChange<Object> aggregateChange) {
50+
51+
Assert.notNull(aggregate, "aggregate must not be null");
52+
53+
RelationalPersistentEntity<?> entity = context.getRequiredPersistentEntity(aggregate.getClass());
54+
55+
if (!entity.hasIdProperty()) {
56+
return aggregate;
57+
}
58+
59+
RelationalPersistentProperty property = entity.getRequiredIdProperty();
60+
PersistentPropertyAccessor<Object> accessor = entity.getPropertyAccessor(aggregate);
61+
62+
if (!entity.isNew(aggregate) || delegate.hasValue(property, accessor) || !property.hasSequence()) {
63+
return aggregate;
64+
}
65+
66+
delegate.generateSequenceValue(property, accessor);
67+
68+
return accessor.getBean();
69+
}
70+
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jdbc.core.convert;
17+
18+
import org.apache.commons.logging.Log;
19+
import org.apache.commons.logging.LogFactory;
20+
21+
import org.springframework.data.mapping.PersistentProperty;
22+
import org.springframework.data.mapping.PersistentPropertyAccessor;
23+
import org.springframework.data.relational.core.dialect.Dialect;
24+
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
25+
import org.springframework.data.relational.core.sql.SqlIdentifier;
26+
import org.springframework.data.util.ReflectionUtils;
27+
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
28+
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
29+
import org.springframework.lang.Nullable;
30+
import org.springframework.util.ClassUtils;
31+
import org.springframework.util.NumberUtils;
32+
33+
/**
34+
* Support class for generating identifier values through a database sequence.
35+
*
36+
* @author Mikhail Polivakha
37+
* @author Mark Paluch
38+
* @since 3.5
39+
* @see org.springframework.data.relational.core.mapping.Sequence
40+
*/
41+
class SequenceEntityCallbackDelegate {
42+
43+
private static final Log LOG = LogFactory.getLog(SequenceEntityCallbackDelegate.class);
44+
private final static MapSqlParameterSource EMPTY_PARAMETERS = new MapSqlParameterSource();
45+
46+
private final Dialect dialect;
47+
private final NamedParameterJdbcOperations operations;
48+
49+
public SequenceEntityCallbackDelegate(Dialect dialect, NamedParameterJdbcOperations operations) {
50+
this.dialect = dialect;
51+
this.operations = operations;
52+
}
53+
54+
@SuppressWarnings("unchecked")
55+
protected void generateSequenceValue(RelationalPersistentProperty property,
56+
PersistentPropertyAccessor<Object> accessor) {
57+
58+
Object sequenceValue = getSequenceValue(property);
59+
60+
if (sequenceValue == null) {
61+
return;
62+
}
63+
64+
Class<?> targetType = ClassUtils.resolvePrimitiveIfNecessary(property.getType());
65+
if (sequenceValue instanceof Number && Number.class.isAssignableFrom(targetType)) {
66+
sequenceValue = NumberUtils.convertNumberToTargetClass((Number) sequenceValue,
67+
(Class<? extends Number>) targetType);
68+
}
69+
70+
accessor.setProperty(property, sequenceValue);
71+
}
72+
73+
protected boolean hasValue(PersistentProperty<?> property, PersistentPropertyAccessor<Object> propertyAccessor) {
74+
75+
Object identifier = propertyAccessor.getProperty(property);
76+
77+
if (property.getType().isPrimitive()) {
78+
79+
Object primitiveDefault = ReflectionUtils.getPrimitiveDefault(property.getType());
80+
return !primitiveDefault.equals(identifier);
81+
}
82+
83+
return identifier != null;
84+
}
85+
86+
private @Nullable Object getSequenceValue(RelationalPersistentProperty property) {
87+
88+
SqlIdentifier sequence = property.getSequence();
89+
90+
if (sequence != null && !dialect.getIdGeneration().sequencesSupported()) {
91+
LOG.warn("""
92+
Aggregate type '%s' is marked for sequence usage but configured dialect '%s'
93+
does not support sequences. Falling back to identity columns.
94+
""".formatted(property.getOwner().getType(), ClassUtils.getQualifiedName(dialect.getClass())));
95+
return null;
96+
}
97+
98+
String sql = dialect.getIdGeneration().createSequenceQuery(sequence);
99+
return operations.queryForObject(sql, EMPTY_PARAMETERS, (rs, rowNum) -> rs.getObject(1));
100+
}
101+
102+
}

Diff for: spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/IdGeneratingBeforeSaveCallback.java

-112
This file was deleted.

Diff for: spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import org.apache.commons.logging.Log;
2727
import org.apache.commons.logging.LogFactory;
28+
2829
import org.springframework.beans.BeansException;
2930
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
3031
import org.springframework.context.ApplicationContext;
@@ -38,7 +39,6 @@
3839
import org.springframework.data.jdbc.core.JdbcAggregateTemplate;
3940
import org.springframework.data.jdbc.core.convert.*;
4041
import org.springframework.data.jdbc.core.dialect.JdbcDialect;
41-
import org.springframework.data.jdbc.core.mapping.IdGeneratingBeforeSaveCallback;
4242
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
4343
import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes;
4444
import org.springframework.data.mapping.model.SimpleTypeHolder;
@@ -121,17 +121,17 @@ public JdbcMappingContext jdbcMappingContext(Optional<NamingStrategy> namingStra
121121
}
122122

123123
/**
124-
* Creates a {@link IdGeneratingBeforeSaveCallback} bean using the configured
124+
* Creates a {@link IdGeneratingEntityCallback} bean using the configured
125125
* {@link #jdbcMappingContext(Optional, JdbcCustomConversions, RelationalManagedTypes)} and
126126
* {@link #jdbcDialect(NamedParameterJdbcOperations)}.
127127
*
128128
* @return must not be {@literal null}.
129129
* @since 3.5
130130
*/
131131
@Bean
132-
public IdGeneratingBeforeSaveCallback idGeneratingBeforeSaveCallback(JdbcMappingContext mappingContext,
132+
public IdGeneratingEntityCallback idGeneratingBeforeSaveCallback(JdbcMappingContext mappingContext,
133133
NamedParameterJdbcOperations operations, Dialect dialect) {
134-
return new IdGeneratingBeforeSaveCallback(mappingContext, dialect, operations);
134+
return new IdGeneratingEntityCallback(mappingContext, dialect, operations);
135135
}
136136

137137
/**

0 commit comments

Comments
 (0)