From 65d7df54889e6b845f72e63385d8f246d0935ce7 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Thu, 12 Sep 2024 22:54:35 +1200 Subject: [PATCH] #3462 Add Paging as alternative to maxRows + firstRow + orderBy Paging is an alternative to specifying the maxRows + firstRow + orderBy on a query. Example: ```java var orderBy = OrderBy.of("lastName desc nulls first, firstName asc"); var paging = Paging.of(0, 100, orderBy); DB.find(Contact.class) .setPaging(paging) .where().startsWith("lastName", "foo") .findList(); ``` --- ebean-api/src/main/java/io/ebean/DPaging.java | 50 ++++++++ ebean-api/src/main/java/io/ebean/OrderBy.java | 7 ++ ebean-api/src/main/java/io/ebean/Paging.java | 90 ++++++++++++++ .../src/main/java/io/ebean/QueryBuilder.java | 2 + .../server/query/DefaultFetchGroupQuery.java | 30 +---- .../server/querydefn/DefaultOrmQuery.java | 16 +++ .../java/io/ebean/typequery/QueryBean.java | 6 + .../java/org/tests/query/TestQueryPaging.java | 114 ++++++++++++++++++ 8 files changed, 291 insertions(+), 24 deletions(-) create mode 100644 ebean-api/src/main/java/io/ebean/DPaging.java create mode 100644 ebean-api/src/main/java/io/ebean/Paging.java create mode 100644 ebean-test/src/test/java/org/tests/query/TestQueryPaging.java diff --git a/ebean-api/src/main/java/io/ebean/DPaging.java b/ebean-api/src/main/java/io/ebean/DPaging.java new file mode 100644 index 0000000000..3560a274d1 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/DPaging.java @@ -0,0 +1,50 @@ +package io.ebean; + +final class DPaging implements Paging { + + static final Paging NONE = new DPaging(0, 0, null); + + static Paging build(int pgIndex, int pgSize, OrderBy orderBy) { + return new DPaging(pgIndex, pgSize, orderBy); + } + + static Paging build(int pgIndex, int pgSize) { + return new DPaging(pgIndex, pgSize, null); + } + + private final int pageNumber; + private final int pageSize; + private final OrderBy orderBy; + + DPaging(int pageNumber, int pageSize, OrderBy orderBy) { + this.pageNumber = pageNumber; + this.pageSize = pageSize; + this.orderBy = orderBy; + } + + @Override + public int pageIndex() { + return pageNumber; + } + + @Override + public int pageSize() { + return pageSize; + } + + @Override + public OrderBy orderBy() { + return orderBy; + } + + @Override + public Paging withPage(int pageNumber) { + return new DPaging(pageNumber, pageSize, orderBy); + } + + @Override + public Paging withOrderBy(String orderByClause) { + return new DPaging(pageNumber, pageSize, OrderBy.of(orderByClause)); + } + +} diff --git a/ebean-api/src/main/java/io/ebean/OrderBy.java b/ebean-api/src/main/java/io/ebean/OrderBy.java index f7b5ebcfe0..87f08b852d 100644 --- a/ebean-api/src/main/java/io/ebean/OrderBy.java +++ b/ebean-api/src/main/java/io/ebean/OrderBy.java @@ -24,6 +24,13 @@ public class OrderBy implements Serializable { private final List list; + /** + * Create an OrderBy parsing the given order by clause. + */ + public static

OrderBy

