Skip to content

Commit 1ddb157

Browse files
committed
Add support for arbitrary where clauses in Single Query Loading.
Closes #1601
1 parent 95fb0c4 commit 1ddb157

14 files changed

+244
-27
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateReader.java

+44-1
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,22 @@
2121
import java.util.Iterator;
2222
import java.util.List;
2323
import java.util.Map;
24+
import java.util.Optional;
25+
import java.util.function.BiFunction;
2426

2527
import org.springframework.dao.IncorrectResultSizeDataAccessException;
2628
import org.springframework.data.relational.core.dialect.Dialect;
2729
import org.springframework.data.relational.core.mapping.AggregatePath;
2830
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
31+
import org.springframework.data.relational.core.query.CriteriaDefinition;
32+
import org.springframework.data.relational.core.query.Query;
33+
import org.springframework.data.relational.core.sql.Condition;
34+
import org.springframework.data.relational.core.sql.Table;
2935
import org.springframework.data.relational.core.sqlgeneration.AliasFactory;
3036
import org.springframework.data.relational.core.sqlgeneration.SingleQuerySqlGenerator;
3137
import org.springframework.data.relational.core.sqlgeneration.SqlGenerator;
3238
import org.springframework.data.relational.domain.RowDocument;
39+
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
3340
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
3441
import org.springframework.lang.Nullable;
3542
import org.springframework.util.Assert;
@@ -89,6 +96,35 @@ public Iterable<T> findAllById(Iterable<?> ids) {
8996
return jdbcTemplate.query(sqlGenerator.findAllById(), Map.of("ids", convertedIds), this::extractAll);
9097
}
9198

99+
public Iterable<T> findAllBy(Query query) {
100+
101+
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
102+
BiFunction<Table, RelationalPersistentEntity, Condition> condition = createConditionSource(query, parameterSource);
103+
return jdbcTemplate.query(sqlGenerator.findAllByCondition(condition), parameterSource, this::extractAll);
104+
}
105+
106+
public Optional<T> findOneByQuery(Query query) {
107+
108+
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
109+
BiFunction<Table, RelationalPersistentEntity, Condition> condition = createConditionSource(query, parameterSource);
110+
111+
return Optional.ofNullable(
112+
jdbcTemplate.query(sqlGenerator.findAllByCondition(condition), parameterSource, this::extractZeroOrOne));
113+
}
114+
115+
private BiFunction<Table, RelationalPersistentEntity, Condition> createConditionSource(Query query, MapSqlParameterSource parameterSource) {
116+
117+
QueryMapper queryMapper = new QueryMapper(converter);
118+
119+
BiFunction<Table, RelationalPersistentEntity, Condition> condition = (table, aggregate) -> {
120+
Optional<CriteriaDefinition> criteria = query.getCriteria();
121+
return criteria
122+
.map(criteriaDefinition -> queryMapper.getMappedObject(parameterSource, criteriaDefinition, table, aggregate))
123+
.orElse(null);
124+
};
125+
return condition;
126+
}
127+
92128
/**
93129
* Extracts a list of aggregates from the given {@link ResultSet} by utilizing the
94130
* {@link RowDocumentResultSetExtractor} and the {@link JdbcConverter}. When used as a method reference this conforms
@@ -115,7 +151,8 @@ private List<T> extractAll(ResultSet rs) throws SQLException {
115151
* to the {@link org.springframework.jdbc.core.ResultSetExtractor} contract.
116152
*
117153
* @param @param rs the {@link ResultSet} from which to extract the data. Must not be {(}@literal null}.
118-
* @return The single instance when the conversion results in exactly one instance. If the {@literal ResultSet} is empty, null is returned.
154+
* @return The single instance when the conversion results in exactly one instance. If the {@literal ResultSet} is
155+
* empty, null is returned.
119156
* @throws SQLException
120157
* @throws IncorrectResultSizeDataAccessException when the conversion yields more than one instance.
121158
*/
@@ -190,9 +227,15 @@ public String findAllById() {
190227
return findAllById;
191228
}
192229

