From c0e4a19932dee2dfb47d5a2f73a97081d3beefab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Mathieu?= Date: Mon, 9 Mar 2020 15:42:52 +0100 Subject: [PATCH 1/2] feat: Hibernate with Panache ranged query --- .../main/asciidoc/hibernate-orm-panache.adoc | 25 ++++++++ .../hibernate/orm/panache/PanacheQuery.java | 10 ++++ .../orm/panache/runtime/PanacheQueryImpl.java | 59 +++++++++++++++++-- .../java/io/quarkus/panache/common/Range.java | 36 +++++++++++ .../io/quarkus/it/panache/TestEndpoint.java | 51 ++++++++++++++++ 5 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 extensions/panache/panache-common/runtime/src/main/java/io/quarkus/panache/common/Range.java diff --git a/docs/src/main/asciidoc/hibernate-orm-panache.adoc b/docs/src/main/asciidoc/hibernate-orm-panache.adoc index 358f4226b4780..d88c1fe0eb4b3 100644 --- a/docs/src/main/asciidoc/hibernate-orm-panache.adoc +++ b/docs/src/main/asciidoc/hibernate-orm-panache.adoc @@ -440,6 +440,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 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 firstRange = livingPersons.list(); + +// to get the next range, you need to call range again +List 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 the following simplified query form: diff --git a/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/PanacheQuery.java b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/PanacheQuery.java index 81be409f43994..c0759c775543e 100644 --- a/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/PanacheQuery.java +++ b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/PanacheQuery.java @@ -116,6 +116,16 @@ public interface PanacheQuery { */ 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 PanacheQuery range(int startIndex, int lastIndex); + /** * Define the locking strategy used for this query. * diff --git a/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/PanacheQueryImpl.java b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/PanacheQueryImpl.java index 5c5be466669d4..e6b6c0ef6aaae 100644 --- a/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/PanacheQueryImpl.java +++ b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/PanacheQueryImpl.java @@ -12,6 +12,7 @@ import io.quarkus.hibernate.orm.panache.PanacheQuery; import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Range; public class PanacheQueryImpl implements PanacheQuery { @@ -27,6 +28,8 @@ public class PanacheQueryImpl implements PanacheQuery { private Page page; private Long count; + private Range range; + PanacheQueryImpl(EntityManager em, javax.persistence.Query jpaQuery, String query, Object paramsArrayOrMap) { this.em = em; this.jpaQuery = jpaQuery; @@ -41,7 +44,7 @@ public class PanacheQueryImpl implements PanacheQuery { @SuppressWarnings("unchecked") public PanacheQuery page(Page page) { this.page = page; - jpaQuery.setFirstResult(page.index * page.size); + this.range = null; // reset the range to be able to switch from range to page return (PanacheQuery) this; } @@ -52,36 +55,43 @@ public PanacheQuery page(int pageIndex, int pageSize) { @Override public PanacheQuery nextPage() { + checkNotInRange(); return page(page.next()); } @Override public PanacheQuery previousPage() { + checkNotInRange(); return page(page.previous()); } @Override public PanacheQuery firstPage() { + checkNotInRange(); return page(page.first()); } @Override public PanacheQuery 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 @@ -90,9 +100,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 PanacheQuery 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) this; + } + @Override public PanacheQuery withLock(LockModeType lockModeType) { jpaQuery.setLockMode(lockModeType); @@ -133,20 +159,20 @@ protected String countQuery() { @Override @SuppressWarnings("unchecked") public List list() { - jpaQuery.setMaxResults(page.size); + manageOffsets(); return jpaQuery.getResultList(); } @Override @SuppressWarnings("unchecked") public Stream stream() { - jpaQuery.setMaxResults(page.size); + manageOffsets(); return jpaQuery.getResultStream(); } @Override public T firstResult() { - jpaQuery.setMaxResults(1); + manageOffsets(1); List list = jpaQuery.getResultList(); return list.isEmpty() ? null : list.get(0); } @@ -159,14 +185,14 @@ public Optional firstResultOptional() { @Override @SuppressWarnings("unchecked") public T singleResult() { - jpaQuery.setMaxResults(page.size); + manageOffsets(); return (T) jpaQuery.getSingleResult(); } @Override @SuppressWarnings("unchecked") public Optional singleResultOptional() { - jpaQuery.setMaxResults(2); + manageOffsets(2); List list = jpaQuery.getResultList(); if (list.size() == 2) { throw new NonUniqueResultException(); @@ -174,4 +200,25 @@ public Optional singleResultOptional() { return list.isEmpty() ? Optional.empty() : Optional.of(list.get(0)); } + + private void manageOffsets() { + if (range != null) { + jpaQuery.setFirstResult(range.getStartIndex()); + // range is 0 based, so we add 1 + jpaQuery.setMaxResults(range.getLastIndex() - range.getStartIndex() + 1); + } else { + jpaQuery.setFirstResult(page.index * page.size); + jpaQuery.setMaxResults(page.size); + } + } + + private void manageOffsets(int maxResults) { + if (range != null) { + jpaQuery.setFirstResult(range.getStartIndex()); + jpaQuery.setMaxResults(maxResults); + } else { + jpaQuery.setFirstResult(page.index * page.size); + jpaQuery.setMaxResults(maxResults); + } + } } diff --git a/extensions/panache/panache-common/runtime/src/main/java/io/quarkus/panache/common/Range.java b/extensions/panache/panache-common/runtime/src/main/java/io/quarkus/panache/common/Range.java new file mode 100644 index 0000000000000..72879029b0e34 --- /dev/null +++ b/extensions/panache/panache-common/runtime/src/main/java/io/quarkus/panache/common/Range.java @@ -0,0 +1,36 @@ +package io.quarkus.panache.common; + +/** + *

