JPA에서 동적쿼리 작성 : QueryDSL

TopOfTheHead·2025년 11월 12일

Spring JPA

목록 보기
8/9

QueryDSL
QClass를 기반으로 HQL ( Hibernate Query Language )를 모든 유형의 Data Type에 맞게 동적 Query를 생성 및 관리하는 라이브러리
JPA에서 JPQL을 편하게 사용하는 용도로 활용됨
▶ 현재는 jooq를 주로 사용

QueryDSLJavaEntity객체를 기반으로 JPQL를 생성
영속성 컨텍스트DB Entity들을 결과값에 포함하여 반환
▶ 다른 SQLDB Table을 기반으로 Query를 작성

자바 소스코드를 통해 Entity를 기반으로하는 Query를 작성하여 어플리케이션 구동 전 컴파일 시점에서 컴파일 오류를 사전에 방지 가능
▶ 다른 Query 언어 ( JPQL , MyBatis )등은 Query문자열로 작성 하여 컴파일 단계에서 오류를 찾을 수 없음.

컴파일소스코드 -> JPQL -> SQL 과정으로 변환

JPA 외에도 SQL, MongoDB 등의 다양한 SQL, NoSQL에 대해서도 서비스를 제공

  • QueryDSL 사용방법
    。다음 명칭의 클래스 & 인터페이스 조합으로 QueryDSL을 사용
  1. {도메인명}RepositoryCustom 인터페이스 + {도메인명}RepositoryImpl 클래스구현체
    。일반적으로 주로 사용

  2. {도메인명}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 클래스 정의
    。내부에 JPAEntityManagerfield로 선언 및 @PersistenceContext를 통해 영속성컨텍스트에서 의존성주입을 수행
    ▶ 해당 @PersistenceContextjarkarta어노테이션으로서 스프링빈과 관련없음.

    。주입된 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);
}
  • 기존 JPARepository 인터페이스{도메인명}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를 수행

    빌드하여 생성한 QClassQEntity객체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
QueryDSLJPAQuery객체를 생성하는 팩토리 역할의 클래스
chain으로 Query를 수행 후 결과를 제공

QEntity객체를 기반으로 동적으로 JPAQuery를 생성

new JPAQuery<>()로 생성할 필요없이 jpaQueryFactory.selectFrom(qUser).where(...)…fetch()Query를 수행

생성자JPAEntityManager객체를 받아서 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에서 선언한 대상이 되는 EntityQClass객체Join을 수행할 QClass객체Join을 수행
      Join을 수행할 Entity연관관계( @OnetoMany 등 )이 사전에 정의된 경우 자동으로 mapping이 수행되어 on()을 정의하지 않아도된다.

      JPAQuery객체.join(QClass2객체).on(부모QClass객체.PK필드.eq(자식QClass객체.부모Entity.PK필드))
      。 이때 Join을 수행할 Entity연관관계가 사전에 Mapping이 되지않은경우 on을 통한 Entitymapping필드를 정의

      leftJoin() , rightJoin등이 존재


  • 조건절 관련
    • JPAQuery객체.where(BooleanExpression객체 or 조건)
      필터링 조건을 설정
      BooleanExpression객체 = null인 경우 필터링을 수행 X
    public 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을 선언한 생성자 메서드를 정의 후 빌드 시 해당 DTOQClass 객체가 생성.

// 검색결과
		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 TypeList<EntityType> 객체로 반환

  • @QueryProjection
    QueryDSL에서 제공하는 어노테이션으로서 DTO 또는 VO생성자 메서드에 선언 시 컴파일 시점에서 해당 DTOQEntity Class를 자동생성하여 QueryDSL에 사용할 수 있도록 설정

Referency Type 뒤에 s가 붙은 경우 해당 Data typesupport 역할의 서포터 클래스 ( Collecions, Arrays, ... )
ex ) Object -> Objects
String -> Strings

profile
공부기록 블로그

0개의 댓글