본문 바로가기

QueryDSL

[Query DSL] 검색 조건 쿼리

 

 

 

package com.example.querytest.controller;

import com.example.querytest.entity.*;

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

import static com.example.querytest.entity.QMember.member;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@Transactional
class QuerydslBasicTest {
    @PersistenceContext
    EntityManager em;
    JPAQueryFactory queryFactory;

    @BeforeEach
    public void memberTeat(){

        queryFactory = new JPAQueryFactory(em);

        Team teamA  = new Team("teamA");
        Team teamB  = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);


        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);
        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);

        em.flush();
        em.clear();

    }

    @Test
    public void startQueryDsl(){

        Member findmember = queryFactory
                .select(member)
                .from(member)
                .where(member.username.eq("member1"))
                .fetchOne();

        assertThat(findmember.getUsername()).isEqualTo("member1");
    }

    @Test
    public void search1(){
        Member findMember = queryFactory
//                .select(member)
//                .from(member)
                .selectFrom(member)
//                .where(member.username.eq("member1")
//                        .and(member.age.eq(10)))
                .where(member.username.eq("member1"),member.age.eq(10))
                .fetchOne();

        assertThat(findMember.getUsername()).isEqualTo("member1");
    }
}

 

- 검색 조건은 .and(), . or() 를 메서드 체인으로 연결할 수 있다.

- select , from `selectFrom` 으로 합칠 수 있음

 

JPQL이 제공하는 모든 검색 조건 제공

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%’ 검색

...

 

 

결과 조회

 

  • fetch(): 결과 리스트 조회, 결과가 없으면 빈 리스트 반환
  • fetchOne(): 단일 결과 조회, 결과가 없으면 null, 결과가 둘 이상이면 예외 발생
  • fetchFirst(): 첫 번째 결과 조회, 내부적으로 limit(1).fetchOne()
  • fetchResults(): 페이징 정보와 함께 결과 조회, total count 쿼리 추가 실행
  • fetchCount(): count 쿼리로 전체 개수 조회

 

 @Test
    public void search2(){

        //List
        List<Member> members = queryFactory
                .selectFrom(member)
                .fetch();

        System.out.println("members : " + members);

        //단 건
        Member findMember = queryFactory
                .selectFrom(member)
                .where(member.username.eq("member1"))
                .fetchOne();

        System.out.println("findMember = " + findMember);

        //처음 한 건 조회
        Member findMember2 = queryFactory
                .selectFrom(member)
                .fetchFirst();

        System.out.println("findMember2 = " + findMember2);


        // 페이징에서 사용
        QueryResults<Member> results = queryFactory
                .selectFrom(member)
                .fetchResults();

        System.out.println("results = " + results.getTotal());

        List<Member> content = results.getResults();

        System.out.println("content = " + content);



        long total = queryFactory
                .selectFrom(member)
                .fetchCount();

        System.out.println("total = " + total);
    }

 

 

정렬

  • desc(): 내림차순 정렬
  • asc(): 오름차순 정렬
  • nullsLast(): null 값을 마지막에 배치
  • nullsFirst(): null 값을 처음에 배치
    @Test
    public void sort(){
        em.persist(new Member(null, 100));
        em.persist(new Member("member5", 100));
        em.persist(new Member("member6", 100));

        List<Member> results = queryFactory
                .selectFrom(member)
                .where(member.age.eq(100))
                .orderBy(member.age.desc(), member.username.asc().nullsLast())
                .fetch();

        Member member5 = results.get(0);
        Member member6 = results.get(1);
        Member memberNull = results.get(2);
        assertThat(member5.getUsername()).isEqualTo("member5");
        assertThat(member6.getUsername()).isEqualTo("member6");
        assertThat(memberNull.getUsername()).isNull();
    }

 

 

페이징

 

