Skip to content

Commit

Permalink
#3462 Add Paging as alternative to maxRows + firstRow + orderBy
Browse files Browse the repository at this point in the history
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();
```
  • Loading branch information
rbygrave committed Sep 12, 2024
1 parent 12648cd commit 65d7df5
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 24 deletions.
50 changes: 50 additions & 0 deletions ebean-api/src/main/java/io/ebean/DPaging.java
Original file line number Diff line number Diff line change
@@ -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));
}

}
7 changes: 7 additions & 0 deletions ebean-api/src/main/java/io/ebean/OrderBy.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ public class OrderBy<T> implements Serializable {

private final List<Property> list;

/**
* Create an OrderBy parsing the given order by clause.
*/
public static <P> OrderBy<P> of(String orderByClause) {
return new OrderBy<>(orderByClause);
}

/**
* Create an empty OrderBy with no associated query.
*/
Expand Down
90 changes: 90 additions & 0 deletions ebean-api/src/main/java/io/ebean/Paging.java
Original file line number Diff line number Diff line change
@@ -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)}.
* <p>
* Example use:
*
* <pre>{@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();
*
* }</pre>
*/
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);

}
2 changes: 2 additions & 0 deletions ebean-api/src/main/java/io/ebean/QueryBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,8 @@ public interface QueryBuilder<SELF, T> extends QueryBuilderProjection<SELF, T> {
*/
SELF setMaxRows(int maxRows);

SELF setPaging(Paging paging);

/**
* Set RawSql to use for this query.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -506,6 +483,11 @@ public Query<T> setMaxRows(int maxRows) {
throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup");
}

@Override
public Query<T> setPaging(Paging paging) {
throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup");
}

@Override
public Query<T> setMapKey(String mapKey) {
throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1827,6 +1827,22 @@ public final Query<T> setMaxRows(int maxRows) {
return this;
}

@Override
@SuppressWarnings("unchecked")
public Query<T> setPaging(@Nullable Paging paging) {
if (paging != null && paging.pageSize() > 0) {
firstRow = paging.pageIndex() * paging.pageSize();
maxRows = paging.pageSize();
orderBy = (OrderBy<T>) 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
114 changes: 114 additions & 0 deletions ebean-test/src/test/java/org/tests/query/TestQueryPaging.java
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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<String> 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<OrderBy.Property> 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();
}
}

0 comments on commit 65d7df5

Please sign in to comment.