230+
@Override
231+
public String findAllByCondition(BiFunction<Table, RelationalPersistentEntity, Condition> conditionSource) {
232+
return delegate.findAllByCondition(conditionSource);
233+
}
234+
193235
@Override
194236
public AliasFactory getAliasFactory() {
195237
return delegate.getAliasFactory();
196238
}
239+
197240
}
198241
}

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SingleQueryDataAccessStrategy.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,13 @@ public <T> Iterable<T> findAll(Class<T> domainType, Pageable pageable) {
7777

7878
@Override
7979
public <T> Optional<T> findOne(Query query, Class<T> domainType) {
80-
return Optional.empty();
80+
return getReader(domainType).findOneByQuery(query);
8181
}
8282

8383
@Override
8484
public <T> Iterable<T> findAll(Query query, Class<T> domainType) {
85-
throw new UnsupportedOperationException();
85+
86+
return getReader(domainType).findAllBy(query);
8687
}
8788

8889
@Override

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SingleQueryFallbackDataAccessStrategy.java

+32-6
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
package org.springframework.data.jdbc.core.convert;
1717

1818
import java.util.Collections;
19+
import java.util.Optional;
1920

2021
import org.springframework.data.mapping.PersistentPropertyPath;
2122
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
23+
import org.springframework.data.relational.core.query.Query;
2224
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
2325
import org.springframework.util.Assert;
2426

@@ -85,13 +87,37 @@ public <T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType) {
8587
return super.findAllById(ids, domainType);
8688
}
8789

90+
public <T> Optional<T> findOne(Query query, Class<T> domainType) {
91+
92+
if (isSingleSelectQuerySupported(domainType) && isSingleSelectQuerySupported(query)) {
93+
return singleSelectDelegate.findOne(query, domainType);
94+
}
95+
96+
return super.findOne(query, domainType);
97+
}
98+
99+
@Override
100+
public <T> Iterable<T> findAll(Query query, Class<T> domainType) {
101+
102+
if (isSingleSelectQuerySupported(domainType) && isSingleSelectQuerySupported(query)) {
103+
return singleSelectDelegate.findAll(query, domainType);
104+
}
105+
106+
return super.findAll(query, domainType);
107+
}
108+
109+
private static boolean isSingleSelectQuerySupported(Query query) {
110+
return !query.isSorted() && !query.isLimited();
111+
}
112+
88113
private boolean isSingleSelectQuerySupported(Class<?> entityType) {
89114

90-
return sqlGeneratorSource.getDialect().supportsSingleQueryLoading()//
91-
&& entityQualifiesForSingleSelectQuery(entityType);
115+
return converter.getMappingContext().isSingleQueryLoadingEnabled()
116+
&& sqlGeneratorSource.getDialect().supportsSingleQueryLoading()//
117+
&& entityQualifiesForSingleQueryLoading(entityType);
92118
}
93119

