BOOK 3 - 자바 ORM 표준 JPA 프로그래밍(10)-2

객체 지향 쿼리 언어 - Criteria & QueryDSL

Criteria

  • Criteria는 JPQL을 자바 코드로 작성하도록 도와주는 빌더 클래스 API다.
    • 문자가 아닌 코드로 JPQL을 작성하므로 문법 오류를 컴파일 타임에 잡을 수 있고, 동적 쿼리를 안전하게 생성할 수 있다.
    • 코드가 복잡해서 직관적인 이해가 힘들다.

Criteria 기초

  • Criteria API는 javax.persistence.criteria 패키지에 있다.
CriteriaBuilder cb = em.getCriteriaBuilder();

CriteriaQuery<Member> cq = cb.createQuery(Member.class);

Root<Member> m = cq.from(Member.class);
cq.select(m);

TypedQuery<Member> query = em.createQuery(cq);
List<Member> members = query.getResultList();
  • em.getCriteriaBuilder()
    • Criteria 쿼리를 생성하려면 먼저 CriteriaBuilder 를 얻어야 한다.
    • EntityManagerEntityManagerFactory 에서 얻을 수 있다.
  • cb.createQuery(Member.class)
    • CriteriaBuilder 에서 CriteriaQuery 를 생성한다.
    • 반환 타입을 지정할 수 있다.
  • cq.from(Member.class)
    • FROM 절을 생성한다.
    • 반환된 m 값은 Criteria에서 사용하는 특별한 별칭이다.
      • 조회의 시작점이라는 의미로 쿼리 루트(Root)라 한다.
  • cq.select(m)
    • SELECT 절을 생성한다.
  • 완성된 Criteria 쿼리를 createQuery() 메소드에 넘겨주기만 하면 된다.

  • where 조건과 order by 조건을 추가해보자.
CriteriaBuilder cb = em.getCriteriaBuilder();

CriteriaQuery cq = cb.createQuery(Member.class);

Root<Member> m = cq.select(Member.class);

Predicate usernameEqual = cb.equal(m.get("username"), "member1");
Order ageDesc = cb.desc(m.get("age"));

cq.select(m)
  .where(usernameEqual)
  .orderBy(ageDesc);

List<Member> resultList = em.createQuery(cq).getResultList();
  • m.get("username")
    • m 은 회원 엔티티의 별칭이다. 즉, m.username 과 같은 표현이다.
  • cb.equal(m.get("username"), "member1")
    • JPQL에서 m.username = 'member1' 과 같은 표현이다.
  • cb.desc(m.get("age"))
    • JPQL에서 m.age desc 와 같은 표현이다.
  • 만든 조건을 where , orderBy 에 넣어서 원하는 쿼리를 생성한다.

쿼리 루트와 별칭

  • 쿼리 루트는 조회의 시작점이다.
  • Criteria에서 사용되는 특별한 별칭이다.
  • 별칭은 엔티티에만 부여할 수 있다.
  • m.get("team").get("name") 과 같이 경로 표현식을 사용할 수도 있다.
Root<Member> m = cq.from(Member.class);

Predicate ageGt = cb.greaterThan(m.<Integer>get("age"), 10);

cq.select(m)
  .where(ageGt)
  .orderBy(cb.desc(m.get("age")));
  • m.get("age") 에서는 age 의 타입 정보를 알지 못하기 때문에 generic으로 반환 타입 정보를 명시해준다.
  • greaterThan(A, B) 메서드는 A > B 와 같은 표현이며 gt() 메서드를 사용할 수도 있다.

Criteria 쿼리 생성

  • Criteria를 사용하려면 CriteriaBuilder.createQuery() 메서드로 Criteria 쿼리를 생성하면 된다.

  • CriteriaQuery 를 생성할 때 반환 타입을 지정하면 em.createQuery() 에서 반환 타입을 지정하지 않아도 된다.
  • 반환 타입을 지정할 수 없거나 반환 타입이 둘 이상이면 ObjectObject[] 로 반환받으면 된다.

조회

조회 대상을 한 건, 여러 건 지정

  • select 에 조회 대상을 하나만 지정하려면 다음처럼 작성한다.

    cq.select(m);
    
  • 조회 대상을 여러 건 지정하려면 multiselect 를 사용한다.

    cq.multiselect(m.get("username"), m.get("age"));
    
    • cb.array 를 사용해도 된다.
    CriteriaBuilder cb = em.getCriteriaBuilder();
    cq.select(cb.array(m.get("username"), m.get("age")));
    

