본문 바로가기
데이터 접근 기술

[Querydsl] 기본

by oncerun 2023. 2. 1.
반응형

공식문서를 읽기 전에 학습 테스트를 만들면서 간단하게 사용해 보자.

 

 

JPQL

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

}

 

Querydsl

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

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

    assert result != null;

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

 

* Querydsl 파라미터 바인딩 방식도 jpql과 동일하게 preparestatement와 같이 사용되어 jpql에 그러하듯 문자열을 합치는 방식보다는 더 보안적이다.

 

* JPAQueryFactory thread-safe 하다. 

아마 이는 주입받는 EntityManager가 thread-safe 해서 가능한 것으로 보인다. EntityManager는 멀티 스레딩 환경에서도 트랜잭션의 동기화 정보를 LocalThread에 저장하기 때문에 멀티 스레드 환경에서도 문제없이 동작한다. 

 

 

Q클래스 인스턴스 

 

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

 

혹은 QMeber을 import static을 통하여 코드 레벨에서 상단에 올리고 static 인스턴스에 접근하도록 코드를 작성하면 매우 가독성이 좋아진다.

import static gauza.querydsl.entity.QMember.member;

@Test
public void startQuerydsl() throws Exception {
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

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

    assert result != null;

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

 

Querydsl을 통해 작성된 jpql을 확인해 보자. 나는 Hibernate 통계를 사용하기 때문에 다음과 같은 로그가 출력된다.

2023-02-01 22:15:27.517 DEBUG 25720 --- [main] o.h.stat.internal.StatisticsImpl: HHH000117:
HQL: select member1 from Member member1 where member1.username = ?1, time: 3ms, rows: 1

 

조회 테스트를 작성해 보자.

@Test
public void search() throws Exception {
    Member result = queryFactory
            .selectFrom(member)
            .where(member.username.eq("member2")
                    .and(member.age.eq(20)))
            .fetchOne();

    assert result != null;

    //then
    assertThat(result.getUsername()).isEqualTo("member2");
    assertThat(result.getAge()).isEqualTo(20);
}

Querydsl은 다음과 같은 검색 조건을 제공합니다:

  • 동일한 값: member.username.eq("member1")
  • 다른 값: member.username.ne("member1")
  • NULL 값이 아님: member.username.isNotNull()
  • 특정 값 범위 내: member.age.in(10, 20)
  • 특정 값 범위 외: member.age.notIn(10, 20)
  • 특정 범위 내: member.age.between(10,30)
  • 같거나 큼: member.age.goe(30)
  • 더 큼: member.age.gt(30)
  • 같거나 작음: member.age.loe(30)
  • 더 작음: member.age.lt(30)
  • 문자열 포함: member.username.like("member%")
  • 문자열 포함: member.username.contains("member") => % member%
  • 문자열 시작: member.username.startsWith("member") => member%
  • 결과 정렬: member.username.asc()와 member.username.desc()
  • 결과 그룹핑: member.groupBy(member.username)
  • 결과 개수 제한: member.limit(10)
  • 조인: member.join(member.team, team)
  • 서브쿼리: member.username.in(JPAExpressions.select(member.username). from(member))

 

조건문을 작성할 때 and로 작성하는 방법도 있지만 다음과 같은 방법도 있다.

 

@Test
public void search() throws Exception {
    Member result = queryFactory
            .selectFrom(member)
            .where(
                    member.username.eq("member2"),
                    member.age.eq(20))
            .fetchOne();

    assert result != null;

    //then
    assertThat(result.getUsername()).isEqualTo("member2");
    assertThat(result.getAge()).isEqualTo(20);
}

 

이 방법의 장점은 조건의 null이 들어가는 경우 자연스레 null이 제거된다.

 

결과 조회

 

Querydsl은 결과를 조회할 때 다음과 같은 메서드를 제공합니다:

  • fetch(): 검색 조건에 맞는 여러 개의 결과를 조회합니다.
  • fetchOne(): 검색 조건에 맞는 한 개의 결과를 조회합니다. 만약 결과가 여러 개일 경우 예외가 발생합니다.
  • fetchFirst(): 검색 조건에 맞는 여러 개의 결과 중 첫 번째 결과를 조회합니다.
  • fetchResults(): 검색 조건에 맞는 결과의 개수와 결과 목록을 동시에 조회합니다.

 

* fetchResults()는 Deprecated가 되었다.

 그 이유는 Querydsl의 fetchResults 기능은 JPQL에서 count query를 계산하기 위해 subquery를 사용하지만, JPQL에서는 subquery로부터 결과를 가져올 수 없기 때문에 계산하기 어렵다

예를 들어서 fetchResult 기능은 SELECT COUNT(*) FROM (subquery)에 대한 구문이 필요한데 이를 JPQL에서 지원하지 않는 문법이다. 그렇기 때문에 subquery를 통해 카운트를 계산해서 가져와야 하는데, JPQL은 하위 쿼리를 기준으로 결과를 계산할 수가 없다. 

 

이를 해결하기 위해 기존 쿼리를 수정하여 계산을 시도했지만, 단순한 쿼리에만 적용되고, group by와 having이 포함된 복잡한 쿼리에서는 동작하지 않는다.

 

multiple group by elements, having 구문이 있는 쿼리는 COUNT(DISTINCT a, b, c)와 같은 JPQL에서 허용되지 않는 표현이 포함되기 때문입니다.

 

결국 이를 지원하기 위해 수정된 쿼리를 작성해야 했는데 이러한 복잡한 쿼리에서는 메모리에서 fetch()의 크기를 계산하여 fetchResults()의 결과로 반환하는 편이 안정적이지만. 이는 큰 결과 집합에서는 성능에 큰 제약이 있을 수 있습니다.

 

이러한 복잡한 쿼리에서 fetchResults 기능을 사용하려면 Blaze-Persistence 연동을 사용하는 것을 권장한다.

 

Blaze-Persistence에서는 JPQL에서 subquery를 사용할 수 있고, fetchResults 기능이 제대로 구현되어 있으므로 결과 개수를 정확하게 계산할 수 있다. 만약 count 계산이 필수가 아니라면 fetch() 함수를 사용하는 것이 좋다.

 

 

반응형

'데이터 접근 기술' 카테고리의 다른 글

Querydsl 기본(3)  (0) 2023.02.07
Querydsl 기본(2)  (0) 2023.02.06
QueryDSL 적용 방법 알아보기.  (0) 2023.02.06
Hibernate Best Practices  (0) 2023.02.01
[Querydsl] 시작  (0) 2023.02.01

댓글