관리 메뉴

나구리의 개발공부기록

프로젝트 환경설정, 기본 문법 시작 - JPQL vs Querydsl, 기본 Q-Type 활용, 검색 조건 쿼리, 결과 조회, 정렬, 페이징, 집합, 조인(기본조인/on절/페치조인), 서브 쿼리, Case문, 상수 및 문자 더하기 본문

인프런 - 스프링부트와 JPA실무 로드맵/실전! Querydsl

프로젝트 환경설정, 기본 문법 시작 - JPQL vs Querydsl, 기본 Q-Type 활용, 검색 조건 쿼리, 결과 조회, 정렬, 페이징, 집합, 조인(기본조인/on절/페치조인), 서브 쿼리, Case문, 상수 및 문자 더하기

소소한나구리 2024. 10. 29. 19:37

출처 : 인프런 - 실전! Querydsl (유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


1. 프로젝트 환경 설정

1) 프로젝트 생성

 

(1) Project

  • Gradle
  • Java 17
  • SpringBoot 3.3.5

(2) Metadata

  • Group: study
  • Artifact: querydsl

(3) Dependencies

  • Spring Web
  • Lombok
  • Spring Data JPA
  • H2 Database

2) Querydsl 설정, 스프링 부트 및 JPA 설정

3) 예제 도메인 모델


2. 기본 문법 시작 - JPQL vs Querydsl

** 참고

  • 기본적으로 JPQL, SQL을 다룰줄 안다면 거의 비슷하기 때문에 배우는데에 어렵지 않고 조금만 다뤄보면 쉽게 적응할 수 있음

1) 테스트를 위한 기본 코드 

(1) 초기화 코드 작성

  • 별도의 테스트 클래스에 @BeforeEach를 작성한 초기화 메서드를 작성하여 테스트 실행 전에 데이터가 입력되도록 설정
@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    @Autowired
    EntityManager em;

    @BeforeEach
    void before() {
        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);
    }
}

2) JPQL VS Querydsl 비교 테스트

(1) 일반적인 JPQL 코드

  • em.createQuery로 JPQL 코드를 작성 후 파라미터 바인딩을 이용하여 코드를 작성
  • 문자이기 때문에 띄어쓰기를 주의해야함
@Test
void startJPQL() {
    // member1 찾기
    Member findMember = em.createQuery("select m from Member m" +
            " where m.username = :username", Member.class)
            .setParameter("username", "member1")
            .getSingleResult();

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

 

(2) Querydsl 코드

  • Querydsl을 사용하기 위해서는 JPQQueryFactory(em)으로 객체를 생성한 참조변수를 이용해야함
  • 생성된 Q클래스의 객체를 생성하여 참조변수를 쿼리에 사용(만약 Q클래스생성이 안되었다면 gradle - clean, build를 해주면 생성됨)
  • Querydsl은 JPQL 빌더역할이라고 보면 되는데 JPQL쿼리는 문자이기 때문에 문법오류가 발생하면 애플리케이션이 실행되고 해당 메서드가 호출되어야 문제가 발생하지만 Querydsl은 컴파일 시점에 오류를 알 수 있다는 것이 매우 큰 장점임
  • 파라미터 바인딩도 직접 할 필요없이 여러가지 지원하는 메서드를 통해 값을 직접 입력해주면 자동으로 PreparedStatement로 동작함
@Test
void startQuerydsl() {
    // member1 찾기
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);
    QMember m = new QMember("m");   // 인수로 별칭 입력(크게 중요하지 않음)

    Member findMember = queryFactory
            .select(m)
            .from(m)
            .where(m.username.eq("member1"))    // 파라미터 바인딩 처리
            .fetchOne();

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

 

(3) JPAQueryFactory를 필드로 이동

  • JPA를 사용할 때 EntityManager처럼 JPAQueryFactory로 필드로 빼서 선언후 별도의 초기화하는 메서드로 JPAQueryFactory를 생성하도록 해도됨
  • 테스트뿐아니라 리포지토리에서도 이런식으로 사용해도 괜찮음
  • 멀티쓰레드에 문제없이 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에 동시성 문제 없이 잘 작동함
public class QuerydslBasicTest {
    // ... 기존 코드 생략

    JPAQueryFactory queryFactory;	// 필드에서 선언
    
    @BeforeEach
    void before() {
        queryFactory = new JPAQueryFactory(em);	// JPAQueryFactory생성을 메서드로 작성
        // ... 기존 코드 생략
    }
}

3. 기본 Q-Type 활용

1) Q클래스 인스턴스를 사용하는 2가지 방법

