From 239a720662bcff92638dea1cf49e3d23aeb8cd12 Mon Sep 17 00:00:00 2001 From: Diego Krupitza Date: Sat, 12 Mar 2022 19:02:31 +0100 Subject: [PATCH] Support of `QueryByExampleExecutor#findAll(Example example, Pageable pageable)`. This commit introduces the find by example pageable method `findAll(Example example, Pageable pageable)` to spring-data-jdbc. MyBatis implementation is missing since I do not have the knowledge for this. Related tickets #1192 --- .../jdbc/core/JdbcAggregateOperations.java | 10 ++ .../data/jdbc/core/JdbcAggregateTemplate.java | 11 ++ .../convert/CascadingDataAccessStrategy.java | 5 + .../jdbc/core/convert/DataAccessStrategy.java | 14 +- .../convert/DefaultDataAccessStrategy.java | 8 + .../convert/DelegatingDataAccessStrategy.java | 5 + .../data/jdbc/core/convert/SqlGenerator.java | 25 ++++ .../mybatis/MyBatisDataAccessStrategy.java | 11 +- .../support/SimpleJdbcRepository.java | 5 +- .../core/convert/SqlGeneratorUnitTests.java | 27 +++- .../JdbcRepositoryIntegrationTests.java | 139 +++++++++++++++++- 11 files changed, 253 insertions(+), 7 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java index 3f83a4db09f..d893e35f7b0 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java @@ -193,4 +193,14 @@ public interface JdbcAggregateOperations { * @return the number of instances stored in the database. Guaranteed to be not {@code null}. */ long count(Example example); + + /** + * Returns a {@link Page} of entities matching the given {@link Example}. In case no match could be found, an empty + * {@link Page} is returned. + * + * @param example must not be null + * @param pageable can be null. + * @return a {@link Page} of entities matching the given {@link Example}. + */ + Page select(Example example, Pageable pageable); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java index 458ab8de1ac..b17e8fb1769 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java @@ -294,6 +294,17 @@ public long count(Example example) { return accessStrategy.count(query, probeType); } + @Override + public Page select(Example example, Pageable pageable) { + Query query = this.exampleMapper.getMappedExample(example); + Class probeType = example.getProbeType(); + + Iterable items = triggerAfterConvert(accessStrategy.select(query, probeType, pageable)); + List content = StreamSupport.stream(items.spliterator(), false).collect(Collectors.toList()); + + return PageableExecutionUtils.getPage(content, pageable, () -> accessStrategy.count(query, probeType)); + } + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.JdbcAggregateOperations#findAll(java.lang.Class) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CascadingDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CascadingDataAccessStrategy.java index 944f2ab2922..d0d7fd4fdce 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CascadingDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CascadingDataAccessStrategy.java @@ -221,6 +221,11 @@ public Iterable select(Query query, Class probeType) { return collect(das -> das.select(query, probeType)); } + @Override + public Iterable select(Query query, Class probeType, Pageable pageable) { + return collect(das -> das.select(query, probeType, pageable)); + } + @Override public boolean exists(Query query, Class probeType) { return collect(das -> das.exists(query, probeType)); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java index a611005713f..1274d14dfad 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java @@ -245,11 +245,23 @@ Iterable findAllByPath(Identifier identifier, * * @param query must not be {@literal null}. * @param probeType the type of entities. Must not be {@code null}. - * @return a non null list with all the matching results. + * @return a non-null list with all the matching results. * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. */ Iterable select(Query query, Class probeType); + /** + * Execute a {@code SELECT} query and convert the resulting items to a {@link Iterable}. Applies the {@link Pageable} + * to the result. + * + * @param query must not be {@literal null}. + * @param probeType the type of entities. Must not be {@literal null}. + * @param pageable the pagination that should be applied. Must not be {@literal null}. + * @return a non-null list with all the matching results. + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. + */ + Iterable select(Query query, Class probeType, Pageable pageable); + /** * Determine whether there is an aggregate of type probeType that matches the provided {@link Query}. * diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java index aa644a10dc9..28e23477309 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java @@ -457,6 +457,14 @@ public Iterable select(Query query, Class probeType) { return operations.query(sqlQuery, parameterSource, (RowMapper) getEntityRowMapper(probeType)); } + @Override + public Iterable select(Query query, Class probeType, Pageable pageable) { + MapSqlParameterSource parameterSource = new MapSqlParameterSource(); + String sqlQuery = sql(probeType).selectByQuery(query, parameterSource, pageable); + + return operations.query(sqlQuery, parameterSource, (RowMapper) getEntityRowMapper(probeType)); + } + @Override public boolean exists(Query query, Class probeType) { MapSqlParameterSource parameterSource = new MapSqlParameterSource(); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DelegatingDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DelegatingDataAccessStrategy.java index f188bad53cc..6ae4e7319f3 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DelegatingDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DelegatingDataAccessStrategy.java @@ -217,6 +217,11 @@ public Iterable select(Query query, Class probeType) { return delegate.select(query, probeType); } + @Override + public Iterable select(Query query, Class probeType, Pageable pageable) { + return delegate.select(query, probeType, pageable); + } + @Override public boolean exists(Query query, Class probeType) { return delegate.exists(query, probeType); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java index 3692d2b0d5e..ede6b2e9064 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java @@ -745,6 +745,31 @@ public String selectByQuery(Query query, MapSqlParameterSource parameterSource) return render(select); } + /** + * Constructs a single sql query that performs select based on the provided query and pagination information. + * Additional the bindings for the where clause are stored after execution into the parameterSource + * + * @param query the query to base the select on. Must not be null. + * @param pageable the pageable to perform on the select. + * @param parameterSource the source for holding the bindings. + * @return a non null query string. + */ + public String selectByQuery(Query query, MapSqlParameterSource parameterSource, Pageable pageable) { + + Assert.notNull(parameterSource, "parameterSource must not be null"); + + SelectBuilder.SelectWhere selectBuilder = selectBuilder(); + + // first apply query and then pagination. This means possible query sorting and limiting might be overwritten by the + // pagination. This is desired. + SelectBuilder.SelectOrdered selectOrdered = applyQueryOnSelect(query, parameterSource, selectBuilder); + selectOrdered = applyPagination(pageable, selectOrdered); + selectOrdered = selectOrdered.orderBy(extractOrderByFields(pageable.getSort())); + + Select select = selectOrdered.build(); + return render(select); + } + /** * Constructs a single sql query that performs select count based on the provided query for checking existence. * Additional the bindings for the where clause are stored after execution into the parameterSource diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java index acce0721047..561683aa606 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java @@ -23,10 +23,10 @@ import java.util.Optional; import java.util.stream.Collectors; -import org.apache.ibatis.session.SqlSession; -import org.mybatis.spring.SqlSessionTemplate; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.ibatis.session.SqlSession; +import org.mybatis.spring.SqlSessionTemplate; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -381,6 +381,13 @@ public Iterable select(Query query, Class probeType) { return null; } + @Override + public Iterable select(Query query, Class probeType, Pageable pageable) { + // TODO: DIEGO find help for this one + // I have zero MyBatis knowledge. + return null; + } + @Override public boolean exists(Query query, Class probeType) { // TODO: DIEGO find help for this one diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java index ddaeaea1838..416251ab828 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java @@ -214,8 +214,9 @@ public Iterable findAll(Example example, Sort sort) { @Override public Page findAll(Example example, Pageable pageable) { - // TODO: impl - return null; + Assert.notNull(example, "Example must not be null!"); + + return this.entityOperations.select(example, pageable); } @Override diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java index 229626958cb..f3dd7e49f38 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java @@ -28,7 +28,6 @@ import org.springframework.data.annotation.Id; import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.annotation.Version; -import org.springframework.data.domain.Example; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -783,6 +782,32 @@ void countByQuerySimpleValidTest() { .containsOnly(entry("x_name", probe.name)); } + @Test + void selectByQueryPaginationValidTest() { + final SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class); + + DummyEntity probe = new DummyEntity(); + probe.name = "Diego"; + + Criteria criteria = Criteria.where("name").is(probe.name); + Query query = Query.query(criteria); + + PageRequest pageRequest = PageRequest.of(2, 1, Sort.by(Sort.Order.asc("name"))); + + MapSqlParameterSource parameterSource = new MapSqlParameterSource(); + + String generatedSQL = sqlGenerator.selectByQuery(query, parameterSource, pageRequest); + assertThat(generatedSQL) // + .isNotNull() // + .contains(":x_name") // + .containsIgnoringCase("ORDER BY dummy_entity.x_name ASC") // + .containsIgnoringCase("LIMIT 1") // + .containsIgnoringCase("OFFSET 2 LIMIT 1"); + + assertThat(parameterSource.getValues()) // + .containsOnly(entry("x_name", probe.name)); + } + private SqlIdentifier getAlias(Object maybeAliased) { if (maybeAliased instanceof Aliased) { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index 4ffef12740c..1b8361b5b0d 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -32,12 +32,17 @@ import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.PropertiesFactoryBean; import org.springframework.context.ApplicationListener; @@ -53,7 +58,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jdbc.core.mapping.AggregateReference; -import org.springframework.data.relational.repository.Lock; import org.springframework.data.jdbc.repository.query.Modifying; import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; @@ -65,6 +69,7 @@ import org.springframework.data.relational.core.mapping.event.AfterConvertEvent; import org.springframework.data.relational.core.mapping.event.AfterLoadEvent; import org.springframework.data.relational.core.sql.LockMode; +import org.springframework.data.relational.repository.Lock; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; @@ -693,6 +698,138 @@ void findAllByExampleShouldGetNone() { .isEmpty(); } + @Test + void findAllByExamplePageableShouldGetOne() { + + DummyEntity dummyEntity1 = createDummyEntity(); + dummyEntity1.setFlag(true); + + repository.save(dummyEntity1); + + DummyEntity dummyEntity2 = createDummyEntity(); + dummyEntity2.setName("Diego"); + + repository.save(dummyEntity2); + + Example example = Example.of(new DummyEntity("Diego")); + Pageable pageRequest = PageRequest.of(0, 10); + + Iterable allFound = repository.findAll(example, pageRequest); + + assertThat(allFound) // + .isNotNull() // + .hasSize(1) // + .extracting(DummyEntity::getName) // + .containsExactly(example.getProbe().getName()); + } + + @Test + void findAllByExamplePageableMultipleMatchShouldGetOne() { + + DummyEntity dummyEntity1 = createDummyEntity(); + repository.save(dummyEntity1); + + DummyEntity dummyEntity2 = createDummyEntity(); + repository.save(dummyEntity2); + + Example example = Example.of(createDummyEntity()); + Pageable pageRequest = PageRequest.of(0, 10); + + Iterable allFound = repository.findAll(example, pageRequest); + + assertThat(allFound) // + .isNotNull() // + .hasSize(2) // + .extracting(DummyEntity::getName) // + .containsOnly(example.getProbe().getName()); + } + + @Test + void findAllByExamplePageableShouldGetNone() { + + DummyEntity dummyEntity1 = createDummyEntity(); + dummyEntity1.setFlag(true); + + repository.save(dummyEntity1); + + Example example = Example.of(new DummyEntity("NotExisting")); + Pageable pageRequest = PageRequest.of(0, 10); + + Iterable allFound = repository.findAll(example, pageRequest); + + assertThat(allFound) // + .isNotNull() // + .isEmpty(); + } + + @Test + void findAllByExamplePageableOutsidePageShouldGetNone() { + + DummyEntity dummyEntity1 = createDummyEntity(); + repository.save(dummyEntity1); + + DummyEntity dummyEntity2 = createDummyEntity(); + repository.save(dummyEntity2); + + Example example = Example.of(createDummyEntity()); + Pageable pageRequest = PageRequest.of(10, 10); + + Iterable allFound = repository.findAll(example, pageRequest); + + assertThat(allFound) // + .isNotNull() // + .isEmpty(); + } + + @ParameterizedTest + @MethodSource("findAllByExamplePageableSource") + void findAllByExamplePageable(Pageable pageRequest, int size, int totalPages, List notContains) { + + for (int i = 0; i < 100; i++) { + DummyEntity dummyEntity = createDummyEntity(); + dummyEntity.setFlag(true); + dummyEntity.setName("" + i); + + repository.save(dummyEntity); + } + + DummyEntity dummyEntityExample = createDummyEntity(); + dummyEntityExample.setName(null); + dummyEntityExample.setFlag(true); + + Example example = Example.of(dummyEntityExample); + + Page allFound = repository.findAll(example, pageRequest); + + // page has correct size + assertThat(allFound) // + .isNotNull() // + .hasSize(size); + + // correct number of total + assertThat(allFound.getTotalElements()).isEqualTo(100); + + assertThat(allFound.getTotalPages()).isEqualTo(totalPages); + + if (!notContains.isEmpty()) { + assertThat(allFound) // + .extracting(DummyEntity::getName) // + .doesNotContain(notContains.toArray(new String[0])); + } + } + + public static Stream findAllByExamplePageableSource() { + return Stream.of( // + Arguments.of(PageRequest.of(0, 3), 3, 34, Arrays.asList("3", "4", "100")), // + Arguments.of(PageRequest.of(1, 10), 10, 10, Arrays.asList("9", "20", "30")), // + Arguments.of(PageRequest.of(2, 10), 10, 10, Arrays.asList("1", "2", "3")), // + Arguments.of(PageRequest.of(33, 3), 1, 34, Collections.emptyList()), // + Arguments.of(PageRequest.of(36, 3), 0, 34, Collections.emptyList()), // + Arguments.of(PageRequest.of(0, 10000), 100, 1, Collections.emptyList()), // + Arguments.of(PageRequest.of(100, 10000), 0, 1, Collections.emptyList()) // + ); + } + @Test void existsByExampleShouldGetOne() {