예제 도메인 모델은 Spring Data JPA에서 사용한 모델링이랑 같다.

 

 

JPQL vs Querydsl


    @Test
    public void startJPQL() throws Exception {
        Member findByJPQL = em.createQuery("select m from Member m where m.username = :username", Member.class)
                .setParameter("username", "member1")
                .getSingleResult();

        Assertions.assertThat(findByJPQL.getUsername()).isEqualTo("member1");

    }

    @Test
    public void startQuerydsl() {
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        QMember m = QMember.member;

        Member findByQuerydsl = queryFactory.select(m)
                .from(m)
                .where(m.username.eq("member1"))
                .fetchOne();

        Assertions.assertThat(findByQuerydsl.getUsername()).isEqualTo("member1");
    }

JPQL과 Querydsl 테스트하는 코드의 차이이다.

 

 

Querydsl 장점

  • 컴파일시점에 오류를 잡아낸다. (JPQL은 메소드가 실행될 때 오류(Runtime Error)를 발견)
  • 자바 코드로 쿼리를 작성할 수 있다. 
  • 파라미터 바인딩을 자동으로 해결해 준다.
  • Code Assistant도 잘되어있다.

 

 

기본 Q-Type 활용


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

        QMember m = new QMember("m");
        QMember m = QMember.member;

참고로 QMember.member은 QMember객체에 만들어놓은 객체이다.

@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QMember extends EntityPathBase<Member> {
    ...
    public static final QMember member = new QMember("member1");
}

 

코드를 더 심플하게 하는 방법

import static study.querydsl.entity.QMember.*;

위와 같이 import를 해주면 아래와 같이 코드를 만들 수 있다.

        JPAQueryFactory queryFactory = new JPAQueryFactory(em);

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

 

Querydsl 쿼리문 보는법

application.yml에 아래와 같이 옵션을 추가해주어야한다.

spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/querydsl
    username: sa
    password:
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        #show_sql: true
        format_sql: true
        use_sql_comments: true #querydsl 쿼리문 보는 옵션

 

 

 

검색 조건 쿼리


기본 검색 쿼리

    @Test
    public void search() {
        Member findMember = queryFactory
                .selectFrom(member)
                .where(member.username.eq("member1")
                        .and(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%’ 검색

AND 조건을 파라미터로 처리

    @Test
    public void searchAndParam() {
        List<Member> result1 = queryFactory
                .selectFrom(member)
                .where(member.username.eq("member1"),
                        member.age.eq(10))
                .fetch();
        assertThat(result1.size()).isEqualTo(1);
    }

where()에 파라미터로 검색 조건을 추가하면 AND조건이 추가된다.

 

 

 

결과 조회


  • fetch()
    • list 조회, 데이터 없으면 빈 리스트 반환
  • fetchOne()
    • 단건 조회
    • 결과가 없으면 Null
    • 결과가 둘 이상이면 : NonUniqueResultException
  • fetchFirst()
    • limit(1).fetchOne()
  • fetchResults()
    • 페이징 정보 포함, total count 쿼리 추가 발생
  • fetchCount()
    • count 쿼리로 변경해서 count 수 조회
//List
List<Member> fetch = queryFactory
          .selectFrom(member)
          .fetch();

//단 건
Member findMember1 = queryFactory
          .selectFrom(member)
          .fetchOne();
          
//처음 한 건 조회
Member findMember2 = queryFactory
          .selectFrom(member)
          .fetchFirst();
          
//페이징에서 사용
QueryResults<Member> results = queryFactory
          .selectFrom(member)
          .fetchResults();
          
//count 쿼리로 변경
long count = queryFactory
          .selectFrom(member)
          .fetchCount();

 

 

 

정렬


   /**
     * 회원 정렬 순서
     * 1. 회원 나이 내림차순(desc)
     * 2. 회원 이름 올림차순(asc)
     * 단, 2에서 회원 이름이 없으면 마지막에 출력(nulls last)
     */
    @Test
    public void sort() throws Exception {
        //given
        em.persist(new Member(null, 100));
        em.persist(new Member("member5", 100));
        em.persist(new Member("member6", 100));

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

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

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

위와 같이 orderBy안에 파라미터로 정렬 할 수 있다.

 

 

페이징


조회 건수 제한

    @Test
    public void paging(){
        List<Member> result = queryFactory
                .selectFrom(member)
                .orderBy(member.username.desc())
                .offset(1)      // 0부터 시작
                .limit(2)       // 최대 2건 조회
                .fetch();

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

전체 조회 수가 필요하다면??

fetch() 대신 fetchResults()로 바꿔주면 된다.
주의
count 쿼리가 실행되니 성능상 주의해야 한다.
성능이 나오지 않는다면, count 쿼리를 분리해야 한다.

 

 

 

집합


집합 함수

    @Test
    public 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은 querydsl이 제공하는 값
        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);
    }

위와 같이 java 코드처럼 사용가능하다.

 

GroupBy

    @Test
    public void group() throws Exception {
        //given
        List<Tuple> result = queryFactory
                .select(team.name, member.age.avg())
                .from(member)
                .join(member.team, team)
                .groupBy(team.name)     // 팀 이름으로 그룹
                .fetch();

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

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

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

 

GroupBy, having

                .groupBy(team.name)     // 팀 이름으로 그룹
                .having(team.name.eq("teamB"))

위처럼 "teamB"와 같은 team만 가져올 수 있습니다.

 

 

 

조인 - 기본 조인


    /**
     * 팀 A에 소속된 모든 회원
     */
    @Test
    public void join() throws Exception {
        List<Member> result = queryFactory
                .selectFrom(member)
                .join(member.team, team)
                .where(team.name.eq("teamA"))
                .fetch();

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

위의 방식으로 join을 사용할 수 있다. leftjoin, rightjoin 모두 사용 가능하다.

연관관계가 없는 경우 from 절에 모든 데이터를 가져온 뒤, where 문으로 theta join할 수 있다.

 

 

조인 - on절


on절을 활용한 조인

  1. 조인 대상 필터링
  2. 연관관계 없는 엔티티 외부 조인

 

조인 대상 필터링

    /**
     * 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
     * jpql : select m, t from Member m left join m.team t on t.name = "teamA"
     */
    @Test
    public void join_on_filtering() throws Exception {
        List<Tuple> result = queryFactory
                .select(member, team)
                .from(member)
                .join(member.team, team)
                .on(team.name.eq("teamA"))
                .fetch();

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

 

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

    /**
     * 연관관계가 없는 엔티티 외부 조인
     * 회원의 이름이 팀 이름과 같은 대상 외부 조인
     * jpql : select m, t from Member m LEFT JOIN Team t on m.username = t.name
     */
    @Test
    public void join_on_no_relation() throws Exception {
        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))
                .fetch();

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

주의해야 할 점은 leftjoin() 부분에 일반 조인과 다르게 엔티티가 하나만 들어갔다.

  • 일반 조인 : leftJoin(member.team, team)
  • on 조인 : from(member).leftJoin(team).on(~~~)

위의 방식으로 조인 문법이 다르다.

 

 

 

조인 - 페치 조인


    @Test
    public void fetchjoinUse() throws Exception {
        em.flush();
        em.clear();

        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().fetchJoin() 을사용하면 fetch join이 된다.

 

 

 

서브 쿼리


com.querydsl.jpa.JPAExpressions 사용

 

서브 쿼리 eq 사용

    /**
     * 나이가 가장 많은 회원 조회
     */
    @Test
    public 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();

        assertThat(result.get(0).getAge()).isEqualTo(40);
    }

같은 QType을 쓰면안되기때문에, 서브쿼리에 넣을 QType은 새롭게 만들어서 넣어줘야 한다.

 

서브 쿼리 goe 사용

    /**
     * 나이가 평균 이상인 회원
     */
    @Test
    public 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.size()).isEqualTo(2);
    }

goe는 >= 라고 보면된다.

 

서브 쿼리 In 사용

    /**
     * 나이가 10 초과인 회원
     */
    @Test
    public 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))
                ))
                .fetch();

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

 

