[QueryDSL] 연관관계, 참조 컬럼이 없는, where 조건절을 쓸 수 없는 테이블의 쿼리가 필요할 때 .join 없이 count쿼리 쓰기.

Walter Mitty·2023년 6월 28일
0

Users 테이블과 EduContents는 참조하고 있는 컬럼이 하나도 없는 상태이다.
Users 테이블과 EduInfo 테이블은 Users의 id를 EduInfo의 userId가 참조하고 있는 상태이다.

이때 Users를 조회하면서 EduContents의 전체 개수와, EduInfo의 개수를 가져오고 싶을 때 어떻게 해야할까?
(참조하는 컬럼이 전혀 없는 테이블의 count값과 참조하는 컬럼이 있는 테이블의 count값을 join 없이 가져오는 방법)

| 문제 코드

import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.vladmihalcea.hibernate.util.StringUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;

import java.util.List;

import static com.querydsl.core.group.GroupBy.groupBy;
import static com.querydsl.core.group.GroupBy.list;

@Repository
@RequiredArgsConstructor
public class UserReaderCustomRepositoryImpl implements UserReaderCustomRepository {

    private final JPAQueryFactory queryFactory;
    private final QUsers users = QUsers.users;
    private final QEduInfo eduInfo = QEduInfo.eduInfo;
    private final QEduContents eduContents = QEduContents.eduContents;
    private final QSurveyAnswer surveyAnswer = QSurveyAnswer.surveyAnswer;
    private final EduContentsRepository eduContentsRepository;

    /**
     * 관리자 > 교육 진행률 목록 조회
     */
    @Override
    public Page<EduReaderListDTO> findEduAllList(EduReaderListRequest eduListReqeust) {
        Pageable pageable = new PagingRequest(eduListReqeust.getPage(), eduListReqeust.getSize()).of();

        long size = queryFactory
            .select(users.id)
            .from(users)
            .where(
                users.name.isNotNull(),
                containsNameOrTelSearch(eduListReqeust.getNameOrTel()),
                eqRegion(eduListReqeust.getUserRegion()),
                eqGender(eduListReqeust.getUserGender()),
                isEduCompleted(eduListReqeust.getIsCompleted())
            ) // 위에도 조건 안달아주면 실제로 나오는 객체 개수(아래)랑 다른 개수가 나온다.
            .fetch().size();

        List<EduReaderListDTO> eduInfoList = queryFactory
            .select(Projections.constructor(EduReaderListDTO.class,
                users.id,
                users.name,
                users.region,
                users.birth,
                users.gender,
                users.tel,
                users.percentage,
                eduInfo.id.countDistinct().as("eduInProgress"), // 대상자가 진행중인 교육 개수
                eduContents.countDistinct().as("wholeEduContents")// 전체 교육 개수
                ))
            .from(users, eduContents)
            .leftJoin(eduInfo).on(eduInfo.userId.eq(users.id))
            .where(
                users.name.isNotNull(),
                containsNameOrTelSearch(eduListReqeust.getNameOrTel()),
                eqRegion(eduListReqeust.getUserRegion()),
                eqGender(eduListReqeust.getUserGender()),
                isEduCompleted(eduListReqeust.getIsCompleted())
            )
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .orderBy(users.id.desc())
            .fetch();

        return new PageImpl<>(eduInfoList, pageable, size);
    }

이렇게 하니까 원하는 대로 나오지 않았다! 만약 eduInfo(현재 진행중인 교육콘텐츠)가 없다하더라도 객체는 나와줘야 하는데 ,

.leftJoin(eduInfo).on(eduInfo.userId.eq(users.id))

때문에 left outer join을 해도 왜그런지 한 객체만 나왔다... 위 코드를 주석처리하고 포스트맨으로 해보니까 그 객체들의 eduInProgress의 값과 wholeEduContents의 값이 고정되어 들어가는 문제가 있었다. (게다가 countDistinct()count()로 쓰니까 row값 * 객체값이 통째로 들어갔으니 각 객체에 테이블의 row값을 넣어줄거면 countDistinct()쓰기)

eduInfo.id.countDistinct().as("eduInProgress"), // 예) user1의 값 = 2
eduContents.countDistinct().as("wholeEduContents")// 예) 12

user2를 검색해줘도 2, 12가 들어갔다. 12는 다 공통값이니까 그렇다 쳐도 user2는 0이 나와야하는데 2가 들어가는 상황.

그러다가 Join없이 할 수 있는 방법을 Chat GPT에 물어봤더니 알려줬다 ^^...짜식...

| 해결 코드

@Override
    public Page<EduReaderListDTO> findEduAllList(EduReaderListRequest eduListReqeust) {
        Pageable pageable = new PagingRequest(eduListReqeust.getPage(), eduListReqeust.getSize()).of();

        long size = queryFactory
            .select(users.id)
            .from(users)
            .where(
                users.name.isNotNull(),
                containsNameOrTelSearch(eduListReqeust.getNameOrTel()),
                eqRegion(eduListReqeust.getUserRegion()),
                eqGender(eduListReqeust.getUserGender()),
                isEduCompleted(eduListReqeust.getIsCompleted())
            )
            .fetch().size();

        List<EduReaderListDTO> eduInfoList = queryFactory
            .select(Projections.constructor(EduReaderListDTO.class,
                users.id,
                users.name,
                users.region,
                users.birth,
                users.gender,
                users.tel,
                users.percentage.coalesce(0), // 프론트 요청에 따라 percentage가 null이면 .coalesce()로 기초값 설정
                JPAExpressions.select(eduInfo.countDistinct()).from(eduInfo).where(eduInfo.userId.eq(users.id)), // 이부분!!
                JPAExpressions.select(eduContents.countDistinct()).from(eduContents) // 이부분!!
                ))
            .from(users)
            .where(
                users.name.isNotNull(),
                containsNameOrTelSearch(eduListReqeust.getNameOrTel()),
                eqRegion(eduListReqeust.getUserRegion()),
                eqGender(eduListReqeust.getUserGender()),
                isEduCompleted(eduListReqeust.getIsCompleted())
            )
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .orderBy(users.id.desc())
            .fetch();

        return new PageImpl<>(eduInfoList, pageable, size);
    }

방법은 JPAExpressions을 사용해서 쿼리문을 붙이는 거였다.
이렇게 하면 제일 상위 select문에 join을 걸지 않아도되고, where 조건을 통해 해당 user에게 맞는 eduInfo 정보가 각 각 잘 들어간다.
또한, 연관관계가 아예없는, 참조 컬럼도 없는 테이블도 where 조건을 쓰지 않은채로 사용 하니까 값이 잘 들어갔다.

참고로 프론트에서 users.percentage가 null로 나오는데 0으로 보내주길 바랐기 떄문에 .coalesce(0)으로 null일 때의 기초값을 0으로 설정해줬다.

0개의 댓글