Skip to content

Commit b469422

Browse files
committed
Improved Composite Id support.
Adds support for id generation by sequence as part of a composite id. Added a proper test for sorting by composite id element. Added a stand in test for projection by composite id element. The latter does not test the actual intended behaviour since projection don't work as intended yet. See #1821 Original pull request #1957 See #574
1 parent 0117231 commit b469422

File tree

8 files changed

+430
-138
lines changed

8 files changed

+430
-138
lines changed

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

+107-51
Original file line numberDiff line numberDiff line change
@@ -2,73 +2,129 @@
22

33
import java.util.Map;
44
import java.util.Optional;
5+
56
import org.apache.commons.logging.Log;
67
import org.apache.commons.logging.LogFactory;
78
import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration;
9+
import org.springframework.data.mapping.Parameter;
810
import org.springframework.data.mapping.PersistentPropertyAccessor;
11+
import org.springframework.data.mapping.context.MappingContext;
12+
import org.springframework.data.mapping.model.ParameterValueProvider;
913
import org.springframework.data.relational.core.conversion.MutableAggregateChange;
14+
import org.springframework.data.relational.core.conversion.RelationalConverter;
1015
import org.springframework.data.relational.core.dialect.Dialect;
11-
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
1216
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
17+
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
1318
import org.springframework.data.relational.core.mapping.event.BeforeSaveCallback;
1419
import org.springframework.data.relational.core.sql.SqlIdentifier;
1520
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
21+
import org.springframework.lang.Nullable;
1622
import org.springframework.util.Assert;
1723

