프로젝션과 결과 반환 - DTO 조회


    @Test
    public void findDtoByJPQL() {
        List<MemberDto> result = em.createQuery("select new study.querydsl.dto.MemberDto(m.username, m.age) from Member m", MemberDto.class)
                .getResultList();

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

이전에 사용한 바 있는 JPQL을 사용해 DTO로 변환해 조회하는 코드이다. 해당 코드는 new Operation을 사용해야하기 때문에 DTO의 package 명을 다 적어줘야 한다. 이때문에 코드가 지저분해지고, 코드 작성에도 불편함이 있다. 또한, 생성자 방식만 지원하는 불편함도 있다.

 

하지만, querydsl은 위의 문제를 깔끔하게 해결하는 방법을 제시한다.

 

Querydsl 빈 생성(Bean population)

결과를 DTO 반환할 때 사용한다. 아래와 같은 3가지 방법을 지원한다.

  • 프로퍼티 접근
  • 필드 직접 접근
  • 생성자 사용

프로퍼티 접근 - Setter

    @Test
    public void findDtoBySetter() {
        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);
        }
    }

 

필드 직접 접근 

    @Test
    public void findDtoByField() {
        List<MemberDto> result = queryFactory
                .select(Projections.fields(MemberDto.class,     // getter, setter 상관없이 값을 넣는다.
                        member.username,
                        member.age))
                .from(member)
                .fetch();

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

 

생성자 접근

    @Test
    public void findDtoByConstructor() {
        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);
        }
    }

 

