본문 바로가기
JPA

[JPA] 객체지향 쿼리 언어(2) : QueryDSL

by 케로베로 2020. 10. 5.

  QueryDSL은 쿼리를 문자가 아닌 코드로 작성해도, 쉽고 간결하며 그 모양도 쿼리와 비슷하게 개발할 수 있는 프로젝트이다. QueryDSL은 오픈소스 프로젝트이며, 이름 그대로 쿼리 즉 데이터를 조회하는 데 기능이 특화되어 있다.

 

QueryDSL 설정

필요 라이브러리

<!--pom.xml 추가-->
<dependecy>
    <groupId>com.mysema.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
    <version>3.6.3</version>
<dependency>

<dependecy>
    <groupId>com.mysema.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
    <version>3.6.3</version>
    <scope>provided</scope>
<dependency>
  • querydsl-jpa: QueryDSL JPA 라이브러리
  • querydsl-apt: 쿼리 타입(Q)을 생성할 때 필요한 라이브러리

 

환경 설정

QueryDSL을 사용하려면 엔티티를 기반으로 쿼리 타입이라는 쿼리용 클래스를 생성해야 한다. 아래와 같이 쿼리 타입 생성용 플러그인을 pom.xml에 추가해야 한다.

<!--쿼리 타입 생성용 pom.xml 추가 -->
<build>
    <plugins>
        <plugin>
            <groupId>com.mysema.maven</groupId>
            <artifactId>apt-maven-plugin</artifactId>
            <version>1.1.3</version>
            <executions>
                <excution>
                    <goals>
                        <goal>process</goal>
                    <goals>
                    <configuration>
                        <outputDirectory>target/geneated-sources/java</outputDirectory>
                        <prosessor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</prosessor>
                    <configuration>
                </excution>
            </excutions>
        </plugin>
    </plugins>
</build>

콘솔에서 mvn compile을 입력하면 outputDirectory에 지정한 target/generated-sources 위치에 QMember.java처럼 Q로 시작하는 쿼리 타입들이 생성된다. 이제 target/generated-sources를 소스 경로에 추가하면 된다.

 

시작

// QueryDSL 시작
public void queryDSL() {
    EntityManager em = emf.createEntityManager();
    
    JPAQuery query = new JPAQuery(em);
    QMember qMember = new QMember("m");
    List<Member> members =
        query.from(qMember)
            .where(qMember.name.eq("회원1"))
            .orderBy(qMember.name.desc())
            .list(qMember);
}

QueryDSL을 사용하려면 우선 com.mysema.jpa.impl.JPAQuery 객체를 생성해야 하는데 이때 엔티티 매니저를 생성자에 넘겨준다. 다음으로 사용할 쿼리 타입(Q)을 생성하는데 생성자에는 별칭을 주면 된다. 이 별칭을 JPQL에서 별칭으로 사용한다. 그 다음에 나오는 from, where, orderBy, list는 코드만 보아도 쉽게 이해될 정도로 JPQL과 비슷하다.

 

기본 Q 생성

public class QMember extends EntityPathBase<Member> {
    public static final QMember member = new QMember("member1");
    ...
}

 

쿼리 타입(Q)은 사용하기 편리하도록 기본 인스턴스를 보관하고 있다. 하지만 같은 엔티티를 조인하거나 같은 엔티티를 서브쿼리에 사용하면 같은 별칭이 사용되므로 이때는 별칭을 직접 지정해서 사용해야 한다.

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

 

쿼리 타입의 기본 인스턴스를 사용하면 아래와 같이 import static을 활용해서 코드를 더 간결하게 작성할 수 있다.

import static jpabook.jpashop.domain.QMember.member; // 기본 인스턴스

public void basic() {
    EntityManager em = emf.createEntityManager();
    
    JPAQuery query = new JPAQuery(em);
    List<MEmber> members =
        query.from(member)
            .where(member.name.eq("회원1"))
            .orderBy(member.name.desc())
            .list(member);
}

 

검색 조건 쿼리

//QueryDSL 기본 쿼리 기능
JPAQuery query = new JPAQuery(em);
Qitem item = Qitem.item;
List<Item> List = query.from(item)
    .where(item.name.eq("좋은 상품").and(item.price.gt(20000)))
    .list(item);
    
//실행된 JPQL
select item
from Item item
wher item.name = ?1 and tem.price > ?2

QueryDSL의 where 절에는 and나 or을 사용할 수 있다. 또한 ','을 이용해 여러 검색조건을 사용해도 된다. 쿼리 타입의 필드는 필요한 대부분의 메소드를 명시적으로 제공한다. between, contains, startWith 등이 있다. 코드로 작성되어 있으므로 IDE가 제공하는 코드 자동 완성 기능의 도움을 받으면 필요한 메소드를 손쉽게 찾을 수 있다.

 