QMember m = new QMember("m");   // 별칭을 직접 지정
QMember m2 = QMember.member;    // QMember 생성시 생성되는 스태틱 멤버 사용

2) 기본 인스턴스를 사용한 Qeurydsl

(1) Querydsl 권장 작성 방법

  • QMember의 static 변수를 static import하며 변수명으로 즉시 사용하는 것을 가장 권장함
  • 실행된 JPQL 쿼리를 보면 member1라는 이름으로 필드명이 실행되는데 QMember를 빌드할 때 자동으로 이렇게 생성했기 때문임
  • 별칭을 직접 입력하여 QMember의 참조변수를 생성하는 방법은 가끔 셀프 조인을 실행할 때 별칭을 다르게 사용할 때의 정도만 사용하고 일반적으로는 Q클래스의 멤버를 static import해서 직접 쿼리에 사용하도록 하자
import static study.querydsl.entity.QMember.member;

@Test
void startQuerydsl2() {
    Member findMember = queryFactory
            .select(member)
            .from(member)
            .where(member.username.eq("member1"))    // 파라미터 바인딩 처리
            .fetchOne();

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

 

** 참고

  • Querydsl을 실행하면 SQL구문만 보이는데 application.yml설정에서 아래의 설정을 추가해주면 실행되는 JPQL을 볼 수 있음
spring.jpa.properties.hibernate.use_sql_comments: true

4. 검색 조건 쿼리

1) 검색 조건 쿼리 예시

(1) 기본 적용

  • select와 From의 대상이 같은경우 selectFrom()로 한번에 조회가 가능함
  • where절의 조건을 .and처럼 메서드체인으로 추가 조건을 입력할 수 있음(and뿐만 아니라 다른 기능이 많음)
// 검색 조건 쿼리
@Test
void search() {
    Member findMember = queryFactory
            .selectFrom(member)     // select와 From의 대상이 같으면 selectFrom으로 한번에 조회가능
            .where(member.username.eq("member1")
                    .and(member.age.eq(10)))   // .and로 추가 조건을 입력할 수 있음
            .fetchOne();

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

 

(2) AND 조건을 파라미터로 처리

  • 조건이 and인 경우에는 .and를 생략하고 파라미터를 ,로 구분하여 조건을 입력하면 자동으로 and 조건으로 처리가 됨
  • 해당 방법으로 and조건을 처리하는 경우 null값이 무시되기 때문에 동정 쿼리를 매우 깔끔하게 작성할 수 있어 조건이 and 연산만 있다면 해당 방법을 사용하는 것을 권장함
@Test
void searchParam() {
    Member findMember = queryFactory
            .selectFrom(member)
            .where(member.username.eq("member1"),
                   member.age.eq(10)
            ).fetchOne();
        // ...
    }

2) JPQL이 제공하는 주요 검색 조건

  • 아래의 주요 메서드 외에도 .을 찍으면 IDE의 도움을 받아 찾아서 활용할 수 있음
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%’ 검색
...

5. 결과 조회

1) 키워드 종류

(1) 설명

  • fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
  • fetchOne() : 단건 조회, 결과가 없으면 null 결과가 둘 이상이면 NonUniqueResultException 에러 발생
  • fetchFirst() : limit(1).fetchOne()
  • fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행 - Deprecated 되었음
  • fetchCount() : count 쿼리로 변경해서 count 수 조회 - Deprecated 되었음
// 결과 조회
@Test
void resultFetch() {
    // 리스트로 조회, 결과 없으면 빈 리스트 반환
    List<Member> MemberList = queryFactory
            .selectFrom(member).fetch();

    // 단건조회, 결과가 없으면 null, 2이상이면 에러 발생
    Member memberOne = queryFactory
            .selectFrom(member).fetchOne();

    // limit(1)로 1개만 조회
    Member memberFirst = queryFactory
            .selectFrom(member).fetchFirst();

    // 페이징 정보가 포함되어 count쿼리가 추가로 발생됨 - Deprecated
    QueryResults<Member> results = queryFactory
            .selectFrom(member).fetchResults();

    results.getTotal();
    List<Member> contents = results.getResults();

    // count 쿼리로 변경 - Deprecated
    long count = queryFactory.selectFrom(member).fetchCount();
}

 