필드 값이 다른 dto 접근 - 프로퍼티 접근법만 예시 (필드, 생성자 접근 동일)

    @Test
    public void findUserDtoBySetter() {
        List<UserDto> result = queryFactory
                .select(Projections.bean(UserDto.class,       //setter를 통해 들어간다.
                        member.username.as("name"),
                        member.age))
                .from(member)
                .fetch();

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

.as("바꾸고싶은 필드명")으로 필드명을 바꿔주면 된다.

 

서브쿼리 추가 dto 접근 - 필드 접근법만 예시 (프로퍼티, 생성자 접근 동일)

    @Test
    public void findUserDtoByFieldSubQuery() {
        QMember memberSub = new QMember("memberSub");
        List<UserDto> result = queryFactory
                .select(Projections.fields(UserDto.class,     // getter, setter 상관없이 값을 넣는다.
                        member.username.as("name"),
                        Expressions.as(JPAExpressions
                                .select(memberSub.age.max())
                                .from(memberSub), "age")
                ))
                .from(member)
                .fetch();

        for (UserDto userDto : result) {
            System.out.println("userDto = " + userDto);
        }
    }
  • ExpressionUtils.as(source, alias) : 필드나 서브쿼리에 별칭 적용

 

 

 

프로젝션과 결과 반환 - @QueryProjection


@QueryProjection을 사용하기 위해서는 준비 작업이 필요합니다.

@Data
@NoArgsConstructor
public class MemberDto {
    private String username;
    private int age;

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

사용하고자하는 dto 클래스의 생성자에 @QueryProjection을 추가해줍니다. 실행을 한번 해보거나 이전에 돌렸던 compilequerydsl을 눌러줍니다. 그렇게되면 @MemberDto가 생성됩니다. 이제 사용해봅시다.

 

생성자 + @QueryProjection

    @Test
    public void findDtoByQueryProjection() {
        List<MemberDto> result = queryFactory
                .select(new QMemberDto(member.username, member.age))
                .from(member)
                .fetch();

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

생성자 dto 보다의 장점은 컴파일 시점에 오류를 잡아준다는 것 입니다. 생성자 dto는 런타임 시점에 오류를 잡아주지만, 생성자+@QueryProjection은 컴파일 시점에 오류를 잡아줍니다.

 

위 @QueryProjection은 다 좋지만 2가지 단점이 존재합니다.

  1. QMemberDto와 같이 Q멤버를 만들어야 해야 합니다.
  2. Q멤버로 만드는 경우, dto는 querydsl에 의존성을 가지게 됩니다.
    • 해당 문제는 아키텍쳐 관점에서 문제가 생긴다고 한. 해당 dto를 repository, service, application 계층 모두 사용하는데 querydsl에 의존적인 dto를 모든 계층에서 사용하는데 어떠한 문제가 있을지는 아직 모르겠다,,

 

distinct

    @Test
    public void findDtoByQueryProjection() {
        List<MemberDto> result = queryFactory
                .select(new QMemberDto(member.username, member.age)).distinct()
                .from(member)
                .fetch();

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

이렇게 select뒤에 distinct 적어주면된다.

 

 

 

동적 쿼리 - BooleanBuilder 사용


    @Test
    public void dynamicQuery_BooleanBuilder() {
        String usernameParam = "member1";
        Integer ageParam = 10;

        List<Member> result = searchMember1(usernameParam, ageParam);
        assertThat(result.size()).isEqualTo(1);
    }

    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();
    }

위와 같이 null이 아닌 경우에만 builder.and(where안에 넣을 조건)을 넣어주면 된다.

 

 

 

동적 쿼리 - Where 다중 파라미터 사용


김영한님이 즐겨쓰는 방식이라고 한다. 

    @Test
    public void dynamicQuery_whereParam() {
        String usernameParam = "member1";
        Integer ageParam = 10;

        List<Member> result = searchMember2(usernameParam, ageParam);
        assertThat(result.size()).isEqualTo(1);
    }

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

    private Predicate usernameEq(String usernameCond) {
        return usernameCond == null ? null : member.username.eq(usernameCond);
    }

    private Predicate ageEq(Integer ageCond) {
        return ageCond == null ? null : member.age.eq(ageCond);
    }

이 기능의 장점은 메소드라 빠지기 때문에, 아래와 같이 조립이 가능하다.

    @Test
    public void dynamicQuery_whereParam() {
        String usernameParam = "member1";
        Integer ageParam = 10;

        List<Member> result = searchMember2(usernameParam, ageParam);
        assertThat(result.size()).isEqualTo(1);
    }

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

    private BooleanExpression usernameEq(String usernameCond) {
        return usernameCond == null ? null : member.username.eq(usernameCond);
    }

    private BooleanExpression ageEq(Integer ageCond) {
        return ageCond == null ? null : member.age.eq(ageCond);
    }

    private BooleanExpression allEq(String usernameCond, Integer ageCond){
        return usernameEq(usernameCond).and(ageEq(ageCond));
    }
  • where 조건에 null 값은 무시된다.
  • 메서드를 다른 쿼리에서도 재활용 할 수 있다. (이 점이 정말 좋은 것 같다.)
  • 쿼리 자체의 가독성이 높아진다. (이전 BooleanBuilder보다 가독성이 뛰어나다.)

 

 

수정, 삭제 배치 쿼리


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

    @Test
    public void bulkUpdate() {
        // member1 = 10 = > 비회원
        // member2 = 20 = > 비회원
        // member1 = 30 = > 그대로
        // member2 = 40 = > 그대로

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

두 컬럼의 데이터를 update했지만, 쿼리는 한번만 나간 것을 볼 수 있다.

참고
벌크 연산은 이전에도 말했지만, 조심해야할 것이 있다.
영속성 컨텍스트를 무시하고 DB에 직접 접근해서 update 쿼리문을 날리기 때문이다. 이것이 문제가 되는 것은 영속성 컨텍스트에 update쿼리문에 날라간 데이터가 있고, 벌크 연산 후 데이터를 가져오면 DB가 아닌 영속성 컨텍스트에서 가져오기 때문에 벌크연산된 데이터가 아닌 이전의 데이터를 가져온다. 그렇기 때문에 벌크 연산후에는 em.clear()를 해주자.

 

쿼리 한번으로 대량 데이터 추가, 곱, 삭제

    @Test
    public void bulkAdd() {
        long count = queryFactory
                .update(member)
                .set(member.age, member.age.add(1)) //곱하기는 add 대신 multiply
                .execute();
    }

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

해당 벌크 연산도 벌크 연산 후 em.clear()를 필수로 해주어야 한다.

 

 

 

SQL function 호출하기


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

 

아래 예시는 member string을 m string으로 replace하는 함수를 사용한 예제이다.

    @Test
    public void slqFunction() {
        List<String> result = queryFactory
                .select(Expressions.stringTemplate("function('replace', {0}, {1}, {2})",
                        member.username, "member", "m"))
                .from(member)
                .fetch();

        for (String s : result) {
            System.out.println("result = " + s);
        }
    }

replace말고도 소문자로 변환하는 경우는 아래예시이다.

    @Test
    public void sqlFunction2() {
        List<String> result = queryFactory
                .select(member.username)
                .from(member)
                .where(member.username.eq(Expressions.stringTemplate("function('upper', {0})", member.username)))
                //.where(member.username.eq(member.username.upper()))
                .fetch();

        for (String s : result) {
            System.out.println("result = " + s);
        }
    }

비교할 데이터를 모두 소문자로 비교한 뒤, 비교할 때는 위와같이 소문자로 변경한 뒤 비교할 수 있다.

 

참고로, lower 같은 ansi 표준 함수들은 querydsl이 상당부분 내장하고 있다. 따라서 아래와 같이 처리해도 결과는 같다.

.where(member.username.eq(member.username.upper()))

 

 

 

 

 

출처

 

실전! Querydsl - 인프런 | 강의

Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!, - 강의 소개 | 인프런...

www.inflearn.com

 

+ Recent posts