94-
private boolean entityQualifiesForSingleSelectQuery(Class<?> entityType) {
120+
private boolean entityQualifiesForSingleQueryLoading(Class<?> entityType) {
95121

96122
boolean referenceFound = false;
97123
for (PersistentPropertyPath<RelationalPersistentProperty> path : converter.getMappingContext()
@@ -113,9 +139,9 @@ private boolean entityQualifiesForSingleSelectQuery(Class<?> entityType) {
113139
}
114140

115141
// AggregateReferences aren't supported yet
116-
if (property.isAssociation()) {
117-
return false;
118-
}
142+
// if (property.isAssociation()) {
143+
// return false;
144+
// }
119145
}
120146
return true;
121147

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

+84-8
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,8 @@
2323
import static org.springframework.data.jdbc.testing.TestDatabaseFeatures.Feature.*;
2424

2525
import java.time.LocalDateTime;
26+
import java.util.*;
2627
import java.util.ArrayList;
27-
import java.util.Collections;
28-
import java.util.HashMap;
29-
import java.util.HashSet;
30-
import java.util.Iterator;
31-
import java.util.List;
32-
import java.util.Map;
33-
import java.util.Objects;
34-
import java.util.Set;
3528
import java.util.function.Function;
3629
import java.util.stream.IntStream;
3730

@@ -42,6 +35,7 @@
4235
import org.springframework.context.annotation.Bean;
4336
import org.springframework.context.annotation.Configuration;
4437
import org.springframework.context.annotation.Import;
38+
import org.springframework.dao.IncorrectResultSizeDataAccessException;
4539
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
4640
import org.springframework.dao.OptimisticLockingFailureException;
4741
import org.springframework.data.annotation.Id;
@@ -64,6 +58,9 @@
6458
import org.springframework.data.relational.core.mapping.MappedCollection;
6559
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
6660
import org.springframework.data.relational.core.mapping.Table;
61+
import org.springframework.data.relational.core.query.Criteria;
62+
import org.springframework.data.relational.core.query.CriteriaDefinition;
63+
import org.springframework.data.relational.core.query.Query;
6764
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
6865
import org.springframework.test.context.ActiveProfiles;
6966

@@ -223,6 +220,62 @@ void findAllById() {
223220
.containsExactlyInAnyOrder(tuple(entity.id, "entity"), tuple(yetAnother.id, "yetAnother"));
224221
}
225222

223+
@Test // GH-1601
224+
void findAllByQuery() {
225+
226+
template.save(SimpleListParent.of("one", "one_1"));
227+
SimpleListParent two = template.save(SimpleListParent.of("two", "two_1", "two_2"));
228+
template.save(SimpleListParent.of("three", "three_1", "three_2", "three_3"));
229+
230+
CriteriaDefinition criteria = CriteriaDefinition.from(Criteria.where("id").is(two.id));
231+
Query query = Query.query(criteria);
232+
Iterable<SimpleListParent> reloadedById = template.findAll(query, SimpleListParent.class);
233+
234+
assertThat(reloadedById).extracting(e -> e.id, e -> e.content.size()).containsExactly(tuple(two.id, 2));
235+
}
236+
237+
@Test // GH-1601
238+
void findOneByQuery() {
239+
240+
template.save(SimpleListParent.of("one", "one_1"));
241+
SimpleListParent two = template.save(SimpleListParent.of("two", "two_1", "two_2"));
242+
template.save(SimpleListParent.of("three", "three_1", "three_2", "three_3"));
243+
244+
CriteriaDefinition criteria = CriteriaDefinition.from(Criteria.where("id").is(two.id));
245+
Query query = Query.query(criteria);
246+
Optional<SimpleListParent> reloadedById = template.findOne(query, SimpleListParent.class);
247+
248+
assertThat(reloadedById).get().extracting(e -> e.id, e -> e.content.size()).containsExactly(two.id, 2);
249+
}
250+
251+
@Test // GH-1601
252+
void findOneByQueryNothingFound() {
253+
254+
template.save(SimpleListParent.of("one", "one_1"));
255+
SimpleListParent two = template.save(SimpleListParent.of("two", "two_1", "two_2"));
256+
template.save(SimpleListParent.of("three", "three_1", "three_2", "three_3"));
257+
258+
CriteriaDefinition criteria = CriteriaDefinition.from(Criteria.where("id").is(4711));
259+
Query query = Query.query(criteria);
260+
Optional<SimpleListParent> reloadedById = template.findOne(query, SimpleListParent.class);
261+
262+
assertThat(reloadedById).isEmpty();
263+
}
264+
265+
@Test // GH-1601
266+
void findOneByQueryToManyResults() {
267+
268+
template.save(SimpleListParent.of("one", "one_1"));
269+
SimpleListParent two = template.save(SimpleListParent.of("two", "two_1", "two_2"));
270+
template.save(SimpleListParent.of("three", "three_1", "three_2", "three_3"));
271+
272+
CriteriaDefinition criteria = CriteriaDefinition.from(Criteria.where("id").not(two.id));
273+
Query query = Query.query(criteria);
274+
275+
assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class)
276+
.isThrownBy(() -> template.findOne(query, SimpleListParent.class));
277+
}
278+
226279
@Test // DATAJDBC-112
227280
@EnabledOnFeature(SUPPORTS_QUOTED_IDS)
228281
void saveAndLoadAnEntityWithReferencedEntityById() {
@@ -1266,6 +1319,29 @@ static class ChildNoId {
12661319
private String content;
12671320
}
12681321

1322+
@SuppressWarnings("unused")
1323+
static class SimpleListParent {
1324+
1325+
@Id private Long id;
1326+
String name;
1327+
List<ElementNoId> content = new ArrayList<>();
1328+
1329+
static SimpleListParent of(String name, String... contents) {
1330+
1331+
SimpleListParent parent = new SimpleListParent();
1332+
parent.name = name;
1333+
1334+
for (String content : contents) {
1335+
1336+
ElementNoId element = new ElementNoId();
1337+
element.content = content;
1338+
parent.content.add(element);
1339+
}
1340+
1341+
return parent;
1342+
}
1343+
}
1344+
12691345
@Table("LIST_PARENT")
12701346
@SuppressWarnings("unused")
12711347
static class ListParent {

spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql

+9-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ DROP TABLE ONE_TO_ONE_PARENT;
66

77
DROP TABLE ELEMENT_NO_ID;
88
DROP TABLE LIST_PARENT;
9+
DROP TABLE SIMPLE_LIST_PARENT;
910

1011
DROP TABLE BYTE_ARRAY_OWNER;
1112

@@ -74,11 +75,18 @@ CREATE TABLE LIST_PARENT
7475
"id4" BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
7576
NAME VARCHAR(100)
7677
);
78+
CREATE TABLE SIMPLE_LIST_PARENT
79+
(
80+
ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
81+
NAME VARCHAR(100)
82+
);
7783
CREATE TABLE ELEMENT_NO_ID
7884
(
7985
CONTENT VARCHAR(100),
8086
LIST_PARENT_KEY BIGINT,
81-
LIST_PARENT BIGINT
87+
SIMPLE_LIST_PARENT_KEY BIGINT,
88+
LIST_PARENT BIGINT,
89+
SIMPLE_LIST_PARENT BIGINT
8290
);
8391
ALTER TABLE ELEMENT_NO_ID
8492
ADD FOREIGN KEY (LIST_PARENT)

spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql

+8
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,17 @@ CREATE TABLE LIST_PARENT
3232
NAME VARCHAR(100)
3333
);
3434

35+
CREATE TABLE SIMPLE_LIST_PARENT
36+
(
37+
ID SERIAL PRIMARY KEY,
38+
NAME VARCHAR(100)
39+
);
40+
3541
CREATE TABLE element_no_id
3642
(
3743
content VARCHAR(100),
44+
SIMPLE_LIST_PARENT_key BIGINT,
45+
SIMPLE_LIST_PARENT INTEGER,
3846
LIST_PARENT_key BIGINT,
3947
LIST_PARENT INTEGER
4048
);

spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql

+7
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ CREATE TABLE Child_No_Id
2626
content VARCHAR(30)
2727
);
2828