결과 조회

쿼리 작성이 끝나고 결과 조회 메소드를 호출하면 실제 데이터베이스를 조회한다. 보통 uniqueResult() 나 list()를 사용하고 파라미터로 프로젝션 대상을 넘겨준다. 결과 조회 API는 com.mysema.query.Projectable에 정의되어 있다.

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

 

페이징과 정렬

QItem item = Qitem.item;

query.from(item)
    .where(item.price.gt(20000))
    .orderBy(item.price.desc(), item.stockQuantity.asc())
    .offset(10).limit(20)
    .list(item);

 정렬은 orderBy를 사용하는데 쿼리 타입(Q)이 제공하는 asc(), desc()를 사용한다. 페이징은 offset과 limit을 적절히 조합해서 사용하면 된다. 페이징은 아래 예제와 같이 restrict() 메소드에 com.mysema.query.QueryModifiers를 파라미터로 사용해도 된다.

//QueryModifiers를 사용한 페이징과 정렬
QueryModifiers queryModifiers = new QueryModifiers(20L, 10L); // limit, offset
List<Item> list =
    query.from(item)
    .restrict(queryModifiers)
    .list(item);

 

실제 페이징 처리를 하려면 검색된 전체 데이터 수를 알아야 한다. 이 때는 list() 대신에 listResults()를 사용한다. listResults()를 사용하면 전체 데이터 조회를 위한 count 쿼리를 한 번 더 실행한다. 그리고 SearchResults를 반환하는데 이 객체에서 전체 데이터 수를 조회 할 수 있다.

// listResults()를 사용한 페이징과 정렬
SearchResult<Item> result =
    query.from(item)
        .where(item.price.gt(10000))
        .offset(10).limit(20)
        .listResults(item);
        
long total = rsult.getTotal(); // 검색된 전체 데이터 수
long limit = result.getLimit();
long offset = result.getOffset();
List<Item> results = result.getResults(); // 조회된 데이터

 

그룹

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

query.from(item)
    .groupBy(item.price)
    .having(item.price.gt(1000))
    .list(item);

 

조인

 조인은 innerJoin(join), leftJoin, rightJoin, fullJoin을 사용할 수 있고 추가로 JPQL의 on과 성능 최적화를 위한 fetch 조인도 사용할 수 있다. 조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭으로 사용할 쿼리 타입을 지정하면 된다.

// 기본 조인
QOrder order = QOrder.order;
QMEmber member = QMember.member;
QOrderItem orderItem = QOrderItem.orderItem;

query.from(order)
    .join(order.member, member)
    .leftJoin(order.orderItems, orderItem)
    .list(order);
    
// 조인 on 사용
query.from(order)
    .leftJoin(order.orderItems, orderITem)
    .on(orderITem.count.gt(2))
    .list(order);
    
// 페치 조인 사용
query.from(order)
    .innerJoin(order.member, member).fetch()
    .leftJoin(order,orderItems, orderItem).fetch()
    .list(order);
    
//from 절에 여러 조건을 사용하는 세타 조인
QOrder order = QOrder.order;
QMember member = QMember.member;

query.from(order, member)
    .where(order.memeber.eq(member))
    .list(order);

 

서브 쿼리

서브 쿼리는 com.mysema.query.jpa.JPASubQuery를 생성해서 사용한다. 서브쿼리의 결과가 하나면 unique(), 여러 건이면 list()를 사용할 수 있다.

//서브 쿼리 예제 - 한 건
QItem item = QItem.item;
QItem itemSub = new QItem("itemSub");

query.from(item)
    .where(item.price.eq(
        new JPASubQuery().from(itemSub).unique(itemSub.price.max())
    ))
    .list(item);
    
//서브 쿼리 예제 - 여러 건
QItem item = QItem.item;
QItem itemSub = new QItem("itemSub");

query.from(item)
    .where(item.in(
        new JPASubQuery().from(itemSub)
            .where(item.name.eq(itemSub.name))
            .list(itemSub)
    ))
    .list(item);

 

프로젝션과 결과 반환

select 절에 조회 대상을 지정하는 것을 프로젝션이라 한다.

 

프로젝션 대상이 하나

프로젝션 대상이 하나면 해당 타입으로 반환한다.

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

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

 

여러 컬럼 반환과 튜플

프로젝션 대상으로 여러 필드를 선택하면 QueryDSL은 기본으로 com.mysema.query.Tuple이라는 Map과 비슷한 내부 타입을 사용한다. 조회 결과는 tuple.get() 메소드에 조회한 쿼리 타입을 지정하면 된다.

