elevne's Study Note
JPA - QueryDSL 본문
JPA Criteria 는 코드로 JPQL 을 작성하므로 문법 오류를 컴파일 단계에서 잡을 수 있고 IDE 의 지원을 받을 수 있는 등의 장점이 있지만, 너무 복잡하고 사용하기 어렵다. 동일하게 쿼리를 문자가 아닌 코드롤 작성해도 쉽고 간결하며 그 모양도 쿼리와 비슷하게 개발할 수 있는 것이 QueryDSL 이다. QueryDSL 은 Criteria 를 대체할 수 있다.
QueryDSL 은 오픈소스 프로젝트로, 처음에는 HQL(하이버네이트 쿼리언어)을 코드로 작성할 수 있도록 해주는 프로젝트로 시작하여 지금은 JPA, JDO, JDBC, Lucene, Hibernate Search, Mongo DB, 자바 컬렉션 등을 다양하게 지원한다. 우선 필요한 라이브러리를 pom.xml 에 추가해준다.
<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>
QueryDSL 을 사용할 때에는 엔티티를 기반으로 쿼리타입이라는 쿼리용 클래스를 생성해야 한다. 쿼리타입 생성용 플러그인을 pom.xml 에 추가해준다.
<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>
위 내용을 추가해준 뒤 mvn compile 을 콘솔에서 실행하면 outputDirectory 에 지정한 target/generated-sources/java 위치에 QMember.java 처럼 Q 로 시작하는 쿼리타입들이 생성된다. 그 다음 generated-sources 폴더를 소스 경로에 추가해준다.
JPAQuery query = new JPAQuery(em);
QMember qMember = new QMember("m"); // JPQL 별칭
List<Member> members = query.from(qMember)
.where(qMember.username.eq("회원1"))
.orderBy(qMember.username.desc())
.list(qMember);
// Static import
List<Member> members2 = query.from(member)
.where(member.username.eq("회원2"))
.list(member);
QueryDSL 을 사용하기 위해서는 우선 JPAQuery 객체를 생성한다. 이 때, 엔티티 매니저를 생성자에 넘겨준다. 그 다음으로는 사용할 쿼리타입을 생성하는데, 생성자에는 별칭을 붙여준다. (붙여주는 별칭을 JPQL 내에서 사용) 그 다음으로 나오는 from, where, orderBy, list 는 코드만 보아도 쉽게 이해가 된다.
JPAQuery query = new JPAQuery(em);
List<Member> members2 = query.from(member)
.where(member.username.eq("회원2"), member.age.gt(10))
.where(member.username.contains("회").or(member.age.eq(10)))
.orderBy(member.age.desc(), member.username.asc())
.offset(10).limit(20)
.list(member);
QueryDSL 의 where 절에는 and 나 or 연산을 사용할 수 있다. where 내에 콤마로 구분지으면 and 조건으로 들어가고, .or() 을 사용하면 or 조건으로 들어간다. (.where() 을 이어서 적어도 and 로 들어간다) 페이징과 정렬도 간단하게 사용할 수 있다. .offset() 으로 시작위치, .limit() 으로 조회 데이터수를 지정한다.
또, 조회결과가 한 건일 때는 .list() 대신에 uniqueResult() 메소드를 사용한다. 조회 결과가 없으면 null 을 반환하고 결과가 하나 이상이면 NonUniqueResultException 이 발생한다. singleResult() 도 사용할 수 있는데, 이는 uniqueResult 와 같지만 결과가 2 개 이상이면 예외를 발생시키지 않고 첫 번째 데이터를 반환한다.
JPAQuery query = new JPAQuery(em);
QProduct product = QProduct.product;
SearchResults<Product> result = query.from(product)
.where(product.price.gt(10000))
.offset(10).limit(20)
.listResults(product);
long total = result.getTotal();
long limit = result.getLimit();
long offset = result.getOffset();
List<Product> results = result.getResults();
그 외에도 listResults() 를 통해 검색된 전체 데이터 수를 파악할 수 있다. (실제 페이징 처리 시 필요한 검색 전체 데이터 수를 알아볼 때 사용) listResults() 를 사용하면 전체 데이터 조회를 위한 count 쿼리를 한 번 더 실행하고 SearchResults 인스턴스를 반환한다. SearchResults 에서는 getTotal, getLimit, getOffset 등의 메소드를 사용할 수 있다.
JPAQuery query = new JPAQuery(em);
QProduct product = QProduct.product;
List<Product> result = query.from(product)
.groupBy(product.price)
.having(product.price.lt(100000))
.list(product);
GROUP BY 를 하기 위해서는 groupBy, HAVING 은 having 을 사용한다.
JPAQuery query = new JPAQuery(em);
QMember member = QMember.member;
QOrders orders = QOrders.orders;
QProduct product = QProduct.product;
query.from(orders)
.join(orders.member, member).fetch()
.leftJoin(orders.product, product).fetch()
.list(orders);
JPAQuery query1 = new JPAQuery(em);
query1.from(orders, product).where(orders.product.eq(product));
조인을 할 때에도 마찬가지로 .join() 을 사용, on() 및 fetch() 도 사용 가능하다.
JPAQuery query = new JPAQuery(em);
QProduct product = QProduct.product;
QProduct productSub = new QProduct("productSub");
query.from(product)
.where(product.price.eq(
new JPASubQuery().from(productSub).unique(productSub.price.max())
)).list(product);
서브쿼리는 JPASubQuery 오브젝트를 생성해 사용한다. 서브 쿼리의 결과가 하나면 unique(), 여러 건이면 list() 를 사용한다.
JPAQuery query = new JPAQuery(em);
QProduct product = QProduct.product;
List<String> result = query.from(product).list(product.name);
JPAQuery query1 = new JPAQuery(em);
QProduct product1 = QProduct.product;
List<Tuple> result1 = query1.from(product1).list(product1.name, product1.price);
JPAQuery query2 = new JPAQuery(em);
QMember member = QMember.member;
List<UserDTO> members = query2.from(member).list(
Projections.bean(UserDTO.class, member.username.as("username"), member.age.as("age"))
);
SELECT 절에 조회 대상을 지정하는 것을 프로젝션이라고 한다. 프로젝션 대상이 하나면 위 첫 번째 블록과 같이 해당 타입으로 변환해준다. 만약 프로젝션 대상으로 여러 필드를 선택하면 QueryDSL 은 기본으로 Tuple 이라는 Map 과 비슷한 내부 타입을 사용한다. (조회결과는 tuple.get() 메소드로 확인 가능) 두 번째 블록처럼 사용할 수 있다.
쿼리 결과를 엔티티가 아닌 특정 객체로 받고싶으면 빈 생성 기능을 사용한다. QueryDSL 은 객체를 생성하는 다양한 방법을 제공한다. 위와 같이 Projections.bean(UserDTO.class, member.username.as("username"), member.age.as("age")) 처럼 작성하면 (bean() 메소드) 해당 객체의 setter 을 사용해서 값을 채운다. (쿼리 결과와 매핑할 프로퍼티 이름이 다르면 as 를 사용해서 별칭을 줄 수 있다) bean() 메소드 외에도 fields() (필드 접근(private 여도 동작)), constructor() (생성자) 메소드를 사용할 수 있다.
QProduct product = QProduct.product;
JPAUpdateClause updateClause = new JPAUpdateClause(em, product);
long count = updateClause.where(product.name.eq("this product"))
.set(product.price, product.price.add(10000))
.execute();
JPADeleteClause deleteClause = new JPADeleteClause(em, product);
long count1 = deleteClause.where(product.name.eq("this product"))
.execute();
QueryDSL 또한 수정, 삭제 같은 배치 쿼리를 지원한다. JPQL 배치 쿼리와 같이 영속성 컨텍스트를 무시하고 데이터베이스를 직접 쿼리한다. 수정 배치 쿼리는 JPAUpdateClause, 삭제 쿼리는 JPADeleteClause 를 사용한다.
BooleanBuilder 객체를 사용하면 특정 조건에 따른 동적 쿼리를 편리하게 생성할 수 있다.
JPAQuery query = new JPAQuery(em);
QProduct product = QProduct.product;
BooleanBuilder builder = new BooleanBuilder();
String nameParam = "TEST";
Integer priceParam = 10000;
if (StringUtils.hasText(nameParam)) {
builder.and(product.name.contains(nameParam));
}
if (priceParam != null) {
builder.and(product.price.gt(priceParam));
}
List<Product> result = query.from(product)
.where(builder)
.list(product);
마지막으로 메소드 위임이라는 것에 대해 알아본다. 이는 쿼리타입에 검색 조건을 직접 정의할 수 있게끔 해준다.
@QueryDelegate(Product.class)
public static BooleanExpression isExpensive(com.example.jpastudy.chapter10.QProduct product, Integer price) {
return product.price.gt(price);
}
메소드 위임 기능을 사용하기 위해서는 우선 위와 같이 static 메소드를 만들고 QueryDelegate 애노테이션 속성으로 이 기능을 적용할 엔티티를 지정한다. mvn compile 을 진행하면 QProduct 에 아래와 같은 메소드가 생성되어 있는 것을 확인할 수 있다.
이를 활용하여 간단하게 조건을 추가하여 쿼리할 수 있다.
JPAQuery query = new JPAQuery(em);
QProduct product = QProduct.product;
query.from(product).where(product.isExpensive(30000)).list(product);
Reference:
자바 ORM 표준 JPA 프로그래밍
'Backend > JPA 프로그래밍' 카테고리의 다른 글
Spring Maven 설정 (0) | 2023.07.17 |
---|---|
JPA - NativeSQL (0) | 2023.07.16 |
JPA - Criteria (0) | 2023.07.14 |
JPA - JPQL (0) | 2023.07.13 |
JPA - Embedded / Collection Value Type (0) | 2023.07.12 |