본문 바로가기
☕Java/JPA

Querydsl 주요 내용 정리

by 캔 2024. 3. 27.

 

Querydsl을 사용하면 컴파일 시점에 SQL 오류를 잡을 수 있음

 

Q클래스(타입)를 직접 생성(new)하거나 이미 생성돼 있는 객체를 사용하면 됨

cf) Q클래스는 메타모델 클래스라고도 함. 이는 Criteria API나 Querydsl에서 보편적으로 쓰이는 용어.

 

보통은 생성된 객체 사용하고, 같은 테이블을 조인해야 하는 경우에만 직접 작성

 

사용 방법(단건 조회)

JPAQueryFactory queryFactory; // 직접 생성하거나 스프링을 통해 주입

queryFactory.select(Q타입).from(Q타입).where(Q타입.필드.eq(조건)).fetchOne();

queryFactory.selectFrom(Q타입).where(Q타입.필드.eq(조건)).fetchOne();

queryFactory.selectFrom(Q타입).where(Q타입.필드.eq(조건).and(Q타입.필드.eq(조건))).fetchOne();

 

검색조건(BooleanExpression)

Q타입의 필드들은 아래와 같은 메서드를 내장하고 있으며 where() 인자에서 사용

  • eq(value)
  • ne(value)
  • eq(value).not()
  • isNotNull()
  • in(value...)
  • notIn(value...)
  • between(value1, value2)
  • goe(value)
  • gt(value)
  • loe(value)
  • lt(value)
  • like(value)
  • contains(value)
  • startsWith(value)
  • endWith(value)

where() 메서드 내 and 조건은 and()로 연결하여 사용하거나 쉼표로 나열하고 or 조건은 or() 메서드 사용

 

조회 메서드

  • fetch(): List 조회
  • fetchOne(): 단건 조회
  • fetchFirst(): limit(1).fetchOne()과 같음
  • fetchResults(): total, limit, offset 정보 및 List<T>를 담은 QueryResults 객체 반환. 페이징 정보 포함. total count 쿼리를 추가 실행. 즉, 조인 없으면 쿼리 2번 실행함. 복잡한 쿼리는 성능 최적화를 위해 fetchResults()를 쓰지 말고 데이터와 count를 별도로 조회하는 것이 좋음.
  • fetchCount(): count 쿼리로 변경해서 count 조회. deprecated됨.

 

정렬

orderBy() 메서드 인자에서 필드 뒤에 asc(), desc(), 그 뒤에 nullFirst()나 nullLast() 붙임

 

페이징

  • offset(value)
  • limit(value)

 

집계 함수(Aggregate Function)

필드 뒤에

  • sum()
  • avg()
  • count()
  • min()
  • max()

를 붙임

 

  • groupBy(필드명)
  • having(조건)

 

튜플

엔티티를 그대로 반환하지 않고 필드를 선택적으로 반환하려고 할 경우, Querydsl에서 제공하는 Tuple 객체로 반환

 

일반 조인

  • join(Q타입.필드명, Q타입) - 내부 조인(=innerJoin())
  • leftJoin(Q타입.필드명, Q타입) - left 조인. on 절 없이 연관 관계 사용해서 조인 가능.
  • rightJoin(Q타입.필드명, Q타입) - right 조인. on 절 없이 연관 관계 사용해서 조인 가능.
  • on() - 조인 조건

 

세타 조인

연관관계가 없는 테이블끼리 조인하기

 

from(테이블1, 테이블2).where(조건)

 

from 절에 여러 테이블 넣고 where 절에 연관관계가 없는 조건

 

on 사용 조인

inner join이면 where() 사용하나 on() 사용하나 차이가 없으므로 좀 더 익숙한 where() 쓰는 게 어떨까 함

 

left join이면 on() 사용해야 함

 

  • leftJoin(Q타입).on(조건) - left 조인.
  • rightJoin(Q타입).on(조건) - right 조인.

 

페치 조인

SQL에서 제공하는 기능은 아니고, SQL 조인 활용해서 연관된 엔티티를 한 번에 조회하는 기능

 

