본문 바로가기
  • 실행력이 모든걸 결정한다
Spring 사전 준비/JPA Hibernate

[JPA] QueryDSL

by 김코더 김주역 2022. 8. 10.
반응형

1. QueryDSL 소개

- SQL, JPQL을 코드로 작성할 수 있도록 도와주는 오픈소스 빌더 API

- 컴파일 시점에 문법 오류를 발견할 수 있음

- IDE의 도움을 받아 코드 자동완성 기능을 이용할 수 있음

- 엔티티를 기반으로 생성한 쿼리 타입이라는 쿼리용 클래스를 사용한다.

 

 

2. 환경 설정

- pom.xml에 querydsl-jpa, querydsl-apt 라이브러리를 추가하자. querydsl-apt 라이브러리는 쿼리 타입을 생성할 때 필요하다.

<dependency>
	<groupId>com.mysema.querydsl</groupId>
	<artifactId>querydsl-jpa</artifactId>
	<version>3.6.3</version>
</dependency>

<dependency>
	<groupId>com.mysema.querydsl</groupId>
	<artifactId>querydsl-apt</artifactId>
	<version>3.6.3</version>
	<scope>provided</scope>
</dependency>

- pom.xml에 쿼리 타입 생성용 플러그인도 추가하자.

<build>
    <plugins>
        <plugin>
            <groupId>com.mysema.maven</groupId>
            <artifactId>apt-maven-plugin</artifactId>
            <version>1.1.3</version>
            <executions>
                <execution>
                    <goals>
                        <goal>process</goal>
                    </goals>
                    <configuration>
                        <outputDirectory>target/generated-sources/java</outputDirectory>
                        <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

- 콘솔에 mvn compile 명령을 입력하면 outputDirectory에 지정한 target/generated-sources 위치에 Q로 시작하는 쿼리 타입들이 생성된다. target/generated-source가 없다면 소스 경로에 추가해두자. 참고로, 이클립스의 LUNA 버전에서는 쿼리 타입 생성과 소스 경로 추가가 자동으로 이루어진다.

※ QueryDSL Q Class 생성이 안되는 경우 참고 : https://velog.io/@yulhee741/JPA-QueryDSL-Q-Class-%EC%83%9D%EC%84%B1-%EC%95%88%EB%90%A0-%EB%95%8C

 

[JPA] QueryDSL Q Class 생성 안될 때

pom.xml에 querydsl관련 의존성과, 플러그인을 설정했는데 Q Class가 생성이 되질 않았다.File - Project Structure - Modules 메뉴에 들어가서 target 폴더 아래의 generated-sources 폴더를 소스코드로 인식할 수

velog.io

 

 

3. 기본적인 동작 원리

- 문자로 직접 JPQL을 작성하는 방식과 QueryDSL을 사용하는 방식을 비교한 것이다. 

// JPQL 코드
select m from Member m where m.age > 14


// QueryDSL 사용
EntityManager em = emf.createEntityManager();
JPAQuery query = new JPAQuery(em); // com.mysema.query.jpa.imlp.JPAQuery
QMember qm = new QMember("m"); // 자동으로 생성되는 JPQL에서 사용될 별칭을 "m"으로 지정하면서 Member에 대한 쿼리 타입을 생성
// QMember qm = QMember.member; // 별칭을 지정하지 않고 기본 인스턴스를 사용하는 방법

List<Member> list = query.from(qm)
    	.where(qm.age.gt(14))
        .orderBy(qm.name.desc())
        .list(qm);

- 위 소스 코드에서 봤듯이, 쿼리 타입을 생성하는 방법으로는 별칭을 직접 지정하는 방법과 기본 인스턴스를 사용하는 방법이 있다. 만약, 같은 엔티티를 조인해야 하는 경우에는 별칭을 직접 지정해서 서로 다른 쿼리 타입을 생성해둬야 한다. 기본 인스턴스를 이용한다면 import static을 활용해도 좋다.

 

 

4. 검색 조건 쿼리

- where()에 작성한다.

- 내부에 and()나 or()을 사용할 수 있다.

- 자주 사용되는 메소드로는 eq(==), ne(!=), like, contains, lt(<), loe(<=), gt(>), goe(>=), between, max, min, avg, count, sum 등이 있다. 그 외의 다양한 메소드들은 아래에 링크한 공식문서에서 찾아볼 수 있다.

 

사용 예시

