BackEnd/JPA

[JPA] QueryDSL 기본문법 - 1

샤아이인 2022. 5. 13.

내가 공부한 것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼 겸 상세히 기록하고 얕은 부분들은 가볍게 포스팅하겠습니다.

 

1. JPQL vs Querydsl

이번시간에는 JPQL과 QueryDSL을 비교해보는 시간이다!

 

우선 다음과 같이 @BeforeEach를 통해 초기 데이터를 추가해주자.

@SpringBootTest
@Transactional
public class QueryDslBasicTest {

    @Autowired
    EntityManager em;

    JPAQueryFactory queryFactory;

    @BeforeEach
    public void before() {
        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", 10, teamB);
        Member member4 = new Member("member4", 20, teamB);

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

위와같이 JPAQueryFactory는 field 로 꺼내도 된다.

멀티 스레드 환경에 safe 하다. 애당도 생성자로 넘겨주는 EntityManager 자체가 Thread Safe하다.

왜냐하면 Spring은 여러 Thread에서 동시에 같은 EntityManager에 접근해도, 트랜잭션마다 별도의 컨텍스트를 제공하기 때문에 동시성 문제가 발생하지 않는다.

 

▶ JPQL 과 QueryDSL

@Test
public void startJPQL() {
    // member1 찾기
    String jpql = "select m from Member m where m.username = :username";
    Member findMember = em.createQuery(jpql, Member.class)
            .setParameter("username", "member1")
            .getSingleResult();
    assertThat(findMember.getUsername()).isEqualTo("member1");
}

@Test
public void startQueryDsl() {
    QMember m = new QMember("m"); // 별칭을 넘겨줘야 함

    Member findMember = queryFactory.select(m)
            .from(m)
            .where(m.username.eq("member1"))
            .fetchOne();
    assertThat(findMember.getUsername()).isEqualTo("member1");
}

 

  • EntityManager 를 JPAQueryFactory의 생성자에 넘기면서 생성
  • Querydsl은 JPQL 빌더 역할을 해준다.
  • JPQL: 문자(실행 시점 오류라 사용자가 해당 기능을 실행할때 오류가 터진다), Querydsl: 코드(컴파일 시점 오류)
  • JPQL: 파라미터 바인딩 직접, Querydsl: 파라미터 바인딩 자동 처리

 

2. 기본 Q-Type 활용

 Q 클래스 인스턴스를 생성하는 방법에는 2가지가 있다.

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

아니면 QMember를 static-import 해주면 다음과 같이 사용할수도 있다.

import static study.querydsl.entitiy.QMember.member;

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

(ps 만약 생성되는 SQL 뿐만 아니라, JPQL까지 확인하고 싶다면 application.yml에 다음과 같은 옵션을 추가해주자!)

spring:
  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        format_sql: true
        use_sql_comments: true #추가된 옵션

실행시 다음과 같은 JPQL이 보인다.

alias 로 member1이 나온것을 위 JPQL에서 확인이 가능하다.

이는 QMember를 만들때 다음과 같이 "member1"이라고 만들어졌기 때문이다.

따라서 다음과 같이 직접 QMember를 생성해서 사용하면 JPQL에는 별칭이 "m1"으로 나온다.

@Test
public void startQueryDsl() {
    QMember m1 = new QMember("m1"); // m1이라는 별칭으로 직접 생성

    Member findMember = queryFactory.select(m1)
            .from(m1)
            .where(m1.username.eq("member1"))
            .fetchOne();
    assertThat(findMember.getUsername()).isEqualTo("member1");
}

이와 같이 Q class 인스턴스를 직접 생성하는 방식은, 동일한 Table을 Join해야 할때 별칭을 다르게 주기 위해 사용된다.

 

3. 검색 조건 쿼리

3-1) 기본 검색 쿼리

기본 검색 쿼리 예시를 살펴보자. 다음과 같다.

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

- 검색조건을 or(), and() 등을 사용하여 메서드 체인으로 연결할 수 있습니다.

- select() 와 from() 에 동일한 인자가 전달되는 경우 selectFrom() 하나로 사용할 수 있습니다.

 

3-2) AND 조건을 메서드 체인 방식이 아닌, 파라미터로 처리할 수 있다.

위 코드는 where 절 안에 and() 조건을 메서드 체인으로 사용하고 있다.

and인 경우 다음과 같이 where 절 안에 ,(쉼표) 로 나누어서 전달하면 자동 and 조건으로 사용된다. 

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

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

이때 null을 넘기는 경우 자동 무시해 준다. -> 나중에 동적 쿼리 생성에 매우 중요한 내용이다.

 

실행되는 SQL은 다음과 같다.

select
    member0_.member_id as member_i1_1_,
    member0_.age as age2_1_,
    member0_.team_id as team_id4_1_,
    member0_.username as username3_1_ 
from
    member member0_ 
where
    member0_.username=? and member0_.age=?

 

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

 

4. 결과 조회

생성한 쿼리들의 결과를 반환하는 함수에 대하여 알아보자!

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

    //단 건
    Member findMember1 = queryFactory
            .selectFrom(member)
            .fetchOne();

    //처음 한 건 조회
    Member findMember2 = queryFactory
            .selectFrom(member)
            .fetchFirst(); // limit(1).fetchOne(); 과 동일

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

    results.getTotal();
    List<Member> contents = results.getResults();
    
    //count 쿼리로 변경
    long count = queryFactory
           .selectFrom(member)
           .fetchCount();
}

fetchResults()같은 경우 쿼리가 2번 나가게된다. select 하는 쿼리와 , count하는 쿼리 가 나가기 때문이다.

----------- count하는 쿼리 -----------
select
    count(member0_.member_id) as col_0_0_ 
from
    member member0_

----------- member 조회 -----------
select
    member0_.member_id as member_i1_1_,
    member0_.age as age2_1_,
    member0_.team_id as team_id4_1_,
    member0_.username as username3_1_ 
from
    member member0_

Querydsl의 fetchCount(), fetchResult() 는 개발자가 작성한 select 쿼리를 기반으로 count용 쿼리를 내부에서 만들어서 실행합니다.

그런데 이 기능은 select 구문을 단순히 count 처리하는 용도로 바꾸는 정도입니다.

따라서 단순한 쿼리에서는 잘 동작하지만, 복잡한 쿼리에서는 제대로 동작하지 않습니다.

 

Querydsl은 향후 fetchCount() , fetchResult() 를 지원하지 않기로 결정했습니다! Deprecated 되었습니다.

사용은 가능한데 권장하지는 않는다고 합니다!

 

따라서 Count 쿼리가 필요하다면 다음과 같이 직접 작성하기를 권장합니다.

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

count(*) 을 사용하고 싶으면 위의 주석으로 처리된 코드처럼 Wildcard.count 를 사용하시면 됩니다.

member.count() 를 사용하면 count(member.id) 로 처리됩니다.

또한 반환 결과는 숫자 하나이므로 fetchOne() 을 사용합니다.

'BackEnd > JPA' 카테고리의 다른 글

[JPA] QueryDSL 기본문법 - 3  (0) 2022.05.14
[JPA] QueryDSL 기본문법 - 2  (0) 2022.05.13
[JPA] 나머지 기능들  (0) 2022.05.07
[JPA] 스프링 데이터 JPA 분석  (0) 2022.05.06
[JPA] 확장 기능  (0) 2022.05.06

댓글