QueryDsl - 검색 동적 쿼리 성능 최적화 (Builder사용)

김건우·2022년 12월 14일
0

QueryDsl

목록 보기
6/8
post-thumbnail

우선 엔티티는 요청과 응답에서 성능을 최적화하고 엔티티를 엔티티만의 보존을 하기위해서 DTO를 제작해야한다. 다음의 코드는 member와 team을 조회하기윈한 dto이다.

MemberTeamDto - 조회 최적화용 DTO

  • 회원의 id , 회원의 이름 , 회원의 나이 , 팀의 id , 팀의 이름을 조회해야한다고 가정해보자
  • QuerryProjection을 사용하여 QType class를 만들 것이다.
package com.spring.jpadata.dto;

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

@Data
public class MemberTeamDto {

    private Long memberId;
    private String username;
    private int age;
    private Long teamId;
    private String teamName;

    @QueryProjection
    public MemberTeamDto(Long memberId, String username, int age, Long teamId, String teamName) {
        this.memberId = memberId;
        this.username = username;
        this.age = age;
        this.teamId = teamId;
        this.teamName = teamName;
    }
}

QMemberTeamDto 클래스 생성됨

위와 같이 조회용 Dto도 @QueryProjection의 어노테이션을 사용해서 compileQueryDsl을 빌드하여 QType class 를 제작해주었다.

  • QType의 QMemberTeamDto 클래스 생성됨 🔽
@Generated("com.querydsl.codegen.DefaultProjectionSerializer")
public class QMemberTeamDto extends ConstructorExpression<MemberTeamDto> {

    private static final long serialVersionUID = 71897820L;

    public QMemberTeamDto(com.querydsl.core.types.Expression<Long> memberId, com.querydsl.core.types.Expression<String> username, com.querydsl.core.types.Expression<Integer> age, com.querydsl.core.types.Expression<Long> teamId, com.querydsl.core.types.Expression<String> teamName) {
        super(MemberTeamDto.class, new Class<?>[]{long.class, String.class, int.class, long.class, String.class}, memberId, username, age, teamId, teamName);
    }
}

검색 조건 클래스

  • 검색의 조건으로는 회원의 이름과 회원의 나이 그리고 팀의 이름을 줄 것이다.

@Data
public class MemberSearchCondition {
    //회원명 팀명 나이

    private String username; // 회원의 이름
    private String teamName; // 팀의 이름
    private Integer ageGoe; // 회원의 나이 범위 (goe)
    private Integer ageLoe; // 회원의 나이 범위 (loe)
}

그럼 이제 repository에 builder를 사용한 검색조건의 코드를 보겠다.

  • QueryDsl을 이용한 검색 repository 보기전의 사전지식

    Null과 공백("")

    아래의 코드에서의 팁이 하나있다. 이전의 블로그글에서 builder를 사용하여 검색조건을 만들 때 null의 조건을 사용하여 제작해야한다고 말했다. 하지만 실무에서나 프로젝트에서 주의할점은 String타입의 검색조건은 Null 뿐만 아니라 공백("") 도 잘 처리를 해주어야한다.

    StringUtils.hasText()

    StringUtils.hasText(값);을 사용하면 값이 있을 경우에는 true를 반환하고
    공백이나 NULL이 들어올 경우에는 false를 반환하게 된다.

    .as() : 알리아스 별칭 사용

    앞서 만들어준 MemberTeamDto에는 member,team엔티티와 필드이름이 다른 필드가 존재했다. memberid, teamId, teamName은 각각의 엔티티완 필드의 이름이 불일치했다. 그래서 .as("별칭")을 사용해주어서 필드 이름의 불일치를 해결해준다.

  • QueryDsl을 이용한 검색 repository🔽
//검색하기
    public List<MemberTeamDto> searchByBulider(MemberSearchCondition condition) {
        BooleanBuilder builder = new BooleanBuilder();
        //null이 들어올 수 있고 ""이 들어올 수도 있다.
        if (StringUtils.hasText(condition.getUsername())) {
            builder.and(member.username.eq(condition.getUsername()));
        }
        if (StringUtils.hasText(condition.getTeamName())) {
            builder.and(team.name.eq(condition.getTeamName()));
        }
        if (condition.getAgeGoe() != null) {
            builder.and(member.age.goe(condition.getAgeGoe()));
        }
        if (condition.getAgeLoe() != null) {
            builder.and(member.age.loe(condition.getAgeLoe()));
        }

        List<MemberTeamDto> memberTeamDtoList = queryFactory
                .select(
                        new QMemberTeamDto(
                                member.id.as("memberId"),
                                member.username,
                                member.age,
                                team.id.as("teamId"),
                                team.name.as("teamName")))
                .from(member)
                .where(builder)
                .leftJoin(member.team, team)
                .fetch();
        return memberTeamDtoList;
    }

위의 코드를 설명하자면 BooleanBuilder 타입의 bulider를 사용하여 각각의 null조건 안에서 where() 문에 들어갈 조건을 더해준다. 또한 조회의 성능 최적화를 위해서 엔티티로 반환하는것이 아닌 @QueryProjection으로 생성된 QMemberTeamDto로 반환되게 해주었다. 이제 test를 진행하겠다.

  • repository Test code
 @Test
    public void searchTest() {
        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);
        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);
        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);

        MemberSearchCondition condition = new MemberSearchCondition();

      
        condition.setAgeGoe(35);
        condition.setAgeLoe(40);
        condition.setTeamName("teamB");

        List<MemberTeamDto> result =
                memberJpaRepository.searchByBulider(condition);
        assertThat(result).extracting("username").containsExactly("member4");
    }

위의 테스트코드는 제한나이의 범위와 팀의 이름을 검색 조건으로 주었다.
🎈주의 : 회원의 이름은 검색조건에 넣지 않았다(즉, null)인 상태이다. 그럼 다음과 같은 SQL이 실행된다.

 select
        member0_.member_id as col_0_0_,
        member0_.username as col_1_0_,
        member0_.age as col_2_0_,
        team1_.team_id as col_3_0_,
        team1_.name as col_4_0_ 
    from
        member member0_ 
    left outer join
        team team1_ 
            on member0_.team_id=team1_.team_id 
    where
        team1_.name=? 
        and member0_.age>=? 
        and member0_.age<=?

앞서 QueryDsl의 동적쿼리에 대한글에 중요한 키포인트 중 하나는 조건의 값이 null인 값은 자동으로 sql이 무시한다고 하였다. 위와 같이 member의 name은 검색 조건에 넣어주지 않아서 where 조건에 무시된 것을 볼 수있다.

profile
Live the moment for the moment.

0개의 댓글