barded

[몇대몇] 다대다 연관관계 querydsl로 처리하기 본문

프로젝트/몇대몇

[몇대몇] 다대다 연관관계 querydsl로 처리하기

barded 2024. 4. 4. 15:20

querydsl을 통한 다대다 구현

몇대몇 서비스는

몇대몇 게시글과 태그를 사이의 중간 테이블인 몇대몇_태그와

퀴즈와 태그 사이의 중간 테이블인 퀴즈_태그 테이블이 있다.

 

이때 몇대몇 게시글을 바탕으로 같은 태그를 가진 퀴즈들을 불려오려고 하는데, 생각보다 어려워서 정리해보고자 한다.

 

Response에 담을 정보는 다음과 같다.

id (퀴즈의 id), question(퀴즈 내용), correct(맞힌 사람 수), submit(제출한 사람 수), tags(연관 태그들)

public record RelatedQuizDto(
    Long id,
    String question,

    Long correct,
    Long submit,
    List<String> tags
) {
}

비즈니스 로직은 크게 4단계로 나눌 수 있다.

public List<RelatedQuizDto> getQuizs(Long mdmId) {
//1. 
        Mdm mdm = mdmRepository.findById(mdmId)
            .orElseThrow(() -> new BaseException(ErrorCode.MDM_NOT_FOUND));
//2.
        List<Long> tags = mdmTagRepository.findByMdmId(mdmId)
            .stream().map(MdmTag::getTag)
            .map(Tag::getId)
            .toList();
//3.
        List<Long> ids = quizRepository.findAllRelatedId(tags);
//4.
        return quizRepository.findAllIds(ids);

    }
  1. 먼저 처음으로는 mdm의 여부를 확인한다. 없는 경우 Error를 던지도록 한다.
  2. MdmTag(몇대몇_태그)테이블로 부터 mdm이 가진 tagId를 가져온다.
  3. 2번에서 가져온 tagId를 바탕으로 태그를 가진 퀴즈들을 찾는다.
  4. 마지막으로 ids를 기반으로 해당하는 퀴즈와 퀴즈의 태그를 한꺼번에 가져온다.

이렇게 정리할 수 있다.

1,2번 같은경우에는 JPA에서 제공하는 기본 함수를 사용하였고, 3,4번은 querydsl을 사용해서 구현하였다. 따라서 3,4번을 살펴보자

3번

@Override
    public List<Long> findAllRelatedId(List<Long> tags) {
        return queryFactory
            .select(quiz.id)
            .from(quiz)
            .leftJoin(quiz.quizTags, quizTag)
            .leftJoin(quizTag.tag, tag)
            .where(tag.id.in(tags))
            .distinct()
            .fetch();
    }
  • select(quiz.id)
    • 퀴즈 id를 가져온다.
  • from(quiz)
    • 퀴즈 테이블
  • leftJoin(quiz.quizTags, quizTag)
    • quiz와 연관된 quiztag정보를 다 가져온다.
  • .leftJoin(quizTag.tag, tag)
    • quiztag와 연관된 tag 정보를 다 가져온다.
  • .where(tag.id.in(tags))
    • tagid 가 tags 리스트 안에 해당하는 경우만
  • .distinct()
    • 중복을 제거한다.

위를 통해서

select
        distinct q1_0.quiz_id       
    from
        quiz q1_0       
    left join
        quiz_tag qt1_0               
            on q1_0.quiz_id=qt1_0.quiz_id               
            and (qt1_0.deleted = 0)        
    left join
        tag t1_0               
            on t1_0.tag_id=qt1_0.tag_id               
            and (t1_0.deleted = 0)       
    where
        (
            q1_0.deleted = 0          
        )           
        and t1_0.tag_id in (578, 10011, 10012, 1039)

같은 쿼리가 나가는 것을 확인할 수 있다.

이렇게 해당하는 퀴즈 아이디를 가져왔으면, 그 아이디를 바탕으로 퀴즈정보와 퀴즈 태그의 정보를 가져와야한다. 그것이 바로 4번이다.

4번