select 절에 subquery

    /**
     * 나이가 10 초과인 회원
     */
    @Test
    public void selectSubQuery() {
        QMember memberSub = new QMember("memberSub");

        List<Tuple> result = queryFactory
                .select(member.username,
                        JPAExpressions
                                .select(memberSub.age.avg())
                                .from(memberSub))
                .from(member)
                .fetch();

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

 

from 절의 서브쿼리 한계

JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다. 

 

from 절의 서브쿼리 해결방안

  • 서쿼리를 Join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다고 한다.)
  • 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
  • nativeSQL을 사용한다.

from절의 서브 쿼리를 줄이기 위한 책을 추천해주었다. SQL AntiPattern 나중에 읽을 기회가 된다면 사서 봐보자.

 

 

 

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

    @Test
    public void complexCase() {
        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);
        }
    }

간단한 case문 예제이다.

 

orderyBy에서 Case문 함께 사용하기

 

예를들어 아래와 같은 임의의 순서로 회원을 출력하고 싶다면?

  1. 0~30살이 아닌 회원을 가장 먼저 출력
  2. 0~20살 회원 출력
  3. 21~30살 회원 출력
    @Test
    public void addCase() {
        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); 
        }
    }

Querydsl은 자바 코드로 작성하기 때문에 rankPath처럼 복잡한 조건을 변수로 선언해서 select절, orderBy절에서 사용할 수 있다.

 

 

 

상수, 문자 더하기


상수 더하기

    /**
     * 상수 더하기
     */
    @Test
    public void constant() {
        List<Tuple> result = queryFactory
                .select(member.username, Expressions.constant("A"))
                .from(member)
                .fetch();

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

Expressions.constant()를 사용하면 된다.

 

문자 더하기

    /**
     * 문자 더하기
     */
    @Test
    public void concat() {
        //{username}_{age}
        List<String> result = queryFactory
                .select(member.username.concat("_").concat(member.age.stringValue()))
                .from(member)
                .where(member.username.eq("member1"))
                .fetch();

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

가져온 값을 .stringValue()를 사용하며 string 변환하면 더할 수 있다.

 

 

 

 

 

 

 

 

출처

 

실전! Querydsl - 인프런 | 강의

Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!, - 강의 소개 | 인프런...

www.inflearn.com

 

+ Recent posts