1824
/**
19-
* Callback for generating ID via the database sequence. By default, it is registered as a
20-
* bean in {@link AbstractJdbcConfiguration}
25+
* Callback for generating ID via the database sequence. By default, it is registered as a bean in
26+
* {@link AbstractJdbcConfiguration}
2127
*
2228
* @author Mikhail Polivakha
2329
*/
2430
public class IdGeneratingBeforeSaveCallback implements BeforeSaveCallback<Object> {
2531

26-
private static final Log LOG = LogFactory.getLog(IdGeneratingBeforeSaveCallback.class);
27-
28-
private final RelationalMappingContext relationalMappingContext;
29-
private final Dialect dialect;
30-
private final NamedParameterJdbcOperations operations;
31-
32-
public IdGeneratingBeforeSaveCallback(
33-
RelationalMappingContext relationalMappingContext,
34-
Dialect dialect,
35-
NamedParameterJdbcOperations namedParameterJdbcOperations
36-
) {
37-
this.relationalMappingContext = relationalMappingContext;
38-
this.dialect = dialect;
39-
this.operations = namedParameterJdbcOperations;
40-
}
41-
42-
@Override
43-
public Object onBeforeSave(Object aggregate, MutableAggregateChange<Object> aggregateChange) {
44-
45-
Assert.notNull(aggregate, "The aggregate cannot be null at this point");
46-
47-
RelationalPersistentEntity<?> persistentEntity = relationalMappingContext.getPersistentEntity(aggregate.getClass());
48-
Optional<SqlIdentifier> idSequence = persistentEntity.getIdSequence();
49-
50-
if (dialect.getIdGeneration().sequencesSupported()) {
51-
52-
if (persistentEntity.getIdProperty() != null) {
53-
idSequence
54-
.map(s -> dialect.getIdGeneration().createSequenceQuery(s))
55-
.ifPresent(sql -> {
56-
Long idValue = operations.queryForObject(sql, Map.of(), (rs, rowNum) -> rs.getLong(1));
57-
PersistentPropertyAccessor<Object> propertyAccessor = persistentEntity.getPropertyAccessor(aggregate);
58-
propertyAccessor.setProperty(persistentEntity.getRequiredIdProperty(), idValue);
59-
});
60-
}
61-
} else {
62-
if (idSequence.isPresent()) {
63-
LOG.warn("""
64-
It seems you're trying to insert an aggregate of type '%s' annotated with @TargetSequence, but the problem is RDBMS you're
65-
working with does not support sequences as such. Falling back to identity columns
66-
"""
67-
.formatted(aggregate.getClass().getName())
68-
);
69-
}
70-
}
71-
72-
return aggregate;
73-
}
32+
private static final Log LOG = LogFactory.getLog(IdGeneratingBeforeSaveCallback.class);
33+
34+
private final MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> relationalMappingContext;
35+
private final Dialect dialect;
36+
private final NamedParameterJdbcOperations operations;
37+
private final RelationalConverter converter;
38+
39+
public IdGeneratingBeforeSaveCallback(Dialect dialect, NamedParameterJdbcOperations namedParameterJdbcOperations,
40+
RelationalConverter converter) {
41+
42+
this.relationalMappingContext = converter.getMappingContext();
43+
this.dialect = dialect;
44+
this.operations = namedParameterJdbcOperations;
45+
this.converter = converter;
46+
}
47+
48+
@Override
49+
public Object onBeforeSave(Object aggregate, MutableAggregateChange<Object> aggregateChange) {
50+
51+
Assert.notNull(aggregate, "The aggregate must not be null at this point");
52+
53+
RelationalPersistentEntity<?> persistentEntity = relationalMappingContext
54+
.getRequiredPersistentEntity(aggregate.getClass());
55+
Optional<SqlIdentifier> idSequence = persistentEntity.getIdSequence();
56+
57+
if (dialect.getIdGeneration().sequencesSupported()) {
58+
59+
if (persistentEntity.hasIdProperty()) {
60+
61+
PersistentPropertyAccessor<Object> accessor = persistentEntity.getPropertyAccessor(aggregate);
62+
63+
idSequence.map(this::querySequence).ifPresent(idValue -> {
64+
RelationalPersistentProperty idProperty = persistentEntity.getRequiredIdProperty();
65+
if (idProperty.isEmbedded()) {
66+
67+
setEmbeddedIdValue(persistentEntity, idProperty, aggregate, idValue, accessor);
68+
69+
} else {
70+
accessor.setProperty(idProperty, idValue);
71+
}
72+
73+
});
74+
return accessor.getBean();
75+
}
76+
} else {
77+
if (idSequence.isPresent()) {
78+
LOG.warn(
79+
"""
80+
It seems you're trying to insert an aggregate of type '%s' annotated with @TargetSequence, but the problem is RDBMS you're
81+
working with does not support sequences as such. Falling back to identity columns
82+
"""
83+
.formatted(aggregate.getClass().getName()));
84+
}
85+
}
86+
87+
return aggregate;
88+
}
89+
90+
private void setEmbeddedIdValue(RelationalPersistentEntity<?> persistentEntity,
91+
RelationalPersistentProperty idProperty, Object aggregate, Long idValue,
92+
PersistentPropertyAccessor<Object> accessor) {
93+
94+
Class<?> idPropertyType = idProperty.getType();
95+
RelationalPersistentEntity<?> idEntity = relationalMappingContext.getRequiredPersistentEntity(idPropertyType);
96+
97+
RelationalPersistentProperty[] propertyHolder = new RelationalPersistentProperty[1];
98+
idEntity.doWithProperties((RelationalPersistentProperty property) -> {
99+
if (propertyHolder[0] != null) {
100+
throw new IllegalStateException(
101+
"There is no unique id property path for %s".formatted(persistentEntity.toString()));
102+
}
103+
propertyHolder[0] = property;
104+
});
105+
106+
Object propertyValue = accessor.getProperty(propertyHolder[0]);
107+
108+
if (propertyValue != null) {
109+
persistentEntity.getPropertyPathAccessor(aggregate).setProperty(propertyHolder[0], idValue);
110+
} else {
111+
Object idInstance = converter.getEntityInstantiators().getInstantiatorFor(idEntity).createInstance(idEntity,
112+
new ParameterValueProvider<RelationalPersistentProperty>() {
113+
@Nullable
114+
@Override
115+
public <T> T getParameterValue(Parameter<T, RelationalPersistentProperty> parameter) {
116+
return (T) idValue;
117+
}
118+
});
119+
accessor.setProperty(idProperty, idInstance);
120+
}
121+
}
122+
123+
private Long querySequence(SqlIdentifier s) {
124+
125+
String sql = dialect.getIdGeneration().createSequenceQuery(s);
126+
Long sequenceValue = operations.queryForObject(sql, Map.of(), Long.class);
127+
Assert.state(sequenceValue != null, () -> "No sequence value found for " + s);
128+
return sequenceValue;
129+
}
74130
}

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,11 @@ public JdbcMappingContext jdbcMappingContext(Optional<NamingStrategy> namingStra
129129
*/
130130
@Bean
131131
public IdGeneratingBeforeSaveCallback idGeneratingBeforeSaveCallback(
132-
JdbcMappingContext mappingContext,
133-
NamedParameterJdbcOperations operations,
134-
Dialect dialect
132+
JdbcMappingContext mappingContext,
133+
NamedParameterJdbcOperations operations,
134+
Dialect dialect, RelationalConverter converter
135135
) {
136-
return new IdGeneratingBeforeSaveCallback(mappingContext, dialect, operations);
136+
return new IdGeneratingBeforeSaveCallback(dialect, operations, converter);
137137
}
138138

139139
/**

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/CompositeIdAggregateTemplateHsqlIntegrationTests.java

+50
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,17 @@
1818
import static org.assertj.core.api.Assertions.*;
1919

2020
import java.util.List;
21+
import java.util.Optional;
2122

23+
import org.junit.jupiter.api.Disabled;
2224
import org.junit.jupiter.api.Test;
2325
import org.springframework.beans.factory.annotation.Autowired;
2426
import org.springframework.context.ApplicationEventPublisher;
2527
import org.springframework.context.annotation.Bean;
2628
import org.springframework.context.annotation.Configuration;
2729
import org.springframework.context.annotation.Import;
2830
import org.springframework.data.annotation.Id;
31+
import org.springframework.data.domain.Sort;
2932
import org.springframework.data.jdbc.core.convert.DataAccessStrategy;
3033
import org.springframework.data.jdbc.core.convert.JdbcConverter;
3134
import org.springframework.data.jdbc.testing.DatabaseType;
@@ -34,6 +37,7 @@
3437
import org.springframework.data.jdbc.testing.TestConfiguration;
3538
import org.springframework.data.relational.core.mapping.Embedded;
3639
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
40+
import org.springframework.data.relational.core.query.Query;
3741
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
3842

3943
/**
@@ -202,6 +206,52 @@ void saveAndLoadWithListAndCompositeId() {
202206
assertThat(reloaded).isEqualTo(entity);
203207
}
204208

209+
@Test // GH-574
210+
void sortByCompositeIdParts() {
211+
212+
SimpleEntityWithEmbeddedPk alpha = template.insert( //
213+
new SimpleEntityWithEmbeddedPk( //
214+
new EmbeddedPk(23L, "x"), "alpha" //
215+
));
216+
SimpleEntityWithEmbeddedPk bravo = template.insert( //
217+
new SimpleEntityWithEmbeddedPk( //
218+
new EmbeddedPk(22L, "a"), "bravo" //
219+
));
220+
SimpleEntityWithEmbeddedPk charlie = template.insert( //
221+
new SimpleEntityWithEmbeddedPk( //
222+
new EmbeddedPk(21L, "z"), "charlie" //
223+
) //
224+
);
225+
226+
assertThat( //
227+
template.findAll(SimpleEntityWithEmbeddedPk.class, Sort.by("embeddedPk.one"))) //
228+
.containsExactly( //
229+
charlie, bravo, alpha //
230+
);
231+
232+
assertThat( //
233+
template.findAll(SimpleEntityWithEmbeddedPk.class, Sort.by("embeddedPk.two").descending())) //
234+
.containsExactly( //
235+
charlie, alpha, bravo //
236+
);
237+
}
238+
239+
@Test // GH-574
240+
void projectByCompositeIdParts() {
241+
242+
SimpleEntityWithEmbeddedPk alpha = template.insert( //
243+
new SimpleEntityWithEmbeddedPk( //
244+
new EmbeddedPk(23L, "x"), "alpha" //
245+
));
246+
247+
Query projectingQuery = Query.empty().columns( "embeddedPk.two", "name");
248+
SimpleEntityWithEmbeddedPk projected = template.findOne(projectingQuery, SimpleEntityWithEmbeddedPk.class).orElseThrow();
249+
250+
// Projection still does a full select, otherwise one would be null.
251+
// See https://github.com/spring-projects/spring-data-relational/issues/1821
252+
assertThat(projected).isEqualTo(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"));
253+
}
254+
205255
private record WrappedPk(Long id) {
206256
}
207257

0 commit comments

Comments
 (0)