diff --git a/r2dbc/boot-jooq-r2dbc-sample/pom.xml b/r2dbc/boot-jooq-r2dbc-sample/pom.xml index 79db02b3c..4d3ca30ec 100644 --- a/r2dbc/boot-jooq-r2dbc-sample/pom.xml +++ b/r2dbc/boot-jooq-r2dbc-sample/pom.xml @@ -336,6 +336,12 @@ + + + src/main/resources/**/*.sql + + + 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 eae3c123d..8458a3c41 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 @@ -4,36 +4,37 @@ import static com.example.jooq.r2dbc.testcontainersflyway.db.tables.PostComments.POST_COMMENTS; import static com.example.jooq.r2dbc.testcontainersflyway.db.tables.Posts.POSTS; import static com.example.jooq.r2dbc.testcontainersflyway.db.tables.Tags.TAGS; -import static org.jooq.impl.DSL.multiset; -import static org.jooq.impl.DSL.select; 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.repository.PostRepository; 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.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; @Component -@RequiredArgsConstructor -@Slf4j public class Initializer implements CommandLineRunner { + private static final Logger log = LoggerFactory.getLogger(Initializer.class); private final DSLContext dslContext; + private final PostRepository postRepository; + + public Initializer(DSLContext dslContext, PostRepository postRepository) { + this.dslContext = dslContext; + this.postRepository = postRepository; + } @Override @Loggable public void run(String... args) { - log.info("Running Initializer....."); + log.info("Running Initializer to use JOOQ only..."); DeleteUsingStep postsTagsRecordDeleteUsingStep = dslContext.deleteFrom(POSTS_TAGS); DeleteUsingStep tagsRecordDeleteUsingStep = dslContext.deleteFrom(TAGS); @@ -89,40 +90,9 @@ public void run(String... args) { "test comments 2") .returningResult(POST_COMMENTS.ID)) .collectList()) - .thenMany( - dslContext - .select( - POSTS.ID, - POSTS.TITLE, - POSTS.CONTENT, - multiset( - select( - POST_COMMENTS.ID, - POST_COMMENTS.CONTENT, - POST_COMMENTS.CREATED_AT) - .from(POST_COMMENTS) - .where( - POST_COMMENTS.POST_ID.eq( - 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) - .orderBy(POSTS.CREATED_AT)) + .thenMany(postRepository.retrievePostsWithCommentsAndTags(null)) .subscribe( - data -> log.debug("Retrieved data: {}", data.into(PostResponse.class)), + data -> log.debug("Retrieved data: {}", data), error -> log.debug("error: ", error), () -> log.debug("done")); } diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/entities/Comment.java b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/entities/Comment.java index 6451c695f..689f62349 100644 --- a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/entities/Comment.java +++ b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/entities/Comment.java @@ -2,23 +2,13 @@ import java.time.LocalDateTime; import java.util.UUID; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import lombok.ToString; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.Id; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Table; -@Getter -@Setter @ToString -@Builder -@NoArgsConstructor -@AllArgsConstructor @Table(value = "post_comments") public class Comment { @@ -35,4 +25,42 @@ public class Comment { @Column("post_id") private UUID postId; + + public Comment() {} + + public UUID getId() { + return id; + } + + public Comment setId(UUID id) { + this.id = id; + return this; + } + + public String getContent() { + return content; + } + + public Comment setContent(String content) { + this.content = content; + return this; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public Comment setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public UUID getPostId() { + return postId; + } + + public Comment setPostId(UUID postId) { + this.postId = postId; + return this; + } } diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/entities/Post.java b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/entities/Post.java index 15377e05d..db9527f5c 100644 --- a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/entities/Post.java +++ b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/entities/Post.java @@ -1,12 +1,8 @@ package com.example.jooq.r2dbc.entities; +import com.example.jooq.r2dbc.model.Status; import java.time.LocalDateTime; import java.util.UUID; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import lombok.ToString; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; @@ -16,12 +12,7 @@ import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Table; -@Getter -@Setter @ToString -@Builder -@NoArgsConstructor -@AllArgsConstructor @Table(value = "posts") public class Post { @@ -36,7 +27,6 @@ public class Post { private String content; @Column("status") - @Builder.Default private Status status = Status.DRAFT; @Column("created_at") @@ -54,4 +44,78 @@ public class Post { @Column("version") @Version private Short version; + + public Post() {} + + public UUID getId() { + return id; + } + + public Post setId(UUID id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public String getContent() { + return content; + } + + public Post setContent(String content) { + this.content = content; + return this; + } + + public Status getStatus() { + return status; + } + + public Post setStatus(Status status) { + this.status = status; + return this; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public Post setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public Post setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public Post setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + return this; + } + + public Short getVersion() { + return version; + } + + public Post setVersion(Short version) { + this.version = version; + return this; + } } diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/entities/Status.java b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/model/Status.java similarity index 64% rename from r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/entities/Status.java rename to r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/model/Status.java index 34266adbd..ad19abf3e 100644 --- a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/entities/Status.java +++ b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/model/Status.java @@ -1,4 +1,4 @@ -package com.example.jooq.r2dbc.entities; +package com.example.jooq.r2dbc.model; public enum Status { DRAFT, diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/model/response/PostResponse.java b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/model/response/PostResponse.java index 970af25fd..e538a7613 100644 --- a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/model/response/PostResponse.java +++ b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/model/response/PostResponse.java @@ -1,5 +1,6 @@ package com.example.jooq.r2dbc.model.response; +import com.example.jooq.r2dbc.model.Status; import java.util.List; import java.util.UUID; @@ -7,5 +8,7 @@ public record PostResponse( UUID id, String title, String content, + String createdBy, + Status status, List comments, List tags) {} diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/PostTagRepository.java b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/PostTagRepository.java new file mode 100644 index 000000000..7330704e0 --- /dev/null +++ b/r2dbc/boot-jooq-r2dbc-sample/src/main/java/com/example/jooq/r2dbc/repository/PostTagRepository.java @@ -0,0 +1,7 @@ +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 PostTagRepository extends R2dbcRepository {} 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 index c844fc6bc..a112fe964 100644 --- 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 @@ -1,10 +1,14 @@ package com.example.jooq.r2dbc.repository.custom; import com.example.jooq.r2dbc.model.response.PostResponse; +import org.jooq.Condition; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public interface CustomPostRepository { Mono> findByKeyword(String keyword, Pageable pageable); + + Flux retrievePostsWithCommentsAndTags(Condition condition); } 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 index 3ead0fdd5..9ec6cfb31 100644 --- 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 @@ -4,14 +4,14 @@ import static com.example.jooq.r2dbc.testcontainersflyway.db.Tables.POSTS_TAGS; import static com.example.jooq.r2dbc.testcontainersflyway.db.Tables.POST_COMMENTS; import static com.example.jooq.r2dbc.testcontainersflyway.db.Tables.TAGS; -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 java.util.List; import org.jooq.Condition; import org.jooq.DSLContext; +import org.jooq.Field; import org.jooq.Record1; import org.jooq.impl.DSL; import org.slf4j.Logger; @@ -19,7 +19,6 @@ 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; @@ -36,77 +35,82 @@ public CustomPostRepositoryImpl(DSLContext dslContext) { public Mono> findByKeyword(String keyword, Pageable pageable) { log.debug("Searching posts with keyword: {}, pageable: {}", keyword, pageable); // Build the where condition dynamically - Condition condition = DSL.trueCondition(); - if (StringUtils.hasText(keyword)) { - condition = - condition.and( - DSL.or( - POSTS.TITLE.likeIgnoreCase( - DSL.concat( - DSL.val("%"), DSL.val(keyword), DSL.val("%"))), - POSTS.CONTENT.likeIgnoreCase( - DSL.concat( - DSL.val("%"), - DSL.val(keyword), - DSL.val("%"))))); - } - - // Construct the main data SQL query - var dataQuery = - dslContext - .selectDistinct( - POSTS.ID, - POSTS.TITLE, - POSTS.CONTENT, - // Fetch comments as a multiset - multiset( - select( - POST_COMMENTS.ID, - POST_COMMENTS.CONTENT, - POST_COMMENTS.CREATED_AT) - .from(POST_COMMENTS) - .where(POST_COMMENTS.POST_ID.eq(POSTS.ID))) - .as("comments") - .convertFrom( - records -> records.into(PostCommentResponse.class)), - // Fetch tags as a multiset - 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(records -> records.map(Record1::value1))) - .from(POSTS) - .where(condition) - .orderBy(getSortFields(pageable.getSort(), POSTS)) - .limit(pageable.getPageSize()) - .offset(pageable.getOffset()); - + Field searchValue = DSL.concat(DSL.val("%"), DSL.val(keyword), DSL.val("%")); + Condition condition = + DSL.or( + POSTS.TITLE.likeIgnoreCase(searchValue), + POSTS.CONTENT.likeIgnoreCase(searchValue)); // Construct the count query var countQuery = dslContext.selectCount().from(POSTS).where(condition); - // Execute queries reactively and build the result page + // Execute the data and count queries reactively and build the result page return Mono.zip( - Flux.from(dataQuery) - .map( - record -> - new PostResponse( - record.value1(), // Post ID - record.value2(), // Post Title - record.value3(), // Post Content - record.value4(), // Comments - record.value5() // Tags - )) + // Fetch data query + retrievePostsWithCommentsAndTags(condition) .doOnError( e -> log.error( - "Error executing data query: {}", - e.getMessage())) - .collectList(), - Mono.from(countQuery).map(Record1::value1)) - .doOnError(e -> log.error("Error executing count query: {}", e.getMessage())) + "Error fetching data query: {}", + e.getMessage(), + e)) + .collectList(), // Collect the result into a list + // Fetch count query + Mono.from(countQuery).map(Record1::value1) // Get the count value + ) + .doOnError(e -> log.error("Error executing queries: {}", e.getMessage(), e)) + // Map into PageImpl .map(tuple -> new PageImpl<>(tuple.getT1(), pageable, tuple.getT2())); } + + @Override + public Flux retrievePostsWithCommentsAndTags(Condition condition) { + // Start with a base condition + Condition whereCondition = DSL.trueCondition(); + + // Add the provided condition if it is not null + if (condition != null) { + whereCondition = whereCondition.and(condition); + } + + // Construct the query + return Flux.from( + dslContext + .select( + POSTS.ID, // Post ID + POSTS.TITLE, // Post Title + POSTS.CONTENT, // Post Content + POSTS.CREATED_BY, // Post Created By + POSTS.STATUS, // Post status + // Fetch comments as a multiset + getCommentsMultiSet(), + // Fetch tags as a multiset + getTagsMultiSet()) + .from(POSTS) + .where(whereCondition) // Apply the dynamic where condition + .orderBy(POSTS.CREATED_AT)) + .map(record -> record.into(PostResponse.class)); + } + + private Field> getCommentsMultiSet() { + return DSL.multiset( + DSL.select( + POST_COMMENTS.ID, + POST_COMMENTS.CONTENT, + POST_COMMENTS.CREATED_AT) + .from(POST_COMMENTS) + .where(POST_COMMENTS.POST_ID.eq(POSTS.ID))) + .as("comments") + .convertFrom(record -> record.into(PostCommentResponse.class)); + } + + private Field> getTagsMultiSet() { + return DSL.multiset( + DSL.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)); + } } 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 d85c5c854..4e9794522 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 @@ -126,13 +126,20 @@ private Mono fetchOrInsertTag(String tagName) { } public Mono> findByKeyword(String keyword, Pageable pageable) { - String sanitizedKeyword = - StringUtils.hasText(keyword) ? keyword.replaceAll("[\n\r\t]", "_") : ""; + // Check if the keyword has text + if (!StringUtils.hasText(keyword)) { + log.debug("findByKeyword called with empty or null keyword"); + return Mono.empty(); + } + // Sanitize the keyword to avoid injection-like issues + String sanitizedKeyword = keyword.replaceAll("[^a-zA-Z0-9\\s-]", "_"); log.debug( - "findByKeyword with sanitizedKeyword :{} with offset :{} and limit :{}", + "findByKeyword [keyword: {}, sanitized: {}, page: {}, size: {}, sort: {}]", + keyword, sanitizedKeyword, - pageable.getOffset(), - pageable.getPageSize()); + pageable.getPageNumber(), + pageable.getPageSize(), + pageable.getSort()); return this.postRepository.findByKeyword(keyword, pageable).map(PaginatedResult::new); } 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 514f1b4c5..8ce2f4151 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,42 +1,49 @@ -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 int 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) -); - +CREATE + EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE + TABLE + posts( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + title text, + content text, + status text, + created_at timestamptz DEFAULT NOW(), + created_by text, + updated_at timestamptz, + version INT 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/common/ContainerConfig.java b/r2dbc/boot-jooq-r2dbc-sample/src/test/java/com/example/jooq/r2dbc/common/ContainerConfig.java index 0b5983928..4ae52be22 100644 --- a/r2dbc/boot-jooq-r2dbc-sample/src/test/java/com/example/jooq/r2dbc/common/ContainerConfig.java +++ b/r2dbc/boot-jooq-r2dbc-sample/src/test/java/com/example/jooq/r2dbc/common/ContainerConfig.java @@ -5,7 +5,6 @@ import org.springframework.context.annotation.Bean; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.utility.DockerImageName; -import org.testcontainers.utility.MountableFile; @TestConfiguration(proxyBeanMethods = false) public class ContainerConfig { @@ -13,9 +12,6 @@ public class ContainerConfig { @Bean @ServiceConnection PostgreSQLContainer postgreSQLContainer() { - return new PostgreSQLContainer<>(DockerImageName.parse("postgres").withTag("17.2-alpine")) - .withCopyFileToContainer( - MountableFile.forClasspathResource("init.sql"), - "/docker-entrypoint-initdb.d/init.sql"); + return new PostgreSQLContainer<>(DockerImageName.parse("postgres").withTag("17.2-alpine")); } } diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/test/java/com/example/jooq/r2dbc/respository/PostRepositoryTest.java b/r2dbc/boot-jooq-r2dbc-sample/src/test/java/com/example/jooq/r2dbc/respository/PostRepositoryTest.java new file mode 100644 index 000000000..95a3d737c --- /dev/null +++ b/r2dbc/boot-jooq-r2dbc-sample/src/test/java/com/example/jooq/r2dbc/respository/PostRepositoryTest.java @@ -0,0 +1,174 @@ +package com.example.jooq.r2dbc.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; + +import com.example.jooq.r2dbc.common.ContainerConfig; +import com.example.jooq.r2dbc.config.JooqConfiguration; +import com.example.jooq.r2dbc.entities.Comment; +import com.example.jooq.r2dbc.entities.Post; +import com.example.jooq.r2dbc.entities.PostTagRelation; +import com.example.jooq.r2dbc.entities.Tags; +import com.example.jooq.r2dbc.model.Status; +import com.example.jooq.r2dbc.model.response.PostCommentResponse; +import com.example.jooq.r2dbc.model.response.PostResponse; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.UUID; +import org.jooq.DSLContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.data.r2dbc.DataR2dbcTest; +import org.springframework.context.annotation.Import; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +@DataR2dbcTest +@Import({ContainerConfig.class, JooqConfiguration.class}) +class PostRepositoryTest { + + @Autowired private PostRepository postRepository; + + @Autowired private TagRepository tagRepository; + + @Autowired private CommentRepository postCommentRepository; + + @Autowired private PostTagRepository postTagRepository; + + @Autowired private DSLContext dslContext; + + @BeforeEach + void cleanup() { + // Ensure existing data is deleted + StepVerifier.create( + postTagRepository + .deleteAll() + .then(tagRepository.deleteAll()) + .then(postCommentRepository.deleteAll()) + .then(postRepository.deleteAll())) + .expectSubscription() + .expectComplete() + .verify(Duration.ofSeconds(30)); + } + + @Test + void testInsertPostViaR2dbcAndRetrieveViaDSLContext() { + + Flux postResponseFlux = + // Step 1: Insert a new post + createPost() + .flatMap( + post -> { + UUID postId = post.getId(); + + // Step 2: Insert a new tag + return tagRepository + .save(new Tags().setName("java")) + .flatMap( + tag -> { + UUID tagId = tag.getId(); + + // Step 3: Link post and tag + return postTagRepository + .save( + new PostTagRelation( + postId, tagId)) + .thenReturn(postId); + }); + }) + + // Step 4: Insert comments + .flatMapMany( + postId -> + createComments(postId, "test comments", "test comments 2")) + .thenMany( + // Step 5: Retrieve data using jOOQ + Flux.from(postRepository.retrievePostsWithCommentsAndTags(null))); + + StepVerifier.create(postResponseFlux) + .expectNextMatches( + postResponse -> { + // Assertions for post data + verifyBasicPostResponse(postResponse); + assertThat(postResponse.comments()).isNotEmpty().hasSize(2); + assertThat(postResponse.tags()).isNotEmpty().hasSize(1); + + // Assertions for + assertPostComments(postResponse); + + // Assertions for tags + assertThat(postResponse.tags().getFirst()).isEqualTo("java"); + + return true; + }) + .expectComplete() + .verify(); + } + + @Test + void testInsertPostOnlyViaR2dbcAndRetrieveViaDSLContext() { + + Flux postResponseFlux = + // Step 1: Insert a new post + createPost() + .thenMany( + // Step 2: Retrieve data using jOOQ + postRepository.retrievePostsWithCommentsAndTags(null)); + + StepVerifier.create(postResponseFlux) + .expectNextMatches( + postResponse -> { + // Assertions for post data + verifyBasicPostResponse(postResponse); + assertThat(postResponse.comments()).isEmpty(); + assertThat(postResponse.tags()).isEmpty(); + + return true; + }) + .expectComplete() + .verify(); + } + + private Mono createPost() { + return postRepository.save( + new Post().setTitle("jooq test").setContent("content of Jooq test")); + } + + private void verifyBasicPostResponse(PostResponse postResponse) { + assertThat(postResponse.id()).isInstanceOf(UUID.class); + assertThat(postResponse.title()).isEqualTo("jooq test"); + assertThat(postResponse.content()).isEqualTo("content of Jooq test"); + assertThat(postResponse.createdBy()).isEqualTo("appUser"); + assertThat(postResponse.status()).isEqualTo(Status.DRAFT); + } + + private void assertPostComments(PostResponse postResponse) { + PostCommentResponse postCommentResponse = postResponse.comments().getFirst(); + assertThat(postCommentResponse.id()).isInstanceOf(UUID.class); + assertThat(postCommentResponse.createdAt()) + .isNotNull() + .isInstanceOf(LocalDateTime.class) + .isCloseTo(LocalDateTime.now(), within(1, ChronoUnit.MINUTES)); + assertThat(postCommentResponse.content()).isEqualTo("test comments"); + + PostCommentResponse last = postResponse.comments().getLast(); + assertThat(last.id()).isInstanceOf(UUID.class); + assertThat(last.createdAt()) + .isNotNull() + .isInstanceOf(LocalDateTime.class) + .isCloseTo(LocalDateTime.now(), within(1, ChronoUnit.MINUTES)); + assertThat(last.createdAt()).isNotNull(); + assertThat(last.content()).isEqualTo("test comments 2"); + } + + private Flux createComments(UUID postId, String... contents) { + return Flux.fromArray(contents) + .flatMap( + content -> + postCommentRepository.save( + new Comment().setPostId(postId).setContent(content))); + } +} diff --git a/r2dbc/boot-jooq-r2dbc-sample/src/test/resources/init.sql b/r2dbc/boot-jooq-r2dbc-sample/src/test/resources/init.sql deleted file mode 100644 index d159cc56a..000000000 --- a/r2dbc/boot-jooq-r2dbc-sample/src/test/resources/init.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE EXTENSION IF NOT EXISTS "uuid-ossp";