@Override
    public List<RelatedQuizDto> findAllByTagsId(List<Long> ids) {
        return queryFactory
            .select(quiz)
            .from(quiz)
            .leftJoin(quiz.quizTags, quizTag)
            .leftJoin(quizTag.tag, tag)
            .where(quiz.id.in(ids))
            .distinct()
            .transform(
                groupBy(quiz.id)
                    .list(Projections.constructor(
                            RelatedQuizDto.class,
                            quiz.id,
                            quiz.question,
                            JPAExpressions
                                .select(submit.count())
                                .from(submit)
                                .where(submit.quiz.eq(quiz),
                                    submit.correct.isTrue()),
                            JPAExpressions
                                .select(submit.count())
                                .from(submit)
                                .where(submit.quiz.eq(quiz)),
                            list(tag.name)
                        )
                    ))
            ;
    }
  • select(quiz)
    • 퀴즈 전체 정보를 가져온다.
  • from(quiz)
    • 퀴즈 테이블
  • leftJoin(quiz.quizTags, quizTag)
    • quiz와 연관된 quiztag정보를 다 가져온다.
  • .leftJoin(quizTag.tag, tag)
    • quiztag와 연관된 tag 정보를 다 가져온다.
  • .where(quiz.id.in(ids))
    • quiz의 id가 ids리스트 안에 해당하는 경우만
  • .distinct()
    • 중복을 제거한다.

이 이후에 transform을 사용하여 응답 타입을 변경시킨다.

  • transform
    • 사실상 querydsl쪽에서 중요한 부분이다. 가져온 값들을 바로 변환시켜 사용하도록 한다.
  • groupBy
    • quiz.id를 기준으로 값들을 그룹화한다..
  • list
    • 에서는 여러 값들을 통합시켜 List형태로 만들어준다.
  • Projections.constructor
    • 여기서 위에 정의해준 RelatedQuizDto를 사용하도록 projection해준다.
  • JPAExpressions
    • 서브쿼리를 통해 필요한 정보를 가져온다.
  • list(tag.name)
    • 태그 명들을 통합시켜 List형태로 만든다.

이런식으로 호출하면,

select
        distinct q1_0.quiz_id,
        q1_0.question,
        (select
            count(s1_0.submit_id)           
        from
            submit s1_0           
        where
            (
                s1_0.deleted = 0              
            )               
            and s1_0.quiz_id=q1_0.quiz_id               
            and s1_0.correct=true),
        (select
            count(s2_0.submit_id)           
        from
            submit s2_0           
        where
            (
                s2_0.deleted = 0              
            )               
            and s2_0.quiz_id=q1_0.quiz_id),
        t1_0.name       
    from
        quiz q1_0       
    left join
        quiz_tag qt1_0               
            on q1_0.quiz_id=qt1_0.quiz_id               
            and (qt1_0.deleted = 0)        
    left join
        tag t1_0               
            on t1_0.tag_id=qt1_0.tag_id               
            and (t1_0.deleted = 0)       
    where
        (
            q1_0.deleted = 0          
        )           
        and q1_0.quiz_id in (168, 643, 2479, 3264, 209, 218, 220, ....)

쿼리가 나가고

응답으로

{
    "code": 200,
    "message": "success",
    "data": [
        {
            "id": 168,
            "question": "게임이론에서 참가자가 각각 선택하는 행동이 무엇이든지 참가자의 이득과 손실의 총합이 0이 되는 게임을 일컫는 말은?",
            "correct": 0,
            "submit": 0,
            "tags": [
                "게임이론",
                "선택하는",
                "행동",
                "이득"
            ]
        },
        {
            "id": 209,
            "question": "특정 시점 이후 일정 가격에 주식으로 바꿀 수 있는 회사채를 무엇이라 하는가?",
            "correct": 1,
            "submit": 2,
            "tags": [
                "회사채",
                "주식",
                "특정_시점",
                "가격"
            ]
        }
    ]
}

이렇게 원하는 정보들을 받을 수 있다.

'프로젝트 > 몇대몇' 카테고리의 다른 글

[몇대몇] Formula를 사용한 서브쿼리  (0) 2024.04.04