29+
CREATE TABLE SIMPLE_LIST_PARENT
30+
(
31+
ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
32+
NAME VARCHAR(100)
33+
);
2934
CREATE TABLE LIST_PARENT
3035
(
3136
"id4" BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
@@ -34,6 +39,8 @@ CREATE TABLE LIST_PARENT
3439
CREATE TABLE ELEMENT_NO_ID
3540
(
3641
CONTENT VARCHAR(100),
42+
SIMPLE_LIST_PARENT_KEY BIGINT,
43+
SIMPLE_LIST_PARENT BIGINT,
3744
LIST_PARENT_KEY BIGINT,
3845
LIST_PARENT BIGINT
3946
);

spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql

+7
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,16 @@ CREATE TABLE LIST_PARENT
3131
`id4` BIGINT AUTO_INCREMENT PRIMARY KEY,
3232
NAME VARCHAR(100)
3333
);
34+
CREATE TABLE SIMPLE_LIST_PARENT
35+
(
36+
ID BIGINT AUTO_INCREMENT PRIMARY KEY,
37+
NAME VARCHAR(100)
38+
);
3439
CREATE TABLE element_no_id
3540
(
3641
CONTENT VARCHAR(100),
42+
SIMPLE_LIST_PARENT_key BIGINT,
43+
SIMPLE_LIST_PARENT BIGINT,
3744
LIST_PARENT_key BIGINT,
3845
LIST_PARENT BIGINT
3946
);

0 commit comments

Comments
 (0)