of(String orderByClause) { + return new OrderBy<>(orderByClause); + } + /** * Create an empty OrderBy with no associated query. */ diff --git a/ebean-api/src/main/java/io/ebean/Paging.java b/ebean-api/src/main/java/io/ebean/Paging.java new file mode 100644 index 0000000000..e4744ee029 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/Paging.java @@ -0,0 +1,90 @@ +package io.ebean; + +import io.avaje.lang.Nullable; + +/** + * Used to specify Paging on a Query as an alternative to setting each of the + * maxRows, firstRow and orderBy clause via: + * {@link Query#setMaxRows(int)} + {@link Query#setFirstRow(int)} + {@link Query#setOrderBy(OrderBy)}. + *

+ * Example use: + * + *

{@code
+ *
+ *   var orderBy = OrderBy.of("lastName desc nulls first, firstName asc");
+ *   var paging = Paging.of(0, 100, orderBy);
+ *
+ *   DB.find(Contact.class)
+ *       .setPaging(paging)
+ *       .where().startsWith("lastName", "foo")
+ *       .findList();
+ *
+ * }
+ */ +public interface Paging { + + /** + * Create a Paging with the given page index size and orderBy. + * + * @param pageIndex the page index starting from zero + * @param pageSize the page size (effectively max rows) + * @param orderBy order by for the query result + */ + static Paging of(int pageIndex, int pageSize, @Nullable OrderBy orderBy) { + return DPaging.build(pageIndex, pageSize, orderBy); + } + + /** + * Create a Paging with a raw order by clause. + * + * @param pageIndex the page index starting from zero + * @param pageSize the page size (effectively max rows) + * @param orderByClause raw order by clause for ordering the query result + */ + static Paging of(int pageIndex, int pageSize, @Nullable String orderByClause) { + return of(pageIndex, pageSize, OrderBy.of(orderByClause)); + } + + /** + * Create a Paging that will use the id property for ordering. + * + * @param pageIndex the page index starting from zero + * @param pageSize the page size (effectively max rows) + */ + static Paging of(int pageIndex, int pageSize) { + return DPaging.build(pageIndex, pageSize); + } + + /** + * Return a Paging that will not apply any pagination to a query. + */ + static Paging ofNone() { + return DPaging.NONE; + } + + /** + * Return the page index. + */ + int pageIndex(); + + /** + * Return the page size. + */ + int pageSize(); + + /** + * Return the order by. + */ + OrderBy orderBy(); + + /** + * Return a Paging using the given page index. + */ + Paging withPage(int pageIndex); + + /** + * Return a Paging using the given order by clause. + */ + Paging withOrderBy(String orderByClause); + +} diff --git a/ebean-api/src/main/java/io/ebean/QueryBuilder.java b/ebean-api/src/main/java/io/ebean/QueryBuilder.java index 35432e073a..1c5f48b34e 100644 --- a/ebean-api/src/main/java/io/ebean/QueryBuilder.java +++ b/ebean-api/src/main/java/io/ebean/QueryBuilder.java @@ -324,6 +324,8 @@ public interface QueryBuilder extends QueryBuilderProjection { */ SELF setMaxRows(int maxRows); + SELF setPaging(Paging paging); + /** * Set RawSql to use for this query. */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultFetchGroupQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultFetchGroupQuery.java index 242acef98e..2177c3ad6f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultFetchGroupQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultFetchGroupQuery.java @@ -2,30 +2,7 @@ import io.avaje.lang.NonNullApi; import io.avaje.lang.Nullable; -import io.ebean.CacheMode; -import io.ebean.CountDistinctOrder; -import io.ebean.Database; -import io.ebean.DtoQuery; -import io.ebean.Expression; -import io.ebean.ExpressionFactory; -import io.ebean.ExpressionList; -import io.ebean.FetchConfig; -import io.ebean.FetchGroup; -import io.ebean.FetchPath; -import io.ebean.FutureIds; -import io.ebean.FutureList; -import io.ebean.FutureRowCount; -import io.ebean.OrderBy; -import io.ebean.PagedList; -import io.ebean.PersistenceContextScope; -import io.ebean.ProfileLocation; -import io.ebean.Query; -import io.ebean.QueryIterator; -import io.ebean.QueryType; -import io.ebean.RawSql; -import io.ebean.Transaction; -import io.ebean.UpdateQuery; -import io.ebean.Version; +import io.ebean.*; import io.ebean.service.SpiFetchGroupQuery; import io.ebeaninternal.api.SpiQueryFetch; import io.ebeaninternal.server.querydefn.OrmQueryDetail; @@ -506,6 +483,11 @@ public Query setMaxRows(int maxRows) { throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup"); } + @Override + public Query setPaging(Paging paging) { + throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup"); + } + @Override public Query setMapKey(String mapKey) { throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup"); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java index 353cc11833..9f67b4590a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -1827,6 +1827,22 @@ public final Query setMaxRows(int maxRows) { return this; } + @Override + @SuppressWarnings("unchecked") + public Query setPaging(@Nullable Paging paging) { + if (paging != null && paging.pageSize() > 0) { + firstRow = paging.pageIndex() * paging.pageSize(); + maxRows = paging.pageSize(); + orderBy = (OrderBy) paging.orderBy(); + if (orderBy == null || orderBy.isEmpty()) { + // should not be paging without any order by clause so set + // orderById such that the Id property is used + orderById = true; + } + } + return this; + } + @Override public final String mapKey() { return mapKey; diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/QueryBean.java b/ebean-querybean/src/main/java/io/ebean/typequery/QueryBean.java index 00654d1fb5..498b7d1b35 100644 --- a/ebean-querybean/src/main/java/io/ebean/typequery/QueryBean.java +++ b/ebean-querybean/src/main/java/io/ebean/typequery/QueryBean.java @@ -319,6 +319,12 @@ public final R alias(String alias) { return root; } + @Override + public R setPaging(Paging paging) { + query.setPaging(paging); + return root; + } + @Override public final R setMaxRows(int maxRows) { query.setMaxRows(maxRows); diff --git a/ebean-test/src/test/java/org/tests/query/TestQueryPaging.java b/ebean-test/src/test/java/org/tests/query/TestQueryPaging.java new file mode 100644 index 0000000000..870bf0fe1c --- /dev/null +++ b/ebean-test/src/test/java/org/tests/query/TestQueryPaging.java @@ -0,0 +1,114 @@ +package org.tests.query; + +import io.ebean.DB; +import io.ebean.OrderBy; +import io.ebean.Paging; +import io.ebean.test.LoggedSql; +import io.ebean.xtest.BaseTestCase; +import org.junit.jupiter.api.Test; +import org.tests.model.basic.Contact; +import org.tests.model.basic.ResetBasicData; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class TestQueryPaging extends BaseTestCase { + + @Test + void example() { + var orderBy = OrderBy.of("lastName desc nulls first, firstName asc"); + var paging = Paging.of(0, 100, orderBy); + + DB.find(Contact.class) + .setPaging(paging) + .where().startsWith("lastName", "foo") + .findList(); + // or instead of findList() use another find method like ... + // findPagedList(), findEach(), findStream(), findMap(), findSet(), findSingleAttributeList(), + + var nextPage = paging.withPage(1); + + DB.find(Contact.class) + .setPaging(nextPage) + .findList(); + } + + @Test + void whenNoOrderBy_expect_orderByIdUsed() { + ResetBasicData.reset(); + + LoggedSql.start(); + + DB.find(Contact.class).select("lastName").setPaging(Paging.of(0, 4)).findList(); + DB.find(Contact.class).select("lastName").setPaging(Paging.of(2, 4)).findList(); + + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(2); + if (isLimitOffset()) { + assertThat(sql.get(0)).contains("order by t0.id limit 4"); + assertThat(sql.get(1)).contains("order by t0.id limit 4 offset 8"); + } + } + + @Test + void whenOrderBy_expect_noExtraIdInTheOrderBy() { + ResetBasicData.reset(); + + LoggedSql.start(); + + DB.find(Contact.class).select("lastName").setPaging(Paging.of(1, 4, "lastName")).findList(); + DB.find(Contact.class).select("lastName").setPaging(Paging.of(1, 4, "lastName, id")).findList(); + + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(2); + if (isLimitOffset()) { + assertThat(sql.get(0)).contains("order by t0.last_name limit 4 offset 4"); + assertThat(sql.get(1)).contains("order by t0.last_name, t0.id limit 4 offset 4"); + } + } + + @Test + void whenNone_expect_noLimitOffsetAtAll() { + ResetBasicData.reset(); + + LoggedSql.start(); + + DB.find(Contact.class).select("lastName").setPaging(Paging.ofNone()).findList(); + + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("select t0.id, t0.last_name from contact t0;"); + } + + @Test + void withPage() { + Paging paging = Paging.of(0, 100); + assertThat(paging.pageIndex()).isEqualTo(0); + assertThat(paging.pageSize()).isEqualTo(100); + + Paging pg1 = paging.withPage(1); + assertThat(pg1.pageIndex()).isEqualTo(1); + assertThat(pg1.pageSize()).isEqualTo(100); + + Paging pg6 = paging.withPage(6); + assertThat(pg6.pageIndex()).isEqualTo(6); + assertThat(pg6.pageSize()).isEqualTo(100); + } + + @Test + void withOrderBy() { + Paging pg1 = Paging.of(1, 100); + assertThat(pg1.pageIndex()).isEqualTo(1); + assertThat(pg1.pageSize()).isEqualTo(100); + + Paging pgWithOrder = pg1.withOrderBy("lastName desc nulls first, firstName asc"); + OrderBy orderBy = pgWithOrder.orderBy(); + List properties = orderBy.getProperties(); + assertThat(properties).hasSize(2); + assertThat(properties.get(0).getProperty()).isEqualTo("lastName"); + assertThat(properties.get(0).isAscending()).isFalse(); + assertThat(properties.get(1).getProperty()).isEqualTo("firstName"); + assertThat(properties.get(1).isAscending()).isTrue(); + } +}