(2) Deprecated 메서드 설명

  • 스프링 부트 3.x부터는 Querydsl 5.0을 사용하는데 Deprecated된 fetchCount()와 fetchResult()는 개발자가 작성한 select 쿼리를 기반으로 count용 쿼리를 내부에서 만들어서 실행할 때 복잡한 쿼리에서는 제대로 동작하지 않는 문제가 있음
  • 그래서 Querydsl에서 두 메서드를 지원하지 않기로 했기 때문에 count쿼리가 필요하다면 직접 count쿼리를 작성해서 사용하는 것을 권장함
@Test
void count() {
    Long totalCount = queryFactory
            //.select(Wildcard.count) //select count(*) 로 쿼리가 발생
            .select(member.count())   //select count(member.id) 로 쿼리가 발생
            .from(member)
            .fetchOne();

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

6. 정렬

1) 예제 코드

  • .orderBy로 정렬 기준을 입력할 수 있음
  • 파라미터로 정렬 기준을 순서대로 입력하여 여러개를 입력할 수 있음
  • 예제에서는 회원 나이 내림차순, 회원 이름 오름차순으로 정렬하되 null은 마지막에 출력
  • 정렬 기준에 nullsLast(), nullsFirst()로 null값이 먼저오도록 정렬할지 마지막에 오도록 정렬할지 선택할 수 있음
@Test
void sort() {
    // 예제 보충을 위한 추가 데이터
    em.persist(new Member(null, 100));
    em.persist(new Member("member5", 100));
    em.persist(new Member("member6", 100));

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

    Member member5 = result.get(0);
    Member member6 = result.get(1);
    Member memberNull = result.get(2);

    assertThat(member5.getUsername()).isEqualTo("member5");
    assertThat(member6.getUsername()).isEqualTo("member6");
    assertThat(memberNull.getUsername()).isNull();
}

7. 페이징

1) 예제 코드

  • JPQL과 동일하게 offset정보와 limit 정보를 입력하면 데이터베이스 방언에따라 실행되는 SQL구문을 확인할 수 있음
  • 전체 조회수가 필요할 때는 위에처럼 별도의 count 쿼리를 만들어서 사용해야함
// 페이징 - 조회 건수 제한
@Test
void paging1() {
    List<Member> result = queryFactory
            .selectFrom(member)
            .orderBy(member.username.asc())
            .offset(1)
            .limit(2)
            .fetch();

    assertThat(result.size()).isEqualTo(2);
}

8. 집합

1) 예제 코드

(1) 집계 함수들 사용

  • 반환타입이 Tuple로 반환되며 꺼내는 방법은 select절에서 입력할 때와 동일함
  • 실무에서는 DTO를 사용하여 반환함
  • JPQL이 제공하는 모든 집합 함수를 제공함
  • select절에 추가 대상을 지정해야하므로 select와 from을 따로 작성해야함
// 집합 테스트 - 반환타입이 Querydsl의 Tuple로 꺼내짐(실무에서는 DTO로 꺼냄)
@Test
void aggregation() {
    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);
    // select 절에서 사용한 함수와 동일한 이름으로 값을 꺼낼 수 있음
    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);
}

 

(2) GroupBy 사용

  • JPQL과 동일한 문법으로 사용가능하며 Group화된 결과에 조건을 적용하기위한 having도 사용 가능함
/**
 * 팀의 이름과 각 팀의 평균 연령을 구하기
 */
@Test
void groupBy() {
    List<Tuple> result = queryFactory
            .select(team.name, member.age.avg())
            .from(member)
            .join(member.team, team)
            .groupBy(team.name)
            .fetch();

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

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

    assertThat(teamB.get(team.name)).isEqualTo("teamB");
    assertThat(teamB.get(member.age.avg())).isEqualTo(35); // 70 / 2
}

9. 조인 - 기본 조인

1) 예제 코드

(1) 기본 조인

  • join()으로 조건을 입력하면 inner join으로 쿼리가 발생함
  • leftjoin(), rightjoin()도 적용할 수 있으며 inner join과 마찬가지로 일반적인 JPQL, SQL문법과 동일하게 사용하고 동일하게 쿼리가 발생함
  • on절을 추가로 입력하여 join 조건을 입력할 수 있음
