BACK/JPA

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

연듀 2024. 3. 15. 21:12

중급 문법

프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있음

프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회

  • 프로젝션: select 대상 지정

 

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

 @Data
 public class MemberDto {
     private String username;
     private int age;
     public MemberDto() {
     }
     public MemberDto(String username, int age) {
         this.username = username;
         this.age = age;
} }
    @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("age = " + age);
            System.out.println("username = " + username);
        }
    }
    // 튜플을 리포지토리 계층에서 쓰는 건 괜찮은데 서비스나 컨트롤러 계층으로 넘어가는건 좋지 않다.
    // querydsl 에서 다른 기술로 바꿀 때에도 앞단(컨트롤러나 서비스)을 바꿀 필요가 없어진다. 튜플은 querydsl의 종속적인 타입이기 때문이다.
    // 바깥 계층으로 가져갈 때는 DTO를 사용하자

 

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

 

순수 JPA에서 DTO 조회 코드

    @Test
    public void findDtoByJPQL(){ // jpql 의 new operation 문법, 생성자 방식만 지원함
        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);
        }
    }

 

 

Querydsl 빈 생성(Bean population)

결과를 DTO 반환할 때 사용

다음 3가지 방법 지원

  1. 프로퍼티 접근
  2. 필드 직접 접근
  3. 생성자 사용
    @Test
    public void findDtoBySetter(){ // memberDto 를 기본 생성자로 생성한 후에 값을 세팅 해야 하기 때문에 dto에는 기본 생성자가 필수로 있어야 한다.
        List<MemberDto> result = queryFactory
                .select(Projections.bean(MemberDto.class,
                        member.username,
                        member.age)) // setter
                .from(member)
                .fetch();
        for (MemberDto memberDto : result) {
            System.out.println("memberDto = " + memberDto);
        }
    }

    @Test
    public void findDtoByField(){ // getter setter 무시하고 바로 필드에
        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);
        }
    }

 

별칭이 다를 때

 package study.querydsl.dto;
 import lombok.Data;
 @Data
 public class UserDto {
     private String name;
     private int age;
 }
    @Test
    public void findUserDto(){
        QMember memberSub = new QMember("memberSub");

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

                        ExpressionUtils.as(JPAExpressions // 서브 쿼리 (모든 나이를 max나이로 출력) - 서브쿼리의 결과가 age 에 매칭
                                .select(memberSub.age.max())
                                .from(memberSub), "age")
                ))
                .from(member)
                .fetch();

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

    }

 

 

프로퍼티나 필드 접근 생성 방식에서 이름이 다를 때

ExpressionUtils.as(source, alias) : 필드나 서브 쿼리에 별칭 적용

username.as(”name”) : 필드에 별칭 적용

 

 

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

 

생성자를 사용하는 방식에서는 Query Projection 방식까지 지원을 해준다.

Dto 를 Q 파일로 생성할 수 있다.

@Data
@NoArgsConstructor
public class MemberDto {
    private String username;
    private int age;
    
    @QueryProjection // 추가 
    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}

 

@QueryProjection를 생성자에 추가한다.

 

./gradlew compileJava

QMemberdto 생성 확인

 

    @Test
    public void findDtoByQueryProjection(){
        // Projections.constructor 는 타입이 안맞으면 런타임 오류가 발생한다.
        List<MemberDto> result = queryFactory
                .select(new QMemberDto(member.username, member.age)) // compile 시점에 타입이 안맞으면 오류
                .from(member)
                .fetch();
        for (MemberDto memberDto : result) {
            System.out.println("memberDto = " + memberDto);
        }
    }

 

 

단점은 DTO 가 querydsl을 의존해야 한다는 점이다.

 

동적 쿼리 - 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();
        //BooleanBuilder builder = new BooleanBuilder(member.username.eq(usernameCond)); // 무조건 username이 있다고 가정해서 초기 값을 넣을 수도 있다.

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

 

동적 쿼리 - 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 ? member.username.eq(usernameCond) : null; // where 절에 null 이 들어오면 무시된다.

    }

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

 

아래와 같이 조립할 수도 있다.

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

 

where 절의 null 값은 무시된다.

메서드를 여러 군데의 where 절에서 재사용 할 수 있다는 장점이 있다.

쿼리의 가독성이 높아진다.

조합이 가능해진다. ex) isServiceable()

 

수정, 삭제 벌크 연산

    @Test
    public void bulkUpdate(){
        long count = queryFactory
                .update(member)
                .set(member.username, "비회원")
                .where(member.age.lt(28))
                .execute(); // 영향을 받은 로우 수

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

        List<Member> result = queryFactory
                .selectFrom(member)
                .fetch();
        for (Member member1 : result) {
            System.out.println("member1 = " + member1);
        }
    }

 

bulk 연산은 영속성 컨텍스트를 무시하고 db에 바로 쿼리가 나간다.

db 의 상태와 영속성 컨텍스트의 상태가 달라져 버린다.

영속성 컨텍스트는 그대로이고 db 는 수정된 상태 이상태에서 select로 멤버를 가져오면 영속성 컨텍스트에 같은 id 값이 있다면 DB 에서 가져온걸 버린다. 영속성 컨텍스트가 유지된다. 영속성 컨텍스트가 항상 우선권을 가진다.

따라서 em.flush(); em.clear();를 해주자

 

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

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

 

SQL function 호출하기

 

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

@Test
    public void sqlFunction(){ // member M으로 변경하는 replace 함수 사용
        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
//                        (Expressions.stringTemplate("function('lower', {0})", member.username)))
                .where(member.username.eq(member.username.lower()))
                .fetch();
        for (String s : result) {
            System.out.println("s = " + s);
        }
    }
    // lower 같은 ansi 표준 함수들은 querydsl이 상당부분 내장하고 있다.

 

 

 

인프런 - 실전! Querydsl

https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84