QItem item = QItem.item;

List<Tuple> result = query.from(item).list(item.name, item.price);

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

 

빈 생성

쿼리 결과를 엔티티가 아닌 특정 객체로 받고 싶다면 빈 생성(Bean population) 기능을 사용한다. QueryDSL은 객체를 생성하는 다양한 방법을 제공한다.

  • 프로퍼티 접근
  • 필드 직접 접근
  • 생성자 사용

 원하는 방법을 지정하기 위해 com.mysema.query.types.Projections를 사용하면 된다.

//예제 itemDTO
public class ItemDTO {
    private String username;
    private int price;
    
    public ItemDTO() {}
    
    public ItemDTO(String username, int price) {
        this.username = username;
        this.price = price;
    }
    
    //Getter, Setter
    public String getUsername() {...}
    public void setUsername(String username) {...}
    public int getPrice() {...}
    public void setPrice(int price) {...}
}

// 프로퍼티 접근(Setter)
QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
    Projections.bean(ItemDTO.class, item.name.as("username"), item.price)); // as를 이용해 별칭사용
    
// 필드 직접 접근
QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
    Projections.fields(ItemDTO.class, item.name.as("username"), item.price));
    
// 생성자 사용
QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
    Projections.constructor(ItemDTO.class, item.name, item.price));

 

DISTINCT

query.distinct().from(item)...

 

수정, 삭제 배치 쿼리

QueryDSL도 수정, 삭제 같은 배치 쿼리를 지원한다. JPQL 배치 쿼리와 같이 영속성 컨텍스트를 무시하고 데이터베이스를 직접 쿼리한다는 점에 유의하자. 수정 배치 쿼리는 com.mysema.jpa.impl.JPAUpdateClause를 사용하며, 삭제 배치 쿼리는 com.mysema.query.jpa.impl.JPADeleteClause를 사용한다.

// 수정 배치 쿼리: 상품 가격 100원 증가시키기
QItem item = QItem.item;
JPAUpdateClause updateClause = new JPAUpdateClause(em, item);
long count = updateClause.where(item.name.eq("JPA 책"))
    .set(item.prce, item.price.add(100))
    .execute();

// 삭제 배치 쿼리: 이름이 같은 상품을 삭제시키기
QItem item = QItem.item;
JPADeleteClause deleteClause = new JPADeleteClause(em, item);
long count = deleteClause.where(item.name.eq("JPA 책"))
    .execute();

 

동적 쿼리

com.mysema.query.BooleanBuilder를 사용하면 특정 조건에 따른 동적 쿼리를 편리하게 생성할 수 있다.

// 동적 쿼리 예제 : 상품 이름과 가격 유무에 따라 동적으로 쿼리 생성
SearchParam param = new SearchParam();
param.setName("시골개발자");
param.setPrice(10000);

QItem item = QItem.item;

BooleanBuilder builder = new BooleanBuilder();
if (StringUtils.hasText(param.getName())) {
    builder.and(item.name.contains(param.getName()));
}
if (param.getPrice() != null) {
    builder.and(item.price.gt(param.getPrice()));
}
List<Item> result = query.from(item)
    .where(builder)
    .list(item);

 

메소드 위임

메소드 위임 기능을 사용하면 쿼리 타입에 검색 조건을 직접 정의할 수 있다. 메소드 위임 기능을 사용하려면 우선 정적 메소드를 만들고 @com.mysema.query.annotations.QueryDelegate 어노테이션에 속성으로 이 기능을 적용할 엔티티를 지정한다. 정적 메소드의 첫 번째 파라미터에는 대상 엔티티의 쿼리 타입(Q)을 지정하고 나머지는 필요한 파라미터를 정의한다.

//검색 조건 정의
public class ItemExpression {
    @QueryDelegate(Item.class)
    public static BooleanExpression isExpensive(QItem item, Integer price) {
        return item.price.gt(price);
    }
}

//쿼리 타입에 생성된 결과
public class QItem extends EntityPathBase<Item> {
    ...
    public com.mysema.query.types.expr.BooleanExpression
        isExpensive(Integer price) {
        return ItemExpression.isExpensive(this, price);
    }
}

//메소드 위임 기능 사용
query.from(item).where(item.isExpensive(30000)).list(item);

 

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

'JPA' 카테고리의 다른 글

[JPA] 객체지향 쿼리 언어(1) : JPQL  (0) 2020.09.30
[JPA] 값 타입  (0) 2020.09.23
[JPA] 프록시와 연관관계 관리  (0) 2020.09.22
[JPA] 고급 매핑  (0) 2020.09.15
[JPA] 연관관계 매핑  (0) 2020.09.10