Skip to content

Commit

Permalink
Merge pull request #34 from Coreoz/feature/pagination
Browse files Browse the repository at this point in the history
implement PaginatedQueries for QueryDsl
  • Loading branch information
amanteaux authored Sep 19, 2024
2 parents d654504 + abe0723 commit 237687b
Show file tree
Hide file tree
Showing 15 changed files with 686 additions and 48 deletions.
117 changes: 104 additions & 13 deletions plume-db-querydsl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ This module helps integrate [Querydsl SQL](https://github.com/querydsl/querydsl/
with [Plume Database](https://github.com/Coreoz/Plume/tree/master/plume-db).
It contains mainly:
- `TransactionManagerQuerydsl`: the main class of this module; it will
read the configuration, initialize the SQL connection pool
and provide helper methods to create Querydsl queries,
read the configuration, initialize the SQL connection pool
and provide helper methods to create Querydsl queries,
- The generic DAO `CrudDaoQuerydsl` for CRUD operations.

Querydsl queries can be created:
- **without a `Connection`**: that means that the query will be executed with a connection
from the SQL connection pool. The `Connection` object will be automaticely released in the pool
once the query is executed.
from the SQL connection pool. The `Connection` object will be automaticely released in the pool
once the query is executed.
- **with a `Connection`**: that means that the query will be executed on this supplied connection.
This mode is almost always used when a **transaction** is needed:
This mode is almost always used when a **transaction** is needed:
```java
transactionManager.execute(connection -> {
transactionManager.insert(QTable.table, connection).populate(bean).execute();
transactionManager.insert(QTable.table, connection).populate(bean).execute();
transactionManager.delete(QTable.table, connection).where(predicate).execute();
// the connection is set to autocommit=false and will be commited at the end of the lambda
// the connection is set to autocommit=false and will be commited at the end of the lambda
});
```

Expand All @@ -35,15 +35,15 @@ Installation
**Maven**:
```xml
<dependency>
<groupId>com.coreoz</groupId>
<artifactId>plume-db-querydsl</artifactId>
<groupId>com.coreoz</groupId>
<artifactId>plume-db-querydsl</artifactId>
</dependency>
<dependency>
<groupId>com.coreoz</groupId>
<artifactId>plume-db-querydsl-codegen</artifactId>
<optional>true</optional>
<groupId>com.coreoz</groupId>
<artifactId>plume-db-querydsl-codegen</artifactId>
<optional>true</optional>
</dependency>
<!-- do not forget to also include the database driver -->
<!-- do not forget to also include the database driver -->
```

**Guice**: `install(new GuiceQuerydslModule());`
Expand All @@ -64,3 +64,94 @@ Code generation
To generate Querydsl entities, a good choice is to use this
[Querydsl code generator](https://github.com/Coreoz/Plume/tree/master/plume-db-querydsl-codegen).

Pagination
----------
### Overview
The `SqlPaginatedQuery` class provides a robust and flexible mechanism for paginating results in a Querydsl query. It abstracts the pagination logic into two generic interfaces —`Slice` and `Page`— which represent paginated results in different ways.

- `Page<U>`: A `Page` contains a list of results, total count of items, total number of pages, and a flag to indicate if there are more pages available.
- `Slice<U>`: A `Slice` contains a list of results and a flag to indicate if there are more items to be fetched, without calculating the total number of items or pages.

So using slices will be more efficient than using pages, though the impact will depend on the number of rows to count.
Under the hood:
- When fetching a page, Querydsl will attempt to execute the fetch request and the count request in the same SQL query, if it is not supported by the database, it will execute two queries.
- When fetching a slice of n items, n+1 items will try to be fetched: if the result contains n+1 items, then the `hasMore` attribute will be set to `true`

Other features:
- **Pagination Logic**: Handles offset-based pagination by calculating the number of records to skip (`offset`) and the number of records to fetch (`limit`) using the page number and page size.
- **Sorting Support**: Allows dynamic sorting of query results by providing an `Expression` and an `Order` (ascending/descending).

### Working with Pagination from a WebService:
First, you need to create a translation between the API sort key and a table column.
This can be done like this:

```java
@Getter
public enum UserSortPath {
// users
EMAIL(QUser.user.email),
FIRST_NAME(QUser.user.firstName),
LAST_NAME(QUser.user.lastName),
LAST_LOGIN_DATE(QUser.user.lastLogin),
;

private final Expression<?> path;
}
```

Then declare your WebService:

```java
@POST
@Path("/search")
@Operation(description = "Retrieves admin users")
@Consumes(MediaType.APPLICATION_JSON)
public Page<AdminUser> searchUsers(
@QueryParam("page") Long page,
@QueryParam("size") Long size,
@QueryParam("sort") String sort,
@QueryParam("sortDirection") Order sortDirection,
UserSearchRequest userSearchRequest
) {
// check the pagination that comes from the API call
if (page < 1) {
throw new WsException(WsError.REQUEST_INVALID, List.of("page"));
}
if (size < 1) {
throw new WsException(WsError.REQUEST_INVALID, List.of("size"));
}
return usersDao.searchUsers(
userSearchRequest,
page,
size,
UserSortPath.valueOf(sort),
sortDirection
);
}
```

Then apply the pagination from the API call with `SqlPaginatedQuery` :

```java
public Page<AdminUser> searchUsers(
UserSearchRequest userSearchRequest,
Long page,
Long size,
Expression<?> path,
Order sortDirection
) {
return SqlPaginatedQuery
.fromQuery(
this.transactionManagerQuerydsl.selectQuery()
.select(QUser.user)
.from(QUser.user)
.where(
QUser.user.firstName.containsIgnoreCase(userSearchRequest.searchText())
.or(QUser.user.lastName.containsIgnoreCase(userSearchRequest.searchText()))
.or(QUser.user.email.containsIgnoreCase(userSearchRequest.searchText()))
)
)
.withSort(path, sortDirection)
.fetchPage(page, size);
}
```
7 changes: 7 additions & 0 deletions plume-db-querydsl/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@
<optional>true</optional>
</dependency>

<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>

<!-- Tests -->
<dependency>
<groupId>com.coreoz</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.coreoz.plume.db.querydsl.dagger;

import javax.inject.Singleton;
import jakarta.inject.Singleton;

import com.coreoz.plume.db.querydsl.transaction.TransactionManagerQuerydsl;
import com.coreoz.plume.db.transaction.TransactionManager;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.coreoz.plume.db.querydsl.pagination;

import com.coreoz.plume.db.pagination.Page;
import com.coreoz.plume.db.pagination.Pages;
import com.coreoz.plume.db.pagination.Slice;
import com.querydsl.core.QueryResults;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.sql.SQLQuery;

import javax.annotation.Nonnull;
import java.util.List;

/**
* Paginated query implementation with Querydsl
* <br>
* @param <U> The type of elements contained in the request.
* <br>
* Usage example:
* <code><pre>
* public Page<User> fetchUsers() {
* return SqlPaginatedQuery
* .fromQuery(
* this.transactionManagerQuerydsl.selectQuery()
* .select(QUser.user)
* .from(QUser.user)
* )
* .withSort(QUser.user.name, Order.DESC)
* .fetchPage(1, 10);
* }
* </pre></code>
*/
public class SqlPaginatedQuery<U> {

private final SQLQuery<U> sqlQuery;

private SqlPaginatedQuery(SQLQuery<U> sqlQuery) {
this.sqlQuery = sqlQuery;
}

public static <U> SqlPaginatedQuery<U> fromQuery(SQLQuery<U> sqlQuery) {
return new SqlPaginatedQuery<>(sqlQuery);
}

@Nonnull
public <E extends Comparable<E>> SqlPaginatedQuery<U> withSort(
@Nonnull Expression<E> expression,
@Nonnull Order sortDirection
) {
return new SqlPaginatedQuery<>(
sqlQuery
.orderBy(
new OrderSpecifier<>(
sortDirection,
expression
)
)
);
}

/**
* Fetches a page of the SQL query provided
* @param pageNumber the number of the page queried (must be >= 1)
* @param pageSize the size of the page queried (must be >= 1)
* @return the corresponding page
*/
@Nonnull
public Page<U> fetchPage(
int pageNumber,
int pageSize
) {
QueryResults<U> paginatedQueryResults = this.sqlQuery
.offset(Pages.offset(pageNumber, pageSize))
.limit(pageSize)
.fetchResults();

return new Page<>(
paginatedQueryResults.getResults(),
paginatedQueryResults.getTotal(),
Pages.pageCount(pageSize, paginatedQueryResults.getTotal()),
pageNumber,
Pages.hasMore(pageNumber, pageSize, paginatedQueryResults.getTotal())
);
}

/**
* Fetches a slice of the SQL query provided
* @param pageNumber the number of the page queried (must be >= 1)
* @param pageSize the size of the page queried (must be >= 1)
* @return the corresponding slice
*/
@Nonnull
public Slice<U> fetchSlice(
int pageNumber,
int pageSize
) {
List<U> slicedQueryResults = this.sqlQuery
.offset(Pages.offset(pageNumber, pageSize))
.limit(pageSize + 1L)
.fetch();

boolean hasMore = slicedQueryResults.size() > pageSize;

// Trim the results to the required size (if needed)
List<U> items = hasMore ? slicedQueryResults.subList(0, pageSize) : slicedQueryResults;

return new Slice<>(
items,
hasMore
);
}
}
Loading

0 comments on commit 237687b

Please sign in to comment.