.where(qm.name.like("kim%").and(qm.age.gt(20))

- 아래와 같이 여러 검색 조건을 사용할 수도 있으며, 이 때는 and 연산이 이루어진다. 복잡한 조건을 사용하는 경우에 좋을 것이다.

.where(qm.name.like("kim%"), qm.age.gt(20))

 

 

5. 결과 조회

- 파라미터로 프로젝션 대상을 넘겨주면 결과를 조회할 수 있다.

  • uniqueResult() : 조회 결과가 한 건일 때 사용한다. 조회 결과가 없으면 null을 반환하고 결과가 하나 이상이면 com.mysema.query.NonUniqueResultException 예외가 발생한다.
  • list() : 결과가 하나 이상일 때 사용한다. 조회 결과가 없으면 빈 컬렉션을 반환한다.
  • singleResult() : 맨 처음 데이터만 반환한다.

사용 예시

.list(qm); // qm=QMember.member

 

 

6. 페이징과 정렬

- orderBy()에 작성한다.

- 정렬은 asc(), desc()을 사용한다.

- 페이징은 offset(), limit()을 조합해서 사용하거나, restrict() 메소드에 QueryModifiers를 파라미터로 넣어서 사용한다.

 

사용 예시

.orderBy(qitem.price.asc(), qitem.stockQuantity.desc())
.offset(20).limit(10) // offset : 시작 위치, limit : 개수
.list(qitem);

 

- 검색된 전체 데이터 수를 파악해서 페이징 처리를 하려면 list() 대신에 listResults()를 사용해서 SearchResults를 반환받으면 된다.

 

사용 예시

SearchResults<Item> result = query.from(qitem)
    .where(qitem.price.gt(20000))
    .offset(20).limit(10)
    .listResults(qitem);

long total = result.getTotal(); // 검색된 데이터 수
List<Item> results = result.getResults();

 

 

7. 그룹

- groupBy()에 작성한다.

- having()을 사용하여 그룹화된 결과에 조건을 걸 수 있다.

 

사용 예시

query.from(qitem)
    .groupBy(qitem.price)
    .having(qitem.price.gt(100000))
    .list(qitem);

 

 

8. 조인

- join(), innerJoin(), leftJoin(), rightJoin(), fullJoin()을 사용한다. join()은 innerJoin()이다.

- on()을 사용하여 조인 조건을 걸 수 있다.

- fetch()를 사용하여 fetch 조인을 할 수 있다.

 

예시 1) 기본 조인 방법

- join의 첫 번째 인자에는 조인 대상을 넣고, 두 번째 인자에는 별칭으로 사용할 쿼리 타입을 넣으면 된다.

QOrder qo = QOrder.order;
QMember qm = QMember.member;
QOrderItem qoi = QOrderItem.orderItem;

List<Order> list = query.from(qo)
    .join(qo.member, qm)
    .leftJoin(qo.orderItems, qoi)
    .list(qo);

 

예시 2) on()

.leftJoin(qo.orderItems, qoi)
.on(qoi.count.gt(2))
.list(qo);

 

예시 3) fetch()

.join(qo.member, qm).fetch()
.leftJoin(qo.orderItems, qoi).fetch()
.list(qo);

 

예시 4) 세타 조인

query.from(qo, qm)
    .where(qo.member.eq(qm))
    .list(qo);

 

 

9. 서브 쿼리

- com.mysema.query.jpa.JPASubQuery를 사용한다.

- 서브 쿼리의 결과가 하나면 unique(), 여러 개면 list()를 사용할 수 있다.

 

예시 1) 하나의 결과 

.where(qitem.price.eq(
    new JPASubQuery().from(qitemSub)
        ...
        .unique(qitemSub.price.max())
))

 

예시 2) 여러 개의 결과

.where(qitem.in(
    new JPASubQuery().from(qitemSub)
        ...
        .list(qitemSub)
))

 

 

 

10. 프로젝션

1) 프로젝션 대상이 하나인 경우

- 해당 타입으로 반환한다.

List<String> result = query.from(qitem).list(qitem.name);

 

 

2) 프로젝션 대상이 둘 이상인 경우

- com.mysema.query.Tuple 타입을 사용한다.

List<Tuple> result = query.from(qitem).list(qitem.name, qitem.price); // list() 메소드에 2개의 프로젝션 대상이 지정됐다.
for(Tuple tuple : result) {
    String name = tuple.get(qitem.name);
    int price = tuple.get(qitem.price);
}

 

 

3) DTO로 받기

- 쿼리 결과를 엔티티가 아닌 특정 객체로 받을 수 있다.

- 엔티티의 필드명과 DTO의 필드명이 다르다면 as() 메소드를 이용해서 DTO의 필드명으로 맞춰주면 된다.

 

사용 예시

- ItemDTO에 String username, int price 필드가 존재하고, getter, setter와 생성자가 모두 만들어져 있다고 가정하자. 

 

(1) 프로퍼티(setter) 접근 방식

- Projections.bean() 메소드를 사용한다.

