QueryDsl - Where 다중 조건

김건우·2022년 12월 14일
1

QueryDsl

목록 보기
7/8
post-thumbnail

QueryDsl의 2번째 동적 쿼리 방식인 where의 다중조건 방법을 소개하겠다.
우선 builder 패턴 보다 이 방법을 추천한다. 그이유는 몇가지가 있는데 첫번째는 동적쿼리를 작성한 메서드를 볼 때 가독성이 더 좋다. 두번째는 메소드의 재사용성이다. Where절의 각각의 조건을 메서드로 생성하여서, 만약 where절에 조건이 많아진다면 메서드를 조립할 수 있다.

🎯 API에서 성능 최적화에서 제일 중요한 것

앞에서 소개한 BooleanBulider를 이용한 동적 쿼리에서도 소개하였지만 API의 성능 최적화에서 가장 강조하는 부분은 바로 엔티티의 직접적인 사용 및 조회는 기피하고 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)
}

Where() 절의 다중 파라미터를 설명하기전에 먼저 QueryDsl의 코드부터 보겠다. 그게 더 이해하는데 빠를 것이고 코드의 제작 순서도 이 순서로 제작하였다.

-QueryDsl where() 조건 사용 코드 in Repository🔽

/**QueryDsl**/
    public List<MemberTeamDto> searchByExpression(MemberSearchCondition condition) {
        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( userNameEq(condition),
                        teamNameEq(condition),
                        ageGoe(condition),
                        ageLoe(condition))
                .leftJoin(member.team, team)
                .fetch();
        return memberTeamDtoList;
    }

동적쿼리 where() 다중 조건의 장점

1. 조회 성능 최적화 - DTO로 조회

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

2. 높은 가독성

BooleanBuilder보다 가독성이 높은 것을 확인할 수 있다. where() 조건을 딱 보더라도 조건이 4개 라는 것을 알아차릴 수 있다. 추후 갯수가 많아지면 하나의 메서드로 만들 수 있다.(하나로 조립)

자! 이제 저 where() 조건 안에 있는 4개의 메서드를 파헤쳐보자 👨🏻‍💻

회원의 이름을 검색하는 동적쿼리 메서드

  • userNameEq() 메서드는 회원의 이름을 검색하는 동적쿼리메서드로 만들 것이다. 타입은 BooleanExpression을 사용할 것이며 그이유는 Predicate 타입은 나중에 메서드가 복잡해진다면 메서드를 조립해야 할 때 조립을 할 수 없기 때문이다. 그래서 메서드의 타입은 BooleanExpression타입으로 제작해준다.
private BooleanExpression userNameEq(MemberSearchCondition condition) {
        if (StringUtils.hasText(condition.getUsername())) {
            return member.username.eq(condition.getUsername());
        }
        return null;
    }

위의 코드는 hasText() 메서드의 사용으로 null과 공백("")의 유효성 처리를 통해 where()에 조건을 더해주었다. 만약에 username이 null이나 공백으로 들어온다면 null로 반환이 된다. 동적쿼리에서는 null이 들어온다면 자연스럽게 SQL이 해당 조건은 무시한다.

팀의 이름을 검색하는 동적쿼리 메서드

  • userNameEq() 메서드와 마찬가지로 타입은 BooleanExpression이다.
  private BooleanExpression teamNameEq(MemberSearchCondition condition) {
        if (StringUtils.hasText(condition.getTeamName())) {
            return team.name.eq(condition.getTeamName());
        }
        return null;
    }

회원의 나이의 범위를 검색하는 동적쿼리 메서드

들어오는 조건(나이) 이상과 이하의 범위를 검색하는 메서드

  • ageGoe() 🔽 : member0_.age>=?
  private BooleanExpression ageLoe(MemberSearchCondition condition) {
        return condition.getAgeLoe() != null ? member.age.loe(condition.getAgeLoe()) : null;
    }
  • ageLoe() 🔽 : member0_.age<=?
   private BooleanExpression ageGoe(MemberSearchCondition condition) {
        return condition.getAgeGoe() != null ? member.age.goe(condition.getAgeGoe()) : null;
    }
    }

우선 뒤에서 방금 소개한 메서드 4개를 다 합쳐볼 것이지만 지금 당장은 위의 2개의 메서드만을 합쳐보겠다. ageBetween()이라는 메서드를 제작하여 합칠 것이다.

  • ageBetween(): ageGoe() 와 ageLoe() 합친 코드🔽
 private BooleanExpression ageBetween(MemberSearchCondition condition) {
        return ageGoe(condition).and(ageLoe(condition));
    }

.and() 의 빌더패턴을 사용해서 조건을 하나의 메서드로 만들 수 있다. 그러면 내친김에 앞서소개한 4개의 메서드를 하나의 메서드로 조립해보겠다.

  • allSearch() : 모든 메서드를 합친 메서드
  /**QueryDsl where 다중 조건 메서드**/
    private BooleanExpression allSearch(MemberSearchCondition condition) {
        return userNameEq(condition)
                .and(teamNameEq(condition))
                .and(ageGoe(condition))
                .and(ageLoe(condition));
    }

이렇게 만든 allsearch는 where() 조건문안에 하나의 메서드로 써도 가능한것이고 조건이 더 많아진다면 다음과 같이 하나의 메서드로 묶으면 가독성 측면에서 매우 큰 강점을 얻는다.

  • 하나의 where() 조건으로 만든 QueryDsl()
    public List<MemberTeamDto> searchByExpression(MemberSearchCondition condition) {
        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(allSearch(condition))
                .leftJoin(member.team, team)
                .fetch();
        return memberTeamDtoList;
    }
  • test code 🔽
        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.setUsername("member4");
        condition.setAgeGoe(35);
        condition.setAgeLoe(40);
        condition.setTeamName("teamB");

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

위의 테스트코드는 username 필드에서 확실하게 포함하고 있는 값은 member4라는 Junit5이다. 다음 아래는 실행된 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
        member0_.username=? 
        and team1_.name=? 
        and member0_.age>=? 
        and member0_.age<=?

결론은 builder를 사용한 동적 쿼리 보다는 where() 조건에 메서드를 넣어서 사용하자! 가독성도 좋고 유지보수 측면에서 메서드의 재사용성도 좋다.

profile
Live the moment for the moment.

0개의 댓글