이 글은 김영한 님의 저서 「자바 ORM 표준 JPA 프로그래밍」을 학습한 내용을 정리한 글입니다. 모든 출처는 해당 저서에 있습니다.
QueryDSL
QueryDSL
설정pom.xml
추가<dependency>
<groupId>com.mysema.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId> //QueryDSL JPA 라이브러리
<version>3.6.3</version>
</dependency>
<dependency>
<groupId>com.mysema.querydsl</groupId>
<artifactId>querydsl-apt</artifactId> //쿼리 타입(Q) 생성 시 필요한 라이브러리
<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.JPAAnnotation Processor</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
mvn compile
입력 시 outputDirectory
에 지정한 target/generated-sources
위치에 Q로 시작하는 쿼리 타입들 생성QMember.java
target/generated-sources
를 소스 경로에 추가public void queryDSL() {
EntityManager em = emf.createEntityManager();
JPAQuery query = new JPAQuery(em);
QMember qmember = new QMember("m"); //생성되는 JPQL 명칭 → 'm'
List<Member> members = query.from(qMember)
.where(qMember.name.eq("회원1"))
.orderBy(qMember.name.desc())
.list(qMember);
}
com.mysema.query.jpa.impl.JPAQuery
객체 생성 및 엔티티 매니저 생성자에 전달select m from Member m
where m.name = ?1
order by m.name desc
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 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);
}
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); //조회할 프로젝션 지정
select item
from Item item
where item.name = ?1 and item.price > ?2
where
: 쿼리 필터 추가
and/or
메서드를 이용하여 필터 추가and
대신 ,
을 이용하여 여러 검색 조건 사용.where(item.name.eq("좋은상품"), item.price.gt(20000))
쿼리 타입 필드는 필요한 대부분의 메소드를 명시적으로 제공
item.name.eq("상품1") //name = '상품1'
item.name.ne("상품1") //name != '상품1'
item.name.eq("상품1").not() // name != '상품1'
item.name.isNotNull() //상품명이 null이 아닌 것
item.price.in(10000, 20000) //가격이 10000원 또는 20000원
item.price.notIn(10000, 20000) //가격이 10000원 또는 20000원이 아닌 것
item.price.between(10000, 20000) //가격이 10000원 ~ 20000원
item.price.goe(10000) //price >= 10000
item.price.gt(10000) //price > 10000
item.price.loe(20000) //price <= 20000
item.price.lt(20000) //price < 20000
item.name.like("고급%") //like 검색, 이름이 '고급'으로 시작하는 상품
item.name.contains("상품1") //like ‘%상품1%’ 검색, '상품1'이라는 이름을 포함한 상품
item.name.startsWith("고급") //like ‘고급%’ 검색, 이름이 '고급'으로 시작하는 상품
결과 조회 메소드 호출 시 실제 데이터베이스 조회
com.mysema.query.Projectable
→ 결과 조회 API 정의
결과 조회 메소드
메소드 | 설명 |
---|---|
uniqueResult() | ◾ 조회 결과가 한 건일 때 사용 ◾ 조회 결과가 없는 경우 → null 반환◾ 조회 결과가 하나 이상인 경우 → com.mysema.query.NonUniqueRsultException 예외 발생 |
singleResult() | ◾ uniqueResult() 와 동일◾ 결과가 하나 이상인 경우 → 처음 데이터 반환 |
list() | ◾ 결과가 하나 이상일 때 사용 ◾ 결과가 없는 경우 → 빈 컬렉션 반환 |
uniqueResult()
나 list()
사용QItem item = QItem.item;
List<Item> list = query.from(item)
.where(item.price.gt(20000))
.orderBy(item.price.desc(), item.stockQuantity.asc())
.offset(10).limit(20)
.list(item);
orderBy
사용asc()
, desc()
사용
asc()
: 오름차순desc()
: 내림차순
offset
과 limit
을 적절히 조합하여 사용
offset
: 결과의 시작 행limit
: 최대 결과 개수
restrict()
메소드에 파라미터로 com.mysema.query.QueryModifiers
사용 가능
restrict()
:limit
과offset
함께 정의
QueryModifiers queryModifiers = new QueryModifiers(20L, 10L); //limit, offset;
List<Item> list = query.from(item)
.restrict(queryModifiers)
.list(item);
SearchResults<Item> result = query.from(item)
.where(item.price.gt(10000))
.offset(10).limit(20)
.listResults(item);
long total = result.getTotal(); //검색된 전체 데이터 수
long limit = result.getLimit();
long offset = result.getOffset();
List<Item> results = result.getResults(); //조회된 데이터
listResults()
count
쿼리를 한 번 더 실행 후 SearchResults
반환SearchResults
객체에서 전체 데이터 수 조회 가능groupBy
: 그룹화having
: 그룹화된 결과 제한List<Item> list = query.from(item)
.groupBy(item.price)
.having(item.price.gt(10000))
.list(item);
innerJoin(join)
leftJoin
rightJoin
fullJoin
on
과 fetch
조인도 사용 가능join(조인 대상, 별칭으로 사용할 쿼리 타입)
QOrder order = QOrder.order;
QMember member = QMember.member;
QOrderItem orderItem = QOrderItem.orderItem;
List<Order> list = query.from(order)
.join(order.member, member)
.leftJoin(order.orderItems, orderItem)
.list(order);
List<Order> list = query.from(order)
.join(order.member, member)
.on(orderItem.count.gt(2))
.list(order);
List<Order> list = query.from(order)
.innerJoin(order.member, member).fetch()
.leftJoin(order.orderItems, orderItem).fetch()
.list(order);
QOrder order = QOrder.order;
QMember member = QMember.member;
List<Order> list = query.from(order, member)
.innerJoin(order.member.eq(member))
.list(order);
com.mysema.query.jpa.JPASubQuery
생성하여 사용
메소드
메소드 | 설명 |
---|---|
unique() | 서브 쿼리의 결과가 한 건인 경우 |
list() | 서브 쿼리의 결과가 여러 건인 경우 |
QItem item = QItem.item;
QItem itemSub = new QItem("itemSub");
List<Item> list =
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");
List<Item> list =
query.from(item)
.where(item.in(
new JPASubQuery().from(itemSub)
.where(item.name.eq(itemSub.name))
.list(itemSub)
))
.list(item);
💡 프로젝션(projection)
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> result = 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));
}
com.mysema.query.types.Projections
를 사용하여 원하는 방법 지정ItemDTO
public lcass 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 getUsername(String username) {...}
public int getPrice() {...}
public void getPrice(int price) {...}
}
Setter
)QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
Projections.bean(itemDTO.class, item.name.as("username"), item.price)
);
Projections.bean()
→ 수정자(Setter)를 사용 해서 값을 채움as
를 사용하여 별칭 지정QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
Projections.fields(itemDTO.class, item.name.as("username"), item.price)
);
private
으로 설정해도 동작함QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
Projections.constructor(itemDTO.class, item.name, item.price)
);
DISTINCT
query.distinct().from(item)...
com.mysema.query.jpa.impl.JPAUpdateClause
사용QItem item = QItem.item;
JPAUpdateClause updateClause = new JPAUpdateClause(em, item);
//상품 가격 100원 증가
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("시골개발자");
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);
public class ItemExpression {
@QueryDelegate(Item.class)
public static BooleanExpression isExpensive(QItem item, Integer price) {
return item.price.gt(price);
}
/* 자바 기본 내장 타입(String, Date 등)에도 사용 가능 */
//@QueryDelegate(String.class)
//public static BooleanExpression isHelloStart(StringPath stringPath) {
// return stringPath.startsWith("Hello");
//}
}
@com.mysema.query.annotations.QueryDelegate
어노테이션의 속성으로 기능 적용할 엔티티 지정public class QItem extends EntityPathBase<Item> {
...
public com.mysema.query.types.expr.BooleanExpression isExpensive(Integer price) {
return ItemExpression.isExpensive(this, price);
}
}
List<Item> list = query.from(item).where(item.isExpensive(30000)).list(item);