Querydsl 기본 문법

Jaca·2021년 9월 20일
0

이제까지 사용했던 JPQL를 Querydsl로 바꿔보자.

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

        assertThat(findMember.getUsername()).isEqualTo("member1");
    }
    @Test
    public void startQuerydsl() {
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        
        Member findMember = queryFactory
                .select(member)
                .from(member)
                .where(member.username.eq("member1"))
                .fetchOne();

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

두 메서드를 살펴보면 Querydsl의 장점이 눈에 들어온다.
척봐도 JPQL의 query는 String이라 보기만해도 끔찍한데 비해
Querydsl은 select, from, where 각 절이 자바 코드로 나누어져있어 보기만해도 마음이 편안하다..

Querydsl의 쿼리내에 사용된 member 변수는 JPQL에서 사용하던 Member 엔티티의 객체가 아니다.
Querydsl은 쿼리 내에 Query type을 사용한다.
Querydsl을 설정하면 각 엔티티별로 Qclass가 생성되는데,
이를 사용한다.

위와 같이 엔티티별로 각각의 Qclass가 생성되고
static final로 기본 인스턴스인 member가 선언되있음을 볼 수 있다.
Querydsl의 쿼리에는 저 인스턴스를 사용하며, 별도의 인스턴스를 생성할 수도 있다.

QMember qMember = new QMember("m");     //직접 별칭 지정
QMember qMember = QMember.member;       //기본 인스턴스

위 메서드에서는 QMember를 Static import 하여, member만 사용할 수 있는 것.

기본 검색 조건

    @Test
    public void search() {
        Member find = queryFactory
                .selectFrom(member)
                .where(member.username.eq("member1")
                        .and(member.age.eq(10)))
                .fetchOne();
               
    }

검색 조건을 .and(), .or() 을 통해 계속 연쇄적으로 걸어줄 수 있다.
select와 from을 하나로 묶어 .selectFrom() 메서드로 사용할 수 있다.

또한 where절을 메서드를 걸지않고, 파라미터로 넘겨주면 자동으로 and 연산으로 추가된다.

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

이 기능의 장점은 위와 같이 null이 파라미터로 들어간다면 자동으로 무시한다. 이 점을 이용해 동적 쿼리를 다룰수 있게 된다.

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

결과 조회

JPQL의 .getSingleResult().getResultList() 처럼 Querydsl도 여러가지 결과 반환 메서드가 있다.

  • fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
  • fetchOne() : 단 건 조회 결과가 없으면 : null,
    결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException
  • fetchFirst() : limit(1).fetchOne()
  • fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행
  • fetchCount() : count 쿼리로 변경해서 count 수 조회

정렬

    @Test
    public 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();

    }

desc() , asc() : 일반 정렬
nullsLast() , nullsFirst() : null 데이터 순서 부여

페이징

    @Test
    public void paging2() {
        QueryResults<Member> re = queryFactory
                .selectFrom(member)
                .orderBy(member.username.desc())
                .offset(1)
                .limit(2)
                .fetchResults();

        assertThat(re.getTotal()).isEqualTo(4);
        assertThat(re.getOffset()).isEqualTo(1);
        assertThat(re.getLimit()).isEqualTo(2);
        assertThat(re.getResults().size()).isEqualTo(2);
    }

문법은 크게 차이 나지않으며, .fetchResults() 가 아니어도 페이징이 가능하다. Result로 결과를 반환시
페이징에 대한 각종 정보를 제공하며 Count 정보도 제공한다.

당연히 Count 쿼리에 대한 유의가 필요하다.

실무에서 페이징 쿼리를 작성할 때, 데이터를 조회하는 쿼리는 여러 테이블을 조인해야 하지만, count 쿼리는 조인이 필요 없는 경우도 있다. 그런데 이렇게 자동화된 count 쿼리는 원본 쿼리와 같이 모두 조인을 해버리기 때문에 성능이 안나올 수 있다. count 쿼리에 조인이 필요없는 성능 최적화가 필요하다면, 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이 제공하는 모든 집합 함수를 제공한다.

@Test
  public void group() throws Exception {
      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);
      assertThat(teamB.get(team.name)).isEqualTo("teamB");
      assertThat(teamB.get(member.age.avg())).isEqualTo(35);
  }

groupBy , 그룹화된 결과를 제한하려면 having 절을 사용하면 된다.

조인

@Test
  public void join() throws Exception {
      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")
              .containsExactly("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"));
      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 절을 통해서만 외부 조인이 가능하다.

Join의 경우 on절을 통해 필터링을 걸어줄 수 있다.

/**
* 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
* JPQL: SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'teamA'
* SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name='teamA'
     */
    @Test
    public void join_on_filtering() throws Exception {
        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);
	} 
    }
    
결과 :
    t=[Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
    t=[Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]
    t=[Member(id=5, username=member3, age=30), null]
    t=[Member(id=6, username=member4, age=40), null]

여기서 살펴봐야 할 점은 select에서 member와 team을 가져왔기 때문에, t에 들어온 team은 member의 속성인 team이 아닌, 조건이 맞는 team의 정보를 가져온 것이다.

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

또, where와 on을 구분해야 한다.

  • ON : JOIN 을 하기 전 필터링을 한다 (=ON 조건으로 필터링이 된 레코들간 JOIN이 이뤄진다)
  • WHERE : JOIN 을 한 후 필터링을 한다 (=JOIN을 한 결과에서 WHERE 조건절로 필터링이 이뤄진다)
 /**
*2. 연관관계 없는 엔티티 외부 조인
*예)회원의 이름과 팀의 이름이 같은 대상 외부 조인
* 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
  public void join_on_no_relation() throws Exception {
      em.persist(new Member("teamA"));
      em.persist(new Member("teamB"));
      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("t=" + tuple);
      } 
  }

하이버네이트 5.1부터 on 을 사용해서 서로 관계가 없는 필드로 외부 조인하는 기능이 추가되었다.
물론 내부 조인도 가능하다.
leftJoin() 부분에 일반 조인과 다르게 엔티티 하나만 들어간다.
일반조인: leftJoin(member.team, team)
on조인: from(member).leftJoin(team).on(xxx)

페치 조인

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

EntituManagerFactory의 .getPersistenceUnitUtil() 를 통해 현재 객체가 로딩되어 있는지 확인할 수 있다.
.fetchJoin() 메서드 하나로 페치 조인은 간단하게 해결할 수 있다.

서브 쿼리

/**
*나이가 평균 나이 이상인 회원
*/
  @Test
  public void subQueryGoe() throws Exception {
      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);
}

서브 쿼리는 JPAExpressions 을 통해 표현해야 하며, 서브쿼리에 사용되는 Qclass는 겉의 쿼리와 다른 객체여야 하므로, Qclass를 직접 선언해서 다른 인스턴스를 사용해야 한다.

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

from 절의 서브쿼리 해결하려면,
1. 서브쿼리를 join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다.)
2. 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
3. nativeSQL을 사용한다.

case 문

case문은 select, 조건절(where), order by에서 사용 가능하다.

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 처럼 복잡한 조건을 CaseBuilder 변수로 선언해서 select 절, orderBy 절에서 함께 사용할 수 있다.

상수, 문자 더하기

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

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

profile
I am me

0개의 댓글