Skip to content

Commit a6fb4df

Browse files
DiegoKrupitzaschauder
authored andcommitted
Support for Query By Example.
Original pull request #1195 Closes #1192
1 parent ee6c2c8 commit a6fb4df

17 files changed

+1447
-12
lines changed

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,13 @@
1515
*/
1616
package org.springframework.data.jdbc.core;
1717

18+
import java.util.Optional;
19+
20+
import org.springframework.data.domain.Example;
1821
import org.springframework.data.domain.Page;
1922
import org.springframework.data.domain.Pageable;
2023
import org.springframework.data.domain.Sort;
24+
import org.springframework.data.relational.core.query.Query;
2125
import org.springframework.lang.Nullable;
2226

2327
/**
@@ -27,6 +31,7 @@
2731
* @author Thomas Lang
2832
* @author Milan Milanov
2933
* @author Chirag Tailor
34+
* @author Diego Krupitza
3035
*/
3136
public interface JdbcAggregateOperations {
3237

@@ -183,4 +188,54 @@ public interface JdbcAggregateOperations {
183188
* @since 2.0
184189
*/
185190
<T> Page<T> findAll(Class<T> domainType, Pageable pageable);
191+
192+
/**
193+
* Execute a {@code SELECT} query and convert the resulting item to an entity ensuring exactly one result.
194+
*
195+
* @param query must not be {@literal null}.
196+
* @param entityClass the entity type must not be {@literal null}.
197+
* @return exactly one result or {@link Optional#empty()} if no match found.
198+
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
199+
*/
200+
<T> Optional<T> selectOne(Query query, Class<T> entityClass);
201+
202+
/**
203+
* Execute a {@code SELECT} query and convert the resulting items to a {@link Iterable} that is sorted.
204+
*
205+
* @param query must not be {@literal null}.
206+
* @param entityClass the entity type must not be {@literal null}.
207+
* @param sort the sorting that should be used on the result.
208+
* @return a non-null sorted list with all the matching results.
209+
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
210+
*/
211+
<T> Iterable<T> select(Query query, Class<T> entityClass, Sort sort);
212+
213+
/**
214+
* Determine whether there are aggregates that match the {@link Query}
215+
*
216+
* @param query must not be {@literal null}.
217+
* @param entityClass the entity type must not be {@literal null}.
218+
* @return {@literal true} if the object exists.
219+
*/
220+
<T> boolean exists(Query query, Class<T> entityClass);
221+
222+
/**
223+
* Counts the number of aggregates of a given type that match the given <code>query</code>.
224+
*
225+
* @param query must not be {@literal null}.
226+
* @param entityClass the entity type must not be {@literal null}.
227+
* @return the number of instances stored in the database. Guaranteed to be not {@code null}.
228+
*/
229+
<T> long count(Query query, Class<T> entityClass);
230+
231+
/**
232+
* Returns a {@link Page} of entities matching the given {@link Query}. In case no match could be found, an empty
233+
* {@link Page} is returned.
234+
*
235+
* @param query must not be {@literal null}.
236+
* @param entityClass the entity type must not be {@literal null}.
237+
* @param pageable can be null.
238+
* @return a {@link Page} of entities matching the given {@link Example}.
239+
*/
240+
<T> Page<T> select(Query query, Class<T> entityClass, Pageable pageable);
186241
}

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.LinkedHashMap;
2121
import java.util.List;
2222
import java.util.Map;
23+
import java.util.Optional;
2324
import java.util.function.Function;
2425
import java.util.stream.Collectors;
2526
import java.util.stream.StreamSupport;
@@ -46,6 +47,7 @@
4647
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
4748
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
4849
import org.springframework.data.relational.core.mapping.event.*;
50+
import org.springframework.data.relational.core.query.Query;
4951
import org.springframework.data.support.PageableExecutionUtils;
5052
import org.springframework.lang.Nullable;
5153
import org.springframework.util.Assert;
@@ -61,6 +63,7 @@
6163
* @author Milan Milanov
6264
* @author Myeonghyeon Lee
6365
* @author Chirag Tailor
66+
* @author Diego Krupitza
6467
*/
6568
public class JdbcAggregateTemplate implements JdbcAggregateOperations {
6669

@@ -71,6 +74,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
7174

7275
private final DataAccessStrategy accessStrategy;
7376
private final AggregateChangeExecutor executor;
77+
7478
private final JdbcConverter converter;
7579

7680
private EntityCallbacks entityCallbacks = EntityCallbacks.create();
@@ -238,6 +242,38 @@ public <T> Page<T> findAll(Class<T> domainType, Pageable pageable) {
238242
return PageableExecutionUtils.getPage(content, pageable, () -> accessStrategy.count(domainType));
239243
}
240244

245+
@Override
246+
public <T> Optional<T> selectOne(Query query, Class<T> entityClass) {
247+
return accessStrategy.selectOne(query, entityClass);
248+
}
249+
250+
@Override
251+
public <T> Iterable<T> select(Query query, Class<T> entityClass, Sort sort) {
252+
return accessStrategy.select(query, entityClass);
253+
}
254+
255+
@Override
256+
public <T> boolean exists(Query query, Class<T> entityClass) {
257+
return accessStrategy.exists(query, entityClass);
258+
}
259+
260+
@Override
261+
public <T> long count(Query query, Class<T> entityClass) {
262+
return accessStrategy.count(query, entityClass);
263+
}
264+
265+
@Override
266+
public <T> Page<T> select(Query query, Class<T> entityClass, Pageable pageable) {
267+
Iterable<T> items = triggerAfterConvert(accessStrategy.select(query, entityClass, pageable));
268+
List<T> content = StreamSupport.stream(items.spliterator(), false).collect(Collectors.toList());
269+
270+
return PageableExecutionUtils.getPage(content, pageable, () -> accessStrategy.count(query, entityClass));
271+
}
272+
273+
/*
274+
* (non-Javadoc)
275+
* @see org.springframework.data.jdbc.core.JdbcAggregateOperations#findAll(java.lang.Class)
276+
*/
241277
@Override
242278
public <T> Iterable<T> findAll(Class<T> domainType) {
243279

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.util.ArrayList;
1919
import java.util.List;
20+
import java.util.Optional;
2021
import java.util.function.Consumer;
2122
import java.util.function.Function;
2223

@@ -25,6 +26,7 @@
2526
import org.springframework.data.mapping.PersistentPropertyPath;
2627
import org.springframework.data.relational.core.conversion.IdValueSource;
2728
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
29+
import org.springframework.data.relational.core.query.Query;
2830
import org.springframework.data.relational.core.sql.LockMode;
2931

3032
import static java.lang.Boolean.*;
@@ -39,6 +41,7 @@
3941
* @author Milan Milanov
4042
* @author Myeonghyeon Lee
4143
* @author Chirag Tailor
44+
* @author Diego Krupitza
4245
* @since 1.1
4346
*/
4447
public class CascadingDataAccessStrategy implements DataAccessStrategy {
@@ -160,6 +163,31 @@ public <T> Iterable<T> findAll(Class<T> domainType, Pageable pageable) {
160163
return collect(das -> das.findAll(domainType, pageable));
161164
}
162165

166+
@Override
167+
public <T> Optional<T> selectOne(Query query, Class<T> probeType) {
168+
return collect(das -> das.selectOne(query, probeType));
169+
}
170+
171+
@Override
172+
public <T> Iterable<T> select(Query query, Class<T> probeType) {
173+
return collect(das -> das.select(query, probeType));
174+
}
175+
176+
@Override
177+
public <T> Iterable<T> select(Query query, Class<T> probeType, Pageable pageable) {
178+
return collect(das -> das.select(query, probeType, pageable));
179+
}
180+
181+
@Override
182+
public <T> boolean exists(Query query, Class<T> probeType) {
183+
return collect(das -> das.exists(query, probeType));
184+
}
185+
186+
@Override
187+
public <T> long count(Query query, Class<T> probeType) {
188+
return collect(das -> das.count(query, probeType));
189+
}
190+
163191
private <T> T collect(Function<DataAccessStrategy, T> function) {
164192

165193
return strategies.stream().collect(new FunctionCollector<>(function));

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.util.List;
1919
import java.util.Map;
20+
import java.util.Optional;
2021

2122
import org.springframework.dao.OptimisticLockingFailureException;
2223
import org.springframework.data.domain.Pageable;
@@ -25,6 +26,7 @@
2526
import org.springframework.data.mapping.PersistentPropertyPath;
2627
import org.springframework.data.relational.core.conversion.IdValueSource;
2728
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
29+
import org.springframework.data.relational.core.query.Query;
2830
import org.springframework.data.relational.core.sql.LockMode;
2931
import org.springframework.lang.Nullable;
3032

@@ -38,6 +40,7 @@
3840
* @author Milan Milanov
3941
* @author Myeonghyeon Lee
4042
* @author Chirag Tailor
43+
* @author Diego Krupitza
4144
*/
4245
public interface DataAccessStrategy extends RelationResolver {
4346

@@ -281,4 +284,54 @@ Iterable<Object> findAllByPath(Identifier identifier,
281284
* @since 2.0
282285
*/
283286
<T> Iterable<T> findAll(Class<T> domainType, Pageable pageable);
287+
288+
/**
289+
* Execute a {@code SELECT} query and convert the resulting item to an entity ensuring exactly one result.
290+
*
291+
* @param query must not be {@literal null}.
292+
* @param probeType the type of entities. Must not be {@code null}.
293+
* @return exactly one result or {@link Optional#empty()} if no match found.
294+
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
295+
*/
296+
<T> Optional<T> selectOne(Query query, Class<T> probeType);
297+
298+
/**
299+
* Execute a {@code SELECT} query and convert the resulting items to a {@link Iterable}.
300+
*
301+
* @param query must not be {@literal null}.
302+
* @param probeType the type of entities. Must not be {@code null}.
303+
* @return a non-null list with all the matching results.
304+
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
305+
*/
306+
<T> Iterable<T> select(Query query, Class<T> probeType);
307+
308+
/**
309+
* Execute a {@code SELECT} query and convert the resulting items to a {@link Iterable}. Applies the {@link Pageable}
310+
* to the result.
311+
*
312+
* @param query must not be {@literal null}.
313+
* @param probeType the type of entities. Must not be {@literal null}.
314+
* @param pageable the pagination that should be applied. Must not be {@literal null}.
315+
* @return a non-null list with all the matching results.
316+
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
317+
*/
318+
<T> Iterable<T> select(Query query, Class<T> probeType, Pageable pageable);
319+
320+
/**
321+
* Determine whether there is an aggregate of type <code>probeType</code> that matches the provided {@link Query}.
322+
*
323+
* @param query must not be {@literal null}.
324+
* @param probeType the type of entities. Must not be {@code null}.
325+
* @return {@literal true} if the object exists.
326+
*/
327+
<T> boolean exists(Query query, Class<T> probeType);
328+
329+
/**
330+
* Counts the rows in the table representing the given probe type, that match the given <code>query</code>.
331+
*
332+
* @param probeType the probe type for which to count the elements. Must not be {@code null}.
333+
* @param query the query which elements have to match.
334+
* @return the count. Guaranteed to be not {@code null}.
335+
*/
336+
<T> long count(Query query, Class<T> probeType);
284337
}

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
import java.sql.ResultSet;
2121
import java.util.Collections;
2222
import java.util.List;
23+
import java.util.Map;
2324
import java.util.Optional;
25+
import java.util.function.Predicate;
2426

2527
import org.springframework.dao.EmptyResultDataAccessException;
2628
import org.springframework.dao.OptimisticLockingFailureException;
@@ -32,10 +34,12 @@
3234
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
3335
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
3436
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
37+
import org.springframework.data.relational.core.query.Query;
3538
import org.springframework.data.relational.core.sql.IdentifierProcessing;
3639
import org.springframework.data.relational.core.sql.LockMode;
3740
import org.springframework.data.relational.core.sql.SqlIdentifier;
3841
import org.springframework.jdbc.core.RowMapper;
42+
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
3943
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
4044
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
4145
import org.springframework.lang.Nullable;
@@ -56,6 +60,7 @@
5660
* @author Yunyoung LEE
5761
* @author Radim Tlusty
5862
* @author Chirag Tailor
63+
* @author Diego Krupitza
5964
* @since 1.1
6065
*/
6166
public class DefaultDataAccessStrategy implements DataAccessStrategy {
@@ -336,6 +341,61 @@ public <T> Iterable<T> findAll(Class<T> domainType, Pageable pageable) {
336341
return operations.query(sql(domainType).getFindAll(pageable), (RowMapper<T>) getEntityRowMapper(domainType));
337342
}
338343

344+
@Override
345+
public <T> Optional<T> selectOne(Query query, Class<T> probeType) {
346+
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
347+
String sqlQuery = sql(probeType).selectByQuery(query, parameterSource);
348+
349+
T foundObject;
350+
try {
351+
foundObject = operations.queryForObject(sqlQuery, parameterSource, (RowMapper<T>) getEntityRowMapper(probeType));
352+
} catch (EmptyResultDataAccessException e) {
353+
foundObject = null;
354+
}
355+
356+
return Optional.ofNullable(foundObject);
357+
}
358+
359+
@Override
360+
public <T> Iterable<T> select(Query query, Class<T> probeType) {
361+
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
362+
String sqlQuery = sql(probeType).selectByQuery(query, parameterSource);
363+
364+
return operations.query(sqlQuery, parameterSource, (RowMapper<T>) getEntityRowMapper(probeType));
365+
}
366+
367+
@Override
368+
public <T> Iterable<T> select(Query query, Class<T> probeType, Pageable pageable) {
369+
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
370+
String sqlQuery = sql(probeType).selectByQuery(query, parameterSource, pageable);
371+
372+
return operations.query(sqlQuery, parameterSource, (RowMapper<T>) getEntityRowMapper(probeType));
373+
}
374+
375+
@Override
376+
public <T> boolean exists(Query query, Class<T> probeType) {
377+
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
378+
379+
String sqlQuery = sql(probeType).existsByQuery(query, parameterSource);
380+
381+
Boolean result = operations.queryForObject(sqlQuery, parameterSource, Boolean.class);
382+
Assert.notNull(result, "The result of an exists query must not be null");
383+
384+
return result;
385+
}
386+
387+
@Override
388+
public <T> long count(Query query, Class<T> probeType) {
389+
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
390+
String sqlQuery = sql(probeType).countByQuery(query, parameterSource);
391+
392+
Long result = operations.queryForObject(sqlQuery, parameterSource, Long.class);
393+
394+
Assert.notNull(result, "The result of a count query must not be null.");
395+
396+
return result;
397+
}
398+
339399
private EntityRowMapper<?> getEntityRowMapper(Class<?> domainType) {
340400
return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), converter);
341401
}

0 commit comments

Comments
 (0)