+ * Utility class to represent ranging information. Range instances are immutable. + *

+ * + *

+ * Usage: + *

+ * + *
+ * Range range = Range.of(0, 5);
+ * 
+ */ +public class Range { + private final int startIndex; + private final int lastIndex; + + public Range(int startIndex, int lastIndex) { + this.startIndex = startIndex; + this.lastIndex = lastIndex; + } + + public static Range of(int startIndex, int lastIndex) { + return new Range(startIndex, lastIndex); + } + + public int getStartIndex() { + return startIndex; + } + + public int getLastIndex() { + return lastIndex; + } +} diff --git a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java index d560a0aa7a30f..aceb978bed3f0 100644 --- a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java +++ b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java @@ -200,6 +200,10 @@ public String testModel() { testPaging(Person.findAll()); testPaging(Person.find("ORDER BY name")); + // range + testRange(Person.findAll()); + testRange(Person.find("ORDER BY name")); + try { Person.findAll().singleResult(); Assertions.fail("singleResult should have thrown"); @@ -633,6 +637,10 @@ public String testModelDao() { testPaging(personDao.findAll()); testPaging(personDao.find("ORDER BY name")); + //range + testRange(personDao.findAll()); + testRange(personDao.find("ORDER BY name")); + try { personDao.findAll().singleResult(); Assertions.fail("singleResult should have thrown"); @@ -851,6 +859,49 @@ private void testPaging(PanacheQuery query) { Assertions.assertEquals(7, query.count()); Assertions.assertEquals(3, query.pageCount()); + + // mix page with range + persons = query.page(0, 3).range(0, 1).list(); + Assertions.assertEquals(2, persons.size()); + Assertions.assertEquals("stef0", persons.get(0).name); + Assertions.assertEquals("stef1", persons.get(1).name); + } + + private void testRange(PanacheQuery query) { + List persons = query.range(0, 2).list(); + Assertions.assertEquals(3, persons.size()); + Assertions.assertEquals("stef0", persons.get(0).name); + Assertions.assertEquals("stef1", persons.get(1).name); + Assertions.assertEquals("stef2", persons.get(2).name); + + persons = query.range(3, 5).list(); + Assertions.assertEquals(3, persons.size()); + Assertions.assertEquals("stef3", persons.get(0).name); + Assertions.assertEquals("stef4", persons.get(1).name); + Assertions.assertEquals("stef5", persons.get(2).name); + + persons = query.range(6, 8).list(); + Assertions.assertEquals(1, persons.size()); + Assertions.assertEquals("stef6", persons.get(0).name); + + persons = query.range(8, 12).list(); + Assertions.assertEquals(0, persons.size()); + + // mix range with page + Assertions.assertThrows(UnsupportedOperationException.class, () -> query.range(0, 2).nextPage()); + Assertions.assertThrows(UnsupportedOperationException.class, () -> query.range(0, 2).previousPage()); + Assertions.assertThrows(UnsupportedOperationException.class, () -> query.range(0, 2).pageCount()); + Assertions.assertThrows(UnsupportedOperationException.class, () -> query.range(0, 2).lastPage()); + Assertions.assertThrows(UnsupportedOperationException.class, () -> query.range(0, 2).firstPage()); + Assertions.assertThrows(UnsupportedOperationException.class, () -> query.range(0, 2).hasPreviousPage()); + Assertions.assertThrows(UnsupportedOperationException.class, () -> query.range(0, 2).hasNextPage()); + Assertions.assertThrows(UnsupportedOperationException.class, () -> query.range(0, 2).page()); + // this is valid as we switch from range to page + persons = query.range(0, 2).page(0, 3).list(); + Assertions.assertEquals(3, persons.size()); + Assertions.assertEquals("stef0", persons.get(0).name); + Assertions.assertEquals("stef1", persons.get(1).name); + Assertions.assertEquals("stef2", persons.get(2).name); } @GET From 52251c8ce47452e7e67bebd3d7cfb1e87ea72bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Mathieu?= Date: Mon, 9 Mar 2020 14:05:56 +0100 Subject: [PATCH 2/2] feat: MongoDB with Panache ranged query --- docs/src/main/asciidoc/mongodb-panache.adoc | 25 +++++++ .../quarkus/mongodb/panache/PanacheQuery.java | 10 +++ .../reactive/ReactivePanacheQuery.java | 10 +++ .../runtime/ReactivePanacheQueryImpl.java | 71 +++++++++++++++---- .../panache/runtime/PanacheQueryImpl.java | 41 ++++++++++- 5 files changed, 142 insertions(+), 15 deletions(-) diff --git a/docs/src/main/asciidoc/mongodb-panache.adoc b/docs/src/main/asciidoc/mongodb-panache.adoc index 2e630104f8742..38fa1bdbee232 100644 --- a/docs/src/main/asciidoc/mongodb-panache.adoc +++ b/docs/src/main/asciidoc/mongodb-panache.adoc @@ -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 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 firstRange = livingPersons.list(); + +// to get the next range, you need to call range again +List 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: diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheQuery.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheQuery.java index 9419bfaac0529..93812db0a965e 100755 --- a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheQuery.java +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheQuery.java @@ -116,6 +116,16 @@ public interface PanacheQuery { */ 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 PanacheQuery range(int startIndex, int lastIndex); + // Results /** diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/reactive/ReactivePanacheQuery.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/reactive/ReactivePanacheQuery.java index 0d4d6bc83b280..dd82e9c92e5ca 100644 --- a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/reactive/ReactivePanacheQuery.java +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/reactive/ReactivePanacheQuery.java @@ -110,6 +110,16 @@ public interface ReactivePanacheQuery { */ 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 ReactivePanacheQuery range(int startIndex, int lastIndex); + // Results /** diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/reactive/runtime/ReactivePanacheQueryImpl.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/reactive/runtime/ReactivePanacheQueryImpl.java index f4ac391ad0b65..ce1e3849975d0 100644 --- a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/reactive/runtime/ReactivePanacheQueryImpl.java +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/reactive/runtime/ReactivePanacheQueryImpl.java @@ -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; @@ -25,6 +26,8 @@ public class ReactivePanacheQueryImpl implements ReactivePanacheQuery count; + private Range range; + ReactivePanacheQueryImpl(ReactiveMongoCollection collection, Class entityClass, Document mongoQuery, Document sort) { @@ -40,6 +43,7 @@ public class ReactivePanacheQueryImpl implements ReactivePanacheQuery ReactivePanacheQuery page(Page page) { this.page = page; + this.range = null; // reset the range to be able to switch from range to page return (ReactivePanacheQuery) this; } @@ -50,36 +54,43 @@ public ReactivePanacheQuery page(int pageIndex, int pageSi @Override public ReactivePanacheQuery nextPage() { + checkNotInRange(); return page(page.next()); } @Override public ReactivePanacheQuery previousPage() { + checkNotInRange(); return page(page.previous()); } @Override public ReactivePanacheQuery firstPage() { + checkNotInRange(); return page(page.first()); } @Override public Uni> lastPage() { + checkNotInRange(); return pageCount().map(pageCount -> page(page.index(pageCount - 1))); } @Override public Uni hasNextPage() { + checkNotInRange(); return pageCount().map(pageCount -> page.index < (pageCount - 1)); } @Override public boolean hasPreviousPage() { + checkNotInRange(); return page.index > 0; } @Override public Uni pageCount() { + checkNotInRange(); return count().map(count -> { if (count == 0) return 1; // a single page of zero results @@ -89,9 +100,25 @@ public Uni 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 ReactivePanacheQuery 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) this; + } + // Results @Override @@ -106,8 +133,7 @@ public Uni count() { @Override @SuppressWarnings("unchecked") public Uni> list() { - FindOptions options = new FindOptions(); - options.sort(sort).skip(page.index).limit(page.size); + FindOptions options = buildOptions(); Multi results = mongoQuery == null ? collection.find(options) : collection.find(mongoQuery, options); return results.collectItems().asList(); } @@ -115,10 +141,8 @@ public Uni> list() { @Override @SuppressWarnings("unchecked") public Multi 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 @@ -129,8 +153,7 @@ public Uni firstResult() { @Override public Uni> firstResultOptional() { - FindOptions options = new FindOptions(); - options.sort(sort).skip(page.index).limit(1); + FindOptions options = buildOptions(1); Multi results = mongoQuery == null ? collection.find(options) : collection.find(mongoQuery, options); return results.collectItems().first().map(o -> Optional.ofNullable(o)); } @@ -138,11 +161,10 @@ public Uni> firstResultOptional() { @Override @SuppressWarnings("unchecked") public Uni singleResult() { - FindOptions options = new FindOptions(); - options.sort(sort).skip(page.index).limit(2); + FindOptions options = buildOptions(2); Multi 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); @@ -152,8 +174,7 @@ public Uni singleResult() { @Override public Uni> singleResultOptional() { - FindOptions options = new FindOptions(); - options.sort(sort).skip(page.index).limit(2); + FindOptions options = buildOptions(2); Multi results = mongoQuery == null ? collection.find(options) : collection.find(mongoQuery, options); return results.collectItems().asList().map(list -> { if (list.size() == 2) { @@ -162,4 +183,28 @@ public Uni> 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; + } } diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQueryImpl.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQueryImpl.java index 6da05e981b7c7..e0a954d40f21c 100644 --- a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQueryImpl.java +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQueryImpl.java @@ -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 implements PanacheQuery { @@ -34,6 +35,8 @@ public class PanacheQueryImpl implements PanacheQuery { private Page page; private Long count; + private Range range; + PanacheQueryImpl(MongoCollection collection, Class entityClass, Document mongoQuery, Document sort) { this.collection = collection; @@ -83,6 +86,7 @@ public PanacheQuery project(Class type) { @SuppressWarnings("unchecked") public PanacheQuery page(Page page) { this.page = page; + this.range = null; // reset the range to be able to switch from range to page return (PanacheQuery) this; } @@ -93,36 +97,43 @@ public PanacheQuery page(int pageIndex, int pageSize) { @Override public PanacheQuery nextPage() { + checkNotInRange(); return page(page.next()); } @Override public PanacheQuery previousPage() { + checkNotInRange(); return page(page.previous()); } @Override public PanacheQuery firstPage() { + checkNotInRange(); return page(page.first()); } @Override public PanacheQuery 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 @@ -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 PanacheQuery 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) this; + } + // Results @Override @@ -153,7 +180,8 @@ public List list() { if (this.projections != null) { find.projection(projections); } - MongoCursor cursor = find.sort(sort).skip(page.index).limit(page.size).iterator(); + manageOffsets(find); + MongoCursor cursor = find.sort(sort).iterator(); try { while (cursor.hasNext()) { @@ -187,7 +215,7 @@ public Optional firstResultOptional() { @SuppressWarnings("unchecked") public T singleResult() { List list = list(); - if (list.isEmpty() || list.size() > 1) { + if (list.size() != 1) { throw new PanacheQueryException("There should be only one result"); } @@ -204,4 +232,13 @@ public Optional 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); + } + } }