본문 바로가기
JAVA/JPA

[QueryDSL] Spring Data JPA + QueryDSL 기본 문법 #2 - from절에 서브쿼리 사용 문제 해결

by 개미가되고싶은사람 2025. 1. 30.

목차

     

    조건 문법

    SQL에서 제공하는 기본적인 조건식을 제공합니다.

    member.username.eq("member1") // username = 'member1'
    member.username.ne("member1") //username != 'member1'
    member.username.eq("member1").not() // username != 'member1'
    
    member.username.isNotNull() //이름이 is not null
    
    member.age.in(10, 20) // age in (10,20)
    member.age.notIn(10, 20) // age not in (10, 20)
    member.age.between(10,30) //between 10, 30
    
    member.age.goe(30) // age >= 30
    member.age.gt(30) // age > 30
    member.age.loe(30) // age <= 30
    member.age.lt(30) // age < 30
    
    member.username.like("member%") //like 검색
    member.username.contains("member") // like ‘%member%’ 검색
    member.username.startsWith("member") //like ‘member%’ 검색

     

    복합 조건 

    QueryDSL은 and와 or 조건을 체인으로 연결할 수 있으며, where() 메소드는 여러 조건을 인자로 받아들이기 때문에 and() 메소드를 사용하지 않고도 쉼표(,)로 조건을 연결할 수 있습니다. 그리고 null 값이 들어가면 해당 조건은 무시됩니다. 이 원리로 동적 쿼리를 쉽게 작성할 수 있습니다.

    public abstract class QueryBase<Q extends QueryBase<Q>> {
        public Q where(Predicate o) {
            return queryMixin.where(o);
        }
    
        public Q where(Predicate... o) {
            return queryMixin.where(o);
        }
    }
        @Test
        public void search() {
            Member findMember1 = queryFactory
                    .selectFrom(member)
                    .where(member.username.eq("member1")
                            .and(member.age.goe(10)))
                    .fetchOne();
    
            Member findMember2 = queryFactory
                    .selectFrom(member)
                    .where(
                            member.username.eq("member1"),
                            member.age.goe(10), 
                            null)
                    .fetchOne();
    
            assertThat(findMember1.getUsername()).isEqualTo("member1");
            assertThat(findMember2.getUsername()).isEqualTo("member1");
        }

     

    반환 타입

    • fetch(): 리스트 조회, 데이터 없으면 빈 리스트 반환
    • fetchOne(): 단 건 조회
      • 결과가 없으면: null
      • 결과가 둘 이상이면 : cohttp://m.querydsl.core.NonUniqueResultException
    • fetchFirst(): 첫번째 레코드만 조회, 결과가 없으면 null 반환
    • fetchResults(): 페이징 정보 포함, total count 쿼리 추가 실행
    • fetchCount(): count 쿼리로 변경해서 count 수 조회, long 반환
    public abstract class AbstractJPAQuery<T, Q extends AbstractJPAQuery<T, Q>> extends JPAQueryBase<T, Q> {
        @Override
        @SuppressWarnings("unchecked")
        public List<T> fetch() {
    	...
        }
    
        @Nullable
        @SuppressWarnings("unchecked")
        @Override
        public T fetchOne() throws NonUniqueResultException {
    	...
        }
    
        @Override
        @Deprecated
        public QueryResults<T> fetchResults() {
    	...
        }
    
        @Override
        @Deprecated
        public long fetchCount() {
    	...
        }
    
    }
    
    
    public abstract class FetchableQueryBase<T, Q extends FetchableQueryBase<T, Q>>
        public final T fetchFirst() {
            return limit(1).fetchOne();
        }
    }

     

     

    정렬

    • desc(): 내림차순
    • asc(): 올림차순
    • nullsLast(): null 데이터 마지막 순서 부여
    • nullsFirst(): null 데이터 처음 순서 부여
    @Test
    public void sortTest() {
            List<Member> result = queryFactory
                    .selectFrom(member)
                    .where(member.age.eq(100))
                    .orderBy(
                            member.age.desc(),
                            member.username.asc().nullsLast())
                    .fetch();
    }

     

     

    페이징

    fetchResults()를 사용하면 페이징 처리를 할 수 있지만, 이 메서드는 comtent 쿼리로 생성된 count 쿼리를 최적화 해주지만 조인과 같은 복잡한 쿼리에서는 최적화를 제공하지 않습니다. 이로 인해 조인 쿼리를 사용할 경우 카운트 쿼리가 복잡해져 성능에 부정적인 영향을 미칠 수 있습니다. 

    정리하자면

    • content 쿼리가 복잡해지면 count 쿼리도 복잡해져서 성능에 안 좋은 영향을 끼칠 수 있음
    • count 쿼리를 분리함으로써 성능 향상 가능
    // Custom Repository 활용
    
    @RequiredArgsConstructor
    public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
    
        private final EntityManager em;
    
        @Override
        public Page<Member> search(MemberSearchCond cond, Pageable pageable) {
            JPAQueryFactory queryFactory = new JPAQueryFactory(em);
    
            // 데이터 조회 쿼리
            List<Member> content = queryFactory
                    .selectFrom(member)
                    .orderBy(member.username.desc())
                    .offset(0)
                    .limit(2)
                    .fetch();
    
            // count 쿼리
            Long total = queryFactory
                    .select(member.count()) // count(member.id)와 동일
                    .from(member)
                    .fetchOne();
    
            return new PageImpl<>(content, pageable, total);
        }
    }
    더보기
    더보기

    해당 코드는 JPAQueryFactory를 직접 사용하고 Member 엔티티를 직접 반환하기 때문에 좋은 코드라고 할 수 없습니다. 더 좋은 코드를 원한다면 JPAQueryFactory를 설정 파일에서 생성하여 Bean으로 등록하고, Member 엔티

    티 대신 DTO를 반환하는 방식으로 개선해셔야 합니다.

     

     

    집계 함수 + Group By

    QueryDSL에서 여러 개 속성의 평균, 총 합을 구하는 메소드는 제공하지 않기 때문에 직접 구해야 합니다.

    // 집계 함수
    List<Tuple> result = queryFactory
    	.select(member.count(), // 총 레코드 수
    		member.age.sum(), // 총 합
    		member.age.avg(), // 평균
    		member.age.max(), // 최대값
    		member.age.min()) // 최소값
    	.from(member)
    	.fetch();
    
    // 집계 함수 조합
    Integer sum = queryFactory
    	.select(
    		member.age.sum().add(member.age.sum()).as("sum")) // age + age 총 합
    	.from(member)
    	.fetchOne();
        
    // Group by 그룹화
    queryFactory
    	.select(team.name, member.age.avg())
    	.from(member)
    	.join(member.team, team)
    	.groupBy(team.name)
    	.fetch();

     

     

    Join - on 조건절

    queryFactory
    	.selectFrom(xxx)
    	.join(정적 타입에 조인할 필드명, 조인된 엔티티에 대한 별칭 지정)
    	.on(member.username.eq("member1"))
    	.fetch();

    join()은 inner join을 의미하며, left / right join을 지원합니다.

     

     

    theta join - 세타 조인

    세타 조인은 엔티티 간에 연관 관계가 없는 필드를 기준으로 조인하는 방식으로, JPA 2.1부터는 left join과 right join도 지원합니다. 세타 조인을 사용할 때는 반드시 조인 조건을 명시해야 하며, 이를 위해 on 절을 사용해야 합니다. 만약 on 절 없이 세타 조인을 시도하면 SemanticException 예외가 발생하는데, 이는 Join 쿼리를 작성할 때 생각해보면 왜 예외가 발생하는지 알 수 있습니다. Join 쿼리를 작성할 때 어떤 조건으로 연결할지 명확하지 않으면 올바른 쿼리가 아니기 때문입니다. 일반적인 조인에서는 JPA가 쿼리를 자동으로 완성해주지만, 세타 조인에서는 조인 조건을 직접 작성해야 하므로 on 절이 필수적입니다.

    queryFactory
    	.select(member, team)
    	.from(member)
       	.leftJoin(team).on(member.username.eq(team.name))
    	.fetch();

    세타 조인을 사용할 때 문법을 보시면 일반 조인과 다르게 연결할 필드명만 작성할 하는 걸 볼 수 있습니다. 

    • 일반 조인: leftJoin(member.team, team)
    • 세타 조인: leftJoin(team).on(xxx)

     

    정리

    • 일반 조인과 세타 조인은 inner / right / left 조인 모두 지원
    • 세타 조인을 사용할 땐 on절 필수
    • 일반 조인과 세타 조인을 사용할 때 문법이 다르다

     

     

    서브 쿼리

    JPA의 한계점 중 하나는 서브쿼리를 where 절에서만 지원한다는 점입니다. 그러나 하이버네이트 구현체를 사용할 경우, select 절에서도 서브쿼리를 지원합니다. QueryDSL을 사용할 때도 마찬가지로, 기본적으로는 where 절에만 서브쿼리를 지원하지만, 하이버네이트 구현체를 활용하면 select 절에서도 서브쿼리를 사용할 수 있습니다.

        @Test
        public void subQuery() {
    
            QMember subQeury = new QMember("memberSub");
            
            List<Member> result = queryFactory
                    .selectFrom(member)
                    .where(member.age.eq(
    
                            JPAExpressions
                                    .select(subQeury.age.max())
                                    .from(subQeury)
                    ))
                    .fetch();
        }

     

     

    그럼 from절의 서브쿼리를 사용하려면?

    1. 최대한 join으로 변경 (가능한 상황도 있고, 불가능한 상황도 있습니다.)
    2. 쿼리를 분리해서 실행
    3. nativeSQL을 사용

     

    만약 위에 방법도 싫고 무조건 from절에 서브쿼리를 사용하려면

    ❗❗하이버네이트 6.1버전부터 Criteria or QueryDSL + Blaze-Persistence 사용하면 from절에 서브쿼리를 사용 가능
    https://github.com/querydsl/querydsl/issues/3438
    https://in.relation.to/2022/06/24/hibernate-orm-61-features/

     

     

    case문 

    일반적으로 쿼리에서 case문을 사용하면 좋지 않으니 간단하게 설명하겠습니다.

        @Test
        public void simpleCase() {
            List<String> result = queryFactory
                    .select(member.age
                            .when(10).then("열살")
                            .when(20).then("스무살")
                            .otherwise("기타"))
                    .from(member)
                    .fetch();
        }
    
        @Test
        public void complexCase() {
            NumberExpression<Integer> rankPath = new CaseBuilder()
                    .when(member.age.between(0, 20)).then(2)
                    .when(member.age.between(21, 30)).then(1)
                    .otherwise(3);
    
            List<Tuple> result = queryFactory
                    .select(member.username, member.age, rankPath)
                    .from(member)
                    .orderBy(rankPath.desc())
                    .fetch();
        }

    복잡한 case문이라는건 CaseBuilder를 사용한다는 의미입니다. 일반적으로 case문에 조건식이 여러개 있는 경우를 의미합니다.

     

     

    상수, 문자 더하기

    상수는 Expressions.constant() 사용 

    queryFactory
       .select(member.username, Expressions.constant("A"))
       .from(member)
       .fetchFirst();

     

     

    문자는 concat

    queryFactory
       .select(member.username.concat("_").concat(member.age.stringValue()))
       .from(member)
       .where(member.username.eq("member1"))
       .fetchOne();

    concat()는 문자 타입의 값에 대해서만 호출될 수 있기 때 숫자를 사용하게 되면 오류가 발생합니다. stringValue() 메소드를 사용하면 반환 값을 문자 타입으로 변경해서 오류를 해결할 수 있습니다.

     

     

     

    참고자료

    https://sjh9708.tistory.com/175#google_vignette

     

    [Spring Boot/JPA] QueryDSL 문법(1) : 기본 검색 (선택, 조건, 정렬, 집계, 그룹화)

    이전 포스팅에서 QueryDSL 사용을 Repository에서 할 수 있도록 설정하는 방법에 대해서 다루어 보았었다. 이제 실제로 자주 사용되는 SQL문을 QueryDSL을 통해 작성해보도록 하자. JpaRepository는 인터페이

    sjh9708.tistory.com

    https://jddng.tistory.com/334

     

    Querydsl - Querydsl 기본 문법

    Querydsl 기본 문법 Querydsl 사용 방법 Q-Type 검색 조건 쿼리 결과 조회 정렬 페이징 집합 조인 - 기본 조인 조인 - on절 조인 - 페치 조인 서브 쿼리 Case 문 상수, 문자 더하기 기본 문법을 테스트하기

    jddng.tistory.com

    https://innysfam.tistory.com/118

     

    QueryDsl 조인

    QueryDsl 에서도 JPQL과 유사하게 다양한 종류의 조인을 지원합니다. 주로 사용되는 조인 유형에는 JPQL과 마찬가지로 내부 조인, 외부 조인, 세타 조인, 크로그 조인이 있습니다. 내부 조인 (Inner Join)

    innysfam.tistory.com

    https://github.com/querydsl/querydsl/issues/3438

     

    Hot features of Hibernate ORM 6.1 - In Relation To

    Hibernate ORM version 6.1.0.Final was just announced a few days ago, but the announcement didn’t go into a too much detail. Read on if you want to know more about some of the hot new features this shiny new release comes with.

    in.relation.to

    https://in.relation.to/2022/06/24/hibernate-orm-61-features/

     

    I've seen that since hibernate 6.1 subqueries are supported in the From clause. If so, is there a plan to use this part to suppo

    Since hibernate 6.1, subqueries are supported. If so, is there a plan to support this part in QueryDsl as well? Here are examples and articles related to it. Support for sub-query in HQL/Criteria f...

    github.com