[JPA] Chapter 10. 객체지향 쿼리 언어 2 - QueryDSL

joyful·2021년 8월 29일
0

JPA

목록 보기
15/18
post-custom-banner

들어가기 앞서

이 글은 김영한 님의 저서 「자바 ORM 표준 JPA 프로그래밍」을 학습한 내용을 정리한 글입니다. 모든 출처는 해당 저서에 있습니다.


10.3 QueryDSL

  • 쿼리를 문자가 아닌 코드로 작성해도 쉽고 간결하며 모양도 쿼리와 비슷하게 개발할 수 있는 프로젝트
  • JPQL 빌더 역할 수행
  • 오픈소스 프로젝트
  • 데이터 조회 기능 특화

10.3.1 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로 시작하는 쿼리 타입들 생성
    ex) QMember.java
  • target/generated-sources를 소스 경로에 추가

10.3.2 시작

💻 QueryDSL 시작

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);
}
  1. com.mysema.query.jpa.impl.JPAQuery 객체 생성 및 엔티티 매니저 생성자에 전달
  2. 사용할 쿼리 타입(Q) 생성 및 생성자에 별칭 지정

💻 실행된 JPQL

select m from Member m
where m.name = ?1
order by m.name desc

✅ 기본 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() {

    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);
    
}

10.3.3 검색 조건 쿼리

💻 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
where item.name = ?1 and item.price > ?2
  • where : 쿼리 필터 추가

    • 가변인자나 and/or 메서드를 이용하여 필터 추가
    • and 대신 ,을 이용하여 여러 검색 조건 사용
      ex) .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 ‘고급%’ 검색, 이름이 '고급'으로 시작하는 상품

10.3.4 결과 조회

  • 결과 조회 메소드 호출 시 실제 데이터베이스 조회

  • com.mysema.query.Projectable → 결과 조회 API 정의

  • 결과 조회 메소드

    메소드설명
    uniqueResult()◾ 조회 결과가 한 건일 때 사용
    ◾ 조회 결과가 없는 경우 → null 반환
    ◾ 조회 결과가 하나 이상인 경우
       → com.mysema.query.NonUniqueRsultException 예외 발생
    singleResult()uniqueResult()와 동일
    ◾ 결과가 하나 이상인 경우 → 처음 데이터 반환
    list()◾ 결과가 하나 이상일 때 사용
    ◾ 결과가 없는 경우 → 빈 컬렉션 반환

    • 보통 uniqueResult()list() 사용
    • 파라미터로 프로젝션 대상을 전달


10.3.5 페이징과 정렬

💻 페이징과 정렬

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 사용
    • 쿼리 타입(Q)이 제공하는 asc(), desc() 사용
      • asc() : 오름차순
      • desc() : 내림차순
  • 페이징
    • offsetlimit을 적절히 조합하여 사용
      • offset : 결과의 시작 행
      • limit : 최대 결과 개수
    • restrict() 메소드에 파라미터로 com.mysema.query.QueryModifiers 사용 가능

      restrict() : limitoffset 함께 정의

💻 페이징과 정렬 QueryModifiers 사용

QueryModifiers queryModifiers = new QueryModifiers(20L, 10L);  //limit, offset;
List<Item> list = query.from(item)
                       .restrict(queryModifiers)
                       .list(item);

💻 페이징과 정렬 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 쿼리를 한 번 더 실행 후 SearchResults 반환
      SearchResults 객체에서 전체 데이터 수 조회 가능


10.3.6 그룹

  • groupBy : 그룹화
  • having : 그룹화된 결과 제한

💻 groupBy() 사용

List<Item> list = query.from(item)
                       .groupBy(item.price)
                       .having(item.price.gt(10000))
                       .list(item);


10.3.7 조인(Join)

  • 메소드 종류
    • innerJoin(join)
    • leftJoin
    • rightJoin
    • fullJoin
  • JPQL의 onfetch 조인도 사용 가능
  • 기본 문법 : 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);

💻 조인에 on 사용

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);

💻 from 절에 여러 조건(세타 조인) 사용

QOrder order = QOrder.order;
QMember member = QMember.member;

List<Order> list = query.from(order, member)
                        .innerJoin(order.member.eq(member))
                        .list(order);


10.3.8 서브 쿼리

  • 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);                    


10.3.9 프로젝션과 결과 반환

💡 프로젝션(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));
}

✅ 빈 생성(Bean population)

  • 쿼리 결과를 엔티티가 아닌 특정 객체로 받고 싶을 경우 사용
  • QueryDSL이 제공하는 객체 생성 방법
    • 프로퍼티 접근
    • 필드 직접 접근
    • 생성자 사용
  • 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)...


10.3.10 수정, 삭제 배치 쿼리

  • 영속성 컨텍스트를 무시하고 데이터베이스를 직접 쿼리함

✅ 수정 배치 쿼리

  • 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();


10.3.11 동적 쿼리

  • 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);


10.3.12 메소드 위임(Delegate methods)

  • 쿼리 타입에 검색 조건 직접 정의할 때 사용

💻 검색 조건 정의

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");
    //}
}
  • 정적(static) 메소드 생성 후, @com.mysema.query.annotations.QueryDelegate 어노테이션의 속성으로 기능 적용할 엔티티 지정
  • 정적 메소드의 파라미터
    • 첫번째 파라미터 → 대상 엔티티의 쿼리 타입(Q) 지정
    • 나머지 → 필요한 파라미터 정의

💻 쿼리 타입에 생성된 결과

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);
profile
기쁘게 코딩하고 싶은 백엔드 개발자
post-custom-banner

0개의 댓글