QueryDSL

원종서·2022년 2월 12일
1

JPA

목록 보기
10/13

QueryDSL

쿼리를 문자 아닌 코드로 작성하고, 쉽고 간결하며 그 모양도 쿼리와 비슷하게 개발할 수 있는 프로젝트

  <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>
    </dependency>

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

in 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.JPAAnnotationProcessor</processor>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

        </plugins>
    </build>

메이븐 컴파일하면 target/generated-sources/java/ 폴더에 메타모델(Q) 클래스 파일들이 생성된다.

시작

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

기본 Q 생성

쿼리타입 (Q)은 사용하기 편리하도록 메타 모델 클래스 안에 스태틱 인스턴스를 보관하고 있다.

public class QMember extends EntityPathBase<Member> {

    public static final QMember member = new QMember("member1");

따라서

QMember qMember= new jpabook.queryDSL.QMember("m");// 직접 별칭 생성
QMember qMember1 = jpabook.queryDSL.QMember.member; // 기본 인스턴스 사용

두개 중 하나를 이용해 쿼리타입으로 사용하면 된다.

검색 조건 쿼리

  QItem item = QItem.item;
  List<Item> items = query.from(item)
  				.where(item.name.eq("상품A").and(item.price.gt(20000)).list(item);

->

select item from Item item where item.name =?1 and item.price > ?2

쿼리타입 필드는 필요한 대부분의 메소드를 명시적으로 제공한다.

item.price.between(100,150);
item.name.contains("상품A"); // like '%상품A%'
item.name.startsWith("고급"); // lite '고급%'

결과 조회

쿼리 작성 후 결과 조회 메서드 호출 시 실제 데베를 조회한다.
1. uniqueResult() : 조회 결과가 한 건 일때 사용하고, 없으면 null 을 반환, 한 건 이상이면 예외발생
2. singleResult(): uniqueResult와 동일하지만 하나 이상의 데이터가 검출되면 첫번째 데이터를 반환한다.
3. list() : 결과가 하나 이상일때 사용하며, 없을시 빈 배열을 반환한다.

페이징 정렬

QItem item = QItem.item;
query.from(item)
	.orderBy(item.price.desc(), item.stockQuantity.asc())
    .offset(10).limit(20)
    .list(item)

페이징은 restrict() 메서드에 QueryModifiers 를 파라미터로 사용할 수 있다.

QueryModifiers qm = new QueryModifier(20L,10L); // limit, offset

List<Item> items = query.from(item).restrict(qm).list(list);

실제 페이징을 하려면 전체 데이터 수를 알아야한다. 이때는 list()대신 listResults() 를 사용한다.

listResults() 카운트 쿼리를 한번 더 사용한다.

SearchResults<Item> results = query.from(item)
	.where(item.price.gt(1000))
    .offset(10).limit(20)
    .listResults(item);
    
long total = results.getTotal();
long limit = results.getLimit();
long offset = results.getOffset();
List<Item> items =results.getResults();

그룹

groupBy() 를 사용하고 그룹된 결과를 제한하려면 having()을 사용한다.

query.from(item)
	.groupBy(item.price)
    .having(item.price.gt(10000))
    .list(item);
    

조인

조인은 innerJoin(join), leftJoin, rightJoin, fullJoin, on,fetch 사용 가능하다.

조인은 첫번째 파라미터에 조인 대상을 지정하고 , 두번째 파라미터에 별칭으로 사용할 쿼리 타입이다.

기본조인
QOrder order = QOrder.order;
QMember member = QMember.member;
QOrderItem orderItem = QOrderItem.orderItem;

query.from(order)
	.join(order.member ,member)
    .leftJoin(order.orderItems, ordreItem)
    .list(order);
    
// 조인 on 사용

query.from(order)
	.leftJoin(order.orderItems, orderItem)
    .on(orderItem.count.gt(2))
    .list(order);
//fetch join

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

서브쿼리

서브쿼리의 결과가 하나면 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);

프로젝션 결과 반환

프로젝션 대상이 one

QItem item = QItem.item;
List<String> result = query.from(item).list(item.name);

여러 컬럼 반환과 튜플

프로젝션 대상으로 여러 필드를 선택하면 QueryDSL 은 ㄱ본적으로 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) {
	soutv("name =" + tuple.get(item.name));
    soutv("price = " + tuple.get(item.price));
}

빈 생성

쿼리 결과가 엔티티가 아닌 특정 객체로 받고 싶으면 빈 생성 기능을 사용함.

QueryDSL 의 객체를 생성하는 다양한 방법 제공
1. 프로퍼티 접근
2. 필드 직접 접근
3. 생상저 접근

public class ItemTDO{
	String username;
    int price;
    
    // 기본생성자
    // 모든 필드 생성자
    
    // 개터 셋터
}
    

1. 프로퍼티 접근 (Setter)

QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
	Projections.bean(ItemDTO.class, item.name.as("username"), item.price));

2. 필드 접근

QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
	Projections.fields(ItemDTO.class, item.name.as("username"), item.price));

2. 생성자 접근

QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
	Projections.constructor(ItemDTO.class, item.name, item.price));

DISTINCT

quert.distinct.from(item)...

수정 삭제 배치 쿼리

QueryDSL 도 수정, 삭제 같은 배치 쿼리를 사용한다.
JPQL 배치 쿼리와 같이 영속성 컨텍스트를 무시하고 데베에 직접 쿼리를 날린다.

// 수정,

QItem item= QItem.item;

JPAUpdateClause updateClause = new JPAUpdateClause(em,item);
long count = updateClause.where(item.name.eq("책A"))
.set(item.price, item.price.add(100))
.excute();

책A의 가격을 100원 더한다.

// 삭제,
QItem item= QItem.item;
JPADeleteClause deleteClause = new JPADeleteClause(em,itme);
long count = deleteClause.where(item.name.eq("책A")).excute();

동적 쿼리

SearchParam param = new SearchParam();
param.setName("bookA");
param.setPrice(1000);

QItem item = QItem.item;

BooleanBuilder buildere = 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)
    .lite(list);

메소드 위임

쿼리 타입에 검색 조건을 직접 정의할 수 있다.

// 검색 조건 정의

import com.mysema.query.annotations.QueryDelegate;
import com.mysema.query.types.expr.BooleanExpression;

public class ItemExpression {

	@QueryDelegate(Item.class) // 엔티티가 아니라 자바 기장 내장 타입도 가능하다.
    public static BooleanExpression isExpensive(QItem item , Integer price) {
    	return item.price.gt(price);
   }
}

클래스 내에 정적 메소드를 만들고 @QueryDelegate 애노테이션에 속성으로 이 기능을 적용할 엔티티를 지정한다.
정적 메서드의 첫 번째 파라미터에는 대상 엔티티의 쿼리타입을 지정하고 나머지는 필요한 파라미터를 정의함. 후 메이븐을 컴파일하면 아래의 메서드가 쿼라타입에 추가된다

public class QItem extends EntityPathBase<Item> {
	...
    public com.mysema.query.types.expr.BooleanExpression isExpensive(Integer price) {
        return ItemExpression.isExpensive(this, price);
    }

이후 쿼리타입에 정의한 기능이 추가된 것을 확인 할 수 있고 , 사용할 수 있다.

query.from(item).where(item.isExpensive(30000)).list(item);

0개의 댓글