Skip to content

Commit b3759db

Browse files
committed
Add support for arbitrary where clauses in Single Query Loading.
Closes #1601
1 parent 0ee61d2 commit b3759db

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
@@ -24,15 +24,8 @@
2424
import static org.springframework.test.context.TestExecutionListeners.MergeMode.*;
2525

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

@@ -44,6 +37,7 @@
4437
import org.springframework.context.annotation.Bean;
4538
import org.springframework.context.annotation.Configuration;
4639
import org.springframework.context.annotation.Import;
40+
import org.springframework.dao.IncorrectResultSizeDataAccessException;
4741
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
4842
import org.springframework.dao.OptimisticLockingFailureException;
4943
import org.springframework.data.annotation.Id;
@@ -67,6 +61,9 @@
6761
import org.springframework.data.relational.core.mapping.MappedCollection;
6862
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
6963
import org.springframework.data.relational.core.mapping.Table;
64+
import org.springframework.data.relational.core.query.Criteria;
65+
import org.springframework.data.relational.core.query.CriteriaDefinition;
66+
import org.springframework.data.relational.core.query.Query;
7067
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
7168
import org.springframework.test.context.ActiveProfiles;
7269
import org.springframework.test.context.ContextConfiguration;
@@ -233,6 +230,62 @@ void findAllById() {
233230
.containsExactlyInAnyOrder(tuple(entity.id, "entity"), tuple(yetAnother.id, "yetAnother"));
234231
}
235232

233+
@Test // GH-1601
234+
void findAllByQuery() {
235+
236+
template.save(SimpleListParent.of("one", "one_1"));
237+
SimpleListParent two = template.save(SimpleListParent.of("two", "two_1", "two_2"));
238+
template.save(SimpleListParent.of("three", "three_1", "three_2", "three_3"));
239+
240+
CriteriaDefinition criteria = CriteriaDefinition.from(Criteria.where("id").is(two.id));
241+
Query query = Query.query(criteria);
242+
Iterable<SimpleListParent> reloadedById = template.findAll(query, SimpleListParent.class);
243+
244+
assertThat(reloadedById).extracting(e -> e.id, e -> e.content.size()).containsExactly(tuple(two.id, 2));
245+
}
246+
247+
@Test // GH-1601
248+
void findOneByQuery() {
249+
250+
template.save(SimpleListParent.of("one", "one_1"));
251+
SimpleListParent two = template.save(SimpleListParent.of("two", "two_1", "two_2"));
252+
template.save(SimpleListParent.of("three", "three_1", "three_2", "three_3"));
253+
254+
CriteriaDefinition criteria = CriteriaDefinition.from(Criteria.where("id").is(two.id));
255+
Query query = Query.query(criteria);
256+
Optional<SimpleListParent> reloadedById = template.findOne(query, SimpleListParent.class);
257+
258+
assertThat(reloadedById).get().extracting(e -> e.id, e -> e.content.size()).containsExactly(two.id, 2);
259+
}
260+
261+
@Test // GH-1601
262+
void findOneByQueryNothingFound() {
263+
264+
template.save(SimpleListParent.of("one", "one_1"));
265+
SimpleListParent two = template.save(SimpleListParent.of("two", "two_1", "two_2"));
266+
template.save(SimpleListParent.of("three", "three_1", "three_2", "three_3"));
267+
268+
CriteriaDefinition criteria = CriteriaDefinition.from(Criteria.where("id").is(4711));
269+
Query query = Query.query(criteria);
270+
Optional<SimpleListParent> reloadedById = template.findOne(query, SimpleListParent.class);
271+
272+
assertThat(reloadedById).isEmpty();
273+
}
274+
275+
@Test // GH-1601
276+
void findOneByQueryToManyResults() {
277+
278+
template.save(SimpleListParent.of("one", "one_1"));
279+
SimpleListParent two = template.save(SimpleListParent.of("two", "two_1", "two_2"));
280+
template.save(SimpleListParent.of("three", "three_1", "three_2", "three_3"));
281+
282+
CriteriaDefinition criteria = CriteriaDefinition.from(Criteria.where("id").not(two.id));
283+
Query query = Query.query(criteria);
284+
285+
assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class)
286+
.isThrownBy(() -> template.findOne(query, SimpleListParent.class));
287+
}
288+
236289
@Test // DATAJDBC-112
237290
@EnabledOnFeature(SUPPORTS_QUOTED_IDS)
238291
void saveAndLoadAnEntityWithReferencedEntityById() {
@@ -1276,6 +1329,29 @@ static class ChildNoId {
12761329
private String content;
12771330
}
12781331

1332+
@SuppressWarnings("unused")
1333+
static class SimpleListParent {
1334+
1335+
@Id private Long id;
1336+
String name;
1337+
List<ElementNoId> content = new ArrayList<>();
1338+
1339+
static SimpleListParent of(String name, String... contents) {
1340+
1341+
SimpleListParent parent = new SimpleListParent();
1342+
parent.name = name;
1343+
1344+
for (String content : contents) {
1345+
1346+
ElementNoId element = new ElementNoId();
1347+
element.content = content;
1348+
parent.content.add(element);
1349+
}
1350+
1351+
return parent;
1352+
}
1353+
}
1354+
12791355
@Table("LIST_PARENT")
12801356
@SuppressWarnings("unused")
12811357
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)