[QueryDSL] EP3. 중급 문법
Web/QueryDSL

[QueryDSL] EP3. 중급 문법

 

 

프로젝션과 결과 반환 - 기본

 

프로젝션 대상이 하나일 땐 그 대상의 타입으로 반환하면됩니다.

하지만 프로젝션 대상이 둘 이상일땐 튜플이나 DTO로 반환합니다.

 

튜플로 반환하는 방법에 대해 알아봅니다.

 

 

튜플

여기서 튜플은 프로젝션 대상이 여러개 일때 사용하도록 querydsl이 만들어놓은 객체를 말합니다.

 

튜플 사용법

querydsl은 프로젝션이 여러개일 경우 튜플을 반환합니다.

 

tuple.get()으로 대상을 꺼내쓸 수 있습니다.

    @Test
    public void tupleProjection() throws Exception {
        //given
        List<Tuple> result = queryFactory
                .select(member.username, member.age)
                .from(member)
                .fetch();

        //when
        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);
        }

        //then
    }

 

 

 

 

 

튜플 사용시 주의점

리포지토리 안에서만 쓰고 다른 계층으로 던지지 않습니다.

(DTO로 변경하기)

 

 

 

 

 

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

 

setter

 

조건

MemberDto에 getter, setter 가 있어야 합니다.

 

MemberDto

package study.querydsl.dto;

import lombok.Data;

@Data
public class MemberDto {

    private String username;
    private int age;

}

 

 

 

select 절에Projections.bean(MemberDto.class, member.username, member.age)를 해주면 됩니다.

 