List<ItemDTO> result = query.from(qitem)
    .list(
        Projection.bean(ItemDTO.class, qitem.name.as("username"), qitem.price)
    );

 

(2) 필드 직접 접근 방식

- Projections.fields() 메소드를 사용한다.

- 필드의 접근자를 private로 지정해도 동작한다.

List<ItemDTO> result = query.from(qitem)
    .list(
        Projection.fields(ItemDTO.class, qitem.name.as("username"), qitem.price)
    );

 

(3) 생성자 사용 방식

- Projections.constructor() 메소드를 사용한다.

- 엔티티의 필드명과 DTO의 필드명이 다르더라도 as() 메소드를 사용하지 않아도 된다. 파라미터의 순서만 같으면 된다.

List<ItemDTO> result = query.from(qitem)
    .list(
        Projection.constructor(ItemDTO.class, qitem.name, qitem.price)
    );

 

 

4) DISTINCT

- 다음과 같이 사용한다.

query.distinct().from(qitem)...

 

 

 

11. 배치 쿼리

- QueryDSL은 여러 연산을 일괄적으로 처리하는 배치 쿼리를 지원한다.

- 영속성 컨텍스트를 무시하고 DB에 직접 쿼리한다는 점에 유의하자.

- 영향 받은 튜플의 수가 반환된다.

 

1) 수정 배치 쿼리

- com.mysema.query.jpa.impl.JPAUpdateClause를 사용한다.

JPAUpdateClause updateClause = new JPAUpdateClause(em, qitem);
long count = updateClause.where(qitem.name.eq("자바 ORM 표준 JPA 프로그래밍"))
    .set(qitem.price, qitem.price.add(1000)) // Item의 price를 1000 증가
    .execute();

 

2) 삭제 배치 쿼리

- com.mysema.query.jpa.impl.JPADeleteClause를 사용한다.

JPADeleteClause deleteClause = new JPADeleteClause(em, qitem);
long count = deleteClause.where(qitem.name.eq("자바 ORM 표준 JPA 프로그래밍"))
    .execute();

 

 

12. 동적 쿼리

- com.mysema.query.BooleanBuilder는 where()에 들어가는 조건을 동적으로 담는 빌더 객체다.

BooleanBuilder builder = new BooleanBuilder();
builder.and(qitem.name.contains("우유"));
builder.and(qitem.price.gt(2500));

List<Item> result = query.from(qitem)
    .where(builder)
    .list(qitem);

 

 

13. 메소드 위임

1) 메소드 위임 기능이란?

- 자주 사용하는 검색 조건이 있다면 쿼리 타입에 검색 조건을 직접 추가할 수 있다.

 

2) 검색 조건 정의

- @com.mysema.query.annotations.QueryDelegate 어노테이션을 이용한다.

- @QueryDelegate의 속성에 엔티티 클래스를 지정한다.

- 정적 메소드의 첫 번째 파라미터에는 대상 엔티티의 쿼리 타입 객체를 지정하고, 두 번째 파라미터부터는 필요한 파라미터를 마음껏 지정하면 된다.

public class ItemExpression {

    @QueryDelegate(Item.class)
    public static BooleanExpression isExpensive(QItem qitem, Integer price) {
        return qitem.price.gt(price);
    }
}

위와 같이 지정해두면 쿼리 타입에 추가한 조건이 반영되어 있을 것이다.

- String, Date와 같은 자바 기본 내장 타입에도 메소드 위임 기능을 사용할 수 있다.

 

3) 메소드 위임 기능 사용

- 쿼리 타입의 메소드를 그대로 사용하면 된다.

List<Item> result = query.from(qitem)
    .where(qitem.isExpensive(2400))
    .list(qitem);

 

 

* QueryDSL 레퍼런스 문서

https://querydsl.com/static/querydsl/4.0.1/reference/ko-KR/html_single/

 

Querydsl - 레퍼런스 문서

본 절에서는 SQL 모듈의 쿼라 타입 생성과 쿼리 기능을 설명한다. com.querydsl.sql.Configuration 클래스를 이용해서 설정하며, Configuration 클래스는 생성자 인자로 Querydsl SQL Dialect를 취한다. 예를 들어, H2

querydsl.com

 

● 참고자료 : 자바 ORM 표준 JPA 프로그래밍

 

반응형

'Spring 사전 준비 > JPA Hibernate' 카테고리의 다른 글

[JPA] 스토어드 프로시저  (0) 2022.08.14
[JPA] 네이티브 SQL  (0) 2022.08.11
[JPA] JPQL의 작성  (0) 2022.08.04
[JPA] 값 타입 컬렉션  (0) 2022.08.03
[JPA] 복합 값 타입과 불변 객체  (0) 2022.08.03

댓글