Skip to content

Commit

Permalink
Implement new findBy() method with FluentQuery for SimpleDatastoreRep…
Browse files Browse the repository at this point in the history
…ository (#836)

Implementing new findBy() method in SimpleDatastoreRepository and corresponding FluentQuery.FetchableFluentQuery interface introduced by spring-data-commons-2.6.0 (#2421).
This is basically implementing already supported query methods to fit functional programming model.

Note: In this PR, leaving project() and as() methods as unsupported for now. As Datastore query by example does not support projection overall yet.

fix for #692
  • Loading branch information
zhumin8 authored Jan 7, 2022
1 parent 022946f commit 95538ab
Show file tree
Hide file tree
Showing 8 changed files with 288 additions and 7 deletions.
8 changes: 8 additions & 0 deletions docs/src/main/asciidoc/datastore.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,7 @@ It enables dynamic query generation based on a user-provided object. See https:/
. Currently, only equality queries are supported (no ignore-case matching, regexp matching, etc.).
. Per-field matchers are not supported.
. Embedded entities matching is not supported.
. Projection is not supported.

For example, if you want to find all users with the last name "Smith", you would use the following code:
[source, java]
Expand All @@ -1022,7 +1023,14 @@ userRepository.findAll(
userRepository.findAll(
Example.of(new User(null, null, "Smith"), ExampleMatcher.matching().withIncludeNullValues())
----
You can also extend query specification initially defined by an example in FluentQuery's chaining style:
----
userRepository.findBy(
Example.of(new User(null, null, "Smith")), q -> q.sortBy(Sort.by("firstName")).firstValue());
userRepository.findBy(
Example.of(new User(null, null, "Smith")), FetchableFluentQuery::stream);
----
==== Custom GQL query methods

Custom GQL queries can be mapped to repository methods in one of two ways:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.function.Function;
import java.util.function.LongSupplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import com.google.cloud.datastore.Cursor;
Expand All @@ -35,13 +36,18 @@
import com.google.cloud.spring.data.datastore.core.DatastoreResultsIterable;
import com.google.cloud.spring.data.datastore.repository.DatastoreRepository;
import com.google.cloud.spring.data.datastore.repository.query.DatastorePageable;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.query.FluentQuery;
import org.springframework.data.util.Streamable;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

/**
Expand All @@ -57,6 +63,8 @@ public class SimpleDatastoreRepository<T, I> implements DatastoreRepository<T, I

private final Class<T> entityType;

private static final Log LOGGER = LogFactory.getLog(SimpleDatastoreRepository.class);

public SimpleDatastoreRepository(DatastoreOperations datastoreTemplate,
Class<T> entityType) {
Assert.notNull(datastoreTemplate, "A non-null DatastoreOperations is required.");
Expand Down Expand Up @@ -160,6 +168,14 @@ public <S extends T> Optional<S> findOne(Example<S> example) {
return iterator.hasNext() ? Optional.of(iterator.next()) : Optional.empty();
}

<S extends T> S findFirstSorted(Example<S> example, Sort sort) {
Iterable<S> entities = this.datastoreTemplate.queryByExample(example,
new DatastoreQueryOptions.Builder().setSort(sort).setLimit(1).build());
Iterator<S> iterator = entities.iterator();
return iterator.hasNext() ? iterator.next() : null;
}


@Override
public <S extends T> Iterable<S> findAll(Example<S> example) {
return this.datastoreTemplate.queryByExample(example, null);
Expand Down Expand Up @@ -217,7 +233,94 @@ public void deleteAllById(Iterable<? extends I> iterable) {
}

@Override
public <S extends T, R> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction) {
throw new UnsupportedOperationException();
public <S extends T, R> R findBy(Example<S> example,
Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction) {
Assert.notNull(example, "Example must not be null!");
Assert.notNull(queryFunction, "Query function must not be null!");

return queryFunction.apply(new DatastoreFluentQueryByExample<>(example));
}

class DatastoreFluentQueryByExample<S extends T> implements FluentQuery.FetchableFluentQuery<S> {
private final Example<S> example;

private final Sort sort;

DatastoreFluentQueryByExample(Example<S> example) {
this(example, Sort.unsorted());
}

DatastoreFluentQueryByExample(Example<S> example, Sort sort) {
this.example = example;
this.sort = sort;
}

@NonNull
@Override
public FetchableFluentQuery<S> sortBy(@NonNull Sort sort) {
return new DatastoreFluentQueryByExample<>(this.example, sort);
}

@NonNull
@Override
public Optional<S> one() {
return SimpleDatastoreRepository.this.findOne(this.example);
}

@Nullable
@Override
public S oneValue() {
Optional<S> one = one();
return one.orElse(null);
}

@Override
public S firstValue() {
if (this.sort.isUnsorted()) {
LOGGER.warn(
"firstValue() used without sorting. "
+ "Use oneValue() instead if order does not matter.");
}
return SimpleDatastoreRepository.this.findFirstSorted(this.example, this.sort);
}

@NonNull
@Override
public List<S> all() {
return stream().collect(Collectors.toList());
}

@NonNull
@Override
public Page<S> page(@NonNull Pageable pageable) {
return SimpleDatastoreRepository.this.findAll(this.example, pageable);
}

@NonNull
@Override
public Stream<S> stream() {
return Streamable.of(SimpleDatastoreRepository.this.findAll(this.example, this.sort)).stream();
}

@Override
public long count() {
return SimpleDatastoreRepository.this.count(this.example);
}

@Override
public boolean exists() {
return SimpleDatastoreRepository.this.exists(this.example);
}

@Override
public FetchableFluentQuery<S> project(Collection properties) {
throw new UnsupportedOperationException();
}

@Override
public <V> FetchableFluentQuery<V> as(Class<V> resultType) {
throw new UnsupportedOperationException();
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
Expand Down Expand Up @@ -77,6 +78,7 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.util.AopTestUtils;
Expand Down Expand Up @@ -1086,6 +1088,65 @@ public void queryByTimestampTest() {
List<TestEntity> results2 = this.testEntityRepository.findByDatetimeGreaterThan(endDate);
assertThat(results2).containsExactly(testEntity2);
}


@Test
public void testFindByExampleFluent() {
Example<TestEntity> exampleRedCircle = Example.of(new TestEntity(null, "red", null, Shape.CIRCLE, null));
Example<TestEntity> exampleRed = Example.of(new TestEntity(null, "red", null, null, null));

List<TestEntity> entityRedAll = this.testEntityRepository.findBy(
exampleRed,
q -> q.all());
assertThat(entityRedAll).containsExactlyInAnyOrder(this.testEntityA, this.testEntityC, this.testEntityD);

List<TestEntity> entityRedAllReverseSortedById = this.testEntityRepository.findBy(
exampleRed,
q -> q.sortBy(Sort.by("id").descending()).all());
assertThat(entityRedAllReverseSortedById).containsExactly(this.testEntityD, this.testEntityC, this.testEntityA);

long countRedCircle = this.testEntityRepository.findBy(
exampleRedCircle,
FetchableFluentQuery::count);
assertThat(countRedCircle).isEqualTo(2);

boolean existsRed = this.testEntityRepository.findBy(
exampleRed,
FetchableFluentQuery::exists);
assertThat(existsRed).isTrue();

TestEntity FirstValueRed = this.testEntityRepository.findBy(
exampleRed,
FetchableFluentQuery::firstValue);
assertThat(FirstValueRed).isEqualTo(testEntityA);

TestEntity oneValueRed = this.testEntityRepository.findBy(
exampleRed,
q -> q.oneValue());
assertThat(oneValueRed.getColor()).isEqualTo("red");

Optional<TestEntity> onePurple = this.testEntityRepository.findBy(
Example.of(new TestEntity(null, "purple", null, null, null)),
FetchableFluentQuery::one);
assertThat(onePurple).isNotPresent();

Pageable pageable = PageRequest.of(0, 2);
Page<TestEntity> pagedResults = this.testEntityRepository.findBy(exampleRed, q -> q.page(pageable));
assertThat(pagedResults).containsExactly(this.testEntityA, this.testEntityC);

Optional<TestEntity> oneRed = this.testEntityRepository.findBy(exampleRed,
q -> q.sortBy(Sort.by("id")).one());
assertThat(oneRed).isPresent().get().isEqualTo(testEntityA);

long firstValueReverseSortedById = this.testEntityRepository.findBy(
exampleRed,
q -> q.sortBy(Sort.by("id").descending()).firstValue().getId());
assertThat(firstValueReverseSortedById).isEqualTo(4L);

List<String> redIdListReverseSorted = this.testEntityRepository.findBy(exampleRed,
q -> q.sortBy(Sort.by("id").descending()).stream().map(x -> x.getId().toString()).collect(Collectors.toList()));
assertThat(redIdListReverseSorted).containsExactly("4", "3", "1");
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
Expand All @@ -64,6 +65,9 @@ public class SimpleDatastoreRepositoryTests {
private final SimpleDatastoreRepository<Object, String> simpleDatastoreRepository = new SimpleDatastoreRepository<>(
this.datastoreTemplate, Object.class);

private final SimpleDatastoreRepository<Object, Object> spyRepo = spy(new SimpleDatastoreRepository<>(
this.datastoreTemplate, Object.class));

@Test
public void saveTest() {
Object object = new Object();
Expand Down Expand Up @@ -357,10 +361,92 @@ public void deleteAllById() {
}

@Test
public void testUnsupportedFindBy() {
public void findByExampleFluentQueryAll() {
Example<Object> example = Example.of(new Object());
Sort sort = Sort.by("id");
Iterable entities = Arrays.asList();
doAnswer(invocationOnMock -> new DatastoreResultsIterable(entities, null))
.when(this.datastoreTemplate).queryByExample(same(example), any());
this.spyRepo.findBy(example, FetchableFluentQuery::all);
verify(this.spyRepo).findAll(same(example), eq(Sort.unsorted()));
this.spyRepo.findBy(example, query -> query.sortBy(sort).all());
verify(this.spyRepo).findAll(same(example), eq(sort));
}

@Test
public void findByExampleFluentQueryOneValue() {
Example<Object> example = Example.of(new Object());
Iterable entities = Arrays.asList();
doAnswer(invocationOnMock -> new DatastoreResultsIterable(entities, null))
.when(this.datastoreTemplate).queryByExample(same(example), any());
this.spyRepo.findBy(example, FetchableFluentQuery::oneValue);
verify(this.spyRepo).findOne(same(example));
}

@Test
public void findByExampleFluentQuerySortAndFirstValue() {
Example<Object> example = Example.of(new Object());
Sort sort = Sort.by("id");
Iterable entities = Arrays.asList(1);
doAnswer(invocationOnMock -> new DatastoreResultsIterable(entities, null))
.when(this.datastoreTemplate).queryByExample(same(example), any());
this.spyRepo.findBy(example, q -> q.sortBy(sort).firstValue());
verify(this.spyRepo).findFirstSorted(same(example), same(sort));
verify(this.datastoreTemplate).queryByExample(same(example),
eq(new DatastoreQueryOptions.Builder().setSort(sort).setLimit(1).build()));
}

@Test
public void findByExampleFluentQueryExists() {
Example<Object> example = Example.of(new Object());
doAnswer(invocationOnMock -> Arrays.asList())
.when(this.datastoreTemplate).keyQueryByExample(same(example),
eq(new DatastoreQueryOptions.Builder().setLimit(1).build()));

this.spyRepo.findBy(example, FetchableFluentQuery::exists);
verify(this.spyRepo).exists(same(example));
}

@Test
public void findByExampleFluentQueryCount() {
Example<Object> example = Example.of(new Object());
doAnswer(invocationOnMock -> Arrays.asList(1, 2, 3))
.when(this.datastoreTemplate).keyQueryByExample(same(example), isNull());

this.spyRepo.findBy(example, FetchableFluentQuery::count);
verify(this.spyRepo).count(same(example));
}

@Test
public void findByExampleFluentQueryPage() {
Example<Object> example = Example.of(new Object());
Sort sort = Sort.by("id");

doAnswer(invocationOnMock -> new DatastoreResultsIterable(Arrays.asList(1, 2), null))
.when(this.datastoreTemplate).queryByExample(same(example),
eq(new DatastoreQueryOptions.Builder().setLimit(2).setOffset(2).setSort(sort)
.build()));

doAnswer(invocationOnMock -> new DatastoreResultsIterable(Arrays.asList(1, 2, 3, 4, 5), null))
.when(this.datastoreTemplate).keyQueryByExample(same(example), isNull());

PageRequest pageRequest = PageRequest.of(1, 2, sort);
this.spyRepo.findBy(example, q -> q.page(pageRequest));
verify(this.spyRepo).findAll(same(example), same(pageRequest));
}

@Test
public void findByExampleFluentQueryAsUnsupported() {
this.expectedEx.expect(UnsupportedOperationException.class);
Example<Object> example = Example.of(new Object());
this.simpleDatastoreRepository.findBy(example, q -> q.as(Object.class).all());
}

@Test
public void findByExampleFluentQueryProjectUnsupported() {
this.expectedEx.expect(UnsupportedOperationException.class);
Example<Object> example = Example.of(new Object());
this.simpleDatastoreRepository.findBy(example, FetchableFluentQuery::all);
this.simpleDatastoreRepository.findBy(example, q -> q.project("firstProperty").all());
}

}
6 changes: 6 additions & 0 deletions spring-cloud-gcp-data-datastore/src/test/resources/index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@ indexes:
properties:
- name: size
- name: color

- kind: test_entities_ci
properties:
- name: color
- name: __key__
direction: desc
Loading

0 comments on commit 95538ab

Please sign in to comment.