@Test
    public void paging1(){
        QMember member = QMember.member;

        List<Member> result1 = queryFactory
                .selectFrom(member)
                .orderBy(member.username.desc())
                .fetch();

        for(Member member1 : result1){
            System.out.println("member username1: " + member1.getUsername());
        }

        List<Member> result2 = queryFactory
                .selectFrom(member)
                .orderBy(member.username.desc())
                .offset(1)
                .limit(2)
                .fetch();

        for(Member member2 : result2){
            System.out.println("member username2: " + member2.getUsername());
        }


        assertThat(result2.size()).isEqualTo(2);

    }


    @Test
    public void paging2(){
        QMember member = QMember.member;

        List<Member> members = queryFactory
                .selectFrom(member)
                .orderBy(member.username.desc())
                .fetch();

        for(Member member1 : members){
            System.out.println("member1 = " + member1);
        }


        QueryResults<Member> queryResults = queryFactory
                .selectFrom(member)
                .orderBy(member.username.desc())
                .offset(2)
                .limit(2)
                .fetchResults();

        System.out.println(queryResults.getTotal());
        System.out.println(queryResults.getLimit());
        System.out.println(queryResults.getOffset());
        System.out.println(queryResults.getResults().size());

        List<Member> member2 = queryResults.getResults();

        for(Member memberData : member2) {
            System.out.println("memberData = " + memberData);
        }
    }

 

 

 

 

  1. Count 쿼리의 성능 문제:
    • 페이징 처리를 할 때, 일반적으로 총 데이터 개수를 파악하기 위해 COUNT 쿼리를 사용합니다.
    • 데이터 조회 쿼리는 여러 테이블을 조인해야 하는 경우가 많습니다. 하지만, COUNT 쿼리에서는 조인이 꼭 필요하지 않을 수 있습니다.
    • JPA나 QueryDSL 등의 라이브러리에서 제공하는 자동화된 페이징 기능은 데이터 조회 쿼리와 유사한 COUNT 쿼리를 생성합니다. 즉, 모든 조인을 포함하여 전체 데이터를 조회하는 쿼리와 유사하게 작성되기 때문에 성능 문제가 발생할 수 있습니다.
  2. 성능 최적화의 필요성:
    • COUNT 쿼리는 단순히 총 데이터 개수를 파악하는 것이 목적이므로, 반드시 모든 테이블을 조인할 필요가 없습니다.
    • 불필요한 조인을 제거하고, 필요한 경우 최소한의 테이블만 조인하여 COUNT 쿼리를 작성하는 것이 성능 최적화에 중요합니다.
    • 따라서, 데이터 조회 쿼리와 별도로 최적화된 COUNT 쿼리를 작성하는 것이 바람직합니다.

실무 적용 예시

예를 들어, Member 테이블과 Team 테이블이 조인된 데이터를 페이징 처리하는 경우를 생각해 봅시다. 데이터 조회 시에는 각 멤버와 그가 속한 팀 정보를 모두 가져와야 하기 때문에 조인이 필요하지만, 전체 멤버 수를 세는 COUNT 쿼리에서는 팀 정보가 필요하지 않습니다. 따라서 COUNT 쿼리에서 조인을 제거하고 Member 테이블의 행 수만 계산하도록 하면 성능이 향상될 수 있습니다.

// 데이터 조회 쿼리
List<Member> members = queryFactory
        .selectFrom(member)
        .leftJoin(member.team, team)
        .fetch();

// 최적화된 COUNT 쿼리
long memberCount = queryFactory
        .select(member.count())
        .from(member)
        .fetchOne();

이러한 최적화는 대량의 데이터가 있을 때 성능에 중요한 영향을 미칩니다. 모든 데이터에 대해 불필요한 조인을 수행하는 COUNT 쿼리는 시스템의 성능을 저하시킬 수 있으므로, 실제로 필요한 데이터만 집계하는 방식으로 쿼리를 작성하는 것이 좋습니다.

 

 

집합

 /**
  * JPQL
  * select
* COUNT(m),
  *    SUM(m.age),
  *    AVG(m.age),
  *    MAX(m.age),
  *    MIN(m.age)
  * from Member m
  */
