BackEnd/JPA

[JPA] QueryDsl에서 Groupy By적용 후 가장 큰 원소 가져오기

샤아이인 2022. 11. 23.

우선 이번 글은 제가 쿼리를 잘 못짠 부분도 있기 때문에, 다른 방식도 있을 수 있습니다.

다만 구글에 검색시 별다른 방법이 보이지 않아 글을 작성해 봅니다.

1. 문제가 된 상황

프로젝트를 진행하다 보니 다음과 같은 상황이 있었습니다.

User와 Post는 중간테이블인 Scrap을 두고 N:M 관계를 형성하고 있습니다.

Post와 PostPhoto는 1: N의 관계로, 하나의 Post당 여러 PostPhoto가 있는 상황이였습니다.

 

자 여기서 의문점!

 

1) 사용자가 다음과 같이 원하는 피드에서 저장하기(스크랩) 버튼을 누른다.

 

2) scrap 엔티티로 저장된다.

3) 이후 다음과 같이 프로필 페이지에서 "관심목록"을 누를경우, 스크랩된 하나의 피드당 하나의 대표 사진만 보여줘야 한다.

 

이를 어떻게 QueryDsl로 풀어낼 것 인가??

 

2. 일단 해결한 방식

우선 초기 데이터는 다음과 같이 삽입해주었다!

Post post = new Post("test_owner_uuid", "bar_uuid", 4.3f, "contents");
post.addPhoto(new PostPhoto("bar_id", "originName", "photo_url_1", ".jpg"));
post.addPhoto(new PostPhoto("bar_id", "originName", "photo_url_2", ".jpg"));
Post savedPost = postRepository.save(post);

Post post2 = new Post("test_owner_uuid", "bar_uuid", 4.0f, "contents");
post2.addPhoto(new PostPhoto("bar_id_2", "originName", "photo_url_3", ".jpg"));
post2.addPhoto(new PostPhoto("bar_id_2", "originName", "photo_url_4", ".jpg"));
Post savedPost2 = postRepository.save(post2);

각 post, post2당 하나의 사진인 "photo_url_1"과 "photo_url_3"을 가져오는것이 목표이다.

즉, {post, photo_url_1}, {post2, photo_url_3} 처럼 대표사진 하나만 가져오는 것 이다!

 

다음 쿼리와 같이 post_id로 group by한후, 그룹당 하나의 image_url을 min()집계함수를 통해서 가져온다.

select post_id, min(uuid_file_url)
from post_photo
group by post_id;

min()을 사용한 이유는, 애당초 사진에 순서가 없기 때문에 아무거나 하나를 가져오면 된다 생각했기 때문이다.

 

실핼결과는 다음과 같을 것 이다.

 

이를 기반으로 다음 코드를 살펴보자.

public List<ScrapedPostLookUpResponse> findAllScrapedPost(String userId) {
    QPostPhoto subPhoto = new QPostPhoto("subPhoto");

    JPQLQuery<Tuple> subQuery = JPAExpressions
            .select(subPhoto.post.id, subPhoto.uuidFileUrl)
            .from(subPhoto)
            .groupBy(subPhoto.post.id)
        .limit(1);

    return queryFactory
            .select(new QScrapedPostLookUpResponse(postScrap.postId, postPhoto.uuidFileUrl))
            .from(postScrap).join(postPhoto)
            .on(postScrap.postId.eq(postPhoto.post.id))
            .where(
                    postScrap.ownerId.eq(userId),
                    subQuery.having(postPhoto.uuidFileUrl.eq(subPhoto.uuidFileUrl.min())).exists()
            )
            .orderBy(postScrap.createdAt.desc())
            .fetch();
}

 

subQuery에 해당되는 부분은 직전에 살펴봤던 쿼리와 동일한 쿼리이다.

 

이를 서브쿼리로 사용하여 두번째 dsl문에서 where절에서 사용하고 있다.

해당 uuidFileUrl이 존재하는지를 판단하는 기준으로 사용하였다.

 

실제로 생성되는 쿼리는 다음과 같다.

select postscrap0_.post_id as col_0_0_, postphoto1_.uuid_file_url as col_1_0_ 
from post_scrap postscrap0_ inner join post_photo postphoto1_ 
on (postscrap0_.post_id=postphoto1_.post_id) 
where postscrap0_.owner_id='5aa6099c84a21b2d0184a21b40e70009' 
and (exists (select 1 from post_photo postphoto2_ where ( postphoto2_.deleted = 0) 
group by postphoto2_.post_id 
having postphoto1_.uuid_file_url=min(postphoto2_.uuid_file_url))) 
order by postscrap0_.created_at desc

(좀 다른 예기이지만, 쿼리에서 "deleted = 0"이 보이는 이유는, 사진이 soft delet가 적용된 상태이기 때문입니다 ㅎㅎ)

 

추가로 subQuery문 내에서 사용하는 PostPhoto와 mainQuery문에서 사용하는 PostPhoto를 구별하기 위해

맨 위에서 다음과 같이 별칭을 추가로 작성해 주었다.

QPostPhoto subPhoto = new QPostPhoto("subPhoto");

 

실행결과 다음과 같이 원하던 대로, 스크랩 한 피드 하나당 하나의 대표 사진만 가져오게 되었다!

 

어찌저찌 성공하긴 했는데, "그냥 애당초 다 퍼올려서 application 수준에서 필터링 해준는게 더 적합했을라나?" 라는 생각도 든다.

어떤 방법

댓글