본문 바로가기
Study/spring

QueryDSL(3) - 프로젝션, distinct, 동적 쿼리, 벌크 연산

by 유경호 2021. 1. 26.
반응형

프로젝션

프로젝션과 기본 결과 반환

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

    @Test
    public void simpleProjection() {
        List<String> result = queryFactory
                .select(member.username)
                .from(member)
                .fetch();
    }
  • 타입을 명확하게 지정할 수 있음.
  • 프로젝션 대상이 둘 이상이면 Tuple이나 DTO로 조회해야함

 

프로젝션 대상이 둘 이상일 경우

Tuple로 조회

    @Test
    public void tupleProjection() {
        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);
            System.out.println("username = " + username);
            System.out.println("age = " + age);
        }
    }
  • com.querydsl.core.Tuple로 결과를 반환 받을 수 있음
  • get()에 쿼리 타입(Q)으로 alias를 주입해 값을 반환 받을 수 있다
    예: tuple.get(member.username)

 

참고! 투플 같은 경우에는 레포지토리 계층에서 사용하는 것은 괜찮으나, 그 하부(서비스, 컨트롤러) 계층으로 내려가게 되면 좋은 설계가 아니다. 앞단의 핵심 비즈니스 로직이 QueryDSL의 의존성으로 엮여버리면 좋지 않다. QueryDSL의 의존성이 앞단까지 넘어가기 때문에 좋지 않음.

 

DTO 조회

회원 엔티티 DTO 예시

import lombok.Data;

@Data
public class MemberDto {

    private String username;

    private int age;

    public MemberDto() {
    }

    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}

QueryDSL에서 결과를 DTO로 조회할 때 빈 생성 방법 3가지를 지원한다.

  • 프로퍼티 접근: setter를 이용하여 값을 주입
  • 필드 직접 접근
  • 생성자 사용

 

    // setter를 이용하여 주입
    @Test
    public void findDtoBySetter() {
        List<MemberDto> result = queryFactory
                .select(Projections.bean(MemberDto.class, 
                            member.username,
                            member.age))
                .from(member)
                .fetch();
    }

    // 필드에 바로 값을 꼽아버림
    @Test
    public void findDtoByField() {
        List<MemberDto> result = queryFactory
                .select(Projections.fields(MemberDto.class, 
                            member.username,
                            member.age))
                .from(member)
                .fetch();
    }

    // 생성자를 이용하여 주입
    @Test
    public void findDtoByConstructor() {
        List<MemberDto> result = queryFactory
                .select(Projections.constructor(MemberDto.class, 
                            member.username,
                            member.age))
                .from(member)
                .fetch();
    }
  • 프로퍼티 접근이나 필드 직접 접근 시 DTO의 필드명과 엔티티의 속성명을 맞춰줘야함.
  • 생성자 사용의 경우에는 순서와 타입만 맞춰주면 된다.

 

프로퍼티 접근이나 필드 직접 접근 시 이름이 다를 때 해결 방안

    @Test
    public void findUserDto() {
        QMember memberSub = new QMember("memberSub");    

        List<UserDto> result = queryFactory
                .select(Projections.bean(UserDto.class, 
                            member.username.as("name"),
                            // 서브 쿼리를 이용하는 방법 ExpressionUtils로 감싸줘야 한다.
                            ExpressionUtils.as(JPAExpressions
                                        .select(memberSub.age.max())
                                        .from(memberSub), "age")))
                .from(member)
                .fetch();
    }
  • ExperssionUtils.as(source, alias): 필드나 서브 쿼리에 별칭 적용
  • username.as("memberName"): 필드에 별칭 적용

 

@QueryProjection 사용하여 DTO 생성자 사용하여 조회

생성자 + @QueryProjection

@Data
public class MemberDto {

    private String username;

    private int age;

    public MemberDto() {
    }

    @QueryProjection
    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}
  • 사용할 생성자에 @QueryProjection 어노테이션을 붙여주면 됨.
  • 어노테이션을 붙였다면 Q타입으로 DTO를 생성하여 사용할 수 있다.

 

@QueryProjection 활용 예시

    @Test
    public void findDtoByQueryProjection() {
        List<MemberDto> result = queryFactory
                .select(new QMemberDto(member.username, member.age))
                .from(member)
                .fetch();
    }
  • 생성자 파라미터 타입만 맞춰주면 잘 주입된다.

@QueryProjection는 QueryDSL에서 제공하는 프로젝션 궁극의 방법이지만 장단점이 존재한다.

