From b29a238c1e8664b7c09c50bb80f2b3a549f7934d Mon Sep 17 00:00:00 2001 From: Raja Kolli Date: Sun, 1 Oct 2023 04:37:59 +0000 Subject: [PATCH] feat: refactor pagination --- .../jooq/r2dbc/config/Initializer.java | 32 +++-- .../jooq/r2dbc/handler/PostHandler.java | 15 ++- .../jooq/r2dbc/handler/TagHandler.java | 35 +++++- .../jooq/r2dbc/repository/PostRepository.java | 9 +- .../repository/PostTagRelationRepository.java | 7 -- .../jooq/r2dbc/repository/TagRepository.java | 3 +- .../custom/CustomPostRepository.java | 10 ++ .../custom/CustomTagRepository.java | 11 ++ .../custom/impl/CustomPostRepositoryImpl.java | 89 ++++++++++++++ .../custom/impl/CustomTagRepositoryImpl.java | 37 ++++++ .../repository/custom/impl/JooqSorting.java | 54 +++++++++ .../jooq/r2dbc/router/WebRouterConfig.java | 44 +++++-- .../jooq/r2dbc/service/PostService.java | 109 +----------------- .../jooq/r2dbc/service/TagService.java | 7 +- .../jooq/r2dbc/utils/AppConstants.java | 4 +- .../db/migration/postgresql/V1__01_init.sql | 80 +++++++------ .../jooq/r2dbc/router/TagRouterIT.java | 32 +++++ 17 files changed, 386 insertions(+), 192 deletions(-) delete mode 100644 r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/PostTagRelationRepository.java create mode 100644 r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/custom/CustomPostRepository.java create mode 100644 r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/custom/CustomTagRepository.java create mode 100644 r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/custom/impl/CustomPostRepositoryImpl.java create mode 100644 r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/custom/impl/CustomTagRepositoryImpl.java create mode 100644 r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/custom/impl/JooqSorting.java create mode 100644 r2dbc/boot-jooq-r2dbc-sample/src/test/java/com/example/jooq/r2dbc/router/TagRouterIT.java diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/config/Initializer.java b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/config/Initializer.java index 8069e3159..25481c44a 100644 --- a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/config/Initializer.java +++ b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/config/Initializer.java @@ -10,9 +10,14 @@ import com.example.jooq.r2dbc.config.logging.Loggable; import com.example.jooq.r2dbc.model.response.PostCommentResponse; import com.example.jooq.r2dbc.model.response.PostResponse; +import com.example.jooq.r2dbc.testcontainersflyway.db.tables.records.PostCommentsRecord; +import com.example.jooq.r2dbc.testcontainersflyway.db.tables.records.PostsRecord; +import com.example.jooq.r2dbc.testcontainersflyway.db.tables.records.PostsTagsRecord; +import com.example.jooq.r2dbc.testcontainersflyway.db.tables.records.TagsRecord; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.jooq.DSLContext; +import org.jooq.DeleteUsingStep; import org.jooq.Record1; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @@ -29,12 +34,24 @@ public class Initializer implements CommandLineRunner { @Loggable public void run(String... args) { log.info("Running Initializer....."); - Mono.from( - dslContext - .insertInto(POSTS) - .columns(POSTS.TITLE, POSTS.CONTENT) - .values("jooq test", "content of Jooq test") - .returningResult(POSTS.ID)) + DeleteUsingStep postsTagsRecordDeleteUsingStep = + dslContext.deleteFrom(POSTS_TAGS); + DeleteUsingStep tagsRecordDeleteUsingStep = dslContext.deleteFrom(TAGS); + DeleteUsingStep postCommentsRecordDeleteUsingStep = + dslContext.deleteFrom(POST_COMMENTS); + DeleteUsingStep postsRecordDeleteUsingStep = dslContext.deleteFrom(POSTS); + + Mono.from(postsTagsRecordDeleteUsingStep) + .then(Mono.from(tagsRecordDeleteUsingStep)) + .then(Mono.from(postCommentsRecordDeleteUsingStep)) + .then(Mono.from(postsRecordDeleteUsingStep)) + .then( + Mono.from( + dslContext + .insertInto(POSTS) + .columns(POSTS.TITLE, POSTS.CONTENT) + .values("jooq test", "content of Jooq test") + .returningResult(POSTS.ID))) .flatMap( postId -> Mono.from( @@ -71,8 +88,7 @@ public void run(String... args) { pid.component1(), "test comments 2") .returningResult(POST_COMMENTS.ID)) - .collectList() // Collect all comment IDs into a list - ) + .collectList()) .thenMany( dslContext .select( diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/handler/PostHandler.java b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/handler/PostHandler.java index 8e314c068..7842541a0 100644 --- a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/handler/PostHandler.java +++ b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/handler/PostHandler.java @@ -10,6 +10,7 @@ import com.example.jooq.r2dbc.model.response.PaginatedResult; import com.example.jooq.r2dbc.model.response.PostSummary; import com.example.jooq.r2dbc.service.PostService; +import com.example.jooq.r2dbc.utils.AppConstants; import java.net.URI; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; @@ -65,16 +66,22 @@ public Mono update(ServerRequest req) { @Loggable public Mono search(ServerRequest req) { - String sortDir = req.queryParam("sortDir").orElse("asc"); - String sortBy = req.queryParam("sortBy").orElse("id"); + String sortDir = req.queryParam("sortDir").orElse(AppConstants.DEFAULT_SORT_DIRECTION); + String sortBy = req.queryParam("sortBy").orElse(AppConstants.DEFAULT_SORT_BY); Sort sort = sortDir.equalsIgnoreCase(Sort.Direction.ASC.name()) ? Sort.by(sortBy).ascending() : Sort.by(sortBy).descending(); // create Pageable instance - int pageNo = req.queryParam("pageNo").map(Integer::parseInt).orElse(0); - int pageSize = req.queryParam("pageSize").map(Integer::parseInt).orElse(10); + int pageNo = + req.queryParam("pageNo") + .map(Integer::parseInt) + .orElse(AppConstants.DEFAULT_PAGE_NUMBER); + int pageSize = + req.queryParam("pageSize") + .map(Integer::parseInt) + .orElse(AppConstants.DEFAULT_PAGE_SIZE); Pageable pageable = PageRequest.of(pageNo, pageSize, sort); return postService .findByKeyword(req.queryParam("keyword").orElse(""), pageable) diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/handler/TagHandler.java b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/handler/TagHandler.java index 15198fa91..9c45388f1 100644 --- a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/handler/TagHandler.java +++ b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/handler/TagHandler.java @@ -6,9 +6,14 @@ import com.example.jooq.r2dbc.config.logging.Loggable; import com.example.jooq.r2dbc.entities.Tags; import com.example.jooq.r2dbc.model.request.TagDto; +import com.example.jooq.r2dbc.model.response.PaginatedResult; import com.example.jooq.r2dbc.service.TagService; +import com.example.jooq.r2dbc.utils.AppConstants; import java.net.URI; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; @@ -16,16 +21,38 @@ @Component @RequiredArgsConstructor +@Loggable public class TagHandler { + // Service responsible for handling Tag-related business logic private final TagService tagService; - @Loggable + // Retrieve all tags based on query parameters for sorting and pagination public Mono getAll(ServerRequest req) { - return ok().body(this.tagService.findAll(), Tags.class); + // Extracting and setting sort direction and field from query parameters + String sortDir = req.queryParam("sortDir").orElse(AppConstants.DEFAULT_SORT_DIRECTION); + String sortBy = req.queryParam("sortBy").orElse(AppConstants.DEFAULT_SORT_BY); + Sort sort = + sortDir.equalsIgnoreCase(Sort.Direction.ASC.name()) + ? Sort.by(sortBy).ascending() + : Sort.by(sortBy).descending(); + + // Creating Pageable instance for pagination + int pageNo = + req.queryParam("pageNo") + .map(Integer::parseInt) + .orElse(AppConstants.DEFAULT_PAGE_NUMBER); + int pageSize = + req.queryParam("pageSize") + .map(Integer::parseInt) + .orElse(AppConstants.DEFAULT_PAGE_SIZE); + Pageable pageable = PageRequest.of(pageNo, pageSize, sort); + + // Returning paginated result of tags + return ok().body(this.tagService.findAll(pageable), PaginatedResult.class); } - @Loggable + // Retrieve a specific tag by its ID public Mono get(ServerRequest req) { return this.tagService .findById(req.pathVariable("id")) @@ -33,7 +60,7 @@ public Mono get(ServerRequest req) { .switchIfEmpty(ServerResponse.notFound().build()); } - @Loggable + // Create a new tag based on the request body public Mono create(ServerRequest req) { return req.bodyToMono(TagDto.class) .flatMap(this.tagService::create) diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/PostRepository.java b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/PostRepository.java index 456d16fd0..a3c4b739b 100644 --- a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/PostRepository.java +++ b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/PostRepository.java @@ -1,13 +1,8 @@ package com.example.jooq.r2dbc.repository; import com.example.jooq.r2dbc.entities.Post; +import com.example.jooq.r2dbc.repository.custom.CustomPostRepository; import java.util.UUID; -import org.springframework.data.r2dbc.repository.Query; import org.springframework.data.r2dbc.repository.R2dbcRepository; -import reactor.core.publisher.Flux; -public interface PostRepository extends R2dbcRepository { - - @Query("SELECT * FROM posts where title like :title") - public Flux findByTitleContains(String title); -} +public interface PostRepository extends R2dbcRepository, CustomPostRepository {} diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/PostTagRelationRepository.java b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/PostTagRelationRepository.java deleted file mode 100644 index 43e5e0fab..000000000 --- a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/PostTagRelationRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.jooq.r2dbc.repository; - -import com.example.jooq.r2dbc.entities.PostTagRelation; -import java.util.UUID; -import org.springframework.data.r2dbc.repository.R2dbcRepository; - -public interface PostTagRelationRepository extends R2dbcRepository {} diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/TagRepository.java b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/TagRepository.java index 7ca65853a..245c8c8a0 100644 --- a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/TagRepository.java +++ b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/TagRepository.java @@ -1,7 +1,8 @@ package com.example.jooq.r2dbc.repository; import com.example.jooq.r2dbc.entities.Tags; +import com.example.jooq.r2dbc.repository.custom.CustomTagRepository; import java.util.UUID; import org.springframework.data.r2dbc.repository.R2dbcRepository; -public interface TagRepository extends R2dbcRepository {} +public interface TagRepository extends R2dbcRepository, CustomTagRepository {} diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/custom/CustomPostRepository.java b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/custom/CustomPostRepository.java new file mode 100644 index 000000000..c844fc6bc --- /dev/null +++ b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/custom/CustomPostRepository.java @@ -0,0 +1,10 @@ +package com.example.jooq.r2dbc.repository.custom; + +import com.example.jooq.r2dbc.model.response.PostResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import reactor.core.publisher.Mono; + +public interface CustomPostRepository { + Mono> findByKeyword(String keyword, Pageable pageable); +} diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/custom/CustomTagRepository.java b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/custom/CustomTagRepository.java new file mode 100644 index 000000000..08fde662d --- /dev/null +++ b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/custom/CustomTagRepository.java @@ -0,0 +1,11 @@ +package com.example.jooq.r2dbc.repository.custom; + +import com.example.jooq.r2dbc.entities.Tags; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import reactor.core.publisher.Mono; + +public interface CustomTagRepository { + + Mono> findAll(Pageable pageable); +} diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/custom/impl/CustomPostRepositoryImpl.java b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/custom/impl/CustomPostRepositoryImpl.java new file mode 100644 index 000000000..04d596a23 --- /dev/null +++ b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/custom/impl/CustomPostRepositoryImpl.java @@ -0,0 +1,89 @@ +package com.example.jooq.r2dbc.repository.custom.impl; + +import static com.example.jooq.r2dbc.testcontainersflyway.db.Tables.*; +import static com.example.jooq.r2dbc.testcontainersflyway.db.Tables.POSTS; +import static org.jooq.impl.DSL.multiset; +import static org.jooq.impl.DSL.select; + +import com.example.jooq.r2dbc.model.response.PostCommentResponse; +import com.example.jooq.r2dbc.model.response.PostResponse; +import com.example.jooq.r2dbc.repository.custom.CustomPostRepository; +import com.example.jooq.r2dbc.testcontainersflyway.db.tables.PostComments; +import com.example.jooq.r2dbc.testcontainersflyway.db.tables.Posts; +import org.jooq.Condition; +import org.jooq.DSLContext; +import org.jooq.Record1; +import org.jooq.impl.DSL; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class CustomPostRepositoryImpl extends JooqSorting implements CustomPostRepository { + + private final DSLContext dslContext; + + public CustomPostRepositoryImpl(DSLContext dslContext) { + this.dslContext = dslContext; + } + + @Override + public Mono> findByKeyword(String keyword, Pageable pageable) { + Condition where = DSL.trueCondition(); + if (StringUtils.hasText(keyword)) { + where = where.and(POSTS.TITLE.likeIgnoreCase("%" + keyword + "%")); + } + var dataSql = + dslContext + .select( + POSTS.ID, + POSTS.TITLE, + POSTS.CONTENT, + multiset( + select( + PostComments.POST_COMMENTS.ID, + PostComments.POST_COMMENTS.CONTENT, + PostComments.POST_COMMENTS + .CREATED_AT) + .from(PostComments.POST_COMMENTS) + .where( + PostComments.POST_COMMENTS.POST_ID + .eq(Posts.POSTS.ID))) + .as("comments") + .convertFrom( + record3s -> + record3s.into(PostCommentResponse.class)), + multiset( + select(TAGS.NAME) + .from(TAGS) + .join(POSTS_TAGS) + .on(TAGS.ID.eq(POSTS_TAGS.TAG_ID)) + .where(POSTS_TAGS.POST_ID.eq(POSTS.ID))) + .as("tags") + .convertFrom(record -> record.map(Record1::value1))) + .from(POSTS.leftJoin(POST_COMMENTS).on(POST_COMMENTS.POST_ID.eq(POSTS.ID))) + .where(where) + .groupBy(POSTS.ID) + .orderBy(getSortFields(pageable.getSort(), POSTS)) + .limit(pageable.getPageSize()) + .offset(pageable.getOffset()); + + var countSql = dslContext.selectCount().from(POSTS).where(where); + + return Mono.zip( + Flux.from(dataSql) + .map( + r -> + new PostResponse( + r.value1(), + r.value2(), + r.value3(), + r.value4(), + r.value5())) + .collectList(), + Mono.from(countSql).map(Record1::value1)) + .map(it -> new PageImpl<>(it.getT1(), pageable, it.getT2())); + } +} diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/custom/impl/CustomTagRepositoryImpl.java b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/custom/impl/CustomTagRepositoryImpl.java new file mode 100644 index 000000000..d5f661245 --- /dev/null +++ b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/custom/impl/CustomTagRepositoryImpl.java @@ -0,0 +1,37 @@ +package com.example.jooq.r2dbc.repository.custom.impl; + +import static com.example.jooq.r2dbc.testcontainersflyway.db.Tables.TAGS; + +import com.example.jooq.r2dbc.entities.Tags; +import com.example.jooq.r2dbc.repository.custom.CustomTagRepository; +import org.jooq.*; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class CustomTagRepositoryImpl extends JooqSorting implements CustomTagRepository { + + private final DSLContext dslContext; + + public CustomTagRepositoryImpl(DSLContext dslContext) { + this.dslContext = dslContext; + } + + @Override + public Mono> findAll(Pageable pageable) { + var dataSql = + dslContext + .select(TAGS.ID, TAGS.NAME) + .from(TAGS) + .orderBy(getSortFields(pageable.getSort(), TAGS)) + .limit(pageable.getPageSize()) + .offset(pageable.getOffset()); + var countSql = dslContext.selectCount().from(TAGS); + return Mono.zip( + Flux.from(dataSql).map(r -> new Tags(r.value1(), r.value2())).collectList(), + Mono.from(countSql).map(Record1::value1)) + .map(it -> new PageImpl<>(it.getT1(), pageable, it.getT2())); + } +} diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/custom/impl/JooqSorting.java b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/custom/impl/JooqSorting.java new file mode 100644 index 000000000..7688dc6a2 --- /dev/null +++ b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/custom/impl/JooqSorting.java @@ -0,0 +1,54 @@ +package com.example.jooq.r2dbc.repository.custom.impl; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import org.jooq.SortField; +import org.jooq.TableField; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.domain.Sort; + +public class JooqSorting { + + List> getSortFields(Sort sortSpecification, T tableType) { + List> querySortFields = new ArrayList<>(); + + if (sortSpecification == null) { + return querySortFields; + } + + for (Sort.Order specifiedField : sortSpecification) { + String sortFieldName = specifiedField.getProperty(); + Sort.Direction sortDirection = specifiedField.getDirection(); + + TableField tableField = getTableField(sortFieldName, tableType); + SortField querySortField = convertTableFieldToSortField(tableField, sortDirection); + querySortFields.add(querySortField); + } + + return querySortFields; + } + + private TableField getTableField(String sortFieldName, T tableType) { + TableField sortField; + try { + Field tableField = + tableType.getClass().getField(sortFieldName.toUpperCase(Locale.ROOT)); + sortField = (TableField) tableField.get(tableType); + } catch (NoSuchFieldException | IllegalAccessException ex) { + String errorMessage = String.format("Could not find table field: %s", sortFieldName); + throw new InvalidDataAccessApiUsageException(errorMessage, ex); + } + return sortField; + } + + private SortField convertTableFieldToSortField( + TableField tableField, Sort.Direction sortDirection) { + if (sortDirection == Sort.Direction.ASC) { + return tableField.asc(); + } else { + return tableField.desc(); + } + } +} diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/router/WebRouterConfig.java b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/router/WebRouterConfig.java index 1026f0311..38f177075 100644 --- a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/router/WebRouterConfig.java +++ b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/router/WebRouterConfig.java @@ -12,6 +12,7 @@ import com.example.jooq.r2dbc.model.request.TagDto; import com.example.jooq.r2dbc.model.response.PaginatedResult; import com.example.jooq.r2dbc.model.response.PostSummary; +import com.example.jooq.r2dbc.utils.AppConstants; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.enums.ParameterIn; @@ -61,7 +62,9 @@ public class WebRouterConfig { operation = @Operation( operationId = "search", - description = "search based on title", + description = "search posts based on title", + summary = + "searches based on title and fetches associated comments and tags using pagination", parameters = { @Parameter( name = "keyword", @@ -82,12 +85,12 @@ public class WebRouterConfig { name = "sortBy", description = "sort By Fields", in = ParameterIn.QUERY, - example = "id"), + example = AppConstants.DEFAULT_SORT_BY), @Parameter( name = "sortDir", description = "sortBy Direction asc/desc", in = ParameterIn.QUERY, - example = "asc") + example = AppConstants.DEFAULT_SORT_DIRECTION) }, responses = @ApiResponse( @@ -184,18 +187,39 @@ RouterFunction postsRouterFunction(PostHandler handler) { operation = @Operation( operationId = "all", + summary = "fetches all tags from database using pagination", + parameters = { + @Parameter( + name = "pageNo", + description = "page Number of page", + in = ParameterIn.QUERY, + example = "0"), + @Parameter( + name = "pageSize", + description = "max Number of records per page", + in = ParameterIn.QUERY, + example = "10"), + @Parameter( + name = "sortBy", + description = "sort By Fields", + in = ParameterIn.QUERY, + example = AppConstants.DEFAULT_SORT_BY), + @Parameter( + name = "sortDir", + description = "sortBy Direction asc/desc", + in = ParameterIn.QUERY, + example = AppConstants.DEFAULT_SORT_DIRECTION) + }, responses = @ApiResponse( responseCode = "200", content = @Content( - array = - @ArraySchema( - schema = - @Schema( - implementation = - Tags - .class)))))), + schema = + @Schema( + implementation = + PaginatedResult + .class))))), @RouterOperation( path = "/tags", method = RequestMethod.POST, diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/service/PostService.java b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/service/PostService.java index 661242e0a..814f0f97a 100644 --- a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/service/PostService.java +++ b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/service/PostService.java @@ -11,34 +11,21 @@ import com.example.jooq.r2dbc.model.request.CreatePostCommand; import com.example.jooq.r2dbc.model.request.CreatePostComment; import com.example.jooq.r2dbc.model.response.PaginatedResult; -import com.example.jooq.r2dbc.model.response.PostCommentResponse; import com.example.jooq.r2dbc.model.response.PostResponse; import com.example.jooq.r2dbc.model.response.PostSummary; import com.example.jooq.r2dbc.repository.PostRepository; -import com.example.jooq.r2dbc.testcontainersflyway.db.tables.PostComments; -import com.example.jooq.r2dbc.testcontainersflyway.db.tables.Posts; import com.example.jooq.r2dbc.testcontainersflyway.db.tables.records.PostCommentsRecord; import com.example.jooq.r2dbc.testcontainersflyway.db.tables.records.PostsTagsRecord; -import java.lang.reflect.Field; -import java.util.ArrayList; import java.util.List; -import java.util.Locale; import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.jooq.Condition; import org.jooq.DSLContext; import org.jooq.Record1; -import org.jooq.SortField; -import org.jooq.TableField; import org.jooq.impl.DSL; import org.jooq.impl.SQLDataType; -import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -139,101 +126,7 @@ public Mono> findByKeyword(String keyword, Pageabl pageable.getOffset(), pageable.getPageSize()); - Condition where = DSL.trueCondition(); - if (StringUtils.hasText(keyword)) { - where = where.and(POSTS.TITLE.likeIgnoreCase("%" + keyword + "%")); - } - var dataSql = - dslContext - .select( - POSTS.ID, - POSTS.TITLE, - POSTS.CONTENT, - multiset( - select( - PostComments.POST_COMMENTS.ID, - PostComments.POST_COMMENTS.CONTENT, - PostComments.POST_COMMENTS - .CREATED_AT) - .from(PostComments.POST_COMMENTS) - .where( - PostComments.POST_COMMENTS.POST_ID - .eq(Posts.POSTS.ID))) - .as("comments") - .convertFrom( - record3s -> - record3s.into(PostCommentResponse.class)), - multiset( - select(TAGS.NAME) - .from(TAGS) - .join(POSTS_TAGS) - .on(TAGS.ID.eq(POSTS_TAGS.TAG_ID)) - .where(POSTS_TAGS.POST_ID.eq(POSTS.ID))) - .as("tags") - .convertFrom(record -> record.map(Record1::value1))) - .from(POSTS.leftJoin(POST_COMMENTS).on(POST_COMMENTS.POST_ID.eq(POSTS.ID))) - .where(where) - .groupBy(POSTS.ID) - .orderBy(getSortFields(pageable.getSort())) - .limit(pageable.getPageSize()) - .offset(pageable.getOffset()); - - var countSql = dslContext.selectCount().from(POSTS).where(where); - - return Mono.zip( - Flux.from(dataSql) - .map( - r -> - new PostResponse( - r.value1(), - r.value2(), - r.value3(), - r.value4(), - r.value5())) - .collectList(), - Mono.from(countSql).map(Record1::value1)) - .map(it -> new PageImpl<>(it.getT1(), pageable, it.getT2())) - .map(PaginatedResult::new); - } - - private List> getSortFields(Sort sortSpecification) { - List> querySortFields = new ArrayList<>(); - - if (sortSpecification == null) { - return querySortFields; - } - - for (Sort.Order specifiedField : sortSpecification) { - String sortFieldName = specifiedField.getProperty(); - Sort.Direction sortDirection = specifiedField.getDirection(); - - TableField tableField = getTableField(sortFieldName); - SortField querySortField = convertTableFieldToSortField(tableField, sortDirection); - querySortFields.add(querySortField); - } - - return querySortFields; - } - - private TableField getTableField(String sortFieldName) { - TableField sortField; - try { - Field tableField = POSTS.getClass().getField(sortFieldName.toUpperCase(Locale.ROOT)); - sortField = (TableField) tableField.get(POSTS); - } catch (NoSuchFieldException | IllegalAccessException ex) { - String errorMessage = String.format("Could not find table field: %s", sortFieldName); - throw new InvalidDataAccessApiUsageException(errorMessage, ex); - } - return sortField; - } - - private SortField convertTableFieldToSortField( - TableField tableField, Sort.Direction sortDirection) { - if (sortDirection == Sort.Direction.ASC) { - return tableField.asc(); - } else { - return tableField.desc(); - } + return this.postRepository.findByKeyword(keyword, pageable).map(PaginatedResult::new); } public Mono findById(String id) { diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/service/TagService.java b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/service/TagService.java index c9eae42f1..29b8c6e84 100644 --- a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/service/TagService.java +++ b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/service/TagService.java @@ -2,11 +2,12 @@ import com.example.jooq.r2dbc.entities.Tags; import com.example.jooq.r2dbc.model.request.TagDto; +import com.example.jooq.r2dbc.model.response.PaginatedResult; import com.example.jooq.r2dbc.repository.TagRepository; import java.util.UUID; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @Service @@ -15,8 +16,8 @@ public class TagService { private final TagRepository tagRepository; - public Flux findAll() { - return this.tagRepository.findAll(); + public Mono> findAll(Pageable pageable) { + return this.tagRepository.findAll(pageable).map(PaginatedResult::new); } public Mono findById(String id) { diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/utils/AppConstants.java b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/utils/AppConstants.java index 15c7cd47f..bc7c56c51 100644 --- a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/utils/AppConstants.java +++ b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/utils/AppConstants.java @@ -5,8 +5,8 @@ public final class AppConstants { public static final String PROFILE_NOT_PROD = "!" + PROFILE_PROD; public static final String PROFILE_TEST = "test"; public static final String PROFILE_NOT_TEST = "!" + PROFILE_TEST; - public static final String DEFAULT_PAGE_NUMBER = "0"; - public static final String DEFAULT_PAGE_SIZE = "10"; + public static final Integer DEFAULT_PAGE_NUMBER = 0; + public static final Integer DEFAULT_PAGE_SIZE = 10; public static final String DEFAULT_SORT_BY = "id"; public static final String DEFAULT_SORT_DIRECTION = "asc"; } diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/main/resources/db/migration/postgresql/V1__01_init.sql b/r2dbc/boot-jooq-r2dbc-sample/src/main/resources/db/migration/postgresql/V1__01_init.sql index 06e2ae1e5..eec3881b1 100644 --- a/r2dbc/boot-jooq-r2dbc-sample/src/main/resources/db/migration/postgresql/V1__01_init.sql +++ b/r2dbc/boot-jooq-r2dbc-sample/src/main/resources/db/migration/postgresql/V1__01_init.sql @@ -1,38 +1,42 @@ -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - -CREATE TABLE posts -( - ID uuid NOT NULL DEFAULT uuid_generate_v4 (), - TITLE text, - CONTENT text, - STATUS varchar(50), - created_at timestamptz DEFAULT NOW(), - created_by text, - updated_at timestamptz, - version BIGINT DEFAULT 0, - PRIMARY KEY (ID) -); - -create table post_comments -( - id uuid not null DEFAULT uuid_generate_v4 (), - content text, - created_at timestamptz DEFAULT NOW(), - POST_ID uuid, - primary key (id), - CONSTRAINT FK_POST_COMMENTS FOREIGN KEY (POST_ID) REFERENCES POSTS(ID) -); - -create table tags -( - id uuid not null DEFAULT uuid_generate_v4 (), - name text unique, - created_at timestamptz DEFAULT NOW(), - primary key (id) -); - -create table posts_tags -( - post_id uuid not null, - tag_id uuid not null -); +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE TABLE posts +( + ID uuid NOT NULL DEFAULT uuid_generate_v4 (), + TITLE text, + CONTENT text, + STATUS varchar(50), + created_at timestamptz DEFAULT NOW(), + created_by text, + updated_at timestamptz, + version BIGINT DEFAULT 0, + PRIMARY KEY (ID) +); + +create table post_comments +( + id uuid not null DEFAULT uuid_generate_v4 (), + content text, + created_at timestamptz DEFAULT NOW(), + POST_ID uuid, + primary key (id), + CONSTRAINT FK_POST_COMMENTS FOREIGN KEY (POST_ID) REFERENCES POSTS(ID) +); + +create table tags +( + id uuid not null DEFAULT uuid_generate_v4 (), + name text unique, + created_at timestamptz DEFAULT NOW(), + primary key (id) +); + +CREATE TABLE posts_tags +( + post_id UUID NOT NULL, + tag_id UUID NOT NULL, + CONSTRAINT FK_POST_TAGS_PID FOREIGN KEY (post_id) REFERENCES posts(id), + CONSTRAINT FK_POST_TAGS_TID FOREIGN KEY (tag_id) REFERENCES tags(id), + CONSTRAINT UK_POST_TAGS UNIQUE (post_id, tag_id) +); + diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/test/java/com/example/jooq/r2dbc/router/TagRouterIT.java b/r2dbc/boot-jooq-r2dbc-sample/src/test/java/com/example/jooq/r2dbc/router/TagRouterIT.java new file mode 100644 index 000000000..2de34b65c --- /dev/null +++ b/r2dbc/boot-jooq-r2dbc-sample/src/test/java/com/example/jooq/r2dbc/router/TagRouterIT.java @@ -0,0 +1,32 @@ +package com.example.jooq.r2dbc.router; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.example.jooq.r2dbc.common.AbstractIntegrationTest; +import com.example.jooq.r2dbc.model.response.PaginatedResult; +import org.junit.jupiter.api.Test; + +class TagRouterIT extends AbstractIntegrationTest { + + @Test + void findAllTags() { + this.webTestClient + .get() + .uri("/tags") + .exchange() + .expectStatus() + .is2xxSuccessful() + .expectBody(PaginatedResult.class) + .value( + paginatedResult -> { + assertThat(paginatedResult.data()).isNotEmpty().hasSize(1); + assertThat(paginatedResult.totalElements()).isEqualTo(1); + assertThat(paginatedResult.pageNumber()).isEqualTo(1); + assertThat(paginatedResult.totalPages()).isEqualTo(1); + assertThat(paginatedResult.isFirst()).isTrue(); + assertThat(paginatedResult.isLast()).isTrue(); + assertThat(paginatedResult.hasNext()).isFalse(); + assertThat(paginatedResult.hasPrevious()).isFalse(); + }); + } +}