QueryDSL
。QClass를 기반으로HQL( Hibernate Query Language )를 모든 유형의Data Type에 맞게동적 Query를 생성 및 관리하는라이브러리
▶JPA에서JPQL을 편하게 사용하는 용도로 활용됨
▶ 현재는jooq를 주로 사용
。QueryDSL은Java의Entity객체를 기반으로JPQL를 생성
▶영속성 컨텍스트내DB Entity들을 결과값에 포함하여 반환
▶ 다른SQL은DB Table을 기반으로Query를 작성
。자바 소스코드를 통해Entity를 기반으로하는Query를 작성하여어플리케이션구동 전컴파일 시점에서컴파일 오류를 사전에 방지 가능
▶ 다른Query 언어(JPQL,MyBatis)등은Query를문자열로 작성 하여컴파일단계에서 오류를 찾을 수 없음.
。컴파일시소스코드->JPQL->SQL과정으로 변환
。JPA외에도SQL, MongoDB등의 다양한SQL,NoSQL에 대해서도 서비스를 제공
QueryDSL사용방법
。다음 명칭의클래스&인터페이스조합으로QueryDSL을 사용
{도메인명}RepositoryCustom 인터페이스+{도메인명}RepositoryImpl 클래스구현체
。일반적으로 주로 사용
{도메인명}Query 클래스
。간단한 경우
QueryDSL사용 전 초기 설정
QueryDSL관련라이브러리 의존성정의// QueryDSL 용 의존성 라이브러리 implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta' annotationProcessor 'jakarta.annotation:jakarta.annotation-api' annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
QEntity클래스를 생성하기위해어플리케이션빌드수행
▶ 다음처럼
Q{EntityClass}의QClass가 생성된 것을 확인가능.
@Configuration 클래스정의
。내부에JPA의EntityManager을field로 선언 및@PersistenceContext를 통해영속성컨텍스트에서의존성주입을 수행
▶ 해당@PersistenceContext는jarkarta의어노테이션으로서스프링빈과 관련없음.
。주입된EntityManager을 기반으로@Bean Method를 통해QueryDSL을 수행하는JPAQueryFactory 객체를 생성 및Spring Bean으로서Spring Context에 등록@Configuration public class QueryDslConfiguration { //。내부에 `JPA`의 `EntityManager 객체`을 `field`로 선언 및 // `@PersistenceContext`를 선언하여 `Spring Context`에서 주입 @PersistenceContext private EntityManager em; //주입된 `EntityManager`을 기반으로 `@Bean Method`를 통해 `JPAQueryFactory 객체`로 생성하여 // `Spring Bean`으로서 `Spring Context`에 등록 @Bean public JPAQueryFactory jpaQueryFactory(){ return new JPAQueryFactory(em); } }
{도메인명}RepositoryCustom 인터페이스+{도메인명}RepositoryImpl 클래스구현체생성
。Order라는도메인아래Repository 인터페이스를 포함한 다음인터페이스와구현체를 생성
{도메인명}RepositoryCustom 인터페이스정의
。내부구현 메서드는{도메인명}RepositoryImpl 클래스구현체에 의해메서드 오버라이딩되어재정의
。해당 `메서드는Repository 인터페이스에확장되어Service Class에서Rpository 인터페이스 구현체에 의해 사용public interface OrderRepositoryCustom { public Page<OrderResponse.Search> search(String keyword, Pageable pageable); }
- 기존
JPA의Repository 인터페이스에{도메인명}RepositoryCustom을확장하도록 설정
。설정할 경우@Service Class에서도Repository 인터페이스 구현체에서 해당{도메인명}RepositoryCustom 인터페이스에서사용자 정의된메서드를 사용가능.
。인터페이스의 경우다중상속을 수행할 수 있는 특징이 존재public interface OrderRepository extends JpaRepository<OrderEntity,Long> , OrderRepositoryCustom{ }
{도메인명}RepositoryCustom를 구현하는{도메인명}RepositoryImpl 클래스구현체정의 및동적 JPAQuery를 수행하는메서드로오버라이딩
。JPAQueryFactory 객체를field에 선언
▶config/QueryDslConfiguration에서EntityManager를 기반으로 생성하여Spring Bean으로 등록한JPAQueryFactory객체를주입
▶ 주입된 해당JPAQueryFactory객체로QEntity객체를 기반으로동적으로JPAQuery를 생성하여Query를 수행
。빌드하여 생성한QClass인QEntity객체를Field에 선언
▶Query작성 시 사용@Repository @RequiredArgsConstructor public class OrderRepositoryImpl implements OrderRepositoryCustom{ // QueryDslConfiguration 클래스에서 등록한 JPAQueryFactory의 스프링빈을 주입 private final JPAQueryFactory jpaQueryFactory; // 빌드된 QEntity객체를 import해서 Query 작성 시 사용 private final QOrderEntity qOrder = QOrderEntity.orderEntity; private final QOrderProductEntity qOrderProduct = QOrderProductEntity.orderProductEntity; private final QProductEntity qProduct = QProductEntity.productEntity; @Override // 메서드 오버라이딩 public Page<OrderResponse.Search> search( String keyword, Pageable pageable ){ var booleanBuilder = new BooleanBuilder() .and(containsProductName(keyword)) // 검색결과 var result = jpaQueryFactory // 클라이언트에게 반환하므로 OrderEntity 대신 DTO를 생성하여 반환 .select(new QOrderResponse_Search( qOrder.id, qOrder.receiver.name, qProduct.name, qOrderProduct.quantity, Expressions.asNumber(0L), qOrder.orderStatus, qOrder.createdAt )) .from(qOrder) .join(qOrderProduct).on(qOrderProduct.order.id.eq(qOrder.id)) .join(qProduct).on(qOrderProduct.product.id.eq(qProduct.id)) .where(booleanBuilder) .orderBy(qOrder.id.desc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); // 총 갯수 // 이미 Entity간 연관관계가 정의되있으므로 on을 생략해도된다 var total = jpaQueryFactory .select(qOrder.id) .from(qOrder) .join(qOrderProduct) .join(qProduct) .where(booleanBuilder) .fetch().size(); // PageImpl : Page 구현체 Page<OrderResponse.Search> page = new PageImpl<OrderResponse.Search>(result, pageable, total); return page; } public BooleanExpression containsProductName(String keyword){ // keyword가 null이거나 ""인 경우 BooleanExpression = null 반환 return (Strings.isNotBlank(keyword))? qProduct.name.containsIgnoreCase(keyword) : null; } }
Service Class에서Repository 인터페이스 구현체를 통해 구현된메서드를 사용
。해당Repository 인터페이스에서 해당RepositoryCustom 인터페이스를확장하므로 해당 구현된메서드를 사용 가능
。Repository 인터페이스가{도메인명}RepositoryCustom 인터페이스를 확장하므로Repository 구현체에서 해당QueryDSL이 구현된메서드를 사용가능private final OrderRepository orderRepository; @Override public Page<OrderEntity> search(Pageable pageable,String keyword){ return orderRepository.search(keyword,pageable); }
QClass
。특정Entity 클래스의메타정보를 포함하여동적쿼리생성 시Type Safety를 보장하는클래스
▶QClass를 통해Entity 클래스 타입에 대한Type Safety를 보장하여컴파일러에 의해오류를 검출하면서동적 Query를 생성 가능
。QueryDSL 의존성 라이브러리를 정의 후어플리케이션 빌드시QEntity클래스명으로Q클래스객체가.build 디렉토리내에서 생성
▶selectFrom(QEntity클래스객체)로Query를 수행하여Entity에 대한Query를 수행
BooleanExpression
。QueryDSL에서조건 연산을 표현하는객체
▶Expression<Boolean>을 구현
BooleanBuilder
。BooleanExpression을 연쇄적으로누적한동적 쿼리를 생성하여 전달하는Builder 클래스
JPAQueryFactory
。QueryDSL의JPAQuery객체를 생성하는팩토리역할의클래스
▶chain으로Query를 수행 후 결과를 제공
。QEntity객체를 기반으로동적으로JPAQuery를 생성
。new JPAQuery<>()로 생성할 필요없이jpaQueryFactory.selectFrom(qUser).where(...)…fetch()로Query를 수행
。생성자에JPA의EntityManager객체를 받아서JPAQueryFactory객체를 생성 후Spring Bean으로 등록하여 활용JPAQuery query = jpaQueryFactory .select(qOrder) .from(qOrder) .offset(pageable.getOffset()) .limit(pageable.getPageSize()); List<OrderEntity> result = query.fetch(); return result;。해당
객체를 기반으로Query를 수행하여 결과를DB Entity로 획득
▶Query대상은QEntity객체
JPAQueryFactory문법
select관련
JPAQuery객체.select(QClass객체.필드)
。선택적으로 반환받기를 원하는Field를 지정
JPAQuery객체.selectFrom(QClass객체)
。select() + from()의 축약표현
▶ 모든property를 포함하는Entity객체를 반환
from,join관련
JPAQuery객체.from(QClass객체)
。영속성 컨텍스트로부터Entity를 조회할QEntity클래스를 지정
JPAQuery객체.join(QClass2객체)
。from또는join에서 선언한 대상이 되는Entity의QClass객체와Join을 수행할QClass객체간Join을 수행
▶Join을 수행할Entity간연관관계(@OnetoMany등 )이 사전에 정의된 경우 자동으로mapping이 수행되어on()을 정의하지 않아도된다.
JPAQuery객체.join(QClass2객체).on(부모QClass객체.PK필드.eq(자식QClass객체.부모Entity.PK필드))
。 이때Join을 수행할Entity간연관관계가 사전에Mapping이 되지않은경우on을 통한Entity간mapping할필드를 정의
。leftJoin(),rightJoin등이 존재
조건절관련
JPAQuery객체.where(BooleanExpression객체 or 조건)
。필터링 조건을 설정
▶BooleanExpression객체 = null인 경우필터링을 수행 Xpublic BooleanExpression containsProductName(String keyword){ return (Strings.isNotBlank(keyword))? qProduct.name.containsIgnoreCase(keyword) : null; }▶ 다음처럼
필터링을 수행할키워드 = null인 경우필터링을 수행하지 않도록 적용가능
。.and(BooleanExpression객체),.or(BooleanExpression객체)의BooleanExpression을 추가설정하여 추가필터링을 설정가능// (age > 20 AND status = ACTIVE) OR name = 'kim' BooleanExpression cond = member.age.gt(20) .and(member.status.eq(Status.ACTIVE)) .or(member.username.eq("kim")); .where(cond)
fetch관련
fetch()
。Query 결과를List<Entity클래스>로 반환
fetchOne()
。단일 Query 결과를Entity클래스 객체로서 반환
▶Query 결과가 없으면null,2건이상이면NonUniqueResultException발생
fetchFirst()
。Query 결과중 첫번째를Entity클래스 객체로 반환
▶limit 1
페이징처리관련
。Pageable 객체에 포함된offset와페이지 내 data 수를 통해페이지 범위의paging처리한데이터를 가져올 수 있음.
limit
。결과에서 반환될데이터수를 지정
offset
。시작범위로부터 설정된 값만큼 떨어진 데이터를 반환하도록 설정.orderBy(qOrderProduct.id.desc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize())
QueryDSL에서 활용할DTO를 정의하는 방법
。해당주문상품 Entity 객체를 그대로 반환하는 경우 해당Entity 객체와연관관계에 존재하는Entity가 같이 반환되므로 필요한Field만축약하여 담는 용도의POJO객체를 생성
▶JPAQueryFactory의.select()절에 결과를 담을DTO용도의QClass객체의생성자에 전달public interface OrderResponse { record Search( Long id, String receiverName, String productName, Long quantity, Long totalPrice, OrderStatus status, LocalDateTime createdAt ) { @QueryProjection public Search { } } }。
DTO내@QueryProjection을 선언한생성자 메서드를 정의 후빌드시 해당DTO의QClass 객체가 생성.// 검색결과 List<OrderResponse.Search> result = jpaQueryFactory .select(new QOrderResponse_Search( qOrder.id, qOrder.receiver.name, qProduct.name, qOrderProduct.quantity, Expressions.constant(0L), qOrder.orderStatus, qOrder.createdAt )) .from(qOrder) .join(qOrderProduct).on(qOrderProduct.order.id.eq(qOrder.id)) .join(qProduct).on(qOrderProduct.product.id.eq(qProduct.id)) .where(booleanBuilder) .orderBy(qOrder.id.desc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch();。
JPAQueryFactory의.select()절에 결과를 담을DTO용도의QClass객체의생성자에Mapping하면서 전달
▶ 이후 결과는 해당DTO Type의List<EntityType> 객체로 반환
@QueryProjection
。QueryDSL에서 제공하는어노테이션으로서DTO또는VO의생성자 메서드에 선언 시컴파일시점에서 해당DTO의QEntity Class를 자동생성하여QueryDSL에 사용할 수 있도록 설정
Referency Type뒤에s가 붙은 경우 해당Data type의support역할의서포터 클래스(Collecions,Arrays, ... )
ex )Object->Objects
String->Strings