Skip to content

Commit 5c3592f

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 3606ca4 commit 5c3592f

File tree

3 files changed

+202
-4
lines changed

3 files changed

+202
-4
lines changed

Diff for: 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

Diff for: spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java

+30-4
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,16 @@
3131
import org.springframework.data.domain.ScrollPosition;
3232
import org.springframework.data.domain.Sort;
3333
import org.springframework.data.domain.Window;
34+
import org.springframework.data.mapping.PersistentPropertyAccessor;
35+
import org.springframework.data.mapping.PropertyHandler;
36+
import org.springframework.data.mapping.context.MappingContext;
3437
import org.springframework.data.r2dbc.convert.R2dbcConverter;
3538
import org.springframework.data.r2dbc.core.R2dbcEntityOperations;
3639
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
3740
import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
3841
import org.springframework.data.r2dbc.core.ReactiveSelectOperation;
3942
import org.springframework.data.r2dbc.repository.R2dbcRepository;
43+
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
4044
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
4145
import org.springframework.data.relational.core.query.Criteria;
4246
import org.springframework.data.relational.core.query.Query;
@@ -67,6 +71,7 @@ public class SimpleR2dbcRepository<T, ID> implements R2dbcRepository<T, ID> {
6771
private final R2dbcEntityOperations entityOperations;
6872
private final Lazy<RelationalPersistentProperty> idProperty;
6973
private final RelationalExampleMapper exampleMapper;
74+
private MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext;
7075

7176
/**
7277
* Create a new {@link SimpleR2dbcRepository}.
@@ -81,11 +86,11 @@ public SimpleR2dbcRepository(RelationalEntityInformation<T, ID> entity, R2dbcEnt
8186

8287
this.entity = entity;
8388
this.entityOperations = entityOperations;
84-
this.idProperty = Lazy.of(() -> converter //
85-
.getMappingContext() //
89+
this.mappingContext = converter.getMappingContext();
90+
this.idProperty = Lazy.of(() -> mappingContext //
8691
.getRequiredPersistentEntity(this.entity.getJavaType()) //
8792
.getRequiredIdProperty());
88-
this.exampleMapper = new RelationalExampleMapper(converter.getMappingContext());
93+
this.exampleMapper = new RelationalExampleMapper(mappingContext);
8994
}
9095

9196
/**
@@ -359,7 +364,28 @@ private RelationalPersistentProperty getIdProperty() {
359364
}
360365

361366
private Query getIdQuery(Object id) {
362-
return Query.query(Criteria.where(getIdProperty().getName()).is(id));
367+
368+
Criteria criteria;
369+
370+
RelationalPersistentProperty idProperty = getIdProperty();
371+
if (idProperty.isEmbedded()) {
372+
373+
Criteria[] criteriaHolder = new Criteria[] { Criteria.empty() };
374+
375+
RelationalPersistentEntity<?> idEntity = mappingContext.getRequiredPersistentEntity(idProperty.getType());
376+
PersistentPropertyAccessor<Object> accessor = idEntity.getPropertyAccessor(id);
377+
idEntity.doWithProperties(new PropertyHandler<RelationalPersistentProperty>() {
378+
@Override
379+
public void doWithPersistentProperty(RelationalPersistentProperty persistentProperty) {
380+
criteriaHolder[0] = criteriaHolder [0].and(persistentProperty.getName()).is(accessor.getProperty(persistentProperty));
381+
}
382+
});
383+
criteria = criteriaHolder[0];
384+
} else {
385+
criteria = Criteria.where(idProperty.getName()).is(id);
386+
}
387+
388+
return Query.query(criteria);
363389
}
364390

365391
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Copyright 2019-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.r2dbc.repository;
17+
18+
import io.r2dbc.spi.ConnectionFactory;
19+
20+
import javax.sql.DataSource;
21+
22+
import org.junit.jupiter.api.BeforeEach;
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.api.extension.ExtendWith;
25+
import org.springframework.beans.factory.annotation.Autowired;
26+
import org.springframework.context.annotation.ComponentScan;
27+
import org.springframework.context.annotation.Configuration;
28+
import org.springframework.context.annotation.FilterType;
29+
import org.springframework.dao.DataAccessException;
30+
import org.springframework.data.annotation.Id;
31+
import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration;
32+
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
33+
import org.springframework.data.r2dbc.testing.H2TestSupport;
34+
import org.springframework.data.relational.core.mapping.Embedded;
35+
import org.springframework.data.relational.core.mapping.Table;
36+
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
37+
import org.springframework.jdbc.core.JdbcTemplate;
38+
import org.springframework.test.context.junit.jupiter.SpringExtension;
39+
import reactor.test.StepVerifier;
40+
41+
import static org.assertj.core.api.Assertions.*;
42+
43+
/**
44+
* Integration tests for repositories of entities with a composite id.
45+
*
46+
* @author Jens Schauder
47+
*/
48+
@ExtendWith(SpringExtension.class)
49+
public class CompositeIdRepositoryIntegrationTests {
50+
51+
@Autowired private WithCompositeIdRepository repository;
52+
private JdbcTemplate jdbc;
53+
54+
@Configuration
55+
@EnableR2dbcRepositories(includeFilters = @ComponentScan.Filter(value = WithCompositeIdRepository.class,
56+
type = FilterType.ASSIGNABLE_TYPE), considerNestedRepositories = true)
57+
static class TestConfiguration extends AbstractR2dbcConfiguration {
58+
@Override
59+
public ConnectionFactory connectionFactory() {
60+
return H2TestSupport.createConnectionFactory();
61+
}
62+
63+
}
64+
65+
@BeforeEach
66+
void before() {
67+
68+
this.jdbc = new JdbcTemplate(createDataSource());
69+
70+
try {
71+
this.jdbc.execute("DROP TABLE with_composite_id");
72+
} catch (DataAccessException e) {}
73+
74+
this.jdbc.execute("""
75+
CREATE TABLE with_composite_id (
76+
one int,
77+
two varchar(255),
78+
name varchar(255),
79+
primary key (one, two))""");
80+
this.jdbc.execute("INSERT INTO with_composite_id VALUES (42, 'HBAR','Walter')");
81+
this.jdbc.execute("INSERT INTO with_composite_id VALUES (23, '2PI','Jesse')");
82+
}
83+
84+
/**
85+
* Creates a {@link DataSource} to be used in this test.
86+
*
87+
* @return the {@link DataSource} to be used in this test.
88+
*/
89+
protected DataSource createDataSource() {
90+
return H2TestSupport.createDataSource();
91+
}
92+
93+
/**
94+
* Creates a {@link ConnectionFactory} to be used in this test.
95+
*
96+
* @return the {@link ConnectionFactory} to be used in this test.
97+
*/
98+
protected ConnectionFactory createConnectionFactory() {
99+
return H2TestSupport.createConnectionFactory();
100+
}
101+
102+
@Test // GH-574
103+
void findAllById() {
104+
repository.findById(new CompositeId(42, "HBAR")) //
105+
.as(StepVerifier::create) //
106+
.consumeNextWith(actual ->{
107+
assertThat(actual.name).isEqualTo("Walter");
108+
}).verifyComplete();
109+
}
110+
111+
interface WithCompositeIdRepository extends ReactiveCrudRepository<WithCompositeId, CompositeId> {
112+
113+
}
114+
115+
@Table("with_composite_id")
116+
record WithCompositeId(@Id @Embedded.Nullable CompositeId pk, String name) {
117+
}
118+
119+
record CompositeId(Integer one, String two) {
120+
}
121+
122+
}

0 commit comments

Comments
 (0)