장점

  • Projections.constructor()를 사용하면 컴파일 오류를 잡을 수 없지만, 해당 방법은 컴파일 오류로 잡을 수 있다.

단점

  • Q파일을 생성해야함.
  • 의존관계가 문제, DTO 자체가 QueryDSL에 의존성이 생김. 그럼 컨트롤러, 서비스 계층에서 QueryDSL의 의존성이 생기기 때문에 DTO가 순수함을 잃어버린다. 아키텍처 설계관점에서 좋진 않다.

@QueryProjection 결론! 실용적인 관점에서는 사용하면 좋지만, 서비스의 운영 시점에서 봤을 때는 고려해볼 사항이다.


Distinct

List<String> result = queryFactory
 .select(member.username).distinct()
 .from(member)
 .fetch();
  • JPQL의 distinct와 같은 기능을 제공한다.

동적 쿼리

동적 쿼리를 사용하면 특정 조건에 따른 쿼리를 편리하게 생성할 수 있다.

동적 쿼리를 작성하는 방식에는 두 가지가 있다.

  • BooleanBuilder 활용
  • where()에 다중 파라미터 사용

 

BoolenBuilder 활용 예시

    @Test
    public void dynamicQuery_BooleanBuilder() {
        String usernameParam = "member1";
        int ageParam = 10;
        List<Member> result = searchMember1(usernameParam, ageParam);
    }

    public 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 인스턴스에 .and()를 사용해 검색 조건을 붙일 수 있다.

 

where 다중 파라미터 활용 예시

    @Test
    public void dynamicQuery_WhereParam() {
        String usernameParam = "member1";
        int ageParam = 10;

        List<Member> result = searchMember(usernameParam, ageParam);
    }
    public List<Member> searchMember(String usernameCond, Integer ageCond) {
        return queryFactory
                .selectFrom(member)
                .where(usernameEq(usernameCond), ageEq(ageCond))
                .fetch();
    }

    // 아래와 같이 동적 조건 쿼리를 메소드로 빼면 재사용성이 증가한다. (다른 쿼리에서 재사용이 가능함)
    private Predicate usernameEq(String usernameCond) {
        return usernameCond != null ? member.username.eq(usernameCond) : null;
    }

    private Predicate ageEq(Integer ageCond) {
        if (ageCond == null) {
            return null;
        }
        return member.age.eq(ageCond);
    }
  • where 조건에 null 값이 있으면 무시되는데 이를 활용함.
  • 메소드를 따로 빼서 생성하면 다른 쿼리에서도 재사용할 수 있음.
  • 쿼리 자체의 가독성이 높아짐.

 

위의 동적 조건 쿼리 메소드를 아래와 같이 조합하여 하나의 메소드로 만들 수도 있다.

private BooleanExpression allEq(String usernameCond, Integer ageCond) {
 return usernameEq(usernameCond).and(ageEq(ageCond));
}
  • BooleanExpression: Predicate을 상속 받아 구현된 추상 클래스
  • BooleanExpression를 이용하면 and()를 체인으로 한 번에 동적 쿼리를 엮을 수 있다.
  • 단 이 경우에는 null 값 처리를 잘 해주어야 한다.

수정, 삭제 시 벌크 연산

수정

    @Test
    public void bulkUpdate() {    
        long count = queryFactory
                .update(member)
                .set(member.username, "비회원")
                .where(member.age.lt(14))
                .execute();
    }
  • 반환되는 long 타입 값은 반영된 데이터의 수

 

아래와 같이 기존 값에 더하기, 곱 연산이 가능하다.

// 더하기 (마이너스 시 음수 주입)
    @Test
    public void bulkAdd() {
        long count = queryFactory
                .update(member)
                .set(member.age, member.age.add(2))
                .execute();
    }

    // 곱
    @Test
    public void bulkMultiply() {
        long count = queryFactory
                .update(member)
                .set(member.age, member.age.multiply(2))
                .execute();
    }

 

삭제

@Test
    public void bulkDelete() {
        long count = queryFactory
                .delete(member)
                .where(member.age.gt(13))
                .execute();
    }

주의! JPQL 배치 쿼리와 마찬가지로, 영속성 컨텍스트에 있는 엔티티를 무시하고 데이터베이스에 쿼리를 날리기 때문에 배치 쿼리를 실행한 후에는 영속성 컨텍스트를 초기화 하는 것이 안전하다.

반응형