Skip to content

Commit

Permalink
feat: MongoDB with Panache ranged query
Browse files Browse the repository at this point in the history
  • Loading branch information
loicmathieu committed Mar 19, 2020
1 parent c0e4a19 commit 52251c8
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 15 deletions.
25 changes: 25 additions & 0 deletions docs/src/main/asciidoc/mongodb-panache.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,31 @@ return Person.find("status", Status.Alive)

The `PanacheQuery` type has many other methods to deal with paging and returning streams.

=== Using a range instead of pages

`PanacheQuery` also allows range-based queries.

[source,java]
----
// create a query for all living persons
PanacheQuery<Person> livingPersons = Person.find("status", Status.Alive);
// make it use a range: start at index 0 until index 24 (inclusive).
livingPersons.range(0, 24);
// get the range
List<Person> firstRange = livingPersons.list();
// to get the next range, you need to call range again
List<Person> secondRange = livingPersons.range(25, 49).list();
----

[WARNING]
----
You cannot mix ranges and pages: if you use a range, all methods that depend on having a current page will throw an `UnsupportedOperationException`;
you can switch back to paging using `page(Page)` or `page(int, int)`.
----

=== Sorting

All methods accepting a query string also accept an optional `Sort` parameter, which allows you to abstract your sorting:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ public interface PanacheQuery<Entity> {
*/
public Page page();

/**
* Switch the query to use a fixed range (start index - last index) instead of a page.
* As the range is fixed, subsequent pagination of the query is not possible.
*
* @param startIndex the index of the first element, starting at 0
* @param lastIndex the index of the last element
* @return this query, modified
*/
public <T extends Entity> PanacheQuery<T> range(int startIndex, int lastIndex);

// Results

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ public interface ReactivePanacheQuery<Entity> {
*/
public Page page();

/**
* Switch the query to use a fixed range (start index - last index) instead of a page.
* As the range is fixed, subsequent pagination of the query is not possible.
*
* @param startIndex the index of the first element, starting at 0
* @param lastIndex the index of the last element
* @return this query, modified
*/
public <T extends Entity> ReactivePanacheQuery<T> range(int startIndex, int lastIndex);

// Results

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io.quarkus.mongodb.panache.reactive.ReactivePanacheQuery;
import io.quarkus.mongodb.reactive.ReactiveMongoCollection;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Range;
import io.quarkus.panache.common.exception.PanacheQueryException;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
Expand All @@ -25,6 +26,8 @@ public class ReactivePanacheQueryImpl<Entity> implements ReactivePanacheQuery<En
private Page page;
private Uni<Long> count;

private Range range;

ReactivePanacheQueryImpl(ReactiveMongoCollection<? extends Entity> collection, Class<? extends Entity> entityClass,
Document mongoQuery,
Document sort) {
Expand All @@ -40,6 +43,7 @@ public class ReactivePanacheQueryImpl<Entity> implements ReactivePanacheQuery<En
@SuppressWarnings("unchecked")
public <T extends Entity> ReactivePanacheQuery<T> page(Page page) {
this.page = page;
this.range = null; // reset the range to be able to switch from range to page
return (ReactivePanacheQuery<T>) this;
}

Expand All @@ -50,36 +54,43 @@ public <T extends Entity> ReactivePanacheQuery<T> page(int pageIndex, int pageSi

@Override
public <T extends Entity> ReactivePanacheQuery<T> nextPage() {
checkNotInRange();
return page(page.next());
}

@Override
public <T extends Entity> ReactivePanacheQuery<T> previousPage() {
checkNotInRange();
return page(page.previous());
}

@Override
public <T extends Entity> ReactivePanacheQuery<T> firstPage() {
checkNotInRange();
return page(page.first());
}

@Override
public <T extends Entity> Uni<ReactivePanacheQuery<T>> lastPage() {
checkNotInRange();
return pageCount().map(pageCount -> page(page.index(pageCount - 1)));
}

@Override
public Uni<Boolean> hasNextPage() {
checkNotInRange();
return pageCount().map(pageCount -> page.index < (pageCount - 1));
}

@Override
public boolean hasPreviousPage() {
checkNotInRange();
return page.index > 0;
}

@Override
public Uni<Integer> pageCount() {
checkNotInRange();
return count().map(count -> {
if (count == 0)
return 1; // a single page of zero results
Expand All @@ -89,9 +100,25 @@ public Uni<Integer> pageCount() {

@Override
public Page page() {
checkNotInRange();
return page;
}

private void checkNotInRange() {
if (range != null) {
throw new UnsupportedOperationException("Cannot call a page related method in a ranged query, " +
"call page(Page) or page(int, int) to initiate pagination first");
}
}

@Override
public <T extends Entity> ReactivePanacheQuery<T> range(int startIndex, int lastIndex) {
this.range = Range.of(startIndex, lastIndex);
// reset the page to its default to be able to switch from page to range
this.page = new Page(0, Integer.MAX_VALUE);
return (ReactivePanacheQuery<T>) this;
}

// Results

@Override
Expand All @@ -106,19 +133,16 @@ public Uni<Long> count() {
@Override
@SuppressWarnings("unchecked")
public <T extends Entity> Uni<List<T>> list() {
FindOptions options = new FindOptions();
options.sort(sort).skip(page.index).limit(page.size);
FindOptions options = buildOptions();
Multi<T> results = mongoQuery == null ? collection.find(options) : collection.find(mongoQuery, options);
return results.collectItems().asList();
}

@Override
@SuppressWarnings("unchecked")
public <T extends Entity> Multi<T> stream() {
FindOptions options = new FindOptions();
options.sort(sort).skip(page.index).limit(page.size);
return mongoQuery == null ? collection.find(options)
: collection.find(mongoQuery, options);
FindOptions options = buildOptions();
return mongoQuery == null ? collection.find(options) : collection.find(mongoQuery, options);
}

@Override
Expand All @@ -129,20 +153,18 @@ public <T extends Entity> Uni<T> firstResult() {

@Override
public <T extends Entity> Uni<Optional<T>> firstResultOptional() {
FindOptions options = new FindOptions();
options.sort(sort).skip(page.index).limit(1);
FindOptions options = buildOptions(1);
Multi<T> results = mongoQuery == null ? collection.find(options) : collection.find(mongoQuery, options);
return results.collectItems().first().map(o -> Optional.ofNullable(o));
}

@Override
@SuppressWarnings("unchecked")
public <T extends Entity> Uni<T> singleResult() {
FindOptions options = new FindOptions();
options.sort(sort).skip(page.index).limit(2);
FindOptions options = buildOptions(2);
Multi<T> results = mongoQuery == null ? collection.find(options) : collection.find(mongoQuery, options);
return results.collectItems().asList().map(list -> {
if (list.size() == 0 || list.size() > 1) {
if (list.size() != 1) {
throw new PanacheQueryException("There should be only one result");
} else {
return list.get(0);
Expand All @@ -152,8 +174,7 @@ public <T extends Entity> Uni<T> singleResult() {

@Override
public <T extends Entity> Uni<Optional<T>> singleResultOptional() {
FindOptions options = new FindOptions();
options.sort(sort).skip(page.index).limit(2);
FindOptions options = buildOptions(2);
Multi<T> results = mongoQuery == null ? collection.find(options) : collection.find(mongoQuery, options);
return results.collectItems().asList().map(list -> {
if (list.size() == 2) {
Expand All @@ -162,4 +183,28 @@ public <T extends Entity> Uni<Optional<T>> singleResultOptional() {
return list.isEmpty() ? Optional.empty() : Optional.of(list.get(0));
});
}

private FindOptions buildOptions() {
FindOptions options = new FindOptions();
options.sort(sort);
if (range != null) {
// range is 0 based, so we add 1 to the limit
options.skip(range.getStartIndex()).limit(range.getLastIndex() - range.getStartIndex() + 1);
} else {
options.skip(page.index * page.size).limit(page.size);
}
return options;
}

private FindOptions buildOptions(int maxResults) {
FindOptions options = new FindOptions();
options.sort(sort);
if (range != null) {
// range is 0 based, so we add 1 to the limit
options.skip(range.getStartIndex()).limit(maxResults);
} else {
options.skip(page.index * page.size).limit(maxResults);
}
return options;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import io.quarkus.mongodb.panache.PanacheQuery;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Range;
import io.quarkus.panache.common.exception.PanacheQueryException;

public class PanacheQueryImpl<Entity> implements PanacheQuery<Entity> {
Expand All @@ -34,6 +35,8 @@ public class PanacheQueryImpl<Entity> implements PanacheQuery<Entity> {
private Page page;
private Long count;

private Range range;

PanacheQueryImpl(MongoCollection<? extends Entity> collection, Class<? extends Entity> entityClass, Document mongoQuery,
Document sort) {
this.collection = collection;
Expand Down Expand Up @@ -83,6 +86,7 @@ public <T> PanacheQuery<T> project(Class<T> type) {
@SuppressWarnings("unchecked")
public <T extends Entity> PanacheQuery<T> page(Page page) {
this.page = page;
this.range = null; // reset the range to be able to switch from range to page
return (PanacheQuery<T>) this;
}

Expand All @@ -93,36 +97,43 @@ public <T extends Entity> PanacheQuery<T> page(int pageIndex, int pageSize) {

@Override
public <T extends Entity> PanacheQuery<T> nextPage() {
checkNotInRange();
return page(page.next());
}

@Override
public <T extends Entity> PanacheQuery<T> previousPage() {
checkNotInRange();
return page(page.previous());
}

@Override
public <T extends Entity> PanacheQuery<T> firstPage() {
checkNotInRange();
return page(page.first());
}

@Override
public <T extends Entity> PanacheQuery<T> lastPage() {
checkNotInRange();
return page(page.index(pageCount() - 1));
}

@Override
public boolean hasNextPage() {
checkNotInRange();
return page.index < (pageCount() - 1);
}

@Override
public boolean hasPreviousPage() {
checkNotInRange();
return page.index > 0;
}

@Override
public int pageCount() {
checkNotInRange();
long count = count();
if (count == 0)
return 1; // a single page of zero results
Expand All @@ -131,9 +142,25 @@ public int pageCount() {

@Override
public Page page() {
checkNotInRange();
return page;
}

private void checkNotInRange() {
if (range != null) {
throw new UnsupportedOperationException("Cannot call a page related method in a ranged query, " +
"call page(Page) or page(int, int) to initiate pagination first");
}
}

@Override
public <T extends Entity> PanacheQuery<T> range(int startIndex, int lastIndex) {
this.range = Range.of(startIndex, lastIndex);
// reset the page to its default to be able to switch from page to range
this.page = new Page(0, Integer.MAX_VALUE);
return (PanacheQuery<T>) this;
}

// Results

@Override
Expand All @@ -153,7 +180,8 @@ public <T extends Entity> List<T> list() {
if (this.projections != null) {
find.projection(projections);
}
MongoCursor<T> cursor = find.sort(sort).skip(page.index).limit(page.size).iterator();
manageOffsets(find);
MongoCursor<T> cursor = find.sort(sort).iterator();

try {
while (cursor.hasNext()) {
Expand Down Expand Up @@ -187,7 +215,7 @@ public <T extends Entity> Optional<T> firstResultOptional() {
@SuppressWarnings("unchecked")
public <T extends Entity> T singleResult() {
List<T> list = list();
if (list.isEmpty() || list.size() > 1) {
if (list.size() != 1) {
throw new PanacheQueryException("There should be only one result");
}

Expand All @@ -204,4 +232,13 @@ public <T extends Entity> Optional<T> singleResultOptional() {

return list.isEmpty() ? Optional.empty() : Optional.of(list.get(0));
}

private void manageOffsets(FindIterable find) {
if (range != null) {
// range is 0 based, so we add 1 to the limit
find.skip(range.getStartIndex()).limit(range.getLastIndex() - range.getStartIndex() + 1);
} else {
find.skip(page.index * page.size).limit(page.size);
}
}
}

0 comments on commit 52251c8

Please sign in to comment.