JPA+QueryDSL階層レビュー,レビューの実現(2)


今回は前編に続き、階層的なコメントや大きなコメントを再整理します.
  • 以前の投稿では,階層的なコメント,大きなコメントが実現されたが,N+1の問題があった.今回はそのN+1問題を解決します.
  •     @Transactional
        public PostOneResponse getOnePost(Long postId) {
            PostOneResponse postOneResponse = postRepository.findOnePostById(postId)
                    .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_POST));
            commentsExtractor(postId, postOneResponse);
            return postOneResponse;
        }
    
        private void commentsExtractor(Long postId, PostOneResponse postOneResponse) {
            postOneResponse.getComments()
                    .forEach(comment -> {
                                List<CommentsChildrenResponse> comments = commentRepository.findPostComments(postId, comment.getCommentId());
                                comment.setChildren(comments);
                    });
        }

  • 上のロジックは、前の記事で作成したN+1に問題が発生したロジックです.

  • 親のコメント数が100개の場合、100번以上のクエリが表示されます.これは非常に悪いコードです.ううう🥲
  • QueryDSLコード
  • @RequiredArgsConstructor
    public class PostRepositoryImpl implements PostCustomRepository {
    
        private final JPAQueryFactory queryFactory;
    
        @Override
        public Optional<PostOneResponse> findOnePostById(Long postId, Long userId) {
            queryFactory.update(post)
                    .set(post.viewCount, post.viewCount.add(1))
                    .where(post.id.eq(postId))
                    .execute();
    
            Optional<PostOneResponse> response = Optional.ofNullable(queryFactory
                    .select(new QPostOneResponse(
                            post.id,
                            post.title,
                            post.content,
                            post.scraps.size(),
                            post.comments.size(),
                            post.postLikes.size(),
                            post.timeEntity.createdDate,
                            post.timeEntity.updatedDate,
                            post.viewCount,
                            user.nickname,
                            JPAExpressions
                                    .selectFrom(post)
                                    .where(user.id.eq(userId))
                                    .exists(),
                            JPAExpressions
                                    .selectFrom(postLike)
                                    .where(postLike.post.eq(post).and(user.id.eq(userId)))
                                    .exists(),
                            JPAExpressions
                                    .selectFrom(scrap)
                                    .where(scrap.post.eq(post).and(user.id.eq(userId)))
                                    .exists()))
                    .from(post)
                    .innerJoin(post.user, user)
                    .where(post.id.eq(postId))
                    .fetchOne());
    
            if (response.isEmpty()) {
                return Optional.empty();
            }
    
            List<PostOneCommentResponse> comments = queryFactory
                    .select(new QPostOneCommentResponse(
                            comment.parent.id,
                            comment.id,
                            comment.content,
                            user.nickname,
                            JPAExpressions
                                    .selectFrom(comment)
                                    .where(user.id.eq(userId))
                                    .exists(),
                            comment.timeEntity.createdDate,
                            comment.timeEntity.updatedDate))
                    .from(comment)
                    .innerJoin(comment.post, post)
                    .innerJoin(comment.user, user)
                    .where(post.id.eq(postId).and(comment.parent.id.isNull()))
                    .orderBy(comment.id.asc())
                    .fetch();
    
            List<CommentsChildrenResponse> childComments = queryFactory
                    .select(new QCommentsChildrenResponse(
                            comment.parent.id,
                            comment.id,
                            comment.content,
                            user.nickname,
                            JPAExpressions
                                    .selectFrom(comment)
                                    .where(user.id.eq(userId))
                                    .exists(),
                            comment.timeEntity.createdDate,
                            comment.timeEntity.updatedDate
                    ))
                    .from(comment)
                    .innerJoin(comment.post, post)
                    .innerJoin(comment.user, user)
                    .where(post.id.eq(postId).and(comment.parent.id.isNotNull()))
                    .fetch();
    
    
            comments.stream()
                    .forEach(parent -> {
                        parent.setChildren(childComments.stream()
                                .filter(child -> child.getParentId().equals(parent.getCommentId()))
                                .collect(Collectors.toList()));
                    });
    
            response.get().setComments(comments);
    
             return response;
        }
    }

  • 上のコードは,記事조회を発行する際に,댓글+대댓글を一度にインポートし,JSONを階層化したクエリとする.

  • この映画に比べて、コードがずいぶん変わりました.😁

  • 上記のコードを簡単に説明すると、文章クエリーロジックが実行された瞬間viewCount1に増加したクエリーが表示され、postに増加したクエリーが表示されます.
  • JPAExpressionsはサブクエリとして使用され、戻りタイプはbooleanであり、QueryDSLによってサポートされる機能であり、ユーザーの投稿、好きかどうか、クリップするかどうかを決定するために使用されます.
  • postなければOptionalを返し、存在する場合は以下の論理を実行する.いろいろな方法でコメントを得ることができますが、私は先に両親のコメントを得ました.(parentId = null)

  • 親のコメントを取得した後、子供のコメントを取得します.(parentId = notNull)

  • このようにインポートすると、親コメントはListに戻り、streamを使用してcommentIdを子コメントのparentIdと比較し、同じ値があればリストタイプに入れる.

  • これにより、クエリーが4回実行されます.(1回更新、3回選択)
  • PostOneResponse
  • @ApiModel(description = "결과 응답 데이터 모델")
    @Getter
    @Setter
    @NoArgsConstructor
    public class PostOneResponse {
    
        @ApiModelProperty(value = "게시글 Id")
        private Long postId;
    
        @ApiModelProperty(value = "게시글 제목")
        private String title;
    
        @ApiModelProperty(value = "게시글 내용")
        private String content;
    
        @ApiModelProperty(value = "해당 게시글의 전체 스크랩 수")
        private int scrapCount;
    
        @ApiModelProperty(value = "해당 게시글의 전체 댓글의 수")
        private int commentCount;
    
        @ApiModelProperty(value = "해당 게시글의 전체 좋아요 수")
        private int likeCount;
    
        @ApiModelProperty(value = "해당 게시글의 생성 시간")
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
        private LocalDateTime createDate;
    
        @ApiModelProperty(value = "해당 게시글의 수정 시간")
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
        private LocalDateTime updateDate;
    
    
        @ApiModelProperty(value = "해당 게시글의 조회수")
        private Integer viewCount;
    
        @ApiModelProperty(value = "해당 게시글의 생성 회원 이름")
        private String username;
    
        @ApiModelProperty(value = "해당 게시글의 유저 본인 확인")
        private boolean myPost;
    
        @ApiModelProperty(value = "해당 게시글의 좋아요 본인 확인")
        private boolean myLike;
    
        @ApiModelProperty(value = "해당 게시글의 스크랩 본인 확인")
        private boolean myScrap;
    
        @ApiModelProperty(value = "해당 게시글의 댓글")
        private List<PostOneCommentResponse> comments = new ArrayList<>();
    
        @QueryProjection
        public PostOneResponse(Long postId, String title, String content, int scrapCount, int commentCount, int likeCount, LocalDateTime createDate, LocalDateTime updateDate, Integer viewCount, String username, boolean myPost, boolean myLike, boolean myScrap) {
            this.postId = postId;
            this.title = title;
            this.content = content;
            this.scrapCount = scrapCount;
            this.likeCount = likeCount;
            this.commentCount = commentCount;
            this.createDate = createDate;
            this.updateDate = updateDate;
            this.viewCount = viewCount;
            this.username = username;
            this.myPost = myPost;
            this.myLike = myLike;
            this.myScrap = myScrap;
        }
    
    }
  • postOneCommentResponse
  • @Getter
    @Setter
    @NoArgsConstructor
    public class PostOneCommentResponse {
    
        private Long parentId;
    
        private Long commentId;
    
        private String content;
    
        private String username;
    
        private boolean myComment;
    
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
        private LocalDateTime createDate;
    
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
        private LocalDateTime updateDate;
    
        private List<CommentsChildrenResponse> children = new ArrayList<>();
    
        @QueryProjection
        public PostOneCommentResponse(Long parentId, Long commentId, String content, String username, boolean myComment, LocalDateTime createDate, LocalDateTime updateDate) {
            this.parentId = parentId;
            this.commentId = commentId;
            this.content = content;
            this.username = username;
            this.myComment = myComment;
            this.createDate = createDate;
            this.updateDate = updateDate;
        }
    
    
    }
  • CommentsChildrenResponse
  • @Getter
    @Setter
    @NoArgsConstructor
    public class CommentsChildrenResponse {
    
        private Long parentId;
    
        private Long commentId;
    
        private String content;
    
        private String username;
    
        private boolean myComment;
    
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
        private LocalDateTime createDate;
    
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
        private LocalDateTime updateDate;
    
        @QueryProjection
        public CommentsChildrenResponse(Long parentId, Long commentId, String content, String username, boolean myComment, LocalDateTime createDate, LocalDateTime updateDate) {
            this.parentId = parentId;
            this.commentId = commentId;
            this.content = content;
            this.username = username;
            this.myComment = myComment;
            this.createDate = createDate;
            this.updateDate = updateDate;
        }
    
    }
  • 結果画面(Postman)

  • N+1の問題が完全に無くなってしまい、前よりもきれいにラッピングされたようです!!🏡

  • 私の知っている限りでは、私が作成した方法のほかに、階層クエリーを作成する方法もいろいろありますが、初めて作成した階層クエリーなので、ちょっと難しい感じがします.

  • 次編では投稿+ファイルアップロード編(AWS S3)を作成します.