From 7ef134e2e43e1a444e2a0e8b67db0187e6db6fbe Mon Sep 17 00:00:00 2001 From: Lucas Amiaud Date: Tue, 11 Jun 2024 18:27:45 +0200 Subject: [PATCH 01/10] implement PaginatedQueries for QueryDsl --- plume-db-querydsl/pom.xml | 10 ++- .../plume/db/querydsl/pagination/Page.java | 15 ++++ .../db/querydsl/pagination/Pageable.java | 46 ++++++++++++ .../pagination/SQLPaginatedQuery.java | 61 ++++++++++++++++ .../db/querydsl/pagination/SortOption.java | 54 ++++++++++++++ .../db/querydsl/pagination/SortPath.java | 8 +++ .../db/querydsl/pagination/PageableTest.java | 72 +++++++++++++++++++ .../pagination/SQLPaginatedQueryTest.java | 67 +++++++++++++++++ 8 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Page.java create mode 100644 plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Pageable.java create mode 100644 plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQuery.java create mode 100644 plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SortOption.java create mode 100644 plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SortPath.java create mode 100644 plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/PageableTest.java create mode 100644 plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQueryTest.java diff --git a/plume-db-querydsl/pom.xml b/plume-db-querydsl/pom.xml index e38f855..862818f 100644 --- a/plume-db-querydsl/pom.xml +++ b/plume-db-querydsl/pom.xml @@ -19,7 +19,7 @@ javax.annotation javax.annotation-api - + com.coreoz plume-db @@ -46,6 +46,12 @@ true + + + org.projectlombok + lombok + + com.coreoz @@ -71,4 +77,4 @@ - \ No newline at end of file + diff --git a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Page.java b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Page.java new file mode 100644 index 0000000..7f702b0 --- /dev/null +++ b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Page.java @@ -0,0 +1,15 @@ +package com.coreoz.plume.db.querydsl.pagination; + +import lombok.Value; + +import java.util.List; + +@Value +public class Page { + List elements; + long totalElements; + long pageCount; + long currentPage; + long currentPageSize; + boolean lastPage; +} diff --git a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Pageable.java b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Pageable.java new file mode 100644 index 0000000..9f5447e --- /dev/null +++ b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Pageable.java @@ -0,0 +1,46 @@ +package com.coreoz.plume.db.querydsl.pagination; + +public class Pageable { + private static final int MIN_PAGE = 1; + private static final int MIN_SIZE = 1; + + private final int page; + private final int size; + + public Pageable(Integer page, Integer size) { + this.page = Math.max(MIN_PAGE, page == null ? MIN_PAGE : page); + this.size = Math.max(MIN_SIZE, size == null ? MIN_SIZE : size); + } + + public static Pageable ofSize(int size) { + return new Pageable(null, size); + } + + public Pageable withPage(int page) { + return new Pageable(page, this.size); + } + + public int getPage() { + return page; + } + + public int getSize() { + return size; + } + + public long offset() { + return (this.page - MIN_PAGE) * this.limit(); + } + + public long limit() { + return this.size; + } + + public long pageCount(long resultNumber) { + return (resultNumber + this.size - 1) / this.size; + } + + public boolean isLastPage(long resultNumber) { + return pageCount(resultNumber) == this.page; + } +} diff --git a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQuery.java b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQuery.java new file mode 100644 index 0000000..3fe7832 --- /dev/null +++ b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQuery.java @@ -0,0 +1,61 @@ +package com.coreoz.plume.db.querydsl.pagination; + +import com.querydsl.core.QueryResults; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.sql.SQLQuery; + +public class SQLPaginatedQuery { + + private final SQLQuery sqlQuery; + + private SQLPaginatedQuery(SQLQuery sqlQuery) { + this.sqlQuery = sqlQuery; + } + + public static SQLPaginatedQuery fromQuery(SQLQuery sqlQuery) { + return new SQLPaginatedQuery<>(sqlQuery); + } + + public SQLPaginatedQuery withSort(SortOption sortOption) { + return new SQLPaginatedQuery<>( + sqlQuery + .orderBy( + new OrderSpecifier<>( + sortOption.sortDirection(), + sortOption.sortOn().getSortColumn() + ) + ) + ); + } + + public Page paginate(Pageable pageable) { + return this.fetchPage( + this.sqlQuery, + pageable + ); + } + + public Page paginate(Integer size, Integer page) { + return this.paginate(Pageable.ofSize(size).withPage(page)); + } + + private Page fetchPage(SQLQuery baseQuery, Pageable pageable) { + QueryResults paginatedQueryResults = applyPagination(baseQuery, pageable) + .fetchResults(); + + return new Page<>( + paginatedQueryResults.getResults(), + paginatedQueryResults.getTotal(), + pageable.pageCount(paginatedQueryResults.getTotal()), + pageable.getPage(), + paginatedQueryResults.getResults().size(), + pageable.isLastPage(paginatedQueryResults.getTotal()) + ); + } + + private static SQLQuery applyPagination(SQLQuery query, Pageable pageable) { + return query + .offset(pageable.offset()) + .limit(pageable.limit()); + } +} diff --git a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SortOption.java b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SortOption.java new file mode 100644 index 0000000..f2511b9 --- /dev/null +++ b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SortOption.java @@ -0,0 +1,54 @@ +package com.coreoz.plume.db.querydsl.pagination; + +import com.querydsl.core.types.Order; + +import javax.annotation.Nullable; + +public class SortOption { + private final SortPath sortOn; + private final Order sortDirection; + + private SortOption( + SortPath sortOn, + Order sortDirection + ) { + this.sortOn = sortOn; + this.sortDirection = sortDirection; + } + + public SortPath sortOn() { + return sortOn; + } + + public Order sortDirection() { + return sortDirection; + } + + public static SortOption from(@Nullable SortPath path, @Nullable String sortDirection) { + if (path == null) { + return null; + } + if (sortDirection == null) { + return new SortOption( + path, + Order.ASC + ); + } + Order sortOrder = orderDirection(sortDirection.toUpperCase()); + return new SortOption( + path, + sortOrder + ); + } + + private static Order orderDirection(@Nullable String sortDirection) { + if (sortDirection == null) { + return null; + } + try { + return Order.valueOf(sortDirection); + } catch (IllegalArgumentException e) { + return null; + } + } +} diff --git a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SortPath.java b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SortPath.java new file mode 100644 index 0000000..db20163 --- /dev/null +++ b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SortPath.java @@ -0,0 +1,8 @@ +package com.coreoz.plume.db.querydsl.pagination; + +import com.querydsl.core.types.dsl.StringPath; + +public interface SortPath { + String name(); + StringPath getSortColumn(); +} diff --git a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/PageableTest.java b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/PageableTest.java new file mode 100644 index 0000000..3a3e4e8 --- /dev/null +++ b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/PageableTest.java @@ -0,0 +1,72 @@ +package com.coreoz.plume.db.querydsl.pagination; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PageableTest { + + @Test + public void pageable_with_0_size_should_return_default() { + Pageable pageable = Pageable.ofSize(0); + + assertThat(pageable.getSize()).isEqualTo(1); + assertThat(pageable.getPage()).isEqualTo(1); + } + + @Test + public void pageable_with_0_limit_should_return_default() { + Pageable pageable = Pageable.ofSize(0).withPage(0); + + assertThat(pageable.getPage()).isEqualTo(1); + } + + @Test + public void pageable_should_return_corresponding_offset() { + Pageable pageable = Pageable.ofSize(10).withPage(2); + + assertThat(pageable.offset()).isEqualTo(10); + } + + @Test + public void pageable_with_page_1_should_return_corresponding_offset() { + Pageable pageable = Pageable.ofSize(150).withPage(1); + + assertThat(pageable.offset()).isZero(); + } + + @Test + public void pageable_should_return_corresponding_size() { + Pageable pageable = Pageable.ofSize(150).withPage(1); + + assertThat(pageable.getSize()).isEqualTo(150); + } + + @Test + public void pageable_should_return_corresponding_page_count() { + Pageable pageable = Pageable.ofSize(150); + + assertThat(pageable.pageCount(475)).isEqualTo(4); + } + + @Test + public void pageable_with_no_results_should_return_corresponding_page_count() { + Pageable pageable = Pageable.ofSize(150); + + assertThat(pageable.pageCount(0)).isZero(); + } + + @Test + public void pageable_should_return_corresponding_is_last_page() { + Pageable pageable = Pageable.ofSize(150).withPage(3); + + assertThat(pageable.isLastPage(450)).isTrue(); + } + + @Test + public void pageable_with_0_results_should_return_corresponding_is_last_page() { + Pageable pageable = Pageable.ofSize(150).withPage(1); + + assertThat(pageable.isLastPage(0)).isFalse(); + } +} diff --git a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQueryTest.java b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQueryTest.java new file mode 100644 index 0000000..197fba5 --- /dev/null +++ b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQueryTest.java @@ -0,0 +1,67 @@ +package com.coreoz.plume.db.querydsl.pagination; + +import com.carlosbecker.guice.GuiceModules; +import com.carlosbecker.guice.GuiceTestRunner; +import com.coreoz.plume.db.querydsl.DbQuerydslTestModule; +import com.coreoz.plume.db.querydsl.db.QUser; +import com.coreoz.plume.db.querydsl.db.User; +import com.coreoz.plume.db.querydsl.transaction.TransactionManagerQuerydsl; +import com.querydsl.core.types.dsl.StringPath; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.inject.Inject; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(GuiceTestRunner.class) +@GuiceModules(DbQuerydslTestModule.class) +public class SQLPaginatedQueryTest { + + @Inject + TransactionManagerQuerydsl transactionManagerQuerydsl; + + @Before + public void before() { + User user = new User(); + user.setId(0L); + user.setName("To fetch"); + this.transactionManagerQuerydsl.insert(QUser.user) + .populate(user) + .execute(); + } + + @Test + public void should_paginate_users() { + Page page = SQLPaginatedQuery + .fromQuery( + this.transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + ) + .withSort(SortOption.from(UserSort.NAME, "DESC")) + .paginate(10, 1); + + assertThat(page.getCurrentPage()).isEqualTo(1); + assertThat(page.getTotalElements()).isEqualTo(1); + assertThat(page.getElements()).hasSize(1); + assertThat(page.getPageCount()).isEqualTo(1); + assertThat(page.isLastPage()).isTrue(); + } + + private enum UserSort implements SortPath { + NAME(QUser.user.name) + ; + + final StringPath sortColumn; + + public StringPath getSortColumn() { + return this.sortColumn; + } + + UserSort(StringPath sortColumn) { + this.sortColumn = sortColumn; + } + } +} From cf75001d13a61cabf3ab4c5abde994c9b028fb58 Mon Sep 17 00:00:00 2001 From: Lucas Amiaud Date: Tue, 11 Jun 2024 18:47:49 +0200 Subject: [PATCH 02/10] add Page.mapElements + rename attribute --- .../plume/db/querydsl/pagination/Page.java | 15 ++++++++- .../db/querydsl/pagination/Pageable.java | 2 +- .../pagination/SQLPaginatedQuery.java | 2 +- .../db/querydsl/pagination/PageTest.java | 31 +++++++++++++++++++ .../db/querydsl/pagination/PageableTest.java | 18 +++++------ .../pagination/SQLPaginatedQueryTest.java | 8 ++--- 6 files changed, 60 insertions(+), 16 deletions(-) create mode 100644 plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/PageTest.java diff --git a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Page.java b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Page.java index 7f702b0..358bdac 100644 --- a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Page.java +++ b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Page.java @@ -3,13 +3,26 @@ import lombok.Value; import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; @Value public class Page { List elements; long totalElements; - long pageCount; + long totalPages; long currentPage; long currentPageSize; boolean lastPage; + + public Page mapElements(Function mapper) { + return new Page<>( + this.elements.stream().map(mapper).collect(Collectors.toList()), + this.totalElements, + this.totalPages, + this.currentPage, + this.currentPageSize, + this.lastPage + ); + } } diff --git a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Pageable.java b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Pageable.java index 9f5447e..3482b86 100644 --- a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Pageable.java +++ b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Pageable.java @@ -12,7 +12,7 @@ public Pageable(Integer page, Integer size) { this.size = Math.max(MIN_SIZE, size == null ? MIN_SIZE : size); } - public static Pageable ofSize(int size) { + public static Pageable ofPageSize(int size) { return new Pageable(null, size); } diff --git a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQuery.java b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQuery.java index 3fe7832..367c00e 100644 --- a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQuery.java +++ b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQuery.java @@ -36,7 +36,7 @@ public Page paginate(Pageable pageable) { } public Page paginate(Integer size, Integer page) { - return this.paginate(Pageable.ofSize(size).withPage(page)); + return this.paginate(Pageable.ofPageSize(size).withPage(page)); } private Page fetchPage(SQLQuery baseQuery, Pageable pageable) { diff --git a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/PageTest.java b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/PageTest.java new file mode 100644 index 0000000..fa9b933 --- /dev/null +++ b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/PageTest.java @@ -0,0 +1,31 @@ +package com.coreoz.plume.db.querydsl.pagination; + +import com.coreoz.plume.db.querydsl.db.User; +import org.junit.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PageTest { + + @Test + public void should_map_users() { + User user = new User(); + user.setId(0L); + user.setName("To fetch"); + Page page = new Page<>( + List.of(user), + 1, + 1, + 1, + 1, + true + ); + + Page userNames = page.mapElements(User::getName); + + assertThat(userNames.getElements().stream().findFirst()).isNotEmpty(); + assertThat(userNames.getElements().stream().findFirst().get()).contains("To fetch"); + } +} diff --git a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/PageableTest.java b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/PageableTest.java index 3a3e4e8..993d5e4 100644 --- a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/PageableTest.java +++ b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/PageableTest.java @@ -8,7 +8,7 @@ public class PageableTest { @Test public void pageable_with_0_size_should_return_default() { - Pageable pageable = Pageable.ofSize(0); + Pageable pageable = Pageable.ofPageSize(0); assertThat(pageable.getSize()).isEqualTo(1); assertThat(pageable.getPage()).isEqualTo(1); @@ -16,56 +16,56 @@ public void pageable_with_0_size_should_return_default() { @Test public void pageable_with_0_limit_should_return_default() { - Pageable pageable = Pageable.ofSize(0).withPage(0); + Pageable pageable = Pageable.ofPageSize(0).withPage(0); assertThat(pageable.getPage()).isEqualTo(1); } @Test public void pageable_should_return_corresponding_offset() { - Pageable pageable = Pageable.ofSize(10).withPage(2); + Pageable pageable = Pageable.ofPageSize(10).withPage(2); assertThat(pageable.offset()).isEqualTo(10); } @Test public void pageable_with_page_1_should_return_corresponding_offset() { - Pageable pageable = Pageable.ofSize(150).withPage(1); + Pageable pageable = Pageable.ofPageSize(150).withPage(1); assertThat(pageable.offset()).isZero(); } @Test public void pageable_should_return_corresponding_size() { - Pageable pageable = Pageable.ofSize(150).withPage(1); + Pageable pageable = Pageable.ofPageSize(150).withPage(1); assertThat(pageable.getSize()).isEqualTo(150); } @Test public void pageable_should_return_corresponding_page_count() { - Pageable pageable = Pageable.ofSize(150); + Pageable pageable = Pageable.ofPageSize(150); assertThat(pageable.pageCount(475)).isEqualTo(4); } @Test public void pageable_with_no_results_should_return_corresponding_page_count() { - Pageable pageable = Pageable.ofSize(150); + Pageable pageable = Pageable.ofPageSize(150); assertThat(pageable.pageCount(0)).isZero(); } @Test public void pageable_should_return_corresponding_is_last_page() { - Pageable pageable = Pageable.ofSize(150).withPage(3); + Pageable pageable = Pageable.ofPageSize(150).withPage(3); assertThat(pageable.isLastPage(450)).isTrue(); } @Test public void pageable_with_0_results_should_return_corresponding_is_last_page() { - Pageable pageable = Pageable.ofSize(150).withPage(1); + Pageable pageable = Pageable.ofPageSize(150).withPage(1); assertThat(pageable.isLastPage(0)).isFalse(); } diff --git a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQueryTest.java b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQueryTest.java index 197fba5..a23ea67 100644 --- a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQueryTest.java +++ b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQueryTest.java @@ -43,10 +43,10 @@ public void should_paginate_users() { .withSort(SortOption.from(UserSort.NAME, "DESC")) .paginate(10, 1); - assertThat(page.getCurrentPage()).isEqualTo(1); - assertThat(page.getTotalElements()).isEqualTo(1); - assertThat(page.getElements()).hasSize(1); - assertThat(page.getPageCount()).isEqualTo(1); + assertThat(page.getCurrentPage()).isNotZero(); + assertThat(page.getTotalElements()).isNotZero(); + assertThat(page.getElements()).isNotEmpty(); + assertThat(page.getTotalPages()).isNotZero(); assertThat(page.isLastPage()).isTrue(); } From eeda6da138e322ff190ec28f00bab984bb89a3ee Mon Sep 17 00:00:00 2001 From: Lucas Amiaud Date: Thu, 19 Sep 2024 09:44:51 +0200 Subject: [PATCH 03/10] re-organization + implem slice --- plume-db-querydsl/pom.xml | 1 + .../plume/db/querydsl/pagination/Page.java | 28 --- .../db/querydsl/pagination/Pageable.java | 46 ---- .../pagination/SQLPaginatedQuery.java | 61 ----- .../db/querydsl/pagination/SortOption.java | 54 ----- .../db/querydsl/pagination/SortPath.java | 8 - .../pagination/SqlPaginatedQuery.java | 113 +++++++++ .../db/querydsl/pagination/PageTest.java | 31 --- .../db/querydsl/pagination/PageableTest.java | 72 ------ .../pagination/SQLPaginatedQueryTest.java | 67 ------ .../pagination/SqlPaginatedQueryTest.java | 214 ++++++++++++++++++ .../resources/db/migration/V1__user_table.sql | 4 +- .../resources/db/migration/V2__add_users.sql | 10 + plume-db/pom.xml | 9 +- .../com/coreoz/plume/db/pagination/Page.java | 50 ++++ .../com/coreoz/plume/db/pagination/Pages.java | 20 ++ .../coreoz/plume/db/pagination/Paginable.java | 32 --- .../com/coreoz/plume/db/pagination/Slice.java | 39 ++++ .../coreoz/plume/db/pagination/Sliceable.java | 24 ++ .../coreoz/plume/db/pagination/PageTest.java | 29 +++ .../coreoz/plume/db/pagination/PagesTest.java | 38 ++++ 21 files changed, 548 insertions(+), 402 deletions(-) delete mode 100644 plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Page.java delete mode 100644 plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Pageable.java delete mode 100644 plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQuery.java delete mode 100644 plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SortOption.java delete mode 100644 plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SortPath.java create mode 100644 plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQuery.java delete mode 100644 plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/PageTest.java delete mode 100644 plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/PageableTest.java delete mode 100644 plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQueryTest.java create mode 100644 plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java create mode 100644 plume-db-querydsl/src/test/resources/db/migration/V2__add_users.sql create mode 100644 plume-db/src/main/java/com/coreoz/plume/db/pagination/Page.java create mode 100644 plume-db/src/main/java/com/coreoz/plume/db/pagination/Pages.java delete mode 100644 plume-db/src/main/java/com/coreoz/plume/db/pagination/Paginable.java create mode 100644 plume-db/src/main/java/com/coreoz/plume/db/pagination/Slice.java create mode 100644 plume-db/src/main/java/com/coreoz/plume/db/pagination/Sliceable.java create mode 100644 plume-db/src/test/java/com/coreoz/plume/db/pagination/PageTest.java create mode 100644 plume-db/src/test/java/com/coreoz/plume/db/pagination/PagesTest.java diff --git a/plume-db-querydsl/pom.xml b/plume-db-querydsl/pom.xml index 862818f..60f3706 100644 --- a/plume-db-querydsl/pom.xml +++ b/plume-db-querydsl/pom.xml @@ -50,6 +50,7 @@ org.projectlombok lombok + provided diff --git a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Page.java b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Page.java deleted file mode 100644 index 358bdac..0000000 --- a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Page.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.coreoz.plume.db.querydsl.pagination; - -import lombok.Value; - -import java.util.List; -import java.util.function.Function; -import java.util.stream.Collectors; - -@Value -public class Page { - List elements; - long totalElements; - long totalPages; - long currentPage; - long currentPageSize; - boolean lastPage; - - public Page mapElements(Function mapper) { - return new Page<>( - this.elements.stream().map(mapper).collect(Collectors.toList()), - this.totalElements, - this.totalPages, - this.currentPage, - this.currentPageSize, - this.lastPage - ); - } -} diff --git a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Pageable.java b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Pageable.java deleted file mode 100644 index 3482b86..0000000 --- a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/Pageable.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.coreoz.plume.db.querydsl.pagination; - -public class Pageable { - private static final int MIN_PAGE = 1; - private static final int MIN_SIZE = 1; - - private final int page; - private final int size; - - public Pageable(Integer page, Integer size) { - this.page = Math.max(MIN_PAGE, page == null ? MIN_PAGE : page); - this.size = Math.max(MIN_SIZE, size == null ? MIN_SIZE : size); - } - - public static Pageable ofPageSize(int size) { - return new Pageable(null, size); - } - - public Pageable withPage(int page) { - return new Pageable(page, this.size); - } - - public int getPage() { - return page; - } - - public int getSize() { - return size; - } - - public long offset() { - return (this.page - MIN_PAGE) * this.limit(); - } - - public long limit() { - return this.size; - } - - public long pageCount(long resultNumber) { - return (resultNumber + this.size - 1) / this.size; - } - - public boolean isLastPage(long resultNumber) { - return pageCount(resultNumber) == this.page; - } -} diff --git a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQuery.java b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQuery.java deleted file mode 100644 index 367c00e..0000000 --- a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQuery.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.coreoz.plume.db.querydsl.pagination; - -import com.querydsl.core.QueryResults; -import com.querydsl.core.types.OrderSpecifier; -import com.querydsl.sql.SQLQuery; - -public class SQLPaginatedQuery { - - private final SQLQuery sqlQuery; - - private SQLPaginatedQuery(SQLQuery sqlQuery) { - this.sqlQuery = sqlQuery; - } - - public static SQLPaginatedQuery fromQuery(SQLQuery sqlQuery) { - return new SQLPaginatedQuery<>(sqlQuery); - } - - public SQLPaginatedQuery withSort(SortOption sortOption) { - return new SQLPaginatedQuery<>( - sqlQuery - .orderBy( - new OrderSpecifier<>( - sortOption.sortDirection(), - sortOption.sortOn().getSortColumn() - ) - ) - ); - } - - public Page paginate(Pageable pageable) { - return this.fetchPage( - this.sqlQuery, - pageable - ); - } - - public Page paginate(Integer size, Integer page) { - return this.paginate(Pageable.ofPageSize(size).withPage(page)); - } - - private Page fetchPage(SQLQuery baseQuery, Pageable pageable) { - QueryResults paginatedQueryResults = applyPagination(baseQuery, pageable) - .fetchResults(); - - return new Page<>( - paginatedQueryResults.getResults(), - paginatedQueryResults.getTotal(), - pageable.pageCount(paginatedQueryResults.getTotal()), - pageable.getPage(), - paginatedQueryResults.getResults().size(), - pageable.isLastPage(paginatedQueryResults.getTotal()) - ); - } - - private static SQLQuery applyPagination(SQLQuery query, Pageable pageable) { - return query - .offset(pageable.offset()) - .limit(pageable.limit()); - } -} diff --git a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SortOption.java b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SortOption.java deleted file mode 100644 index f2511b9..0000000 --- a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SortOption.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.coreoz.plume.db.querydsl.pagination; - -import com.querydsl.core.types.Order; - -import javax.annotation.Nullable; - -public class SortOption { - private final SortPath sortOn; - private final Order sortDirection; - - private SortOption( - SortPath sortOn, - Order sortDirection - ) { - this.sortOn = sortOn; - this.sortDirection = sortDirection; - } - - public SortPath sortOn() { - return sortOn; - } - - public Order sortDirection() { - return sortDirection; - } - - public static SortOption from(@Nullable SortPath path, @Nullable String sortDirection) { - if (path == null) { - return null; - } - if (sortDirection == null) { - return new SortOption( - path, - Order.ASC - ); - } - Order sortOrder = orderDirection(sortDirection.toUpperCase()); - return new SortOption( - path, - sortOrder - ); - } - - private static Order orderDirection(@Nullable String sortDirection) { - if (sortDirection == null) { - return null; - } - try { - return Order.valueOf(sortDirection); - } catch (IllegalArgumentException e) { - return null; - } - } -} diff --git a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SortPath.java b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SortPath.java deleted file mode 100644 index db20163..0000000 --- a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SortPath.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.coreoz.plume.db.querydsl.pagination; - -import com.querydsl.core.types.dsl.StringPath; - -public interface SortPath { - String name(); - StringPath getSortColumn(); -} diff --git a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQuery.java b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQuery.java new file mode 100644 index 0000000..60013ae --- /dev/null +++ b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQuery.java @@ -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 + *
+ * @param The type of elements contained in the request. + *
+ * Usage example: + *
+ * public Page fetchUsers() {
+ *   return SqlPaginatedQuery
+ *             .fromQuery(
+ *                 this.transactionManagerQuerydsl.selectQuery()
+ *                     .select(QUser.user)
+ *                     .from(QUser.user)
+ *             )
+ *             .withSort(QUser.user.name, Order.DESC)
+ *             .fetchPage(1, 10);
+ * }
+ * 
+ */ +public class SqlPaginatedQuery { + + private final SQLQuery sqlQuery; + + private SqlPaginatedQuery(SQLQuery sqlQuery) { + this.sqlQuery = sqlQuery; + } + + public static SqlPaginatedQuery fromQuery(SQLQuery sqlQuery) { + return new SqlPaginatedQuery<>(sqlQuery); + } + + @Nonnull + public > SqlPaginatedQuery withSort( + @Nonnull Expression 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 fetchPage( + int pageNumber, + int pageSize + ) { + QueryResults 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 fetchSlice( + int pageNumber, + int pageSize + ) { + List slicedQueryResults = this.sqlQuery + .offset(Pages.offset(pageNumber, pageSize)) + .limit(pageSize + 1) + .fetch(); + + boolean hasMore = slicedQueryResults.size() > pageSize; + + // Trim the results to the required size (if needed) + List items = hasMore ? slicedQueryResults.subList(0, pageSize) : slicedQueryResults; + + return new Slice<>( + items, + hasMore + ); + } +} diff --git a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/PageTest.java b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/PageTest.java deleted file mode 100644 index fa9b933..0000000 --- a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/PageTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.coreoz.plume.db.querydsl.pagination; - -import com.coreoz.plume.db.querydsl.db.User; -import org.junit.Test; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -public class PageTest { - - @Test - public void should_map_users() { - User user = new User(); - user.setId(0L); - user.setName("To fetch"); - Page page = new Page<>( - List.of(user), - 1, - 1, - 1, - 1, - true - ); - - Page userNames = page.mapElements(User::getName); - - assertThat(userNames.getElements().stream().findFirst()).isNotEmpty(); - assertThat(userNames.getElements().stream().findFirst().get()).contains("To fetch"); - } -} diff --git a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/PageableTest.java b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/PageableTest.java deleted file mode 100644 index 993d5e4..0000000 --- a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/PageableTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.coreoz.plume.db.querydsl.pagination; - -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -public class PageableTest { - - @Test - public void pageable_with_0_size_should_return_default() { - Pageable pageable = Pageable.ofPageSize(0); - - assertThat(pageable.getSize()).isEqualTo(1); - assertThat(pageable.getPage()).isEqualTo(1); - } - - @Test - public void pageable_with_0_limit_should_return_default() { - Pageable pageable = Pageable.ofPageSize(0).withPage(0); - - assertThat(pageable.getPage()).isEqualTo(1); - } - - @Test - public void pageable_should_return_corresponding_offset() { - Pageable pageable = Pageable.ofPageSize(10).withPage(2); - - assertThat(pageable.offset()).isEqualTo(10); - } - - @Test - public void pageable_with_page_1_should_return_corresponding_offset() { - Pageable pageable = Pageable.ofPageSize(150).withPage(1); - - assertThat(pageable.offset()).isZero(); - } - - @Test - public void pageable_should_return_corresponding_size() { - Pageable pageable = Pageable.ofPageSize(150).withPage(1); - - assertThat(pageable.getSize()).isEqualTo(150); - } - - @Test - public void pageable_should_return_corresponding_page_count() { - Pageable pageable = Pageable.ofPageSize(150); - - assertThat(pageable.pageCount(475)).isEqualTo(4); - } - - @Test - public void pageable_with_no_results_should_return_corresponding_page_count() { - Pageable pageable = Pageable.ofPageSize(150); - - assertThat(pageable.pageCount(0)).isZero(); - } - - @Test - public void pageable_should_return_corresponding_is_last_page() { - Pageable pageable = Pageable.ofPageSize(150).withPage(3); - - assertThat(pageable.isLastPage(450)).isTrue(); - } - - @Test - public void pageable_with_0_results_should_return_corresponding_is_last_page() { - Pageable pageable = Pageable.ofPageSize(150).withPage(1); - - assertThat(pageable.isLastPage(0)).isFalse(); - } -} diff --git a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQueryTest.java b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQueryTest.java deleted file mode 100644 index a23ea67..0000000 --- a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SQLPaginatedQueryTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.coreoz.plume.db.querydsl.pagination; - -import com.carlosbecker.guice.GuiceModules; -import com.carlosbecker.guice.GuiceTestRunner; -import com.coreoz.plume.db.querydsl.DbQuerydslTestModule; -import com.coreoz.plume.db.querydsl.db.QUser; -import com.coreoz.plume.db.querydsl.db.User; -import com.coreoz.plume.db.querydsl.transaction.TransactionManagerQuerydsl; -import com.querydsl.core.types.dsl.StringPath; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import javax.inject.Inject; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(GuiceTestRunner.class) -@GuiceModules(DbQuerydslTestModule.class) -public class SQLPaginatedQueryTest { - - @Inject - TransactionManagerQuerydsl transactionManagerQuerydsl; - - @Before - public void before() { - User user = new User(); - user.setId(0L); - user.setName("To fetch"); - this.transactionManagerQuerydsl.insert(QUser.user) - .populate(user) - .execute(); - } - - @Test - public void should_paginate_users() { - Page page = SQLPaginatedQuery - .fromQuery( - this.transactionManagerQuerydsl.selectQuery() - .select(QUser.user) - .from(QUser.user) - ) - .withSort(SortOption.from(UserSort.NAME, "DESC")) - .paginate(10, 1); - - assertThat(page.getCurrentPage()).isNotZero(); - assertThat(page.getTotalElements()).isNotZero(); - assertThat(page.getElements()).isNotEmpty(); - assertThat(page.getTotalPages()).isNotZero(); - assertThat(page.isLastPage()).isTrue(); - } - - private enum UserSort implements SortPath { - NAME(QUser.user.name) - ; - - final StringPath sortColumn; - - public StringPath getSortColumn() { - return this.sortColumn; - } - - UserSort(StringPath sortColumn) { - this.sortColumn = sortColumn; - } - } -} diff --git a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java new file mode 100644 index 0000000..6a868a8 --- /dev/null +++ b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java @@ -0,0 +1,214 @@ +package com.coreoz.plume.db.querydsl.pagination; + +import com.carlosbecker.guice.GuiceModules; +import com.carlosbecker.guice.GuiceTestRunner; +import com.coreoz.plume.db.pagination.Page; +import com.coreoz.plume.db.pagination.Slice; +import com.coreoz.plume.db.querydsl.DbQuerydslTestModule; +import com.coreoz.plume.db.querydsl.db.QUser; +import com.coreoz.plume.db.querydsl.db.User; +import com.coreoz.plume.db.querydsl.transaction.TransactionManagerQuerydsl; +import com.querydsl.core.types.Order; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.inject.Inject; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * file V2__add_users.sql + * 10 users were added + */ +@RunWith(GuiceTestRunner.class) +@GuiceModules(DbQuerydslTestModule.class) +public class SqlPaginatedQueryTest { + + @Inject + TransactionManagerQuerydsl transactionManagerQuerydsl; + + @Test + public void fetch_page_with_correct_pagination_should_paginate_users() { + Page page = SqlPaginatedQuery + .fromQuery( + this.transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + ) + .withSort(QUser.user.name, Order.DESC) + .fetchPage(1, 10); + + assertThat(page.pagesCount()).isNotZero(); + assertThat(page.totalCount()).isNotZero(); + assertThat(page.items()).isNotEmpty(); + assertThat(page.pagesCount()).isNotZero(); + assertThat(page.hasMore()).isFalse(); + } + + @Test + public void fetch_page_with_wrong_pagination_should_return_empty_items() { + Page page = SqlPaginatedQuery + .fromQuery( + this.transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + ) + .withSort(QUser.user.name, Order.DESC) + .fetchPage(2, 10); + + assertThat(page.pagesCount()).isNotZero(); + assertThat(page.totalCount()).isNotZero(); + assertThat(page.items()).isEmpty(); + assertThat(page.pagesCount()).isNotZero(); + assertThat(page.hasMore()).isFalse(); + } + + @Test + public void fetch_page_with_minimum_page_and_page_size_should_return_results() { + Page page = SqlPaginatedQuery + .fromQuery( + this.transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + ) + .withSort(QUser.user.name, Order.ASC) + .fetchPage(1, 1); // Minimum page number and page size + + assertThat(page.pagesCount()).isNotZero(); + assertThat(page.totalCount()).isNotZero(); + assertThat(page.items()).hasSize(1); // Only one item expected due to page size of 1 + assertThat(page.hasMore()).isTrue(); // Has more items since page size is small + } + + @Test + public void fetch_page_with_page_size_larger_than_total_results_should_return_all() { + Page page = SqlPaginatedQuery + .fromQuery( + this.transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + ) + .withSort(QUser.user.name, Order.ASC) + .fetchPage(1, 100); // Large page size compared to available results + + assertThat(page.totalCount()).isGreaterThan(0); + assertThat(page.items().size()).isLessThanOrEqualTo(100); // Should return all available users + assertThat(page.hasMore()).isFalse(); // No more items because page size exceeds total results + } + + @Test(expected = IllegalArgumentException.class) + public void fetch_page_with_invalid_negative_page_number_should_throw_exception() { + SqlPaginatedQuery + .fromQuery( + this.transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + ) + .withSort(QUser.user.name, Order.ASC) + .fetchPage(-1, 10); // Invalid negative page number + } + + @Test(expected = IllegalArgumentException.class) + public void fetch_page_with_invalid_negative_page_size_should_throw_exception() { + SqlPaginatedQuery + .fromQuery( + this.transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + ) + .withSort(QUser.user.name, Order.ASC) + .fetchPage(1, -10); // Invalid negative page size + } + + @Test + public void fetch_page_without_sorting_should_paginate_users() { + Page page = SqlPaginatedQuery + .fromQuery( + this.transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + ) + .fetchPage(1, 10); // No sorting applied + + assertThat(page.pagesCount()).isNotZero(); + assertThat(page.totalCount()).isNotZero(); + assertThat(page.items()).isNotEmpty(); + assertThat(page.hasMore()).isFalse(); // Assuming there aren't more than 10 users in the test + } + + @Test + public void fetch_page_with_empty_query_should_return_no_results() { + Page page = SqlPaginatedQuery + .fromQuery( + this.transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + .where(QUser.user.name.eq("Non-existent user")) // Query with no matches + ) + .fetchPage(1, 10); + + assertThat(page.totalCount()).isEqualTo(0); // No total count + assertThat(page.items()).isEmpty(); // No items should be returned + assertThat(page.hasMore()).isFalse(); // No more pages since there's no data + } + + @Test + public void fetch_slice_with_minimum_page_and_page_size_should_return_results() { + Slice slice = SqlPaginatedQuery + .fromQuery( + this.transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + ) + .withSort(QUser.user.name, Order.ASC) + .fetchSlice(1, 1); // Minimum page number and page size + + assertThat(slice.items()).hasSize(1); // Only one item expected due to page size of 1 + assertThat(slice.hasMore()).isTrue(); // Has more items because of page size being small + } + + @Test + public void fetch_slice_with_empty_query_should_return_no_results() { + Slice slice = SqlPaginatedQuery + .fromQuery( + this.transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + .where(QUser.user.name.eq("Non-existent user")) // Query with no matches + ) + .fetchSlice(1, 10); + + assertThat(slice.items()).isEmpty(); // No items should be returned + assertThat(slice.hasMore()).isFalse(); // No more items since there's no data + } + + @Test + public void fetch_slice_with_correct_pagination_should_slice_users() { + Slice page = SqlPaginatedQuery + .fromQuery( + this.transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + ) + .withSort(QUser.user.name, Order.ASC) + .fetchSlice(1, 50); + + assertThat(page.items()).isNotEmpty(); + assertThat(page.hasMore()).isFalse(); + } + + @Test + public void fetch_slice_with_wrong_pagination_should_return_empty_items() { + Slice page = SqlPaginatedQuery + .fromQuery( + this.transactionManagerQuerydsl.selectQuery() + .select(QUser.user) + .from(QUser.user) + ) + .withSort(QUser.user.name, Order.ASC) + .fetchSlice(2, 50); + + assertThat(page.items()).isEmpty(); + assertThat(page.hasMore()).isFalse(); + } +} diff --git a/plume-db-querydsl/src/test/resources/db/migration/V1__user_table.sql b/plume-db-querydsl/src/test/resources/db/migration/V1__user_table.sql index ac32f66..749820c 100644 --- a/plume-db-querydsl/src/test/resources/db/migration/V1__user_table.sql +++ b/plume-db-querydsl/src/test/resources/db/migration/V1__user_table.sql @@ -1,5 +1,5 @@ -CREATE TABLE "USER" ( - "ID" BIGINT NOT NULL, +CREATE TABLE "USER" ( + "ID" BIGINT NOT NULL, "NAME" VARCHAR(255), "ACTIVE" BOOLEAN, "CREATION_DATE" TIMESTAMP, diff --git a/plume-db-querydsl/src/test/resources/db/migration/V2__add_users.sql b/plume-db-querydsl/src/test/resources/db/migration/V2__add_users.sql new file mode 100644 index 0000000..f6e6361 --- /dev/null +++ b/plume-db-querydsl/src/test/resources/db/migration/V2__add_users.sql @@ -0,0 +1,10 @@ +INSERT INTO "USER" VALUES (1, 'Admin 1', true, NOW()); +INSERT INTO "USER" VALUES (2, 'Admin 2', true, NOW()); +INSERT INTO "USER" VALUES (3, 'Admin 3', true, NOW()); +INSERT INTO "USER" VALUES (4, 'Admin 4', true, NOW()); +INSERT INTO "USER" VALUES (5, 'Admin 5', true, NOW()); +INSERT INTO "USER" VALUES (6, 'Admin 6', true, NOW()); +INSERT INTO "USER" VALUES (7, 'Admin 7', true, NOW()); +INSERT INTO "USER" VALUES (8, 'Admin 8', true, NOW()); +INSERT INTO "USER" VALUES (9, 'Admin 9', true, NOW()); +INSERT INTO "USER" VALUES (10, 'Admin 10', true, NOW()); diff --git a/plume-db/pom.xml b/plume-db/pom.xml index 5d26d4b..8059b9d 100644 --- a/plume-db/pom.xml +++ b/plume-db/pom.xml @@ -35,6 +35,13 @@ true
+ + + org.projectlombok + lombok + provided + + com.coreoz @@ -55,4 +62,4 @@ - \ No newline at end of file + diff --git a/plume-db/src/main/java/com/coreoz/plume/db/pagination/Page.java b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Page.java new file mode 100644 index 0000000..71df49c --- /dev/null +++ b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Page.java @@ -0,0 +1,50 @@ +package com.coreoz.plume.db.pagination; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Represents a page of data in a paginated structure. + * + * @param items the items in the page + * @param totalCount the total number of items in the collection + * @param pagesCount the total number of pages + * @param currentPage the current page + * @param hasMore boolean set to true if there is another page after + * + * @param The type of items contained in the page. + */ +public record Page( + @Nonnull List items, + long totalCount, + long pagesCount, + long currentPage, + boolean hasMore +) implements Sliceable { + + /** + * Maps the items of the paginated list + * @param mapper the mapper + * @return the page with mapped items + * @param The type of elements to be mapped to in the page. + *
+ * Usage example: + *
+     * public Page fetchUsers() {
+     *   return this.userService.fetchPage(1, 10).map(user -> new UserUpdated(user));
+     * }
+     * 
+ */ + public Page map(@Nonnull Function mapper) { + return new Page<>( + this.items.stream().map(mapper).toList(), + this.totalCount, + this.pagesCount, + this.currentPage, + this.hasMore + ); + } + +} diff --git a/plume-db/src/main/java/com/coreoz/plume/db/pagination/Pages.java b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Pages.java new file mode 100644 index 0000000..743b186 --- /dev/null +++ b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Pages.java @@ -0,0 +1,20 @@ +package com.coreoz.plume.db.pagination; + +public class Pages { + + private Pages() { + // hide implicit constructor + } + + public static long offset(int pageNumber, int pageSize) { + return (long) (pageNumber - 1) * pageSize; + } + + public static long pageCount(int pageSize, long resultNumber) { + return (resultNumber + pageSize - 1) / pageSize; + } + + public static boolean hasMore(int pageNumber, int pageSize, long resultNumber) { + return pageNumber < pageCount(pageSize, resultNumber); + } +} diff --git a/plume-db/src/main/java/com/coreoz/plume/db/pagination/Paginable.java b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Paginable.java deleted file mode 100644 index 9787437..0000000 --- a/plume-db/src/main/java/com/coreoz/plume/db/pagination/Paginable.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.coreoz.plume.db.pagination; - -import java.util.List; - -/** - * Describe an object that handles pagination. - * - * @param The paginable element - */ -public interface Paginable { - - /** - * Returns the number of elements available - */ - long count(); - - /** - * Returns all the available elements list. - */ - List fetch(); - - /** - * Fetch a page of elements. - * - * @param page The page sought, starts at 0 - * @param pageSize The number of elements by page - * @return the elements list on the page sought - * @throws IndexOutOfBoundsException If page < 0 or if page*pageSize > {@link #count()} - */ - List fetch(int page, int pageSize); - -} diff --git a/plume-db/src/main/java/com/coreoz/plume/db/pagination/Slice.java b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Slice.java new file mode 100644 index 0000000..0c80362 --- /dev/null +++ b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Slice.java @@ -0,0 +1,39 @@ +package com.coreoz.plume.db.pagination; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.function.Function; + +/** + * Represents a portion (or slice) of a larger dataset. + * + * @param items the items in the slice + * @param hasMore boolean set to true if there is another slice after + * + * @param The type of elements contained in the slice. + */ +public record Slice( + @Nonnull List items, + boolean hasMore +) implements Sliceable { + + /** + * Maps the items of the slice + * @param mapper the mapper + * @return the slice with mapped items + * @param The type of elements to be mapped to in the slice. + *
+ * Usage example: + *
+     * public Page fetchUsers() {
+     *   return this.userService.fetchSlice(1, 10).map(user -> new UserUpdated(user));
+     * }
+     * 
+ */ + public Slice map(@Nonnull Function mapper) { + return new Slice<>( + this.items.stream().map(mapper).toList(), + this.hasMore + ); + } +} diff --git a/plume-db/src/main/java/com/coreoz/plume/db/pagination/Sliceable.java b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Sliceable.java new file mode 100644 index 0000000..da24320 --- /dev/null +++ b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Sliceable.java @@ -0,0 +1,24 @@ +package com.coreoz.plume.db.pagination; + +import java.util.List; + +/** + * Common interface for {@link Page} or {@link Slice} data structures. + * + * @param The type of elements contained in the pageable structure. + */ +public interface Sliceable { + /** + * Returns the list of elements contained in this sliceable structure. + * + * @return A list of elements. + */ + List items(); + + /** + * Indicates whether there are more elements available beyond this sliceable structure. + * + * @return {@code true} if there are more elements, {@code false} otherwise. + */ + boolean hasMore(); +} diff --git a/plume-db/src/test/java/com/coreoz/plume/db/pagination/PageTest.java b/plume-db/src/test/java/com/coreoz/plume/db/pagination/PageTest.java new file mode 100644 index 0000000..eb690ea --- /dev/null +++ b/plume-db/src/test/java/com/coreoz/plume/db/pagination/PageTest.java @@ -0,0 +1,29 @@ +package com.coreoz.plume.db.pagination; + +import org.assertj.core.api.Assertions; +import org.junit.Test; + +import java.util.List; + +public class PageTest { + + @Test + public void should_map_users() { + User user = new User(1L, "To fetch"); + Page page = new Page<>( + List.of(user), + 1, + 1, + 1, + true + ); + + Page userNames = page.map(User::name); + + Assertions.assertThat(userNames.items().stream().findFirst()).isNotEmpty(); + Assertions.assertThat(userNames.items().stream().findFirst().get()).contains("To fetch"); + } + + private record User(long id, String name) { + } +} diff --git a/plume-db/src/test/java/com/coreoz/plume/db/pagination/PagesTest.java b/plume-db/src/test/java/com/coreoz/plume/db/pagination/PagesTest.java new file mode 100644 index 0000000..2f2d85a --- /dev/null +++ b/plume-db/src/test/java/com/coreoz/plume/db/pagination/PagesTest.java @@ -0,0 +1,38 @@ +package com.coreoz.plume.db.pagination; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PagesTest { + + @Test + public void pageable_should_return_corresponding_offset() { + assertThat(Pages.offset(2, 10)).isEqualTo(10); + } + + @Test + public void pageable_with_page_1_should_return_corresponding_offset() { + assertThat(Pages.offset(1, 150)).isZero(); + } + + @Test + public void pageable_should_return_corresponding_page_count() { + assertThat(Pages.pageCount(150, 475)).isEqualTo(4); + } + + @Test + public void pageable_with_no_results_should_return_corresponding_page_count() { + assertThat(Pages.pageCount(150, 0)).isZero(); + } + + @Test + public void pageable_should_return_corresponding_has_more() { + assertThat(Pages.hasMore(3, 150, 450)).isFalse(); + } + + @Test + public void pageable_with_0_results_should_return_corresponding_has_more() { + assertThat(Pages.hasMore(1, 150, 0)).isFalse(); + } +} From 156738b00bf8776eb6b3e238346a31d13c05a9ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Manteaux?= Date: Thu, 19 Sep 2024 10:43:58 +0200 Subject: [PATCH 04/10] Use Jakarta annotations --- .../coreoz/plume/db/querydsl/dagger/DaggerQuerydslModule.java | 2 +- .../plume/db/querydsl/pagination/SqlPaginatedQueryTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/dagger/DaggerQuerydslModule.java b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/dagger/DaggerQuerydslModule.java index a313221..3361620 100644 --- a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/dagger/DaggerQuerydslModule.java +++ b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/dagger/DaggerQuerydslModule.java @@ -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; diff --git a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java index 6a868a8..b7de6ac 100644 --- a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java +++ b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java @@ -12,7 +12,7 @@ import org.junit.Test; import org.junit.runner.RunWith; -import javax.inject.Inject; +import jakarta.inject.Inject; import static org.assertj.core.api.Assertions.assertThat; From a6d6830ae82ecbd58976a7537721b648f5b9ec4e Mon Sep 17 00:00:00 2001 From: Lucas Amiaud Date: Thu, 19 Sep 2024 11:16:50 +0200 Subject: [PATCH 05/10] add readme doc for pagination --- plume-db-querydsl/README.md | 111 ++++++++++++++++++ .../pagination/SqlPaginatedQueryTest.java | 26 ++-- .../com/coreoz/plume/db/pagination/Page.java | 5 +- .../com/coreoz/plume/db/pagination/Slice.java | 2 +- 4 files changed, 126 insertions(+), 18 deletions(-) diff --git a/plume-db-querydsl/README.md b/plume-db-querydsl/README.md index 928b485..32e648a 100644 --- a/plume-db-querydsl/README.md +++ b/plume-db-querydsl/README.md @@ -64,3 +64,114 @@ 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`: 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`: 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. + +This allows you to manage and paginate large datasets efficiently when working with QueryDSL. + +**Key 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). +- **Efficient Slicing**: Fetches a "slice" of data without loading the entire dataset, useful when you only need to know if there are more results to load (e.g., in infinite scroll scenarios). +- **Full Page Information**: Provides detailed information about the paginated dataset, including the total count, total pages, and whether there are more results. + +**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 +public enum SortPath { + + // users + USER_LIST_EMAIL("email", QUser.user.email), + USER_LIST_FIRST_NAME("first_name", QUser.user.firstName), + USER_LIST_LAST_NAME("last_name", QUser.user.lastName), + USER_LIST_LAST_LOGIN_DATE("last_login_date", QUser.user.lastLogin), + // more complex cases + PRIORITY( + "user_priority", + new CaseBuilder() + .when(QUser.user.priority.eq(StudyPriority.MEDIUM.name())).then(1) + .when(QUser.user.priority.eq(StudyPriority.HIGH.name())).then(2) + .when(QUser.user.priority.eq(StudyPriority.VERY_HIGH.name())).then(3) + .otherwise(1) + ), + ; + + private final String sortKey; + private final Expression path; + + @Nullable + public static SortPath fromSortKey(String sortKey) { + return Arrays.stream(SortPath.values()) + .filter(entry -> entry.sortKey.equals(sortKey)) + .findFirst() + .orElse(null); + } +} +``` + +Then declare your WebService: + +```java +@POST +@Path("/search") +@Operation(description = "Retrieves admin users") +@Consumes(MediaType.APPLICATION_JSON) +public Page 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, + SortPath.fromSortKey(sort), + sortDirection + ); +} +``` + +Then apply the pagination from the API call with `SqlPaginatedQuery` : + +```java +public Page 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); +} +``` diff --git a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java index b7de6ac..17e80c8 100644 --- a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java +++ b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java @@ -38,10 +38,9 @@ public void fetch_page_with_correct_pagination_should_paginate_users() { .withSort(QUser.user.name, Order.DESC) .fetchPage(1, 10); - assertThat(page.pagesCount()).isNotZero(); - assertThat(page.totalCount()).isNotZero(); - assertThat(page.items()).isNotEmpty(); - assertThat(page.pagesCount()).isNotZero(); + assertThat(page.pagesCount()).isEqualTo(1); + assertThat(page.totalCount()).isEqualTo(10); + assertThat(page.items()).hasSize(10); assertThat(page.hasMore()).isFalse(); } @@ -56,10 +55,9 @@ public void fetch_page_with_wrong_pagination_should_return_empty_items() { .withSort(QUser.user.name, Order.DESC) .fetchPage(2, 10); - assertThat(page.pagesCount()).isNotZero(); - assertThat(page.totalCount()).isNotZero(); + assertThat(page.pagesCount()).isEqualTo(1); + assertThat(page.totalCount()).isEqualTo(10); assertThat(page.items()).isEmpty(); - assertThat(page.pagesCount()).isNotZero(); assertThat(page.hasMore()).isFalse(); } @@ -74,8 +72,8 @@ public void fetch_page_with_minimum_page_and_page_size_should_return_results() { .withSort(QUser.user.name, Order.ASC) .fetchPage(1, 1); // Minimum page number and page size - assertThat(page.pagesCount()).isNotZero(); - assertThat(page.totalCount()).isNotZero(); + assertThat(page.pagesCount()).isEqualTo(10); + assertThat(page.totalCount()).isEqualTo(10); assertThat(page.items()).hasSize(1); // Only one item expected due to page size of 1 assertThat(page.hasMore()).isTrue(); // Has more items since page size is small } @@ -91,8 +89,8 @@ public void fetch_page_with_page_size_larger_than_total_results_should_return_al .withSort(QUser.user.name, Order.ASC) .fetchPage(1, 100); // Large page size compared to available results - assertThat(page.totalCount()).isGreaterThan(0); - assertThat(page.items().size()).isLessThanOrEqualTo(100); // Should return all available users + assertThat(page.totalCount()).isEqualTo(10); + assertThat(page.items()).hasSize(10); // Should return all available users assertThat(page.hasMore()).isFalse(); // No more items because page size exceeds total results } @@ -130,9 +128,9 @@ public void fetch_page_without_sorting_should_paginate_users() { ) .fetchPage(1, 10); // No sorting applied - assertThat(page.pagesCount()).isNotZero(); - assertThat(page.totalCount()).isNotZero(); - assertThat(page.items()).isNotEmpty(); + assertThat(page.pagesCount()).isEqualTo(1); + assertThat(page.totalCount()).isEqualTo(10); + assertThat(page.items()).hasSize(10); assertThat(page.hasMore()).isFalse(); // Assuming there aren't more than 10 users in the test } diff --git a/plume-db/src/main/java/com/coreoz/plume/db/pagination/Page.java b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Page.java index 71df49c..bb897b6 100644 --- a/plume-db/src/main/java/com/coreoz/plume/db/pagination/Page.java +++ b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Page.java @@ -3,7 +3,6 @@ import javax.annotation.Nonnull; import java.util.List; import java.util.function.Function; -import java.util.stream.Collectors; /** * Represents a page of data in a paginated structure. @@ -11,8 +10,8 @@ * @param items the items in the page * @param totalCount the total number of items in the collection * @param pagesCount the total number of pages - * @param currentPage the current page - * @param hasMore boolean set to true if there is another page after + * @param currentPage the current page, starts at 1 + * @param hasMore boolean set to true if there is another page after this one * * @param The type of items contained in the page. */ diff --git a/plume-db/src/main/java/com/coreoz/plume/db/pagination/Slice.java b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Slice.java index 0c80362..56bf93b 100644 --- a/plume-db/src/main/java/com/coreoz/plume/db/pagination/Slice.java +++ b/plume-db/src/main/java/com/coreoz/plume/db/pagination/Slice.java @@ -8,7 +8,7 @@ * Represents a portion (or slice) of a larger dataset. * * @param items the items in the slice - * @param hasMore boolean set to true if there is another slice after + * @param hasMore boolean set to true if there is another slice after this one * * @param The type of elements contained in the slice. */ From ff67d6d1505c6b5017750e2e41689837d68dbcaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Manteaux?= Date: Thu, 19 Sep 2024 11:31:40 +0200 Subject: [PATCH 06/10] Fix unit tests --- .../pagination/SqlPaginatedQueryTest.java | 101 ++++++++++-------- .../resources/db/migration/V2__add_users.sql | 10 -- 2 files changed, 58 insertions(+), 53 deletions(-) delete mode 100644 plume-db-querydsl/src/test/resources/db/migration/V2__add_users.sql diff --git a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java index 17e80c8..5f92c05 100644 --- a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java +++ b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java @@ -1,18 +1,16 @@ package com.coreoz.plume.db.querydsl.pagination; -import com.carlosbecker.guice.GuiceModules; -import com.carlosbecker.guice.GuiceTestRunner; import com.coreoz.plume.db.pagination.Page; import com.coreoz.plume.db.pagination.Slice; import com.coreoz.plume.db.querydsl.DbQuerydslTestModule; import com.coreoz.plume.db.querydsl.db.QUser; import com.coreoz.plume.db.querydsl.db.User; +import com.coreoz.plume.db.querydsl.db.UserDao; import com.coreoz.plume.db.querydsl.transaction.TransactionManagerQuerydsl; +import com.google.inject.Guice; +import com.google.inject.Injector; import com.querydsl.core.types.Order; import org.junit.Test; -import org.junit.runner.RunWith; - -import jakarta.inject.Inject; import static org.assertj.core.api.Assertions.assertThat; @@ -20,43 +18,58 @@ * file V2__add_users.sql * 10 users were added */ -@RunWith(GuiceTestRunner.class) -@GuiceModules(DbQuerydslTestModule.class) public class SqlPaginatedQueryTest { - - @Inject - TransactionManagerQuerydsl transactionManagerQuerydsl; + static final int USER_COUNT = 100; + + static TransactionManagerQuerydsl transactionManagerQuerydsl; + + static { + Injector injector = Guice.createInjector(new DbQuerydslTestModule()); + transactionManagerQuerydsl = injector.getInstance(TransactionManagerQuerydsl.class); + UserDao userDao = injector.getInstance(UserDao.class); + long userCountToInsert = transactionManagerQuerydsl.selectQuery().select(QUser.user).from(QUser.user).fetchCount(); + if (userCountToInsert > USER_COUNT) { + throw new IllegalStateException("There is already "+ userCountToInsert +" users, which is more than " + USER_COUNT + ", USER_COUNT should be increased"); + } + for (int i = 0; i < (USER_COUNT - userCountToInsert); i++) { + User user = new User(); + user.setName("Page user"); + userDao.save(user); + } + } @Test public void fetch_page_with_correct_pagination_should_paginate_users() { + int pageSize = 10; Page page = SqlPaginatedQuery .fromQuery( - this.transactionManagerQuerydsl.selectQuery() + transactionManagerQuerydsl.selectQuery() .select(QUser.user) .from(QUser.user) ) .withSort(QUser.user.name, Order.DESC) - .fetchPage(1, 10); + .fetchPage(1, pageSize); - assertThat(page.pagesCount()).isEqualTo(1); - assertThat(page.totalCount()).isEqualTo(10); - assertThat(page.items()).hasSize(10); - assertThat(page.hasMore()).isFalse(); + assertThat(page.pagesCount()).isEqualTo(USER_COUNT / pageSize); + assertThat(page.totalCount()).isEqualTo(USER_COUNT); + assertThat(page.items()).hasSize(pageSize); + assertThat(page.hasMore()).isTrue(); } @Test public void fetch_page_with_wrong_pagination_should_return_empty_items() { + int pageSize = 10; Page page = SqlPaginatedQuery .fromQuery( - this.transactionManagerQuerydsl.selectQuery() + transactionManagerQuerydsl.selectQuery() .select(QUser.user) .from(QUser.user) ) .withSort(QUser.user.name, Order.DESC) - .fetchPage(2, 10); + .fetchPage(11, pageSize); - assertThat(page.pagesCount()).isEqualTo(1); - assertThat(page.totalCount()).isEqualTo(10); + assertThat(page.pagesCount()).isEqualTo(USER_COUNT / pageSize); + assertThat(page.totalCount()).isEqualTo(USER_COUNT); assertThat(page.items()).isEmpty(); assertThat(page.hasMore()).isFalse(); } @@ -65,15 +78,15 @@ public void fetch_page_with_wrong_pagination_should_return_empty_items() { public void fetch_page_with_minimum_page_and_page_size_should_return_results() { Page page = SqlPaginatedQuery .fromQuery( - this.transactionManagerQuerydsl.selectQuery() + transactionManagerQuerydsl.selectQuery() .select(QUser.user) .from(QUser.user) ) .withSort(QUser.user.name, Order.ASC) .fetchPage(1, 1); // Minimum page number and page size - assertThat(page.pagesCount()).isEqualTo(10); - assertThat(page.totalCount()).isEqualTo(10); + assertThat(page.pagesCount()).isEqualTo(USER_COUNT); + assertThat(page.totalCount()).isEqualTo(USER_COUNT); assertThat(page.items()).hasSize(1); // Only one item expected due to page size of 1 assertThat(page.hasMore()).isTrue(); // Has more items since page size is small } @@ -82,15 +95,16 @@ public void fetch_page_with_minimum_page_and_page_size_should_return_results() { public void fetch_page_with_page_size_larger_than_total_results_should_return_all() { Page page = SqlPaginatedQuery .fromQuery( - this.transactionManagerQuerydsl.selectQuery() + transactionManagerQuerydsl.selectQuery() .select(QUser.user) .from(QUser.user) ) .withSort(QUser.user.name, Order.ASC) - .fetchPage(1, 100); // Large page size compared to available results + .fetchPage(1, USER_COUNT + 1); // Large page size compared to available results - assertThat(page.totalCount()).isEqualTo(10); - assertThat(page.items()).hasSize(10); // Should return all available users + assertThat(page.pagesCount()).isEqualTo(1); + assertThat(page.totalCount()).isEqualTo(USER_COUNT); + assertThat(page.items()).hasSize(USER_COUNT); // Should return all available users assertThat(page.hasMore()).isFalse(); // No more items because page size exceeds total results } @@ -98,7 +112,7 @@ public void fetch_page_with_page_size_larger_than_total_results_should_return_al public void fetch_page_with_invalid_negative_page_number_should_throw_exception() { SqlPaginatedQuery .fromQuery( - this.transactionManagerQuerydsl.selectQuery() + transactionManagerQuerydsl.selectQuery() .select(QUser.user) .from(QUser.user) ) @@ -110,7 +124,7 @@ public void fetch_page_with_invalid_negative_page_number_should_throw_exception( public void fetch_page_with_invalid_negative_page_size_should_throw_exception() { SqlPaginatedQuery .fromQuery( - this.transactionManagerQuerydsl.selectQuery() + transactionManagerQuerydsl.selectQuery() .select(QUser.user) .from(QUser.user) ) @@ -120,25 +134,26 @@ public void fetch_page_with_invalid_negative_page_size_should_throw_exception() @Test public void fetch_page_without_sorting_should_paginate_users() { + int pageSize = 10; Page page = SqlPaginatedQuery .fromQuery( - this.transactionManagerQuerydsl.selectQuery() + transactionManagerQuerydsl.selectQuery() .select(QUser.user) .from(QUser.user) ) - .fetchPage(1, 10); // No sorting applied + .fetchPage(1, pageSize); // No sorting applied - assertThat(page.pagesCount()).isEqualTo(1); - assertThat(page.totalCount()).isEqualTo(10); - assertThat(page.items()).hasSize(10); - assertThat(page.hasMore()).isFalse(); // Assuming there aren't more than 10 users in the test + assertThat(page.pagesCount()).isEqualTo(USER_COUNT / pageSize); + assertThat(page.totalCount()).isEqualTo(USER_COUNT); + assertThat(page.items()).hasSize(pageSize); + assertThat(page.hasMore()).isTrue(); } @Test public void fetch_page_with_empty_query_should_return_no_results() { Page page = SqlPaginatedQuery .fromQuery( - this.transactionManagerQuerydsl.selectQuery() + transactionManagerQuerydsl.selectQuery() .select(QUser.user) .from(QUser.user) .where(QUser.user.name.eq("Non-existent user")) // Query with no matches @@ -154,7 +169,7 @@ public void fetch_page_with_empty_query_should_return_no_results() { public void fetch_slice_with_minimum_page_and_page_size_should_return_results() { Slice slice = SqlPaginatedQuery .fromQuery( - this.transactionManagerQuerydsl.selectQuery() + transactionManagerQuerydsl.selectQuery() .select(QUser.user) .from(QUser.user) ) @@ -169,7 +184,7 @@ public void fetch_slice_with_minimum_page_and_page_size_should_return_results() public void fetch_slice_with_empty_query_should_return_no_results() { Slice slice = SqlPaginatedQuery .fromQuery( - this.transactionManagerQuerydsl.selectQuery() + transactionManagerQuerydsl.selectQuery() .select(QUser.user) .from(QUser.user) .where(QUser.user.name.eq("Non-existent user")) // Query with no matches @@ -184,14 +199,14 @@ public void fetch_slice_with_empty_query_should_return_no_results() { public void fetch_slice_with_correct_pagination_should_slice_users() { Slice page = SqlPaginatedQuery .fromQuery( - this.transactionManagerQuerydsl.selectQuery() + transactionManagerQuerydsl.selectQuery() .select(QUser.user) .from(QUser.user) ) .withSort(QUser.user.name, Order.ASC) - .fetchSlice(1, 50); + .fetchSlice(1, USER_COUNT + 1); - assertThat(page.items()).isNotEmpty(); + assertThat(page.items()).hasSize(USER_COUNT); assertThat(page.hasMore()).isFalse(); } @@ -199,12 +214,12 @@ public void fetch_slice_with_correct_pagination_should_slice_users() { public void fetch_slice_with_wrong_pagination_should_return_empty_items() { Slice page = SqlPaginatedQuery .fromQuery( - this.transactionManagerQuerydsl.selectQuery() + transactionManagerQuerydsl.selectQuery() .select(QUser.user) .from(QUser.user) ) .withSort(QUser.user.name, Order.ASC) - .fetchSlice(2, 50); + .fetchSlice(2, USER_COUNT); assertThat(page.items()).isEmpty(); assertThat(page.hasMore()).isFalse(); diff --git a/plume-db-querydsl/src/test/resources/db/migration/V2__add_users.sql b/plume-db-querydsl/src/test/resources/db/migration/V2__add_users.sql deleted file mode 100644 index f6e6361..0000000 --- a/plume-db-querydsl/src/test/resources/db/migration/V2__add_users.sql +++ /dev/null @@ -1,10 +0,0 @@ -INSERT INTO "USER" VALUES (1, 'Admin 1', true, NOW()); -INSERT INTO "USER" VALUES (2, 'Admin 2', true, NOW()); -INSERT INTO "USER" VALUES (3, 'Admin 3', true, NOW()); -INSERT INTO "USER" VALUES (4, 'Admin 4', true, NOW()); -INSERT INTO "USER" VALUES (5, 'Admin 5', true, NOW()); -INSERT INTO "USER" VALUES (6, 'Admin 6', true, NOW()); -INSERT INTO "USER" VALUES (7, 'Admin 7', true, NOW()); -INSERT INTO "USER" VALUES (8, 'Admin 8', true, NOW()); -INSERT INTO "USER" VALUES (9, 'Admin 9', true, NOW()); -INSERT INTO "USER" VALUES (10, 'Admin 10', true, NOW()); From a09b573f6e074e97d104039f758a3952350ab758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Manteaux?= Date: Thu, 19 Sep 2024 11:34:52 +0200 Subject: [PATCH 07/10] Fix sonar issues --- .../coreoz/plume/db/querydsl/pagination/SqlPaginatedQuery.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQuery.java b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQuery.java index 60013ae..c5ec754 100644 --- a/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQuery.java +++ b/plume-db-querydsl/src/main/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQuery.java @@ -97,7 +97,7 @@ public Slice fetchSlice( ) { List slicedQueryResults = this.sqlQuery .offset(Pages.offset(pageNumber, pageSize)) - .limit(pageSize + 1) + .limit(pageSize + 1L) .fetch(); boolean hasMore = slicedQueryResults.size() > pageSize; From 312e7dd7082ec327e2e486ec4b22046f81bee067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Manteaux?= Date: Thu, 19 Sep 2024 11:34:53 +0200 Subject: [PATCH 08/10] Fix sonar issues --- .../plume/db/querydsl/pagination/SqlPaginatedQueryTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java index 5f92c05..9f0bfd8 100644 --- a/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java +++ b/plume-db-querydsl/src/test/java/com/coreoz/plume/db/querydsl/pagination/SqlPaginatedQueryTest.java @@ -160,7 +160,7 @@ public void fetch_page_with_empty_query_should_return_no_results() { ) .fetchPage(1, 10); - assertThat(page.totalCount()).isEqualTo(0); // No total count + assertThat(page.totalCount()).isZero(); // No total count assertThat(page.items()).isEmpty(); // No items should be returned assertThat(page.hasMore()).isFalse(); // No more pages since there's no data } From 2c2a7c62b88d6a63605f86e9ec738f53e1502433 Mon Sep 17 00:00:00 2001 From: Lucas Amiaud Date: Thu, 19 Sep 2024 11:46:37 +0200 Subject: [PATCH 09/10] add slice test + fix doc --- plume-db-querydsl/README.md | 3 ++- .../coreoz/plume/db/pagination/SliceTest.java | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 plume-db/src/test/java/com/coreoz/plume/db/pagination/SliceTest.java diff --git a/plume-db-querydsl/README.md b/plume-db-querydsl/README.md index 32e648a..ef9e388 100644 --- a/plume-db-querydsl/README.md +++ b/plume-db-querydsl/README.md @@ -110,10 +110,11 @@ public enum SortPath { private final Expression path; @Nullable - public static SortPath fromSortKey(String sortKey) { + public static Expression fromSortKey(String sortKey) { return Arrays.stream(SortPath.values()) .filter(entry -> entry.sortKey.equals(sortKey)) .findFirst() + .map(sortPath -> sortPath.path) .orElse(null); } } diff --git a/plume-db/src/test/java/com/coreoz/plume/db/pagination/SliceTest.java b/plume-db/src/test/java/com/coreoz/plume/db/pagination/SliceTest.java new file mode 100644 index 0000000..681eb14 --- /dev/null +++ b/plume-db/src/test/java/com/coreoz/plume/db/pagination/SliceTest.java @@ -0,0 +1,26 @@ +package com.coreoz.plume.db.pagination; + +import org.assertj.core.api.Assertions; +import org.junit.Test; + +import java.util.List; + +public class SliceTest { + + @Test + public void should_map_users() { + User user = new User(1L, "To fetch"); + Slice page = new Slice<>( + List.of(user), + true + ); + + Slice userNames = page.map(User::name); + + Assertions.assertThat(userNames.items().stream().findFirst()).isNotEmpty(); + Assertions.assertThat(userNames.items().stream().findFirst().get()).contains("To fetch"); + } + + private record User(long id, String name) { + } +} From cdd22aee7a8f11ac51e9d082420edbc96cd56f9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Manteaux?= Date: Thu, 19 Sep 2024 11:48:50 +0200 Subject: [PATCH 10/10] Fix documentation --- plume-db-querydsl/README.md | 52 ++++++++++++------------------------- 1 file changed, 16 insertions(+), 36 deletions(-) diff --git a/plume-db-querydsl/README.md b/plume-db-querydsl/README.md index 32e648a..542d851 100644 --- a/plume-db-querydsl/README.md +++ b/plume-db-querydsl/README.md @@ -66,56 +66,36 @@ To generate Querydsl entities, a good choice is to use this 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. +### 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`: 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`: 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` -This allows you to manage and paginate large datasets efficiently when working with QueryDSL. - -**Key Features**: - +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). -- **Efficient Slicing**: Fetches a "slice" of data without loading the entire dataset, useful when you only need to know if there are more results to load (e.g., in infinite scroll scenarios). -- **Full Page Information**: Provides detailed information about the paginated dataset, including the total count, total pages, and whether there are more results. - -**Working with Pagination from a WebService**: +### 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 -public enum SortPath { - +@Getter +public enum UserSortPath { // users - USER_LIST_EMAIL("email", QUser.user.email), - USER_LIST_FIRST_NAME("first_name", QUser.user.firstName), - USER_LIST_LAST_NAME("last_name", QUser.user.lastName), - USER_LIST_LAST_LOGIN_DATE("last_login_date", QUser.user.lastLogin), - // more complex cases - PRIORITY( - "user_priority", - new CaseBuilder() - .when(QUser.user.priority.eq(StudyPriority.MEDIUM.name())).then(1) - .when(QUser.user.priority.eq(StudyPriority.HIGH.name())).then(2) - .when(QUser.user.priority.eq(StudyPriority.VERY_HIGH.name())).then(3) - .otherwise(1) - ), + EMAIL(QUser.user.email), + FIRST_NAME(QUser.user.firstName), + LAST_NAME(QUser.user.lastName), + LAST_LOGIN_DATE(QUser.user.lastLogin), ; - private final String sortKey; private final Expression path; - - @Nullable - public static SortPath fromSortKey(String sortKey) { - return Arrays.stream(SortPath.values()) - .filter(entry -> entry.sortKey.equals(sortKey)) - .findFirst() - .orElse(null); - } } ``` @@ -144,7 +124,7 @@ public Page searchUsers( userSearchRequest, page, size, - SortPath.fromSortKey(sort), + UserSortPath.valueOf(sort), sortDirection ); }