DISTINCT

  • distinctselect 다음에 distinct(true) 를 사용하면 된다.
CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
Root<Member> m  = cq.from(Member.class);
cq.multiselect(m.get("username"), m.get("age")).distinct(true);

TypeQuery<Object[]> query = em.createQuery(cq);
List<Object[]> resultList = query.getResultList();

NEW, construct()

  • JPQL에서 select new 생성자() 구문을 Criteria에서는 cb.construct(클래스 타입, 생성자 파라미터) 로 사용한다.
CriteriaQuery<MemberDTO> cq = cb.createQuery(MemberDTO.class);
Root<Member> m = cq.from(Member.class);

cq.select(cb.construct(MemberDTO.class, m.get("username"), m.get("age")));

TypedQuery<MemberDTO> query = em.createQuery(cq);
List<MemberDTO> resultList = query.getResultList();
  • JPQL에서는 new 로 사용자 지정 클래스의 인스턴스를 생성할 때 패키지명을 포함한 클래스 명을 작성해야 했지만, Criteria는 코드를 직접 다루므로 패키지명을 포함하지 않아도 된다.

튜플

  • Criteria는 Map과 비슷한 Tuple 이라는 특별한 반환 객체를 제공한다.
    • cb.createTupleQuery() 또는 cb.createQuery(Tuple.class) 로 Criteria를 생성한다.
CriteriaBuilder cb = em.getCriteriaBuilder();

CriteriaQuery<Tuple> cq = cb.createTupleQuery();

Root<Member> m = cq.from(Member.class);
cq.multiselect(
	m.get("username").alias("username"),
  m.get("age").alias("age")
);

TypedQuery<Tuple> query = em.createQuery(cq);
List<Tuple> resultList = query.getResultList();
for(Tuple tuple : resultList) {
  String username = tuple.get("username", String.class);
  Integer age = tuple.get("age", Integer.class);
}
  • 튜플은 튜플의 검색 키로 사용할 튜플 전용 별칭을 필수로 할당해야 한다.
    • 별칭은 alias() 메서드로 지정할 수 있다.
  • 선언해둔 별칭으로 튜플에서 데이터를 조회할 수 있다.
  • 튜플은 이름 기반이므로 순서 기반의 Obejct[] 보다 안전하다.
  • 튜플은 엔티티도 조회할 수 있다.
CriteriaQuery<Tuple> cq = cb.createTupleQuery();
Root<Member> m = cq.from(Member.class);
cq.select(cb.tuple(
	m.alias("m"),
  m.get("username").alias("username")
));

TypedQuery<Tuple> query = em.createQuery(cq);
List<Tuple> resultList = query.getResultList();
for(Tuple tuple : resultList) {
  Member member = tuple.get("m", Member.class);
  String username = tuple.get("username", String.class);
}
  • cq.multiselect(...)cq.select(cb.tuple(...)) 은 같은 기능을 한다.

집합

GROUP BY

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
Root<Member> m = cq.from(Member.class);

Expression maxAge = cb.max(m.<Integer>get("age"));
Expression minAge = cb.min(m.<Integer>get("age"));

cq.multiselect(m.get("team").get("name"), maxAge, minAge);
cq.groupBy(m.get("team").get("name"));

TypedQuery<Object[]> query = em.createQuery(cq);
List<Object[]> resultList = query.getResultList();

HAVING

cq.multiselect(m.get("team").get("name"), maxAge, minAge)
  .groupBy(m.get("team").get("name"))
  .having(cb.gt(minAge), 10);
  • having(cb.gt(minAge), 10)having min(m.age) > 10 과 같다.

정렬

  • cb.desc() 또는 cb.asc() 로 생성할 수 있다.
cq.select(m)
  .where(ageGt)
  .orderBy(cb.desc(m.get("age")));
  • 여러개의 정렬 조건을 포함하고 싶을 때 orderBy() 메서드에 List<Order> 타입의 파라미터를 전달할 수도 있다.

조인

  • 조인은 join() 메서드와 JoinType 클래스를 사용한다.

    public enum JoinType {
      INNER, 		// 내부 조인
      LEFT, 		// LEFT OUTER JOIN
      RIGHT			// RIGHT OUTER JOIN
    }
    
Root<Member> m = cq.from(Member.class);
Join<Member, Team> t = m.join("team", JoinType.INNER);