주로 성능 최적화에 사용

 

  • join(Q타입.필드명, Q타입).fetchJoin()

위와 같이 조인 메서드들 뒤에 fetchJoin() 메서드를 붙이면 됨

 

* 테스트 시에 @PersistencUnit EntityManagerFactory emf; 사용 emf.getPersistenceUnitUtils().isLoaded() 로딩 됐는지 여부 확인

  • fetchJoin()

 

서브쿼리

JPAExpressions로 서브쿼리문 만들 수 있음

 

* JPA 서브쿼리는 from 절에서 서브쿼리 사용이 불가능함. 그러므로 Querydsl도 마찬가지.

* 따라서 join으로 변경하거나 쿼리를 2번 분리하여 실행하거나 native SQL을 사용해야 한다.

* 애플리케이션 로직이나 프레젠테이션 로직에서 해야 할 부분은 분리해서 서브쿼리를 줄이자.

* 혹은 쿼리를 여러 번 실행하는 경우가 더 간단할 수도 있다.

 

case 문

필드에 아래 메서드를 붙여서 사용

  • when() - 조건
  • then() - 조건 충족 시 수행할 내용
  • otherwise() - 충족하는 조건 없을 경우 수행할 내용

* 분기 로직은 가급적 SQL이 아니라 애플리케이션 로직에서 해결하려고 하자.

 

상수

Expressions.constant(value) - 엔티티에서 선언되지 않은 필드로 상수를 출력할 때

 

문자 연결

필드에 다음 메서드를 붙여서 사용

  • concat()

 

문자열 변환

필드에 다음 메서드를 붙여서 사용

  • stringValue()

* enum 등 타입을 문자열로 변환해야 할 때 유용

 

프로젝션

프로젝션 대상이 하나(Q타입 하나 또는 Q타입 필드 하나)면 타입 지정 가능

 

둘 이상이면 튜플이나 DTO로 조회해야 됨

 

List<Tuple>은 tuple.get(Q클래스.필드)로 꺼낼 수 있음

 

* 튜플은 QueryDsl에서 사용하는 객체이기 때문에 데이터 계층을 벗어나서 사용은 지양하자.

 

방법 1(프로퍼티 접근법) - select() 메서드 인자에 Projections.bean(DTO 클래스, Q클래스.필드...) 사용 가능. DTO에는 기본 생성자, setter 필요. 또는 @Data 사용 필요.

 

방법 2(필드 직접 접근법) - select() 메서드 인자에 Projections.fields(DTO 클래스, Q클래스.필드...) 사용 가능. Getter, Setter 필요 없음.

 

방법 3(생성자 접근법) - select() 메서드 인자에 Projections.constructor(DTO 클래스, Q클래스.필드...) 사용 가능. Getter, Setter 필요 없음. 기본 생성자, 인수 있는 생성자 필요(생성자 인수 순서 맞춰야 함). 필드 이름 다를 경우 as() 사용. 서브쿼리 사용 시에는 ExpressionUtils로 쿼리 후 as() 메서드로 필드명 지정. 

 

다른 방법 - @QueryProjection

DTO 생성자에 @QueryProjection 부착하고 컴파일하면 Q클래스 생성됨. select() 메서드 인자에서 Q클래스를 생성(new)하여 사용 가능. Projections 사용 시 필드가 맞지 않는 등의 문제를 컴파일 타임에 찾을 수 없음. DTO가 Querydsl에 의존하게 된다는 문제점은 있음.

 

동적 쿼리

방법 1 - BooleanBuilder - BooleanBuilder 생성 후 조건 지정하고 결과물을 where 절에 추가. and(), or() 메서드로 조건 추가 가능

 

방법 2 - where 다중 파라미터 사용 - Predicate나 BooleanExpression(Predicate 구현체, and()로 조합 가능)을 반환하는 메서드를 추출하여 사용하고 그것을 where 절에 추가. 조건에 이름을 붙일 수 있어 가독성 좋아짐. 조건 메서드를 재활용할 수 있음. null 체크에 주의 필요.

 