쿼리도 username, age만 나가는 것을 볼 수 있습니다.

 

    public void findDtoBySetter() throws Exception {
        //given
        List<MemberDto> result = queryFactory
                .select(Projections.bean(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();

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

        //then
    }

 

 

 

 

 

 

field

getter, setter없어도 필드에 값을 때려박는 방식입니다.

 

조건

  1. 이름이 맞아야합니다.

 

MemberDto

package study.querydsl.dto;

public class MemberDto {

    private String username;
    private int age;

}

 

 

select 절에 Projections.fields(MemberDto.class, member.username, member.age)를 해주면 됩니다.

 

쿼리도 username, age만 나가는 것을 볼 수 있습니다.

 

    @Test
    public void findDtoByField() throws Exception {
        //given
        List<MemberDto> result = queryFactory
                .select(Projections.fields(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();

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

        //then
    }

 

 

 

주의점

 

Setter 방식과 Field 방식은 필드 이름이 다를경우 가져오지 못합니다.

 

 

UserDto

 

MemberDto와 다르게 username이 아닌 name 입니다.

package study.querydsl.dto;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class UserDto {

    private String name;
    private int age;

    public UserDto(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

 

member.username을 UserDto의 name으로 가져와보겠습니다.

    @Test
    public void findUserDto() throws Exception {
        QMember memberSub = new QMember("memberSub");
        //given
        List<UserDto> result = queryFactory
                .select(Projections.bean(UserDto.class,
                        member.username,
                        member.age
                ))
                .from(member)
                .fetch();

        //when
        for (UserDto userDto : result) {
            System.out.println("userDto = " + userDto);
        }

        //then
    }



 

name을 가져오지 못하는 것을 볼 수 있습니다.

 

 

 

해결방법

별칭지정을 해주면 됩니다.

 

아래 두 가지 방법으로 가능합니다.

  1. member.username.as("name(실제 dto필드 이름)")
  2. ExpressionUtils.as(member.username, "name(실제 dto필드 이름)")

만약 sub query로 projection을 하려면 2번 방법 (ExpressionUtils.as)만 사용이 가능합니다.

    @Test
    public void findUserDto() throws Exception {
        QMember memberSub = new QMember("memberSub");
        //given
        List<UserDto> result = queryFactory
                .select(Projections.fields(UserDto.class,
                        member.username.as("name"),
//                        ExpressionUtils.as(member.username, "name"),

                        ExpressionUtils.as(JPAExpressions
                                .select(memberSub.age.max())
                                    .from(memberSub), "age")
                ))
                .from(member)
                .fetch();


        //when
        for (UserDto userDto : result) {
            System.out.println("userDto = " + userDto);
        }

        //then
    }

 

 

 

 

 

 

 

생성자 접근

생성자를 이용하는 것이므로 생성자에 들어가는 파라미터 타입이 딱 맞아야합니다.

 

    @Test
    public void findDtoByConstructor() throws Exception {
        //given
        List<MemberDto> result = queryFactory
                .select(Projections.constructor(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();


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

        //then
    }

 

 

 

 

생성자 접근 방식은 생성자를 이용하기 때문에 Setter 방식이나 field 방식과 달리 이름이 달라도 타입만 같으면 상관없습니다.

 

    @Test
    public void findDtoByConstructor() throws Exception {
        //given

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

        //when

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

        //then
    }

 

 

 

 

 

 

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

 

사용법

+ distinct 가능

 

사용법

  1. DTO의 생성자에 @QueryProjection을 해줍니다.
  2. querydsl을 compile해서 DTO의 Q타입파일을 생성합니다.
  3. 사용시에는 new operation을 이용합니다.

 

 

MemberDto

package study.querydsl.dto;

import com.querydsl.core.annotations.QueryProjection;
import lombok.Data;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@Data
public class MemberDto {

    private String username;
    private int age;

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

QMemberDto 생성됨

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

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

        //then
    }

 

 

 

 

생성자 방식과의 차이점

 

생성자 방식에서 파라미터 작성을 잘못했을시 컴파일시점 오류에는 오류를 발견하지 못합니다.

하지만 런타임에서 실제 그 메소드를 실행했을 때 오류가 발생합니다. (런타임 오류 발생)

 

 

 

MemberDto의 생성자는 username, age만 있습니다. 하지만 member.id를 넣어도 오류를 발견하지 못합니다.

    @Test
    public void findDtoByConstructor() throws Exception {
        //given
        List<MemberDto> result = queryFactory
                .select(Projections.constructor(MemberDto.class,
                        member.username,
                        member.age,
                        member.id))
                .from(member)
                .fetch();

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


        //then
    }

 

 

실행시점에서는 오류가 발생합니다.

 

 

 

반면에 @QueryProjection을 사용하면 컴파일 시점에서 바로 오류를 잡아냅니다.

 

 

 

 

단점

DTO도 Q파일을 생성해 Querydsl에 의존하게됩니다.

 

DTO는 보통 Controller, Repository 등 여러곳에서 사용되므로 어떤 라이브러리에도 의존하지 않고 순수하게 존재하는 것이 좋습니다.

하지만 DTO에 @QueryProjection을 달아놓음으로 인해서 이제는 Querydsl에 의존을 하게 되었습니다.

 

 

결정

그냥 querydsl을 계속 쓸거같고 여러곳에서 쓰고있으면 그냥 안고 가는 편이 개발하기 편합니다.

 

 

 

 

 

 

 

 

 

동적 쿼리 - BooleanBuilder 사용

 

 

여러개의 조건중에 null이 포함될 수 있을때 사용합니다.

    @Test
    public void dynamicQuery_BooleanBuilder() throws Exception {
        //given
        String usernameParam = "member1";
        Integer ageParam = null;

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

        //when

        //then
    }

 

searchMember1

 

사용법

  1. BooleanBuilder를 생성합니다.
  2. 각 조건들에 대해서 null이 아닐 경우 builder에 and 조건을 추가하도록 합니다.
  3. builder를 queryFactory의 where절에 넣습니다.

 

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

    }

 

쿼리가 age에 대한 조건없이 username에 대한 조건으로 나갑니다.

 

 

 

 

 

초기조건

초기조건에 항상 들어가야할 조건 넣기가 가능합니다.

BooleanBuilder builder = new BooleanBuilder(member.username.eq(usernameCond));

 

 

 

 

 

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

 

where절에 null값이 무시되는 것을 이용하는 방법입니다.

    @Test
    public void dynamicQuery_WhereParam() throws Exception {
        //given
        String usernameParam = "member1";
        Integer ageParam = null;

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

        //when

        //then
    }

 

 

where절 안에서 메서드를 이용해 조건을 받습니다.

 

각 메서드 안에서 null이 아닐경우 조건을 반환하고 null 일 경우 null을 반환하도록 만들면 됩니다.

 

이 메서드들은 다른 쿼리에서도 조합해서 사용하기 위해서는 메서드들의 반환타입을 BooleanExpression으로 해주어야 합니다.

    private List<Member> searchMember2(String usernameCond, Integer ageCond) {
        return queryFactory
                .selectFrom(member)
                .where(usernameEq(usernameCond), ageEq(ageCond))
//                .where(allEq(usernameCond, 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;
    }

 

 

 

장점

  1. booleanBuilder보다 훨씬 가독성이 좋습니다.
  2. 각 메서드들을 다른 쿼리에서 조합해 사용할 수 있습니다.

 

 

다른쿼리에서 조합해 사용하는 예시

 

  1. 조합할 각 메서드들은 BooleanExpression 타입으로 반환되어야 합니다.
  2. null 체크를 주의해서 사용해야 합니다.
    private Predicate allEq(String usernameCond, Integer ageCond) {
        return usernameEq(usernameCond).and(ageEq(ageCond));
    }

 

 

 

 

 

수정, 삭제 벌크 연산

 

쿼리 한번으로 대량의 데이터를 수정해야할 때 사용하면 좋습니다.

 

예시) 실무에서 자주쓰이는 모든 멤버 나이 (+1, -1, *2 등)

 

벌크연산시 주의할 점

벌크연산은 영속성 컨텍스트를 거치지 않고 DB에 바로 쿼리를 날립니다.

그래서 영속성 컨텍스트와 DB의 데이터가 다르게 됩니다.

 

자세한 내용은 아래 링크에서 벌크성 수정쿼리를 보시면 됩니다.

[Web/JPA] - [Data JPA] EP 3. 쿼리 메소드 기능 -2

 

[Data JPA] EP 3. 쿼리 메소드 기능 -2

목차 (클릭시 해당 목차로 이동) 벌크성 수정쿼리 벌크성 수정쿼리는 영속성 컨텍스트를 거치지 않고 바로 DB에 쿼리를 날려 update를 하는 쿼리입니다. 예를들어 "어떤 나이 이상인 사람들의 나이

ksabs.tistory.com

 

해결방법

영속성 컨텍스트를 초기화해주면 됩니다.

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

 

 

 

벌크수정코드

    @Test
    @Commit
    public void bulkUpdate() throws Exception {

        //member1 = 10 -> 비회원
        //member2 = 20 -> 비회원
        //member3 = 30 -> 유지
        //member4 = 40 -> 유지

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

        em.flush();
        em.clear();
        //when

        List<Member> result = queryFactory
                .selectFrom(member)
                .fetch();


        //then
        for (Member member1 : result) {
            System.out.println("member1 = " + member1);
        }
    }

 

 

 

또 다른 벌크 수정연산, 삭제연산 쿼리 예시 코드

 

   @Test
    public void bulkAdd() throws Exception {
        //given
        long count = queryFactory
                .update(member)
//                .set(member.age, member.age.add(1))
                .set(member.age, member.age.multiply(2))
                .execute();
        //when

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

        //when
        
        //then
    }

 

 

 

 

 

 

sql function

 

사용중인 DB의 dialect에 있는 sql function을 사용할 수 있습니다.

 

 

replace 사용

    @Test
    public void sqlFunction() throws Exception {
        //given
        List<String> result = queryFactory
                .select(Expressions.stringTemplate(
                        "function('replace', {0}, {1}, {2})",
                        member.username, "member", "M"))
                .from(member)
                .fetch();
        //when

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

        //then
    }

 

 

 

 

lower 사용

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

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

 

 

 

lower 같이 anci 표준으로 거의 모든 DB에서 쓰이는 함수 같은 경우에는 querydsl에서도 함수로 지원을 해줍니다.

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

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