본문 바로가기
JPA

Querydsl - 2편

by 성건희 2022. 2. 8.
반응형

프로젝션

프로젝션이란 select 대상을 지정하는 것이다.

프로젝션 대상이 하나인 경우

List<String> result = queryFactory.select(member.username)
                                  .from(member)
                                  .fetch();
List<Member> result = queryFactory.select(member)
                                  .from(member)
                                  .fetch();
  • 프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있다.
  • 프로젝션 대상이 둘 이상이면 튜플이나 DTO 로 조회

튜플 조회

프로젝션 대상이 둘 이상일 때 사용

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

for (Tuple tuple : result) {
    String username = tuple.get(member.username);
    Integer age = tuple.get(member.age);
}
  • tuple.get() 을 통해 원하는 데이터를 쉽게 가져올 수 있다.

참고로, 튜플은 repository 계층에서 쓰는건 괜찮지만, controller 나 service 계층에서 쓰는것은 좋은 설계가 아니다.
따라서 repository 안에서만 쓰고 반환할때는 튜플을 DTO 로 변환해서 반환하는 것을 권장한다.

DTO 조회

순수 JPA 에서 DTO 조회

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

for (MemberDto memberDto : memberDtoList) {
    System.out.println("memberDto = " + memberDto);
}
  • 순수 JPA 에서 DTO 조회 시 new 명령어를 사용해야함
  • DTO 패키지 이름을 다 적어줘야해서 지저분함
  • 생성자 방식만 지원함

Querydsl 빈 생성 (Bean population)
결과를 DTO 반환할 때 사용. 다음 3가지 방법 지원


1) 프로퍼티 접근 - setter

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

for (MemberDto memberDto : result) {
    System.out.println("memberDto = " + memberDto);
}

참고로 Querydsl 이 기본 생성자로 객체를 만든 후 Setter 주입을 하므로 MemberDto 에 기본 생성자가 있어야한다.

2) 필드 직접 접근

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

for (MemberDto memberDto : result) {
    System.out.println("memberDto = " + memberDto);
}

3) 생성자 사용

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

for (MemberDto memberDto : result) {
    System.out.println("memberDto = " + memberDto);
}

생성자를 통해 객체를 만드므로 파라미터의 순서에 주의해서 넣어주어야 한다.

별칭이 다른 경우
Member 의 속성 명은 username, UserDto 의 속성 명은 name 인 경우,
이름이 맞지않아 값이 null 로 들어가는 문제가 있다.
이럴때는 아래와 같이 as 별칭이나 ExpressionUtils.as(source, alias) 사용하면 된다.

  • as 별칭 : 필드에 별칭 사용
  • ExpressionUtils.as(source, alias) : 필드나 서브쿼리에 별칭 사용
List<UserDto> result = queryFactory.select(Projections.fields(UserDto.class,
                                           member.username.as("name"),
                                           member.age))
                                   .from(member)
                                   .fetch();

또는

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

ExpressionUtils.as 방식은 지저분하다보니 필드 별칭사용은 가급적 as 별칭 방법을 사용하자.

서브쿼리를 select 필드로 쓰고싶은 경우에 별칭을 지정하는 법

QMember memberSub = new QMember("memberSub");

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

@QueryProjection

  1. DTO 의 생성자에 @QueryProjection 어노테이션을 추가

  1. DTO 의 Q클래스를 만든다. (gradle complieQuerydsl 실행)
  2. select 절에서 간단하게 DTO 를 사용 가능

@QueryProjection 장점

  • 필드, setter, 생성자 방식보다 사용하기가 편리하다.
  • 생성자 방식의 경우 파라미터를 잘못 넣었을 때 runtime 에서 에러가 발생한다. (사용자가 실제 사용했을 때 에러가 발생)
    하지만 QueryProjection 방식은 complie 타임에 에러를 캐치할 수 있다.

@QueryProjection 단점

  • DTO 를 사용할 때 Q 클래스를 만들어주어야 한다.
  • DTO 에 Querydsl 의존이 생긴다. (아키텍쳐 적으로 순수한 상태가 아니게 됨)

실무에서는 어느 Projection 방식을 가장 많이 사용할까?

정답은 없다.
실용적인 관점에서는 @QueryProjection 방식이 가장 편리하고 좋다.

동적 쿼리

2가지 방법으로 동적 쿼리를 짤 수 있다.

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

BooleanBuilder


위처럼 BooleanBuilder 를 만들어서 where 절에 넣어주기만 하면 된다.

Where 다중 파라미터 사용

영한님이 추천하는 방식. (이 방식을 몰라서 BooleanBuilder 를 쓰는 분도 많다고 함)

  • 참고로, 분리한 메서드의 반환 타입을 조합(usernameEq().and() ... )하기 위해서는 BooleanExpression 을 사용해야한다.
    (Predicate 타입을 쓰는것은 비추천)
  • where 절에 null 이 들어오면 무시되므로 동적 쿼리로 동작이 가능한 것이다.

Where 다중 파라미터 방식의 장점

  • 메서드 명만 보고 해당 메서드 내부를 보지 않아도 코드 유추가 가능해짐 (가독성이 높아짐)
  • 메서드 재활용이 가능해진다.
  • 조합이 가능해진다.

    null 체크는 주의해서 처리해야함.

수정, 삭제 벌크 연산

// 나이가 28 보다 낮은 회원은 비회원으로 이름 변경
long count = queryFactory.update(member)
                         .set(member.username, "비회원")
                         .where(member.age.lt(28))
                         .execute();

단, 벌크 연산 처리는 영속성 컨텍스트를 거치지 않고 DB 에 직접 쿼리를 날리기 때문에
영속성 컨텍스트의 엔티티와 데이터 불일치 문제가 생길 수 있다.

이것을 해결하려면 벌크 연산 수행 후, 영속성 컨텍스트를 초기화 해주어야한다.

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

나이를 +1 업데이트 하고 싶다면?

long count = queryFactory.update(member)
                         .set(member.age, member.age.add(1))
                         .execute();

삭제 쿼리

// 니이가 18 이상 인 member 제거
long count = queryFactory.delete(member)
                         .where(member.age.gt(18))
                         .execute();

SQL function 호출

SQL function 은 Dialect 에 등록된 내용만 호출할 수 있다.

// member 를 M 으로 변경하는 replace 함수 사용
List<String> result = queryFactory.select(Expressions.stringTemplate("function('replace', {0}, {1}, {2})", member.username, "member", "M"))
                                  .from(member)
                                  .fetch();

H2 의 경우 아래와 같이 replace 가 등록되어있어 사용이 가능하다.

참고

  • 실전! Querysql
반응형

'JPA' 카테고리의 다른 글

SpringBoot 3.0 에서의 Querydsl 적용  (0) 2023.08.31
Querydsl - 3편  (0) 2022.02.15
Querydsl - 1편  (0) 2022.02.08
JPA 를 공부하면서 알게 된 내용 정리 4  (0) 2022.02.05
JPA 를 공부하면서 알게 된 내용 정리 3  (0) 2022.02.02

댓글