* where 절에 null이 들어가면 그 조건은 무시된다.

 

벌크 연산(수정, 삭제)

update(Q클래스).set(Q클래스.필드, 변경할 내용).where(조건).execute()

delete(Q클래스).where(조건).execute()

 

벌크 연산 후에는 flush(), clear() 해줘야 조회 시 영속성 컨텍스트와 DB 간 불일치 없앨 수 있음

 

SQL function(DBMS 내장 함수들)

Expression.stringTemplate("function('replace', {0}, {1}, {2}", 필드1, 필드2, 필드3)

 

DBMS가 지원하는 함수여야 하고, 커스텀 함수의 경우 DB Dialect 클래스를 상속해서 수정해야 함

 

스프링 데이터 JPA와 Querydsl 같이 사용하기

커스텀 리포지터리 인터페이스와 구현체(커스텀 리포지터리 인터페이스를 구현하고 ~Impl로 끝나는 클래스)를 만들어서 

스프링 데이터 JPA 리포지터리가 상속하도록 하면 됨

 

이 커스텀 리포지터리 인터페이스 구현체에 Querydsl을 구현해 놓으면 이제 기존 스프링 데이터 JPA 리포지터리 만으로도 Querydsl 메서드를 사용할 수 있게 됨

 

* 그러나 특정 도메인에만 특화된 Querydsl 메서드라면 굳이 커스텀 리포지터리 만들 필요 없이 따로 Querydsl 리포지터리 만들어서 사용하는 것도 좋음.

 

페이징이 있는 쿼리

기존 Querydsl 메서드에 스프링 데이터 JPA의 페이징 인터페이스인 Pageable을 인수로 받아서 사용

 

pageabled의 size와 offset으로 limit와 offset을 설정해 준 뒤, fetchResults()로 조회한 결과(List<T>, total)를 바탕으로 PageImpl(Page의 구현체. List<T>, Pageable, total을 받아 생성)을 반환하면 됨

 

* fetchResults()의 경우, where 절이나 join 대상에 모두 count를 조회하기 때문에 성능 최적화를 위해서는 fetch()로 조회 결과를 가져오고 count 쿼리를 별도로 가져오는 것을 권장함

 

페이징 성능 최적화

페이지 시작이면서 content 사이즈가 페이지 사이즈보다 작을 때, 마지막 페이지일 때(offset + content 사이즈) count 쿼리를 생략할 수 있음

 

위에서 content와 count를 분리하여 조회했을 때 PageImpl이 아닌 PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount)를 반환하도록 하면 됨

 

정렬

스프링 데이터 JPA Pageable은 Sort는 사용 조건이 복잡해지면 사용이 어려워지므로 Querydsl에서 제공하는 OrderSpecifier를 사용함

 

orderBy() 메서드에 OrderSpecifier를 생성(new) 사용할 수 있음

 

보조 클래스

QuerydlslPredicateExecutor<T> - 이 인터페이스를 상속하는 리포지터리는 findAll() 메서드 만으로 조건 절을 자유롭게 넣어 사용이 가능함

 

* 조인이 필요한 쿼리는 사용이 불가하여 한계가 있음. 서비스가 Querydsl에 종속되는 문제가 있음.

 

QuerdyslWebSupport - 컨트롤러 메서드에 @QuerydslPredicate(root = 엔티티클래스) 메서드를 붙인 Predicate 인수를 받으면 쿼리 파라미터를 Predicate에 바인딩해 줌. equals, contains, in 조건 정도만 지원. 조건 커스텀이 매우 어려워 한계점이 큼.

 

QuerydslRespositorySupport - Querydsl 라이브러리를 사용하는 리포지터리 구현체가 상속하면 좋은 추상 클래스. 부모 클래스에 엔티티 클래스를 설정해 놓으면 JPAQueryFactory 호출 없이 메서드만(from() 등)으로 쿼리를 만들 수 있음. getQueryDsl().applyPagination(pageable, jpaQuery) 메서드로 페이징도 가능.(그러나 정렬은 불가)