//회원수 //나이 합 //평균 나이 //최대 나이 //최소 나이
@Test
public void aggregation() throws Exception {
    List<Tuple> result = queryFactory
            .select(member.count(),
                    member.age.sum(),
                    member.age.avg(),
                    member.age.max(),
                    member.age.min())
            .from(member)
            .fetch();
    Tuple tuple = result.get(0);
    assertThat(tuple.get(member.count())).isEqualTo(4);
    assertThat(tuple.get(member.age.sum())).isEqualTo(100);
    assertThat(tuple.get(member.age.avg())).isEqualTo(25);
    assertThat(tuple.get(member.age.max())).isEqualTo(40);
    assertThat(tuple.get(member.age.min())).isEqualTo(10);
}

 

- JPQL이 제공하는 모든 집합 함수를 제공한다.

 

템플릿 컷스텀

 

 

GroupBy 사용

groupBy` , 그룹화된 결과를 제한하려면 `having`

 @Test
    public void group() throws Exception{

        QTeam team = QTeam.team;
        QMember member = QMember.member;

        List<Tuple> result = queryFactory
                .select(team.name,member.age.avg())
                .from(member)
                .join(member.team, team)
                .groupBy(team.name)
                .fetch();

        for(var a : result.toArray()) {
            System.out.println("info : " + a.toString());
        }

        Tuple teamA = result.get(0);
        Tuple teamB = result.get(1);

        System.out.println("teamA : " + teamA.toString());
        System.out.println("teamB : " + teamB.toString());

        assertThat(teamA.get(team.name)).isEqualTo("teamA");
        assertThat(teamA.get(member.age.avg())).isEqualTo(15);

        assertThat(teamB.get(team.name)).isEqualTo("teamB");
        assertThat(teamB.get(member.age.avg())).isEqualTo(35);


// 팀별 총 나이와 멤버 수를 저장할 Map
        Map<String, List<Integer>> teamData = new HashMap<>();

        List<Member> members = queryFactory
                .selectFrom(member)
                .fetch();

        for (Member member1 : members) {
            String teamName = member1.getTeam().getName();
            int age = member1.getAge();

            // 팀 이름을 키로 사용하여 나이와 카운트를 저장
            teamData.computeIfAbsent(teamName, k -> new ArrayList<>(Arrays.asList(0, 0)));
            List<Integer> ageAndCount = teamData.get(teamName);
            ageAndCount.set(0, ageAndCount.get(0) + age); // 총 나이
            ageAndCount.set(1, ageAndCount.get(1) + 1); // 멤버 수
        }

// 평균 나이를 저장할 Map
        Map<String, Double> teamAvgAgeMap = new HashMap<>();

        for (Map.Entry<String, List<Integer>> entry : teamData.entrySet()) {
            String teamName = entry.getKey();
            List<Integer> ageAndCount = entry.getValue();
            double avgAge = ageAndCount.get(0) / (double) ageAndCount.get(1);
            teamAvgAgeMap.put(teamName, avgAge);
        }

// 팀별 평균 나이 출력
        System.out.println("teamAvgAgeMap: " + teamAvgAgeMap);


        List<Object[]> result2 = em.createQuery(
                        "SELECT t.name, AVG(m.age) FROM Member m JOIN m.team t GROUP BY t.name")
                .getResultList();

        for (Object[] row : result2) {
            String teamName = (String) row[0];
            Double ageSum = (Double) row[1];
            System.out.println("Team: " + teamName + ", AVG Age: " + ageSum);
        }
    }

 

각 접근 방식은 데이터베이스에서 데이터를 조회하고 처리하는 다양한 방법을 제공합니다. 이를 통해 각각의 장단점과 특징을 이해할 수 있습니다.

1. 직접 엔티티 접근

장점:

  • 간단함: 기본적인 CRUD 작업에서는 가장 간단한 접근 방법입니다. 별도의 쿼리 언어를 배우지 않고도 데이터를 쉽게 조작할 수 있습니다.
  • 직관적: 객체 지향 프로그래밍 개념에 익숙한 개발자에게는 익숙하고 직관적입니다.

단점:

  • 성능 문제: 대량의 데이터를 다룰 때 비효율적일 수 있으며, N+1 문제 등 성능 문제가 발생할 수 있습니다.
  • 제한된 기능: 복잡한 집계나 그룹화 작업을 수행하기 어렵습니다.

2. JPQL (Java Persistence Query Language)

장점:

  • 유연성: 복잡한 쿼리와 집계, 그룹화 작업을 수행할 수 있습니다.
  • 표준화: JPA의 표준 쿼리 언어로, 다양한 JPA 구현체에서 사용할 수 있습니다.

단점:

  • 문자열 기반: 쿼리가 문자열로 작성되기 때문에, 쿼리 오류는 런타임 시점에서만 발견될 수 있습니다.
  • 타입 안전성 부족: 문자열 기반 쿼리로 인해 타입 안전성이 떨어집니다.

3. QueryDSL

장점:

  • 타입 안전성: 쿼리가 컴파일 시점에 검증되므로, 런타임 오류를 줄일 수 있습니다.
  • 유연성: 복잡한 동적 쿼리를 작성하기 용이하며, 메서드 체이닝을 통해 가독성을 높일 수 있습니다.
  • 자동 생성 코드: 엔티티를 기반으로 Q 클래스가 자동 생성되어, IDE의 코드 완성 기능을 사용할 수 있습니다.

단점:

  • 복잡성: 다른 방법에 비해 초기 설정과 학습 곡선이 더 가파릅니다.
  • 추가 빌드 단계: Q 클래스 생성 등의 추가 빌드 단계가 필요합니다.

결론

직접 엔티티 접근 방식이 간단한 CRUD 작업에 적합하며, 간단하고 직관적입니다. JPQL은 표준화된 쿼리 언어로 복잡한 쿼리와 집계 작업을 수행할 수 있어 강력한 기능을 제공합니다. QueryDSL은 타입 안전성과 유연성을 제공하지만, 초기 설정과 학습이 필요합니다.

 

조인 - 기본 조인 기본 조인

조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭(alias)으로 사용할 Q 타입을 지정하면 된다.

 

    @Test
    public void join(){
        QMember member = QMember.member;
        QTeam team = QTeam.team;
        List<Member> result = queryFactory
                .selectFrom(member)
                .join(member.team, team)
                .where(team.name.eq("teamA"))
                .fetch();

        assertThat(result)
                .extracting("username")
                .contains("member1", "member2");
    }

 

join()` , `innerJoin()` : 내부 조인(inner join) `

leftJoin()` : left 외부 조인(left outer join)

