Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement new findBy() method with FluentQuery for SimpleDatastoreRepository #836

Merged
merged 21 commits into from
Jan 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7ad20d5
add basics without projection. test incomplete.
zhumin8 Dec 28, 2021
b2c71bf
add basic test w/o projections.
zhumin8 Dec 28, 2021
893ec8b
add fluentQ page. mark projection as unsupported. other minor changes.
zhumin8 Dec 28, 2021
7cfeafc
style check and formats.
zhumin8 Dec 29, 2021
f030300
add example to sample.
zhumin8 Dec 29, 2021
48e9263
minor style fixes.
zhumin8 Dec 29, 2021
236d0a9
switch sample added for fluent query, so no need to add index.
zhumin8 Dec 29, 2021
264dad3
change sample used for fluent query by example.
zhumin8 Dec 30, 2021
1715fce
remove javadoc copied from source, change to ref.
zhumin8 Dec 30, 2021
aa11433
add test and reformat.
zhumin8 Dec 30, 2021
fda7c67
add nonnull tags. And fix/streamline firstValue().
zhumin8 Jan 4, 2022
27a09ea
add unit tests.
zhumin8 Jan 4, 2022
f2ce434
it test update, adding sort. Add to index.yaml to enable this sort. Also
zhumin8 Jan 4, 2022
61a358b
streamline all() and update corresponding unit test.
zhumin8 Jan 4, 2022
eaf1d87
added test for sort and all.
zhumin8 Jan 4, 2022
87a29c4
removing R parameter. also removing resultType and domainType as we d…
zhumin8 Jan 5, 2022
e7dd2ea
add warning to firstValue if used with no sortBy before.
zhumin8 Jan 5, 2022
0962886
add test for unsupported methods.
zhumin8 Jan 5, 2022
6b92c52
fix style typo.
zhumin8 Jan 6, 2022
db6e722
resolve changes wrongly broughtin by merge. empty commit after rebase.
zhumin8 Jan 6, 2022
a0deddb
add to description to documentation.
zhumin8 Jan 6, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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());
}

}
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
elefeint marked this conversation as resolved.
Show resolved Hide resolved
properties:
- name: color
- name: __key__
direction: desc
zhumin8 marked this conversation as resolved.
Show resolved Hide resolved
Loading