// 기본 Join - teamA에 소속된 모든 회원
@Test
void join() {
    List<Member> result = queryFactory
            .selectFrom(member)
            .join(member.team, team)
            .where(team.name.eq("teamA"))
            .fetch();

    assertThat(result).extracting("username")   // username의 필드를 검증
            .containsExactly("member1", "member2");   // 검증할 값을 입력
}

 

(2) 세타조인

  • 연관관계가 없는 엔터티끼리도 from절에 엔터티를 입력하여 세타 조인을 할 수 있음
  • 세타 조인은 일반적으로 외부 조인이 불가능하며 on절을 이용해야 외부 조인이 가능함
  • 세타 조인은 카르테시안 곱(교차곱)으로 쿼리가 수행되며 DB마다 차이가 있지만 대부분 내부적으로 최적화가 동작하여 실행됨
@Test
void theta_join() {
    em.persist(new Member("teamA"));
    em.persist(new Member("teamB"));

    List<Member> result = queryFactory
            .select(member)
            .from(member, team)   // from절에 join할 엔터티를 입력하면 join이 가능함
            .where(member.username.eq(team.name))
            .fetch();

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

10. 조인 - on절 

** JPA 2.1부터 지원

1) 조인 대상 필터링

(1) 회원과 팀을 조인하면서 팀 이름이 teamA인 팀만 조인하고 회원은 모두 조회

  • on절에 left join 조건을 입력하여 회원이름은 전부 출력하고 team 정보는 teamA의 값만 입력됨
  • left join이 동작하여 on절 조건이 맞지않은 값들은 null이 입력됨

** 참고

  • on절을 활용해 조인 대상을 필터링할 때 외부조인이 아닌 내부 조인을 사용하면 where절에서 필터링하는 것과 동일한 기능임
  • 내부조인인 경우 익숙한 where문으로 필터링하고 외부조인이 필요한 경우 on절을 이용하는 것을 권장함
/**
 * on절 활용 - 조인대상 필터링
 * 회원과 팀을 join, 팀 이름이 teamA인 팀만 조인하고 회원은 모두 조회
 * JPQL : select m, t from Member m left join m.team t on t.name = 'teamA'
 */
@Test
void join_on_filtering() {
    List<Tuple> result = queryFactory
            .select(member, team)
            .from(member)
            // inner조인일 경우 on이나 where나 동일한 기능을 하게됨
//            .join(member.team, team).where(team.name.eq("teamA"))
//            .join(member.team, team).on(team.name.eq("teamA"))
            .leftJoin(member.team, team).on(team.name.eq("teamA"))
            .fetch();

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

/* 출력 결과
tuple = [Member(id=1, username=member1, age=10), Team(id=1, name=teamA)]
tuple = [Member(id=2, username=member2, age=20), Team(id=1, name=teamA)]
tuple = [Member(id=3, username=member3, age=30), null]
tuple = [Member(id=4, username=member4, age=40), null]
*/

2) 연관관계 없는 엔터티 외부 조인

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

  • leftJoin()에 연관관계가 없는 엔터티를 입력하고 on절에 join 조건을 입력하여 연관관계가 없는 엔터티를 외부조인 할 수 있음
  • 출력결과를 보면 Member의 데이터는 전부 출력되고 on절의 조건에 부합되는 Team이름만 출력되고 나머지는 null로 출력되어 left Join이 정상적으로 이루어진 것을 확인할 수 있음
  • 실행된 SQL과 JPQL도 정상적으로 Left Join 쿼리를 전송함
/**
 * on절 활용 - 연관관계 없는 엔터티 외부 조인
 * 회원의 이름과 팀의 이름이 같은 대상 외부 조인
 * JPQL : select m, t from Member m left join Team t on m.username = t.name
 * SQL : select m.*, t.* from Member m left join Team t on m.username = t.name
 */
@Test
void join_on_no_relation() {
    em.persist(new Member("teamA"));
    em.persist(new Member("teamB"));
    em.persist(new Member("teamC"));

    List<Tuple> result = queryFactory
            .select(member, team)
            .from(member)
            .leftJoin(team).on(member.username.eq(team.name))   // 연관관계가 없는 team을 leftjoin
            .fetch();

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

/* 출력 결과
tuple = [Member(id=1, username=member1, age=10), null]
tuple = [Member(id=2, username=member2, age=20), null]
tuple = [Member(id=3, username=member3, age=30), null]
tuple = [Member(id=4, username=member4, age=40), null]
tuple = [Member(id=5, username=teamA, age=0), Team(id=1, name=teamA)]
tuple = [Member(id=6, username=teamB, age=0), Team(id=2, name=teamB)]
tuple = [Member(id=7, username=teamC, age=0), null]
*/

11. 조인 - 페치조인

1) 설명

  • SQL에서 제공하는 기능이아닌 SQL조인을 활용하여 연관된 엔터티를 SQL한번에 조회하는 기능으로 N + 1 문제를 해결하기위한 성능 최적화에 사용하는 방법임

