Skip to content

Commit 239a720

Browse files
committed
Support of QueryByExampleExecutor#findAll(Example<S> example, Pageable pageable).
This commit introduces the find by example pageable method `findAll(Example<S> example, Pageable pageable)` to spring-data-jdbc. MyBatis implementation is missing since I do not have the knowledge for this. Related tickets spring-projects#1192
1 parent a9f653b commit 239a720

File tree

11 files changed

+253
-7
lines changed

11 files changed

+253
-7
lines changed

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

+10
Original file line numberDiff line numberDiff line change
@@ -193,4 +193,14 @@ public interface JdbcAggregateOperations {
193193
* @return the number of instances stored in the database. Guaranteed to be not {@code null}.
194194
*/
195195
<T> long count(Example<T> example);
196+
197+
/**
198+
* Returns a {@link Page} of entities matching the given {@link Example}. In case no match could be found, an empty
199+
* {@link Page} is returned.
200+
*
201+
* @param example must not be null
202+
* @param pageable can be null.
203+
* @return a {@link Page} of entities matching the given {@link Example}.
204+
*/
205+
<T> Page<T> select(Example<T> example, Pageable pageable);
196206
}

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

+11
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,17 @@ public <T> long count(Example<T> example) {
294294
return accessStrategy.count(query, probeType);
295295
}
296296

297+
@Override
298+
public <T> Page<T> select(Example<T> example, Pageable pageable) {
299+
Query query = this.exampleMapper.getMappedExample(example);
300+
Class<T> probeType = example.getProbeType();
301+
302+
Iterable<T> items = triggerAfterConvert(accessStrategy.select(query, probeType, pageable));
303+
List<T> content = StreamSupport.stream(items.spliterator(), false).collect(Collectors.toList());
304+
305+
return PageableExecutionUtils.getPage(content, pageable, () -> accessStrategy.count(query, probeType));
306+
}
307+
297308
/*
298309
* (non-Javadoc)
299310
* @see org.springframework.data.jdbc.core.JdbcAggregateOperations#findAll(java.lang.Class)

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

+5
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ public <T> Iterable<T> select(Query query, Class<T> probeType) {
221221
return collect(das -> das.select(query, probeType));
222222
}
223223

224+
@Override
225+
public <T> Iterable<T> select(Query query, Class<T> probeType, Pageable pageable) {
226+
return collect(das -> das.select(query, probeType, pageable));
227+
}
228+
224229
@Override
225230
public <T> boolean exists(Query query, Class<T> probeType) {
226231
return collect(das -> das.exists(query, probeType));

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

+13-1
Original file line numberDiff line numberDiff line change
@@ -245,11 +245,23 @@ Iterable<Object> findAllByPath(Identifier identifier,
245245
*
246246
* @param query must not be {@literal null}.
247247
* @param probeType the type of entities. Must not be {@code null}.
248-
* @return a non null list with all the matching results.
248+
* @return a non-null list with all the matching results.
249249
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
250250
*/
251251
<T> Iterable<T> select(Query query, Class<T> probeType);
252252

