QueryDSL

 

querydsl

 

프로젝션과 반환

프로젝션이란?
질의하는 대상이다.

select member.name from member처럼 간단한 질의의 프로젝션은 String, 하나의 타입만을 가진다. 물론 select member from member 역시 하나의 타입, Member를 가진다.

두 개 이상의 타입을 가지면 querydsl에서 제공하는 Tuple 타입을 사용하게 된다.
Tuple타입을 persistence layer 이외에서 사용하는 것은 좋지 않다.
이를 변환할 DTO를 정의하여 사용한다.

DTO 조회

그럼 이제 DTO로 조회하는 법을 알아보자.

순수 JPA

순수 JPA로 작성하면 다음과 같다.

List<MemberDto> result = em.createQuery(
   "select new study.querydsl.dto.MemberDto(m.username, m.age) " +
   "from Member m", MemberDto.class
   ).getResultList();

MemberDto는 멤버 변수로 String username과 int age를 갖는다.
특이한 점은 new study.querydsl.dto.MemberDto() String으로 DTO 생성자를 부르는 것이다.
덕분에 패키지까지 넘겨줘서 불편하고 오로지 생성자 방식만 지원한다.

QueryDSL Bean Population

위 방법을 개선하여 querydsl은 어떻게 구현하나 알아보자.

com.querydsl.core.types.Projections 을 이용한다.

3가지 방법이 존재한다.

  • 프로퍼티 접근: 기본 생성자를 만들고 setter로 멤버의 값 할당
  • 필드 직접 접근: 멤버 변수에 직접 접근(private이여도 가능)
  • 생성자 사용

Projections.bean()

List<MemberDto> result = queryFactory
   .select(Projections.bean(MemberDto.class,
       member.username,
       member.age))
   .from(member)
   .fetch();

Projections.fields()

List<MemberDto> result = queryFactory
   .select(Projections.fields(MemberDto.class,
       member.username,
       member.age))
   .from(member)
   .fetch();

만약 Entity의 column이 DTO의 멤버 변수와 이름이 다르면
member.username.as("name")처럼 as를 통해 맞춰준다.

또한 서브 쿼리를 넘긴다면 별칭을 정해서 넘겨줘야 한다.
그래야 멤버 변수를 인식하고 값을 전달할 수 있다.
예시는 다음과 같다.

List<UserDto> fetch = queryFactory
     .select(Projections.fields(UserDto.class,
             member.username.as("name"),
             ExpressionUtils.as(
                     JPAExpressions
                               .select(memberSub.age.max())
                               .from(memberSub), "age")
                    )
             )
 .from(member)
 .fetch();

ExpressionUtils.as에서 두 번째 인수 "age"로 UserDto의 멤버 변수 "age"와 맞췄다.

Projections.constructor()

List<MemberDto> result = queryFactory
   .select(Projections.constructor(MemberDto.class,
       member.username,
       member.age))
   .from(member)
   .fetch();

생성자는 fields와는 다르게 타입으로 값을 할당하기 때문에 멤버 변수 이름을 신경쓰지 않아도 된다. 생성자를 호출하듯이 사용하면 된다.

QueryProjection

@QueryProjection 을 DTO 생성자에 추가한다.
그리고 ./gradlew comileQuerydsl 혹은 Tasks -> other -> compileQuerydsl
로 DTO Q-Type 생성

이 방법의 장점은 Projections이 컴파일에 잡지 못하는, 타입 체크를 통해 구문 오류를 잡을 수 있다.
그러나 DTO가 querydsl을 의존하게 된다는 점과 DTO의 Q 를 생성해야하는 단점이 있다.

예시는 다음과 같다.

List<MemberDto> result = queryFactory
   .select(new QMemberDto(member.username, member.age))
   .from(member)
   .fetch();

동적 쿼리

동적 쿼리란?
query에 필요한 변수들이 optional할 때, null이면 포함시키지 않고 null 아니면 질의에 포함시킨다. querydsl이 아니면 if-else로 처리할 수 있으나 조건이 많아질수록 복잡해진다.

동적 쿼리는 다음의 두 가지 방식으로 해결할 수 있다.

  • BooleanBuilder
  • Where 다중 파라미터 사용

BooleanBuilder

private List<Member> searchMember1(String usernameCond, Integer ageCond) {
     BooleanBuilder builder = new BooleanBuilder();
     if (usernameCond != null) {
         builder.and(member.username.eq(usernameCond));
     }
     if (ageCond != null) {
         builder.and(member.age.eq(ageCond));
     }
     return queryFactory
         .selectFrom(member)
         .where(builder)
         .fetch();
}

우선, BooleanBuilder를 생성한다.
조건문을 사용해서 null을 걸러내고 builder.and()에 조건을 전달한다.
마지막으로 where에 builder를 포함시켜 질의한다.

참고

BooleanBuilder를 생성할 때, 반드시 포함되어야할 조건문을 포함시켜 생성할 수 있다.
해당 조건문의 변수는 null이 아닐 수 밖에 없도록 검증된 상태여야한다.

where의 다중 파라미터 사용

권장하는 방법.

QueryFactory의 where의 파라미터로 null이 들어가면 조건에서 제외되게 구현된 것을 활용

private List<Member> searchMember2(String usernameCond, Integer ageCond) {
 return queryFactory
     .selectFrom(member)
     .where(usernameEq(usernameCond), ageEq(ageCond))
     .fetch();
}

private BooleanExpression usernameEq(String usernameCond) {
     return usernameCond != null ? member.username.eq(usernameCond) : null;
}
private BooleanExpression ageEq(Integer ageCond) {
     return ageCond != null ? member.age.eq(ageCond) : null;
}

위 예시에서 usernaeEq를 IDE refactoring으로 생성하면 Predicator 인터페이스로 생성될텐데, BooleanExpression으로 바꿔줘야만 조건문의 체인닝이 가능하다.

수정, 삭제를 한번에

쿼리 한 번으로 대량 데이터 수정

JPA는 dirty checking으로 entity의 변화를 감지하여 트랜잭션 커밋을 한다.
각 entity마다 query가 전송되기 때문에 bulk update에서는 성능이 떨어질 수 있다.
querydsl로 개선해보자. 아래를 벌크 연산이라고 부른다.

long count = queryFactory
   .update(member)
   .set(member.username, "비회원")
   .where(member.age.lt(28))
   .execute();

JPAQueryFactory의 update로 시작하는 것과 execute()로 반환하는 것을 기억하자.

그리고 반드시 기억해야할 점.
JPQL 배치와 마찬가지로 영속성 컨텍스트를 무시하고 DB에 전송되는 쿼리이기 때문에 위 예시만으로 실행하면 영속성 컨텍스트와 DB가 동기화되지 않은 문제가 생긴다.

그래서 벌크 연산 이후에는 영속성 컨텍스트를 초기화를 하는 것이 안전하다.

em.flush();
em.clear();

'스프링' 카테고리의 다른 글

QueryDSL 비벼먹기  (0) 2023.10.24
QueryDSL 쪄먹기  (1) 2023.10.23
QueryDSL 다져먹기  (0) 2023.10.21
QueryDSL 먹어버리기  (0) 2023.10.20
테스트 코드 정의와 이점, 왜 해야 할까? 그리고 무엇을 해야 할까?  (0) 2023.07.08