2) 예제 코드

(1) 페치 조인 미적용

  • Member만 조회하면 당연히 Member만 조회하게되고 Team의 정보는 영속성 컨텍스트에 로딩되어있지 않음
  • EntityManagerFactory의 getPersistenceUnitUtil() 메서드로 인자의 대상이 영속화 되었는지의 여부를 확인할 수 있음
  • 이 상태에서 조회된 findMember에서 Team의 필드에 접근하는 순간 지연로딩이 적용되어 select 쿼리가 발생됨(N+1문제, JPA강의 및 JPA 활용편에서 엄청 강조한 내용)
@PersistenceUnit
private EntityManagerFactory emf;

// 페치조인 미적용
@Test
void fetchJoinNo() {
    // 영속성컨텍스트를 비워서 처음부터 시작하도록 설정
    em.flush();
    em.clear();

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

    // 영속성 컨텍스트에 조회결과인 findMember의 안에 Team이 로딩 되었는지 확인 - fetch join을 적용안했으므로 false가 나와야함
    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
    assertThat(loaded).as("페치 조인 미적용").isFalse();
    
    String teamName = findMember.getTeam().getName();
    boolean loadedTeam = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
    assertThat(loadedTeam).as("team 로드").isTrue();
}

 

(2) 페치 조인 적용

  • join을 작성할 때 메서드 체인으로 fetchJoin()을 적용하면 Team의 정보가 영속성 컨텍스트에 저장되어있음
  • 발생되는 쿼리를 보면 join 쿼리가 발생되어 한번에 연관된 Team의 정보를 가져오는 쿼리가 발생된 것을 확인할 수 있음
// 페치조인 적용
@Test
void fetchJoinUse() {
    // 영속성컨텍스트를 비워서 처음부터 시작하도록 설정
    em.flush();
    em.clear();

    Member findMember = queryFactory
            .selectFrom(member)
            .join(member.team, team).fetchJoin()    // 페치 조인 적용
            .where(member.username.eq("member1"))
            .fetchOne();

    // 페치 조인을 적용했으므로 Team의 정보가 이미 영속화 되어있음
    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
    assertThat(loaded).as("페치 조인 적용").isTrue();
}

 


12. 서브쿼리

1) 예제 쿼리

  • JPAExpressions를 사용하여 서브쿼리를 입력할 수 있으며 Static Import 적용이 가능함

(1) 나이가 가장 많은 회원 조회 - 동등 연산 사용

  • 같은 엔터티로 select 절에 서브쿼리가 들어가기 때문에 같은 별칭을 사용할 수 없어 새로운 별칭으로 참조변수를 생성하여 서브쿼리를 작성하면됨
  • where절의 eq( = )의 조건에 서브쿼리를 입력
// 서브쿼리1 - 나이가 가장 많은 회원 조회
@Test
void subQuery() {
    QMember memberSub = new QMember("memberSub");   // 동일한 엔터티를 조회하기때문에 새로운 별칭을 적용

    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.eq(
                    JPAExpressions
                            .select(memberSub.age.max())
                            .from(memberSub)
            ))
            .fetch();
    // result의 age가 40
    assertThat(result).extracting("age").containsExactly(40);
}

 

(2) 나이가 평균 이상인 회원 - 비교연산 사용

  • where절의 goe(>=) 조건에 서브쿼리 작성
// 서브쿼리2 - 나이가 평균 이상인 회원
@Test
void subQueryGoe() {
    QMember memberSub = new QMember("memberSub");

    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);
}

 

(3) 나이가 10을 초과하는 회원 조회 - in 사용

  • where절의 in 조건에 서브쿼리 작성
  • 서브쿼리의 gt는 > 연산을 뜻함
// 서브쿼리3 - age가 10 초과인 회원 조회
@Test
void subQueryIn() {
    QMember memberSub = new QMember("memberSub");

    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.in(
                    JPAExpressions
                            .select(memberSub.age)
                            .from(memberSub)
                            .where(memberSub.age.gt(10))    // gt (>)
            ))
            .fetch();
    assertThat(result).extracting("age").containsExactly(20, 30, 40);
}

 