253+
/**
254+
* Execute a {@code SELECT} query and convert the resulting items to a {@link Iterable}. Applies the {@link Pageable}
255+
* to the result.
256+
*
257+
* @param query must not be {@literal null}.
258+
* @param probeType the type of entities. Must not be {@literal null}.
259+
* @param pageable the pagination that should be applied. Must not be {@literal null}.
260+
* @return a non-null list with all the matching results.
261+
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
262+
*/
263+
<T> Iterable<T> select(Query query, Class<T> probeType, Pageable pageable);
264+
253265
/**
254266
* Determine whether there is an aggregate of type <code>probeType</code> that matches the provided {@link Query}.
255267
*

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

+8
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,14 @@ public <T> Iterable<T> select(Query query, Class<T> probeType) {
457457
return operations.query(sqlQuery, parameterSource, (RowMapper<T>) getEntityRowMapper(probeType));
458458
}
459459

460+
@Override
461+
public <T> Iterable<T> select(Query query, Class<T> probeType, Pageable pageable) {
462+
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
463+
String sqlQuery = sql(probeType).selectByQuery(query, parameterSource, pageable);
464+
465+
return operations.query(sqlQuery, parameterSource, (RowMapper<T>) getEntityRowMapper(probeType));
466+
}
467+
460468
@Override
461469
public <T> boolean exists(Query query, Class<T> probeType) {
462470
MapSqlParameterSource parameterSource = new MapSqlParameterSource();

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

+5
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,11 @@ public <T> Iterable<T> select(Query query, Class<T> probeType) {
217217
return delegate.select(query, probeType);
218218
}
219219

220+
@Override
221+
public <T> Iterable<T> select(Query query, Class<T> probeType, Pageable pageable) {
222+
return delegate.select(query, probeType, pageable);
223+
}
224+
220225
@Override
221226
public <T> boolean exists(Query query, Class<T> probeType) {
222227
return delegate.exists(query, probeType);

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

+25
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,31 @@ public String selectByQuery(Query query, MapSqlParameterSource parameterSource)
745745
return render(select);
746746
}
747747

748+
/**
749+
* Constructs a single sql query that performs select based on the provided query and pagination information.
750+
* Additional the bindings for the where clause are stored after execution into the <code>parameterSource</code>
751+
*
752+
* @param query the query to base the select on. Must not be null.
753+
* @param pageable the pageable to perform on the select.
754+
* @param parameterSource the source for holding the bindings.
755+
* @return a non null query string.
756+
*/
757+
public String selectByQuery(Query query, MapSqlParameterSource parameterSource, Pageable pageable) {
758+
759+
Assert.notNull(parameterSource, "parameterSource must not be null");
760+
761+
SelectBuilder.SelectWhere selectBuilder = selectBuilder();
762+
763+
// first apply query and then pagination. This means possible query sorting and limiting might be overwritten by the
764+
// pagination. This is desired.
765+
SelectBuilder.SelectOrdered selectOrdered = applyQueryOnSelect(query, parameterSource, selectBuilder);
766+
selectOrdered = applyPagination(pageable, selectOrdered);
767+
selectOrdered = selectOrdered.orderBy(extractOrderByFields(pageable.getSort()));
768+
769+
Select select = selectOrdered.build();
770+
return render(select);
771+
}
772+
748773
/**
749774
* Constructs a single sql query that performs select count based on the provided query for checking existence.
750775
* Additional the bindings for the where clause are stored after execution into the <code>parameterSource</code>

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java

+9-2
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@
2323
import java.util.Optional;
2424
import java.util.stream.Collectors;
2525

26-
import org.apache.ibatis.session.SqlSession;
27-
import org.mybatis.spring.SqlSessionTemplate;
2826
import org.apache.commons.logging.Log;
2927
import org.apache.commons.logging.LogFactory;
28+
import org.apache.ibatis.session.SqlSession;
29+
import org.mybatis.spring.SqlSessionTemplate;
3030
import org.springframework.dao.EmptyResultDataAccessException;
3131
import org.springframework.data.domain.Pageable;
3232
import org.springframework.data.domain.Sort;
@@ -381,6 +381,13 @@ public <T> Iterable<T> select(Query query, Class<T> probeType) {
381381
return null;
382382
}
383383

384+
@Override
385+
public <T> Iterable<T> select(Query query, Class<T> probeType, Pageable pageable) {
386+
// TODO: DIEGO find help for this one
387+
// I have zero MyBatis knowledge.
388+
return null;
389+
}
390+
384391
@Override
385392
public <T> boolean exists(Query query, Class<T> probeType) {
386393
// TODO: DIEGO find help for this one

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,9 @@ public <S extends T> Iterable<S> findAll(Example<S> example, Sort sort) {
214214

215215
@Override
216216
public <S extends T> Page<S> findAll(Example<S> example, Pageable pageable) {
217-
// TODO: impl
218-
return null;
217+
Assert.notNull(example, "Example must not be null!");
218+
219+
return this.entityOperations.select(example, pageable);
219220
}
220221

221222
@Override

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

+26-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import org.springframework.data.annotation.Id;
2929
import org.springframework.data.annotation.ReadOnlyProperty;
3030
import org.springframework.data.annotation.Version;
31-
import org.springframework.data.domain.Example;
3231
import org.springframework.data.domain.PageRequest;
3332
import org.springframework.data.domain.Pageable;
3433
import org.springframework.data.domain.Sort;
@@ -783,6 +782,32 @@ void countByQuerySimpleValidTest() {
783782
.containsOnly(entry("x_name", probe.name));
784783
}
785784

785+
@Test
786+
void selectByQueryPaginationValidTest() {
787+
final SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class);
788+
789+
DummyEntity probe = new DummyEntity();
790+
probe.name = "Diego";
791+
792+
Criteria criteria = Criteria.where("name").is(probe.name);
793+
Query query = Query.query(criteria);
794+
795+
PageRequest pageRequest = PageRequest.of(2, 1, Sort.by(Sort.Order.asc("name")));
796+
797+
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
798+
799+
String generatedSQL = sqlGenerator.selectByQuery(query, parameterSource, pageRequest);
800+
assertThat(generatedSQL) //
801+
.isNotNull() //
802+
.contains(":x_name") //
803+
.containsIgnoringCase("ORDER BY dummy_entity.x_name ASC") //
804+
.containsIgnoringCase("LIMIT 1") //
805+
.containsIgnoringCase("OFFSET 2 LIMIT 1");
806+
807+
assertThat(parameterSource.getValues()) //
808+
.containsOnly(entry("x_name", probe.name));
809+
}
810+
786811
private SqlIdentifier getAlias(Object maybeAliased) {
787812

788813
if (maybeAliased instanceof Aliased) {

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java

+138-1
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,17 @@
3232
import java.time.ZoneOffset;
3333
import java.util.ArrayList;
3434
import java.util.Arrays;
35+
import java.util.Collections;
3536
import java.util.List;
3637
import java.util.Optional;
38+
import java.util.stream.Stream;
3739

3840
import org.junit.jupiter.api.BeforeEach;
3941
import org.junit.jupiter.api.Test;
4042
import org.junit.jupiter.api.extension.ExtendWith;
43+
import org.junit.jupiter.params.ParameterizedTest;
44+
import org.junit.jupiter.params.provider.Arguments;
45+
import org.junit.jupiter.params.provider.MethodSource;
4146
import org.springframework.beans.factory.annotation.Autowired;
4247
import org.springframework.beans.factory.config.PropertiesFactoryBean;
4348
import org.springframework.context.ApplicationListener;
@@ -53,7 +58,6 @@
5358
import org.springframework.data.domain.Pageable;
5459
import org.springframework.data.domain.Slice;
5560
import org.springframework.data.jdbc.core.mapping.AggregateReference;
56-
import org.springframework.data.relational.repository.Lock;
5761
import org.springframework.data.jdbc.repository.query.Modifying;
5862
import org.springframework.data.jdbc.repository.query.Query;
5963
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
@@ -65,6 +69,7 @@
6569
import org.springframework.data.relational.core.mapping.event.AfterConvertEvent;
6670
import org.springframework.data.relational.core.mapping.event.AfterLoadEvent;
6771
import org.springframework.data.relational.core.sql.LockMode;
72+
import org.springframework.data.relational.repository.Lock;
6873
import org.springframework.data.repository.CrudRepository;
6974
import org.springframework.data.repository.core.NamedQueries;
7075
import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries;
@@ -693,6 +698,138 @@ void findAllByExampleShouldGetNone() {
693698
.isEmpty();
694699
}
695700

701+
@Test
702+
void findAllByExamplePageableShouldGetOne() {
703+
704+
DummyEntity dummyEntity1 = createDummyEntity();
705+
dummyEntity1.setFlag(true);
706+
707+
repository.save(dummyEntity1);
708+
709+
DummyEntity dummyEntity2 = createDummyEntity();
710+
dummyEntity2.setName("Diego");
711+
712+
repository.save(dummyEntity2);
713+
714+
Example<DummyEntity> example = Example.of(new DummyEntity("Diego"));
715+
Pageable pageRequest = PageRequest.of(0, 10);
716+
717+
Iterable<DummyEntity> allFound = repository.findAll(example, pageRequest);
718+
719+
assertThat(allFound) //
720+
.isNotNull() //
721+
.hasSize(1) //
722+
.extracting(DummyEntity::getName) //
723+
.containsExactly(example.getProbe().getName());
724+
}
725+
726+
@Test
727+
void findAllByExamplePageableMultipleMatchShouldGetOne() {
728+
729+
DummyEntity dummyEntity1 = createDummyEntity();
730+
repository.save(dummyEntity1);
731+
732+
DummyEntity dummyEntity2 = createDummyEntity();
733+
repository.save(dummyEntity2);
734+
735+
Example<DummyEntity> example = Example.of(createDummyEntity());
736+
Pageable pageRequest = PageRequest.of(0, 10);
737+
738+
Iterable<DummyEntity> allFound = repository.findAll(example, pageRequest);
739+
740+
assertThat(allFound) //
741+
.isNotNull() //
742+
.hasSize(2) //
743+
.extracting(DummyEntity::getName) //
744+
.containsOnly(example.getProbe().getName());
745+
}
746+
747+
@Test
748+
void findAllByExamplePageableShouldGetNone() {
749+
750+
DummyEntity dummyEntity1 = createDummyEntity();
751+
dummyEntity1.setFlag(true);
752+
753+
repository.save(dummyEntity1);
754+
755+
Example<DummyEntity> example = Example.of(new DummyEntity("NotExisting"));
756+
Pageable pageRequest = PageRequest.of(0, 10);
757+
758+
Iterable<DummyEntity> allFound = repository.findAll(example, pageRequest);
759+
760+
assertThat(allFound) //
761+
.isNotNull() //
762+
.isEmpty();
763+
}
764+
765+
@Test
766+
void findAllByExamplePageableOutsidePageShouldGetNone() {
767+
768+
DummyEntity dummyEntity1 = createDummyEntity();
769+
repository.save(dummyEntity1);
770+
771+
DummyEntity dummyEntity2 = createDummyEntity();
772+
repository.save(dummyEntity2);
773+
774+
Example<DummyEntity> example = Example.of(createDummyEntity());
775+
Pageable pageRequest = PageRequest.of(10, 10);
776+
777+
Iterable<DummyEntity> allFound = repository.findAll(example, pageRequest);
778+
779+
assertThat(allFound) //
780+
.isNotNull() //
781+
.isEmpty();
782+
}
783+
784+
@ParameterizedTest
785+
@MethodSource("findAllByExamplePageableSource")
786+
void findAllByExamplePageable(Pageable pageRequest, int size, int totalPages, List<String> notContains) {
787+
788+
for (int i = 0; i < 100; i++) {
789+
DummyEntity dummyEntity = createDummyEntity();
790+
dummyEntity.setFlag(true);
791+
dummyEntity.setName("" + i);
792+
793+
repository.save(dummyEntity);
794+
}
795+
796+
DummyEntity dummyEntityExample = createDummyEntity();
797+
dummyEntityExample.setName(null);
798+
dummyEntityExample.setFlag(true);
799+
800+
Example<DummyEntity> example = Example.of(dummyEntityExample);
801+
802+
Page<DummyEntity> allFound = repository.findAll(example, pageRequest);
803+
804+
// page has correct size
805+
assertThat(allFound) //
806+
.isNotNull() //
807+
.hasSize(size);
808+
809+
// correct number of total
810+
assertThat(allFound.getTotalElements()).isEqualTo(100);
811+
812+
assertThat(allFound.getTotalPages()).isEqualTo(totalPages);
813+
814+
if (!notContains.isEmpty()) {
815+
assertThat(allFound) //
816+
.extracting(DummyEntity::getName) //
817+
.doesNotContain(notContains.toArray(new String[0]));
818+
}
819+
}
820+
821+
public static Stream<Arguments> findAllByExamplePageableSource() {
822+
return Stream.of( //
823+
Arguments.of(PageRequest.of(0, 3), 3, 34, Arrays.asList("3", "4", "100")), //
824+
Arguments.of(PageRequest.of(1, 10), 10, 10, Arrays.asList("9", "20", "30")), //
825+
Arguments.of(PageRequest.of(2, 10), 10, 10, Arrays.asList("1", "2", "3")), //
826+
Arguments.of(PageRequest.of(33, 3), 1, 34, Collections.emptyList()), //
827+
Arguments.of(PageRequest.of(36, 3), 0, 34, Collections.emptyList()), //
828+
Arguments.of(PageRequest.of(0, 10000), 100, 1, Collections.emptyList()), //
829+
Arguments.of(PageRequest.of(100, 10000), 0, 1, Collections.emptyList()) //
830+
);
831+
}
832+
696833
@Test
697834
void existsByExampleShouldGetOne() {
698835

0 commit comments

Comments
 (0)