QueryDSL
- QueryDSL은 Criteria처럼 JPQL 빌더 역할을 하는데 복잡하고 어려운 JPA Criteria를 대체할 수 있음
- QueryDSL은 오픈소스 프로젝트로 처음에는 HQL(하이버네이트 쿼리언언)을 코드로 작성할 수 있는 프로젝트로 시작해서
지금은 다양하게 지원
- 이름 그대로 쿼리 즉 데이터를 조회하는데 기능이 특화
QueryDSL 설정
// pom.xml 추가
// 3.6.3 버전
<dependency>
<groupId>com.mysema.querdsl</groupId>
<artifactId>querydsl-jpa</artifactId> // QueryDSL JPA 라이브러리
<version>3.6.3</version>
</dependency>
<dependency>
<groupId>com.mysema.querdsl</groupId>
<artifactId>querydsl-apt</artifactId> // 쿼리 타입(Q)을 생성할 때 필요한 라이브러리
<version>3.6.3</version>
<scope>provided</scope>
</dependency>
- 환경 설정
QueryDSL을 사용하려면 Criteria의 메타 모델처럼 엔티티를 기반으로 쿼리 타입이라는 쿼리용 클래스를 생성해야 함
이를 위해 쿼리 타입 생성용 플러그인을 pom.xml에 추가
// 쿼리 타입 생성용 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>
</goal>
<configuration>
<outputDirectory>target/generated-sources/java
</outputDirectory>
<processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</process>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
// 콘솔에서 mvn compile을 입력하면 outputDirectory에 지정한 위치에 Q로 시작하는 쿼리 타입들이 생성
// 예) QMember.java
시작
// QueryDSL 시작
public void queryDSL() {
EntityManager em = emf.createEntityManager();
// com.mysema.query.jpa.impl.JPAQuery 객체를 생성하기 위해 엔티티 매니저를 생성자에게 넘겨 줌
JPAQuery query = new JPAQuery(em);
// 사용할 쿼리 타입(Q)을 생성하기 위해 생성자에 별칭을 줌
QMember qMbember = new QMember("m"); // 생성되는 JPQL의 별칭이 m
List<Member> members =
query.from(qMember)
.where(qMember.name.eq("회원1"))
.orderBy(qMember.name.desc())
.list(qMember);
}
- 기본 Q 생성
쿼리 타입(Q)은 사용하기 편리하도록 기본 인스턴스를 보관하고 있음
하지만 엔티티를 조인하거나 같은 엔티티를 서브 쿼리에 사용하면 같은 별칭이 사용되므로 이때는 별칭을 직접 지정해서 사용
// Member 쿼리 타입
public class QMember extends EntityPathBase<Member> {
public static final QMember member = new QMember("member1");
...
}
// 쿼리 타입 사용
QMember qMember = new QMember("m"); // 직접 지정
QMember qMember = QMember.member; // 기본 인스턴스 사용
// 쿼리 타입의 기본 인스턴스를 사용하면 import static을 활용해서 코드를 더 간결하게 작성 가능
import static jpabook.jpashop.domain.QMember.member; // 기본 인스턴스
public void basic() {
EntityMananger 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)
/* QueryDSL의 where절에는 and나 or을 사용할 수 있음
그 외에도 쿼리 타입의 필드는 필요한 대부분의 메소드를 명시적으로 제공
item.price.between(10000, 20000); // 가격이 10000원 ~ 20000원 상품
item.name.contains("상품1"); // 상품1이라는 이름을 포함한 상품
// SQL에서 like '%상품%' 검색
item.name.startsWith("고급"); // 이름이 고급으로 시작하는 상품
// SQL에서 like '고급%' 검색 */
.where(item.name.eq("좋은상품").and(item.price.gt(20000)))
/* 또는 검색 조건을 사용해도 위와 동일
.where(item.name.eq("좋은상품"), item.price.gt(20000)) */
.list(item); // 조회할 프로젝션 지정
// 실행된 JPQL
select item
from Item item
where item.name = ?1 and item.price > ?2
결과 조회
- 쿼리 작성이 끝나고 결과 조회 메소드를 호출하면 실제 데이터베이스를 조회
- 결과 조회 메소드는 파라미터로 프로젝션 대상을 넘겨주며
결과 조회 API는 com.mysema.query.Projectable에 정의되어 있음
- 대표적인 결과 조회 메소드
- uniqueResult() : 조회 결과가 한 건일 때 사용하며 조회 결과가 없으면 null을 반환하고 결과가 하나 이상이면 예외 발생
- singResult() : uniqueResult()와 같지만 결과가 하나 이상이면 처음 데이터를 반환
- list() : 결과가 하나 이상일 때 사용하며 결과가 없으면 빈 컬렉션을 반환
페이징과 정렬
- 정렬은 orderBy를 사용하는데 쿼리 타입(Q)이 제공하는 asc(), desc()를 사용
- 페이징은 offset과 limit을 적절히 조합해서 사용하거나
restrict() 메소드에 com.mysema.query.QueryModifiers를 파라미터로 사용
// 페이징과 정렬
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);
// 페이징과 정렬 QueryModifers 사용
QueryModifiers queryModifiers = new QueryModifiers(20L, 10L); // limit, offset
List<Item> list =
query.from(item)
.restrict(queryModifiers)
.list(item);
// 페이징과 정렬 listResults() 사용
// 실제 페이징 처리를 하려면 검색된 전체 데이터 수를 알아야 하므로 list() 대신 listResults()를 사용
SearchResults<Item> result =
query.from(item)
.where(item.price.gt(10000))
.offset(10).limit(20)
.listResults(item);
// listResults()를 사용하면 전체 데이터 조회를 위한 count 쿼리를 한 번 더 실행한 후
// SearchResults를 반환하는데 이 객체에서 전체 데이터 수를 조회할 수 있음
long total = result.getTotal(); // 검색된 전체 데이터 수
long limit = result.getLimit();
long offset = result.getOffset();
List<Item> results = result.getResults(); // 조회된 데이터
그룹
- groupBy를 사용하고 그룹화된 결과를 제한하려면 having을 사용
// groupBy() 사용
query.from(item)
.groupBy(item.price)
.having(item.price.gt(1000))
.list(item);
조인
- innerJoin(join), leftJoin, rightJoin, fullJoin을 사용할 수 있고
추가로 JPQL의 on과 성능 최적화를 위한 fetch 조인도 사용할 수 있음
- 조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭으로 사용할 쿼리 타입을 지정하면 됨
join(조인 대상, 별칭으로 사용할 쿼리 타입)
// 기본 조인
QOrder order = QOrder.order;
QMember member = QMember.member;
QOrderItem orderItem = QOrderItem.orderItem;
query.from(order)
.join(order.member, member)
.leftJoin(order.orderItmes, 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 절에 여러 조인을 사용하는 세타 조인
// from 절에 여러 조건 사용
QOrder order = QOrder.order;
QMember member = QMember.member;
query.from(order, member)
.where(order.member.eq(member))
.list(order);
서브 쿼리
- com.mysema.query.jpa.JPASubQuery를 생성해서 사용
- 서브 쿼리의 결과가 하나일 때는 unique() 사용
// 서브 쿼리 예제 - 한 건
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);
- 서브 쿼리의 결과가 여러 건일 때는 list() 사용
// 서브 쿼리 예제 - 여러 건
QItem item = QItem.item;
QItem itemSub = new QItem("itemSub");
query.from(item)
.where(item.price.eq(
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);
}
- 여러 컬럼 반환과 튜플
프로젝션 대상으로 여러 필드를 선택하면 com.mysema.query.Tuple이라는 Map과 비슷한 내부 타입을 사용
조회 결과는 tuple.get() 메소드에 조회한 쿼리 타입을 지정
// 튜플 사용 예제
QItem item = QItem.item;
List<Tuple> result = query.from(item).list(item.name, item.price);
// List<Tuple> reuslt = query.from(item).list(new QTuple(item.name, item.price));와 같음
for (Tuple tuple : result) {
System.out.println("name = " + tuple.get(item.name));
System.out.println("price = " + tuple.get(item.price));
- 빈 생성
쿼리 결과를 엔티티가 아닌 특정 객체로 받고 싶으면 빈 생성 기능을 사용
QueryDSL은 객체를 생성하는 다양한 방법을 제공하며
원하는 방법을 지정하기 위해 com.mysema.query.types.Projections을 사용
// 예제 ItemDTO
public class ItemDTO {
private String username;
private int price;
public ItemDTO() {}
public ItemDTO(Strign 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 =
/* Projections.bean() 메소드는 수정자(Setter)를 사용해서 값을 채움
쿼리 결과는 name인데 ItemDTO는 username 프로퍼티를 가지고 있으므로
쿼리 결과가 매핑할 프로퍼티 이름이 다르면 as를 사용해서 별칭을 줌 */
query.from(item).list(Projections.bean(ItemDTO.class, item.name.as("username"), item.price));
// 필드 직접 접근
QItem item = QItem.item;
List<ItemDTO> result =
/* Projections.field() 메소드를 사용하면 필드에 직접 접근해서 값을 채움
필드를 private로 설정해도 동작함 */
query.from(item).list(Projections.fields(ItemDTO.class, item.name.as("username"), item.price));
// 생성자 사용
QItem item = QItem.item;
List<ItemDTO> result =
/* Projections.bean() 메소드는 생성자를 사용
지정한 프로젝션과 파라미터 순서가 같은 생성자가 필요 */
query.from(item).list(Projections.constructor(ItemDTO.class, item.name, item.price));
query.distinct().from(item)...
수정, 삭제 배치 쿼리
- 수정, 삭제 같은 배치 쿼리를 지원
JPQL 배치 쿼리와 같이 영속성 컨텍스트를 무시하고 데이터베이스를 직접 쿼리함
// 수정 배치 쿼리
// com.mysema.query.jpa.impl.JPAUpdateClause를 사용
// 상품의 가격을 100원 증가시킴
QItem item = QItem.item;
JPAUpdateClause updateClause = new JPAUpdateClause(em, item);
long count = upDateClause.where(item.name.eq("시골개발자의 JPA 책"))
.set(item.price, item.price.add(100))
.execute();
// 삭제 배치 쿼리
// com.mysema.query.jpa.impl.JPADeleteClause를 사용
// 이름이 같은 상품을 삭제
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("시골개발자");
parma.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);
메소드 위임
- 메소드 위임 기능을 사용하면 쿼리 타입에 검색 조건을 직접 정의할 수 있음
// 검색 조건 정의
public class ItemExpression {
/* 메소드 위임 기능을 사용하려면 정적 메소드를 만들고
QueryDelegate 어노테이션에 속성으로 이 기능을 적용할 엔티티를 지정
정적 메소드의 첫 번째 파라미터에는 대상 엔티티의 쿼리 타입(Q)를 지정하고
나머지는 필요한 파라미터를 정의 */
@QueryDelegate(Item.class)
public static BooleanExpression isExpensive(QItem item, Integer price) {
return item.price.gt(price);
}
}
// 쿼리 타입에 생성된 결과
public class QItem extends EntityPathBase<Item> {
...
public vom.mysema.query.types.expr.BooleanExperssion isExpensive(Integet price) {
return ItemExpression.isExpensive(this, price);
}
}
// 메소드 위임 기능 사용
query.from(item).where(item.isExpensive(30000)).list(item);
- 필요하다면 String, Date 같은 자바 기본 내장 타입에도 메소드 위임 기능을 사용할 수 있음
@QueryDelegate(String.class)
public static BooleanExpresstion isHelloStart(StringPath stringPath) {
return stringPath.startsWith("Hello");
}