(4) select 절에 서브쿼리를 적용

  • JPAExpressions static import 적용한 예제
  • select 절에 서브쿼리를 적용하여 age의 평균값을 모든 member.username과 함께 반환
// 서브쿼리4 - select절에 서브쿼리
@Test
void selectSubQuery() {
    QMember memberSub = new QMember("memberSub");

    List<Tuple> result = queryFactory
            .select(member.username,
                    select(memberSub.age.avg()) // age의 평균값 반환
                        .from(memberSub))
            .from(member)
            .fetch();

    for (Tuple tuple : result) {
        System.out.println("tuple = " + tuple);
        assertThat(tuple.get(1, Double.class)).isEqualTo(25.0);
    }

 

** 참고

  • 기존에는 from절 서브쿼리(인라인 뷰)를 지원하지 않았지만 하이버네이트 6.1부터 from절 서브쿼리를 지원하기 시작하였으나 Querydsl에서는 아직까지는 공식적으로 지원하고 있지는 않아 서드파티를 적용해야 함
  • 인라인뷰의 사용은 항상 성능 문제의 이슈가 있어 사용할 때 쿼리를 분리하는 것을 고려해야 할 수도 있음

12. Case문

  • JPQL에서 지원하는 case문의 코드 모두 지원
  • 그러나 꼭 DB에서 이런 변환하는 기능 사용해야하는지에 대해서는 고민이 필요함
  • DB에서는 최소한의 필터링, 그룹설정하는등의 계산을 할 수는 있지만 이렇게 DB의 값을 전환하거나 변환하는 작업은 애플리케이션에서 하는 것을 권장함

1) 예제 코드

(1) 단순한 조건

  • 단순하게 값을입력하는 case문
// case 문 - 단순한 조건
@Test
public void basicCase() {
    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);
    }
}

 

(2) 복잡한 조건

  • CaseBuilder()를 사용하여 when절에 조건을 입력할 수 있음
// case 문 - 복잡한 조건
@Test
public void complexCase() {
    List<String> result = queryFactory
            .select(new CaseBuilder()
                    .when(member.age.between(0, 20)).then("20세 이하")
                    .when(member.age.between(21, 30)).then("21 ~ 30세")
                    .otherwise("31세 이상"))
            .from(member)
            .fetch();

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

 

(3) orderBy 에서 case문 함께 사용

  • new CaseBuilder()의 조건을 숫자로 지정하여 orderBy를 이용하여 조건으로 출력 순서를 지정할 수 있음
// case 문 - orderBy와 함께 사용
@Test
public void orderByCase() {

    // CaseBuilder의 각 조건의 결과를 숫자로 지정
    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();

    for (Tuple tuple : result) {
        String username = tuple.get(member.username);
        Integer age = tuple.get(member.age);
        Integer rank = tuple.get(rankPath);
        System.out.println("username = " + username + " age = " + age + " rank = " + rank);
    }
}

13. 상수, 문자 더하기

1) 상수가 필요할 때

  • Expressions.constant()를 사용하여 상수의 값을 입력할 수 있음
  • 실행해보면 SQL tuple에 상수값이 같이 나오지만 실행된 SQL이나 JPQL을 보면 상수가 보이질 않는데 그이유는 내부적으로 최적화가 적용되어 성능을 높이기 때문임
  • 만약 Querydsl쿼리에서 복잡한 상수작업이 필요하여 최적화를 못하게 되면 SQL쿼리에 상수를 직접 넘김
// 상수가 필요할 때
@Test
void constant() {
    List<Tuple> result = queryFactory
            .select(member.username, Expressions.constant("A"))
            .from(member)
            .fetch();

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

2) 문자 더하기

  • SQL의 concat()함수와 동일
  • 문자가 아닌 다른 타입은 .stringValue()를 통해 문자로 변환해줘야하는데 ENUM을 처리할 때 자주 사용함
// 문자 더하기, concat
@Test
public void concat() {

    // username_age 처럼 적용해보기
    List<String> result = queryFactory
            // age는 숫자기이 때문에 .stringValue()로 타입 캐스팅을 해줘야함(ENUM 처리할때 자주 씀)
            .select(member.username.concat("_").concat(member.age.stringValue()))
            .from(member)
            .fetch();

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