Skip to content

Commit

Permalink
Support of `QueryByExampleExecutor#findAll(Example<S> example, Pageab…
Browse files Browse the repository at this point in the history
…le 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
  • Loading branch information
DiegoKrupitza committed Mar 12, 2022
1 parent a9f653b commit 239a720
Show file tree
Hide file tree
Showing 11 changed files with 253 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,14 @@ public interface JdbcAggregateOperations {
* @return the number of instances stored in the database. Guaranteed to be not {@code null}.
*/
<T> long count(Example<T> 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}.
*/
<T> Page<T> select(Example<T> example, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,17 @@ public <T> long count(Example<T> example) {
return accessStrategy.count(query, probeType);
}

@Override
public <T> Page<T> select(Example<T> example, Pageable pageable) {
Query query = this.exampleMapper.getMappedExample(example);
Class<T> probeType = example.getProbeType();

Iterable<T> items = triggerAfterConvert(accessStrategy.select(query, probeType, pageable));
List<T> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,11 @@ public <T> Iterable<T> select(Query query, Class<T> probeType) {
return collect(das -> das.select(query, probeType));
}

@Override
public <T> Iterable<T> select(Query query, Class<T> probeType, Pageable pageable) {
return collect(das -> das.select(query, probeType, pageable));
}

@Override
public <T> boolean exists(Query query, Class<T> probeType) {
return collect(das -> das.exists(query, probeType));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,23 @@ Iterable<Object> 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.
*/
<T> Iterable<T> select(Query query, Class<T> 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.
*/
<T> Iterable<T> select(Query query, Class<T> probeType, Pageable pageable);

/**
* Determine whether there is an aggregate of type <code>probeType</code> that matches the provided {@link Query}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,14 @@ public <T> Iterable<T> select(Query query, Class<T> probeType) {
return operations.query(sqlQuery, parameterSource, (RowMapper<T>) getEntityRowMapper(probeType));
}

@Override
public <T> Iterable<T> select(Query query, Class<T> probeType, Pageable pageable) {
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
String sqlQuery = sql(probeType).selectByQuery(query, parameterSource, pageable);

return operations.query(sqlQuery, parameterSource, (RowMapper<T>) getEntityRowMapper(probeType));
}

@Override
public <T> boolean exists(Query query, Class<T> probeType) {
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,11 @@ public <T> Iterable<T> select(Query query, Class<T> probeType) {
return delegate.select(query, probeType);
}

@Override
public <T> Iterable<T> select(Query query, Class<T> probeType, Pageable pageable) {
return delegate.select(query, probeType, pageable);
}

@Override
public <T> boolean exists(Query query, Class<T> probeType) {
return delegate.exists(query, probeType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>parameterSource</code>
*
* @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 <code>parameterSource</code>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -381,6 +381,13 @@ public <T> Iterable<T> select(Query query, Class<T> probeType) {
return null;
}

@Override
public <T> Iterable<T> select(Query query, Class<T> probeType, Pageable pageable) {
// TODO: DIEGO find help for this one
// I have zero MyBatis knowledge.
return null;
}

@Override
public <T> boolean exists(Query query, Class<T> probeType) {
// TODO: DIEGO find help for this one
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,9 @@ public <S extends T> Iterable<S> findAll(Example<S> example, Sort sort) {

@Override
public <S extends T> Page<S> findAll(Example<S> example, Pageable pageable) {
// TODO: impl
return null;
Assert.notNull(example, "Example must not be null!");

return this.entityOperations.select(example, pageable);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<DummyEntity> example = Example.of(new DummyEntity("Diego"));
Pageable pageRequest = PageRequest.of(0, 10);

Iterable<DummyEntity> 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<DummyEntity> example = Example.of(createDummyEntity());
Pageable pageRequest = PageRequest.of(0, 10);

Iterable<DummyEntity> 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<DummyEntity> example = Example.of(new DummyEntity("NotExisting"));
Pageable pageRequest = PageRequest.of(0, 10);

Iterable<DummyEntity> 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<DummyEntity> example = Example.of(createDummyEntity());
Pageable pageRequest = PageRequest.of(10, 10);

Iterable<DummyEntity> allFound = repository.findAll(example, pageRequest);

assertThat(allFound) //
.isNotNull() //
.isEmpty();
}

@ParameterizedTest
@MethodSource("findAllByExamplePageableSource")
void findAllByExamplePageable(Pageable pageRequest, int size, int totalPages, List<String> 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<DummyEntity> example = Example.of(dummyEntityExample);

Page<DummyEntity> 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<Arguments> 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() {

Expand Down

0 comments on commit 239a720

Please sign in to comment.