cq.multiselect(m, t)
  .where(cb.equal(t.get("name"), "teamA"));
  • 쿼리 루트 m 에서 바로 m.join("team") 메소드를 호출해서 회원과 팀 테이블을 조인했다.

    • 조인한 team 에는 t 라는 alias를 주었다.
  • 조인 타입을 생략하면 기본 값으로 내부 조인을 사용한다.

  • 페치 조인은 fetch() 메서드를 사용한다.

    Root<Member> m = cq.from(Member.class);
    m.fetch("team", JoinType.LEFT);
      
    cq.select(m);
    

서브 쿼리

간단한 서브 쿼리

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> mainQuery = cb.createQuery(Member.class);

SubQuery<Double> subQuery = mainQuery.subquery(Double.class);
Root<Member> m2 = subQuery.from(Member.class);
subQuery.select(cb.avg(m2.<Integer>get("age")));

Root<Member> m = mainQuery.from(Member.class);
mainQuery.select(m)
  .where(cb.ge(m.<Integer>get("age"), subQuery));
  • mainQuery.subquery(...) : 서브 쿼리를 생성한다.
  • where(..., subQuery) : 생성한 서브 쿼리를 메인 쿼리의 where 절에서 사용한다.

상호 관련 서브 쿼리

  • 서브 쿼리에서 메인 쿼리의 정보를 사용하려면 메인 쿼리에서 사용한 별칭을 얻어야 한다.
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> mainQuery = cb.createQuery(Member.class);

Root<Member> m = mainQuery.from(Member.class);

SubQuery<Team> subQuery = mainQuery.subquery(Team.class);
Root<Member> subM = subQuery.correlate(m);
Join<Member, Tema> t = subM.join("team");
subQuery.select(t)
  .where(cb.equal(t.get("name"), "teamA"));

mainQuery.select(m).where(cb.exists(subQuery));

List<Member> resultList = em.createQuery(mainQuery).getResultList();
  • correlate() 메서드를 사용해서 메인쿼리의 별칭을 서브 쿼리에서 사용할 수 있다.
  • 위 코드에서 생성한 JPQL은 다음과 같다.
select m from Member m
where exists (select t from m.team t where t.name = 'teamA')

IN 식

  • IN 식은 Criteria 빌더에서 in() 메서드를 사용한다.
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> cq = cb.createQuery(Member.class);
Root<Member> m = cq.from(Member.class);

cq.select(m)
  .where(cb.in(m.get("username"))
        .value("member1")
        .value("member2"));

CASE 식

  • CASE 식에는 selectCase() 메서드와 when() , otherwise() 메서드를 사용한다.
Root<Member> m = cq.from(Member.class);

cq.multiselect(
	m.get("username"),
  cb.selectCase()
  .when(cb.ge(m.<Integer>get("age"), 60), 600)
  .when(cb.le(m.<Integer>get("age"), 15), 500)
  .otherwise(1000)
)

파라미터 정의

CriteriaBuilder cb = em.getCriteriaBuilder();
CrietriaQuery<Member> cq = cb.createQuery(Member.class);

Root<Member> m = cq.from(Member.class);

cq.select(m)
  .where(cb.equal(m.get("username"), cb.parameter(String.class, "usernameParam")));

List<Member> resultList = em.createQuery(cq)
  .setParameter("usernameParam", "member1")
  .getResultList();
  • cb.paramter(파라미터 타입, 파라미터 이름) : 쿼리에 사용할 파라미터 정의한다.
  • setParameter(파라미터 이름, 파라미터 값) : 해당 파라미터에 사용할 값을 바인딩한다.

네이티브 함수 호출

  • 네이티브 SQL 함수를 호출하려면 cb.function(...) 메서드를 사용한다.
Root<Member> m = cq.from(Member.class);
Expression<Long> function = cb.function("SUM", Long.class, m.get("age"));
cq.select(function);

동적 쿼리

  • 다양한 검색 조건에 따라 실행 시점에 쿼리를 생성하는 것을 동적 쿼리라 한다.
  • 동적 쿼리는 문자 기반인 JPQL보다 코드 기반인 Criteria로 작성하는 것이 더 편리하다.
Integer age = 10;
String username = null;
String teamName = "teamA";

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> cq = cb.createQuery(Member.class);

Root<Member> m = cq.from(Member.class);
Join<Member, join> t = m.join("team");

List<Predicate> criteria = new ArrayList<>();

if(age != null) 
  criteria.add(cb.equal(m.<Integer>get("age"), cb.parameter(Integer.class, "age")));
if(username != null)
  criteria.add(cb.equal(m.get("username"), cb.parameter(String.class, "username")));
if(teamName != null) 
  criteria.add(cb.equal(t.get("name"), cb.parameter(String.class, "teamName")));