rightJoin()` : rigth 외부 조인(rigth outer join)

 

 

세타 조인

연관관계가 없는 필드로 조인

    @Test
    public void theta_join() throws Exception{
        em.persist(new Member("teamA"));
        em.persist(new Member("teamB"));

        QMember member = QMember.member;
        QTeam team = QTeam.team;

        List<Member> result = queryFactory
                .select(member)
                .from(member, team)
                .where(member.username.eq(team.name))
                .fetch();

        assertThat(result)
                .extracting("username")
                .containsExactly("teamA", "teamB");

    }

- from 절에 여러 엔티티를 선택해서 세타 조인
- 외부조인불가능 ->
on을사용하면외부조인가능

 

QueryDSL에서 사용되는 .join(member.team, team) 구문은 member 엔티티와 team 엔티티 간의 관계를 조인(Join)하는 것을 의미합니다. 이는 SQL의 JOIN과 유사하게 두 테이블 사이의 관계를 기반으로 데이터를 결합하는 작업입니다.

이해하기 위한 세부 사항

  1. 엔티티 관계: 이 코드에서 member는 Member 엔티티를, team은 Team 엔티티를 나타냅니다. 일반적으로 Member 엔티티에는 Team 엔티티와의 관계가 설정되어 있습니다. 예를 들어, Member 엔티티 클래스에는 Team을 참조하는 필드가 있을 것입니다.
public class Member {
    @ManyToOne
    private Team team;
    // other fields and methods
}
  1. 조인 설정: .join(member.team, team) 부분은 member 엔티티의 team 필드를 기준으로 Team 엔티티와 조인하는 것입니다. 즉, Member 엔티티의 team 필드와 Team 엔티티의 id 또는 기본 키를 기준으로 SQL JOIN을 수행합니다.
  2. 데이터베이스 조인: 이 조인은 실제 데이터베이스에서 수행되며, 데이터베이스는 Member와 Team 테이블을 조인하여 두 테이블 간의 관계에 따라 결과를 반환합니다. 조인의 기준은 외래 키와 기본 키의 매핑에 의해 결정됩니다.

SQL 쿼리로 번역

이 쿼리는 SQL로 번역하면 다음과 같습니다:

SELECT m.*
FROM Member m
JOIN Team t ON m.team_id = t.id
WHERE t.name = 'teamA'

 

여기서 m.team_id는 Member 테이블의 외래 키로, t.id는 Team 테이블의 기본 키입니다.

메모리 비교가 아닌 데이터베이스 비교

중요한 점은 이 조인은 메모리에서 수행되는 것이 아니라 데이터베이스에서 수행된다는 것입니다. 즉, 데이터베이스 레벨에서 조인 조건에 맞는 데이터를 선택하고 결과를 반환합니다. QueryDSL이나 JPA는 이 쿼리를 Java 코드로 작성할 수 있게 해주지만, 실제 조인 연산은 데이터베이스 엔진에 의해 처리됩니다.

따라서, team.name.eq("teamA") 조건은 데이터베이스 내의 Team 테이블에서 name이 teamA인 행을 선택하는 WHERE 조건에 해당하며, 조인된 Member 엔티티의 데이터를 필터링하는 역할을 합니다.

 

 

 

    /*
    예) 회원과 팁을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
    JPQL: select m, t from Member m left join m.team t on t.name = 'teamA'
    * */

    @Test
    public void join_on_filtering(){
        QMember member = QMember.member;
        QTeam team = QTeam.team;

        List<Tuple> result = queryFactory
                .select(member, team)
                .from(member)
                .leftJoin(member.team, team).on(team.name.eq("teamA"))
                .fetch();

        for(Tuple tuple : result){
            System.out.println("tuple = " + tuple);
        }
    }

    @Test
    public void join_on_filtering(){
        QMember member = QMember.member;
        QTeam team = QTeam.team;

        List<Tuple> result = queryFactory
                .select(member, team)
                .from(member)
                .join(member.team, team)
                .where(team.name.eq("teamA"))
//                .leftJoin(member.team, team).on(team.name.eq("teamA"))
                .fetch();

        for(Tuple tuple : result){
            System.out.println("tuple = " + tuple);
        }
    }

 

on 절을 활용해 조인 대상을 필터링 할 때, 외부 조인이 아니라 내부조인(inner join)을 사용하면, where 절에서 필터링 하는 것과 기능이 동일하다. 따라서 on 절을 활용한 조인 대상 필터링을 사용할 때, 내부조인 이면 익숙한 where 절로 해결하고, 정말 외부조인이 필요한 경우에만 이 기능을 사용하자.

 

 

연관관계 없는 엔티티 외부 조인

예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인

/*
    연관관계 없는 엔티티 외부 조인
    회원의 이름이 팀 이름과 같은 대상 외부 조인
    * */

    @Test
    public void join_on_no_relation() {
        em.persist(new Member("teamA"));
        em.persist(new Member("teamB"));
        em.persist(new Member("teamC"));


        QMember member = QMember.member;
        QTeam team = QTeam.team;

        List<Tuple> result = queryFactory
                .select(member, team)
                .from(member)
                .leftJoin(team).on(member.username.eq(team.name))
                .fetch();


        for(Tuple tuple : result){
            System.out.println("tuple = " + tuple);
        }
    }

 

- 하이버네이트 5.1 부터 on을 사용해서 서로 관계가 없는 필드로 외부 조인하는 기능이 추가되었다. 물로 내부 조인도 가능하다.

- 주의! 문법을 잘 봐야 한다. leftjoin() 부분에 일반 조인과 다르게 엔티티 하나만 들어간다.

- 일반조인 : leftjoin(member.team, team)

-  on 조인 : from(member).leftJoin(team).on(xxx)

 

 

 

 

조인 - 페치 조인

페치 조인은 SQL에서 제공하는 기능은 아니다. SQL조인을 활용해서 연관된 엔티티를 SQL 한번에 조회하는 기능이

. 주로 성능 최적화에 사용하는 방법이다.

 

 

**페치 조인 미적용**

지연로딩으로 Member, Team SQL 쿼리 각각 실행

    @PersistenceUnit
    EntityManagerFactory emf;

    @Test
    public void fetchJoinNo(){
        em.flush();
        em.clear();

        QMember member = QMember.member;

        Member findMember = queryFactory
                .selectFrom(member)
                .where(member.username.eq("member1"))
                .fetchOne();

        boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
        assertThat(loaded).as("패치 조인 미적용").isFalse();
    }

 

 

**페치 조인 적용**
즉시로딩으로 Member, Team SQL 쿼리 조인으로 한번에 조회

 @Test
    public void fetchJoinUse(){
        em.flush();
        em.clear();

        QMember member = QMember.member;
        QTeam team = QTeam.team;

        Member findMember = queryFactory
                .selectFrom(member)
                .join(member.team, team).fetchJoin()
                .where(member.username.eq("member1"))
                .fetchOne();

        boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
        assertThat(loaded).as("패치 조인 적용").isTrue();
    }

 

- join(), leftJoin() 등 조인 기능 뒤에 fetchJoin()이라고 추가하면 된다.

 

JPQL에서 Fetch Join 사용

fetch join을 사용하는 JPQL 쿼리의 예시는 다음과 같습니다:

String jpql = "SELECT m FROM Member m JOIN FETCH m.team WHERE m.username = :username";
List<Member> members = em.createQuery(jpql, Member.class)
                         .setParameter("username", "member1")
                         .getResultList();

 

 

서브 쿼리

com.querydsl.jpa.JPAExpressions 사용

   /*
    * 나이가 가장 많은 회원 조회
    * */
    @Test
    public void subQuery(){
        QMember memberSub = new QMember("memberSub");
        QMember member  = QMember.member;

        List<Member> result = queryFactory
                .selectFrom(member)
                .where(member.age.eq(
                        JPAExpressions
                                .select(memberSub.age.max())
                                .from(memberSub)
                ))
                .fetch();


        assertThat(result).extracting("age")
                .containsExactly(40);
    }

 

  • assertThat(result): result 객체를 검증 대상으로 설정합니다.
  • extracting("age"): result 객체의 age 속성 값을 추출합니다.
  • containsExactly(40): 추출된 값이 정확히 40인지 확인합니다.

 

 

서브 쿼리 goe 사용

    /*
     * 나이가 평균 이상인 회원
     * */
    @Test
    public void subQueryGoe(){
        QMember memberSub = new QMember("memberSub");
        QMember member  = QMember.member;

        List<Member> result = queryFactory
                .selectFrom(member)
                .where(member.age.goe(
                        JPAExpressions
                                .select(memberSub.age.avg())
                                .from(memberSub)
                ))
                .fetch();


        assertThat(result).extracting("age")
                .containsExactly(30, 40);
    }
}

 

 

서브쿼리 여러 건 처리 in 사용

    /*
     * 서브쿼리 여러 건 처리,in 사용
     * */
    @Test
    public void subQueryIn(){
        QMember memberSub = new QMember("memberSub");
        QMember member  = QMember.member;

        List<Member> result = queryFactory
                .selectFrom(member)
                .where(member.age.in(
                        JPAExpressions
                                .select(memberSub.age)
                                .from(memberSub)
                                .where(member.age.gt(10))
                ))
                .fetch();


        assertThat(result).extracting("age")
                .containsExactly(20, 30, 40);
    }

 

 

 

select 절에 subquery

    import static com.querydsl.jpa.JPAExpressions.*;
    
    @Test
    public void selectSubQuery(){
        QMember member = QMember.member;
        QMember memberSub = new QMember("memberSub");
        List<Tuple> result = queryFactory
                .select(member.username,
                        select(memberSub.age.avg())
                                .from(memberSub))
                .from(member)
                .fetch();

        for(Tuple tuple : result){
            System.out.println("tuple = " + tuple);
        }
    }

 

from 절의 서브쿼리 한계

JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다. 당연히 Querydsl도 지원하지 않는다. 하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다. Querydsl도 하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원한다.

 

from 절의ㅏ 서브쿼리 해결방안

1. 서브쿼리를 join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다.)

2. 애플리케이션에서 쿼리를 2번 분리해서 실행한다.

3. nativeSQL을 사용한다.

 

1. 서브쿼리를 Join으로 변경

서브쿼리를 Join으로 변경할 수 있는 상황에서의 예시입니다.

JPQL 서브쿼리 (지원되지 않음)

SELECT e
FROM Employee e
WHERE e.departmentId IN (SELECT d.id FROM Department d WHERE d.name = 'HR')

 

Join으로 변경된 JPQL

String jpql = "SELECT e FROM Employee e JOIN e.department d WHERE d.name = :deptName";
TypedQuery<Employee> query = em.createQuery(jpql, Employee.class);
query.setParameter("deptName", "HR");
List<Employee> results = query.getResultList();

 

2. 애플리케이션에서 쿼리를 2번 분리해서 실행

서브쿼리를 애플리케이션 레벨에서 분리하는 예시입니다.

 

첫 번째 쿼리: 서브쿼리로 사용할 데이터 조회

String jpql1 = "SELECT d.id FROM Department d WHERE d.name = :deptName";
TypedQuery<Long> query1 = em.createQuery(jpql1, Long.class);
query1.setParameter("deptName", "HR");
List<Long> departmentIds = query1.getResultList();

 

두 번째 쿼리: 조회한 데이터를 기반으로 메인 쿼리 실행

String jpql2 = "SELECT e FROM Employee e WHERE e.department.id IN :deptIds";
TypedQuery<Employee> query2 = em.createQuery(jpql2, Employee.class);
query2.setParameter("deptIds", departmentIds);
List<Employee> results = query2.getResultList();

 

3. Native SQL 사용

JPA에서 네이티브 SQL을 사용하는 예시입니다.

네이티브 SQL 사용

String sql = "SELECT e.* FROM Employee e WHERE e.department_id IN (SELECT d.id FROM Department d WHERE d.name = 'HR')";
Query nativeQuery = em.createNativeQuery(sql, Employee.class);
List<Employee> results = nativeQuery.getResultList();

 

 

Case 문

- select, 조건절(where), order by에서 사용 가능

 

단순한 조건 & 복잡한 조건

    @Test
    public void basicCase(){
        QMember member = QMember.member;
        List<String> result = queryFactory
                .select(member.age
                        .when(10).then("열살")
                        .when(20).then("스무살")
                        .otherwise("기타"))
                .from(member)
                .fetch();

        for(String s : result){
            System.out.println("s = " + s);
        }
    }

    @Test
    public void complexCase(){
        QMember member = QMember.member;
        List<String> result = queryFactory
                .select(new CaseBuilder()
                        .when(member.age.between(0,20)).then("0~20살")
                        .when(member.age.between(21,30)).then("21~30살")
                        .otherwise("기타"))
                .from(member)
                .fetch();

        for(String s : result){
            System.out.println("s = " + s);
        }
    }

- 애플리케이션에서 이런 로직들은 10이면 10살이고 20이면 20살로 바꾸거나 이런 거는 애플리케이션에나 또는 프레젠테이션 로직이면 화면 프레젠테이션 레이어에서 해결하셔야 됩니다. DB에서 이런 것들을 하는 것은 좋다지 않다고 봅니다.

 

이유 1: 유지보수성과 가독성

애플리케이션 코드에서 로직을 처리하면 코드가 더 읽기 쉽고 유지보수하기 쉬워집니다. 다음 예제를 봅시다.

 
@Test
public void basicCase() {
    QMember member = QMember.member;
    List<Integer> ages = queryFactory
            .select(member.age)
            .from(member)
            .fetch();

    List<String> result = ages.stream()
            .map(age -> {
                if (age == 10) return "열살";
                if (age == 20) return "스무살";
                return "기타";
            })
            .collect(Collectors.toList());

    for (String s : result) {
        System.out.println("s = " + s);
    }
}

 

이 코드는 데이터베이스에서 필요한 데이터를 가져오고, 애플리케이션 레벨에서 변환합니다. 이렇게 하면 변환 로직을 이해하기 쉽고, 필요할 때 쉽게 수정할 수 있습니다.

이유 2: 데이터베이스 부하 감소

데이터베이스는 데이터 저장 및 검색을 위해 최적화되어 있습니다. 복잡한 변환 로직을 데이터베이스에 추가하면 쿼리가 복잡해지고, 데이터베이스에 불필요한 부하를 줄 수 있습니다.

List<String> result = queryFactory
        .select(new CaseBuilder()
                .when(member.age.between(0, 20)).then("0~20살")
                .when(member.age.between(21, 30)).then("21~30살")
                .otherwise("기타"))
        .from(member)
        .fetch();
 

이 코드는 데이터베이스에서 변환을 수행합니다. 이는 쿼리를 복잡하게 만들고 데이터베이스의 성능에 부정적인 영향을 미칠 수 있습니다.

이유 3: 비즈니스 로직의 위치

비즈니스 로직은 보통 애플리케이션 레이어에 두는 것이 좋습니다. 데이터베이스는 단순한 CRUD 작업을 수행하는 것이 가장 이상적입니다. 비즈니스 로직을 애플리케이션 레이어에 두면 다음과 같은 이점이 있습니다:

  • 일관성: 모든 비즈니스 로직이 한 곳에 모여 있어 일관성 있게 관리할 수 있습니다.
  • 재사용성: 동일한 비즈니스 로직을 여러 곳에서 재사용할 수 있습니다.
  • 테스트 용이성: 애플리케이션 코드로 비즈니스 로직을 처리하면 단위 테스트 작성이 더 쉽습니다.

 

상수, 문자 더하기
상수가 필요하면 `Expressions.constant(xxx)` 사용

    @Test
    public void constant(){
        QMember member = QMember.member;

        List<Tuple> result = queryFactory
                .select(member.username, Expressions.constant("A"))
                .from(member)
                .fetch();

        for(Tuple tuple : result){
            System.out.println("tuple = " + tuple);
        }
    }

 

참고: 위와 같이 최적화가 가능하면 SQLconstant 값을 넘기지 않는다. 상수를 더하는 것 처럼 최적화가 어려 우면 SQLconstant 값을 넘긴다.

 

 

 

문자 더하기 concat

    @Test
    public void concat(){
        QMember member = QMember.member;

        List<String> result = queryFactory
                .select(member.username.concat("_").concat(member.age.toString()))
                .from(member)
                .where(member.username.eq("member1"))
                .fetch();

        for(String s : result){
            System.out.println("s = " + s);
        }
    }

결과 : s = member1_member1.age

 

`member.age.stringValue()` 부분이 중요한데, 문자가 아닌 다른 타입들은 `stringValue()` 로 문 자로 변환할 수 있다. 이 방법은 ENUM을 처리할 때도 자주 사용한다.