Queydsl 중급 문법
( 프로젝션, 동적쿼리, 벌크연산, SQL function)

Queydsl 중급 문법 ( 프로젝션, 동적쿼리, 벌크연산, SQL function)

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

import static study.querydsl.entity.QMember.*;
...

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    @PersistenceContext
    EntityManager em;

    JPAQueryFactory queryFactory;

    ...

    @Test
    public void simpleProjection() {
        List<String> result = queryFactory
            .select(member.username)
            .from(member)
            .fetch();

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

    @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);
        }
    }
}
  • 프로젝션 대상이 하나

    • 타입을 명확하게 지정할 수 있다
  • 프로젝션 대상이 둘 이상

    • 튜플이나 DTO로 조회

    • com.querydsl.core.Tuple은 리포지토리 계층에서만 쓰고 서비스나 컨트롤러 계층은 모르는 것이 좋다(의존성 최소화)

1. 프로젝션과 결과 반환 - DTO 조회(순수 JPA에서 DTO 조회)

  • MemberDto
@Data
@NoArgsConstructor
public class MemberDto {

    private String username;
    private int age;

    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}
import static study.querydsl.entity.QMember.*;
...

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    @PersistenceContext
    EntityManager em;

    JPAQueryFactory queryFactory;

    ...

    @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);
        }
    }
}
  • 순수 JPA에서 DTO를 조회할 때는 new 명령어를 사용해야한다

  • DTO의 package이름을 다 적어줘야해서 지저분하다

  • 생성자 방식만 지원한다

2. 프로젝션과 결과 반환 - DTO 조회(Querydsl 빈 생성)(Bean population)

import static study.querydsl.entity.QMember.*;
...

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    @PersistenceContext
    EntityManager em;

    JPAQueryFactory queryFactory;

    ...

    @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,
                    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);
        }
    }
}
  • 3가지 방법을 지원한다

    1. 프로퍼티 접근(Setter 접근)

    2. 필드 직접 접근

    3. 생성자 사용

3. 프로젝션과 결과 반환 - DTO 조회(프로퍼티나, 필드 접근 방근 방식에서 이름이 다를 때 해결 방안)

  • UserDto
@Data
public class UserDto {

    private String name;
    private int age;

    public UserDto() {
    }

    public UserDto(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
import static study.querydsl.entity.QMember.*;
...

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    @PersistenceContext
    EntityManager em;

    JPAQueryFactory queryFactory;

    ...

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

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

    @Test
    public void findUserDtoByConstructor() {
        List<UserDto> result = queryFactory
            .select(
                Projections.constructor(
                    UserDto.class,
                    member.username,
                    member.age
                )
            )
            .from(member)
            .fetch();

        for (UserDto userDto : result) {
            System.out.println("userDto = " + userDto);
        }
    }
}
  • 컬럼명이 다른 경우 필드에 별칭을 적용할 수 있다

  • 생성자 방식을 사용할 경우 DTO에 파라미터 타입이 맞는 생성자가 있어야 한다

4. 프로젝션과 결과 반환 - 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를 추가한다

    • compileQuerydsl를 실행한다

    • QMemberDto 생성된다

import static study.querydsl.entity.QMember.*;
...

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    @PersistenceContext
    EntityManager em;

    JPAQueryFactory queryFactory;

    ...

    @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에 QueryDSL 어노테이션을 유지해야 하고 DTO까지 Q파일을 생성해야 한다

      • DTO는 리포지토리, 서비스, 컨트롤러에서 많이 사용될 수 있는데 QueryDSL에 의존하게 된다

      • DTO의 의존성 최소와 실용성 사이에서 상황에 맞게 적합하게 선택해야 한다

동적 쿼리

1. BooleanBuilder

import static study.querydsl.entity.QMember.*;
...

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    @PersistenceContext
    EntityManager em;

    JPAQueryFactory queryFactory;

    ...

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

2. Where 다중 파라미터 사용

import static study.querydsl.entity.QMember.*;
...

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    @PersistenceContext
    EntityManager em;

    JPAQueryFactory queryFactory;

    ...

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

    private BooleanExpression allEq(String usernameCond, Integer ageCond) {
        return usernameEq(usernameCond).and(ageEq(ageCond));
    }
}
  • where 조건에 null 값은 무시된다

  • 장점

    • 적절한 메서드명을 통해 쿼리 자체의 가독성이 높아진다

    • 메서드를 다른 쿼리에서도 재활용 할 수 있다

    • 메서드의 조합이 가능하다

  • 주의사항

    • null 체크를 주의해야 한다

수정, 삭제 벌크 연산

import static com.querydsl.jpa.JPAExpressions.*;
import static study.querydsl.entity.QMember.*;
import static study.querydsl.entity.QTeam.*;
...

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    @PersistenceContext
    EntityManager em;

    JPAQueryFactory queryFactory;

    ...

    @Test
    public void bulkUpdate() {
        //member1 = 10 -> 영속성 컨텍스트 member1
        //member2 = 20 -> 영속성 컨텍스트 member2
        //member3 = 30 -> 영속성 컨텍스트 member3
        //member4 = 40 -> 영속성 컨텍스트 member3

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

        //member1 = 10 -> 영속성 컨텍스트 member1    DB 비회원
        //member2 = 20 -> 영속성 컨텍스트 member2    DB 비회원
        //member3 = 30 -> 영속성 컨텍스트 member3
        //member4 = 40 -> 영속성 컨텍스트 member3

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

        //member1 = 10 -> 영속성 컨텍스트 비회원        DB 비회원
        //member2 = 20 -> 영속성 컨텍스트 비회원        DB 비회원
        //member3 = 30 -> 영속성 컨텍스트 member3
        //member4 = 40 -> 영속성 컨텍스트 member3

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

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

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

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

    • JPQL 배치와 마찬가지로, 영속성 컨텍스트에 있는 엔티티를 무시하고 실행되기 때문에 배치 쿼리를 실행하고 나면 영속성 컨텍스트를 초기화 하는 것이 안전하다

SQL function 호출하기

import static com.querydsl.jpa.JPAExpressions.*;
import static study.querydsl.entity.QMember.*;
import static study.querydsl.entity.QTeam.*;
...

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    @PersistenceContext
    EntityManager em;

    JPAQueryFactory queryFactory;

    ...

    @Test
    public void sqlFunction() {
        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("s = " + s);
        }
    }

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

        for (String s : result) {
            System.out.println("s = " + s);
        }
    }
}
  • SQL function은 JPA와 같이 Dialect에 등록된 내용만 호출할 수 있다

참고 자료