cq.where(cb.and(criteria.toArray(new Predicate[0])));

TypedQuery<Member> query = em.createQuery(cq);
if(age != null) query.setParameter("age", age);
if(username != null) query.setParamter("username", username);
if(teamName != null) query.setParamter("teamName", teamName);

List<Member> resultList = query.getResultList();

Criteria 메타 모델 API

  • Criteria는 코드 기반이므로 컴파일 시점에 오류를 발견할 수 있다.
    • 하지만 m.get("age") 에서 age 는 문자이므로 오타가 발생해도 컴파일 시점에 발견하지 못한다.
    • 따라서 완전한 코드 기반이라 할 수 없다.
    • 이런 부분까지 코드로 작성하려면 메타 모델 API를 사용하면 된다.
cq.select(m)
  .where(cb.gt(m.get(Member_.age), 20))
  .orderBy(cb.desc(m.get(Member_.age)));
  • m.get("age")m.get(Member_.age) 처럼 정적인 코드 기반으로 변경된 것을 볼 수 있다.
    • Member_ 클래스를 메타 모델 클래스 라고 한다.
  • 메타 모델 클래스는 코드 자동 생성기가 엔티티 클래스를 기반으로 만들어 준다.
    • JPAMetaModelEntityProcessor 코드 생성기는 모든 엔티티 클래스를 찾아서 엔티티명_ 모양의 메타 모델 클래스를 생성해준다.
    • 코드 생성기는 Maven 이나 Gradle 같은 빌드 도구를 사용해서 실행한다.
      • target/generated-sources/annotations 하위에 메타 모델 클래스들이 생성된다.

QueryDSL

  • Criteria는 코드 기반이므로 컴파일 단계에서 오류를 잡을 수 있다는 장점이 있지만, 코드가 너무 복잡하고 직관적으로 이해하기가 어렵다는 단점이 있다.
  • 쿼리를 문자가 아닌 코드로 작성해도 쉽고 간결하며 그 모양도 쿼리와 비슷하게 개발할 수 있는 것이 QueryDSL 이다.

QueryDSL 설정

필요 라이브러리

  • querydsl-jpa : QueryDSL JPA 라이브러리
  • querydsl-apt : 쿼리 타입을 생성할 때 필요한 라이브러리

환경 설정

  • QueryDSL을 사용하려면 Criteria의 메타 모델처럼 엔티티를 기반으로 쿼리 타입이라는 쿼리용 클래스를 생성해야 한다.
  • JPAAnnotationProcessor 코드 생성기가 target/generated-sources 위치에 Q엔티티명 의 쿼리 타입들을 생성한다.

시작

EntityManager em = emf.createEntityManager();

JPAQuery query = new JPAQuery(em);
QMember qMember = new QMember("m");
List<Member> members = query.from(qMember)
  .where(qMember.name.eq("member1"))
  .orderBy(qMember.name.desc())
  .list(qMember);
  • QueryDSL을 사용하려면 JPAQuery 객체를 생성해야 하는데, 이 때 엔티티 매니저를 생성자에 넘겨준다.
  • 사용할 쿼리 타입을 생성할 때는 생성자에 별칭을 주면 된다.
    • 이 때 설정된 alias는 JPQL에서 별칭으로 사용한다.

기본 Q 생성

  • 쿼리 타입은 사용하기 편리하도록 기본 인스턴스를 보관하고 있다.

    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 com.domain.jpa.QMember.member;
      
    public void basic() {
      ...
      JPAQuery query = new JPAQuery(em);
      List<Member> members = query.from(member)
        .where(member.name.eq("member1"))
        .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("goods").and(item.price.gt(20000)))
  .list(item);
  • where 절에 andor 를 사용할 수 있다.

    • 여러 검색 조건을 파라미터로 나열하여 넘겨주어도 된다. 이 때는 and 연산이 된다.

      .where(item.name.eq("goods"), item.price.gt(20000))
      
  • 쿼리 타입의 필드는 필요한 대부분의 메소드를 명시적으로 제공한다.

    item.price.between(10000, 20000);
    item.name.contains("item1");			// SQL에서 like '%item1%' 로 검색
    item.name.startsWith("it")				// SQL에서 like 'it%' 로 검색
    

결과 조회

  • 쿼리 작성이 끝나고 결과 조회 메소드를 호출하면 실제 데이터베이스를 조회한다.
  • uniqueResult()list() 를 주로 사용하고 파라미터로 프로젝션 대상을 넘겨준다.
    • uniqueResult() : 조회 결과가 한 건일 때 사용한다.
      • 조회 결과가 없으면 null 을 반환하고, 결과가 두 개 이상이면 NonUniqueResultException 예외를 발생시킨다.
    • 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() 메서드를 사용하고, 쿼리 타입이 제공하는 asc() , desc() 를 사용한다.

  • 페이징은 offset()limit() 을 조합해서 사용한다.

  • 페이징은 restrict() 메서드에 QueryModifiers 를 파라미터로 전달해 사용해도 된다.

    QueryModifiers queryModifiers = new QueryModifiers(20L, 10L);  // limit, offset
    List<Item> list = query.from(item)
      .restrict(queryModifiers)
      .list(item);
    
  • 실제 페이징 처리를 하려면 검색된 전체 데이터 수를 알아야 한다.

    • 이 때 list() 대신 listResults() 를 사용할 수 있다.
    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 쿼리를 한 번 더 실행한다.
      • 전체 데이터 수는 getTotal() 메서드로 반환받을 수 있다.

그룹

  • 그룹은 groupBy() 를 사용하고, 그룹화된 결과를 제한하려면 having() 을 사용한다.
query.from(item)
  .groupBy(item.price)
  .having(item.price.gt(1000))
  .list(item);

조인

  • 조인은 innerJoin , leftJoin , rightJoin , fullJoin 을 사용할 수 있다.
    • join(조인 대상, 별칭으로 사용할 쿼리 타입)
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.member.eq(member))
  .list(order);

서브 쿼리

  • 서브 쿼리는 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은 Tuple 이라는 내부 타입을 사용한다.
  • 조회 결과는 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));
}

빈 생성

  • 쿼리 결과를 Entity가 아닌 사용자가 정의한 특정 객체로 받고 싶으면 빈 생성 기능을 사용한다.
  • QueryDSL은 객체를 생성하는 다양한 방법을 제공한다.
    • 프로퍼티 접근
    • 필드 직접 접근
    • 생성자 사용
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
  ...
}
프로퍼티 접근
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));
  • Projections.fields() 메서드는 필드에 직접 접근해서 값을 채운다.
    • 필드를 private 로 설정해도 동작한다.
생성자 사용
QItem item = QItem.item;
List<ItemDTO> result = query.from(item)
  .list(Projections.constructor(ItemDTO.class, item.name, item.price));
  • Projections.constructor() 메서드는 생성자를 사용해서 빈을 생성한다.
    • 지정한 프로젝션과 파라미터 순서가 같은 생성자가 선언되어 있어야 한다.

DISTINCT

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

수정, 삭제 배치 쿼리

  • QueryDSL도 수정, 삭제 같은 배치 쿼리를 지원한다.

수정 배치 쿼리

QItem item = QItem.item;
JPAUpdateClause updateClause = new JPAUpdateClause(em, item);
long count = updateClause.where(item.name.eq("item1"))
  .set(item.price, item.price.add(100))
  .execute();

삭제 배치 쿼리

QItem item = QItem.item;
JPADeleteClause deleteClause = new JPADeleteClause(em, item);
long count = deleteClause.where(item.name.eq("item1")).execute();

동적 쿼리

  • BooleanBuilder 를 사용하면 특정 조건에 따른 동적 쿼리를 편리하게 생성할 수 있다.
SearchParam param = new SearchParam();
param.setName("item1");
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);

메소드 위임

  • Delegate methods 기능을 사용하면 쿼리 타입에 검색 조건을 직접 정의할 수 있다.
  • 메소드 위임 기능을 사용하려면 static 메서드를 만들고, @QueryDelegate 어노테이션에 속성으로 이 기능을 적용할 엔티티를 지정한다.
public class ItemExpression {
  @QueryDelegate(Item.class)
  public static BooleanExpression isExpensive(QItem item, Integer price) {
    return item.price.gt(price);
  }
}
  • static 메서드의 첫번째 파라미터는 대상 엔티티의 쿼리 타입을 지정하고, 그 다음 파라미터 부터는 필요한 파라미터를 정의한다.

  • 생성된 쿼리 타입을 보면 해당 메서드가 추가된 것을 확인할 수 있다.

    public class QItem extends EntityPathBase<Item> {
      ...
      public BooleanExpression isExpensive(Integer price) {
        return ItemExpression.isExpression(this, price);
      }
    }
    
    • 위에서 생성된 쿼리 타입의 메서드를 다음과 같이 사용할 수 있다.
    query.from(item).where(item.isExpensive(30000)).list(item);