본문 바로가기

Spring/Querydsl

Querydsl- 동적쿼리

BooleanBuilder

 querydsl에서는 BooleanBuilder 객체를 이용해서 동적 쿼리를 발생시킬 수 있다.

 

예를 들어 Member 엔티티를 조회하되, 해당 엔티티의 이름과 나이로 조회를 하는 메서드를 구현한다고 하자.

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

    // searchMember1:
    // 1) 인자가 모두 들어간 경우 두 조건을 모두 만족하는 엔티티 조회
    // 2) 인자가 1개만 들어간 경우(나머지는 null인 경우) 들어간 조건으로만 엔티티 조회
    // 3) 인자가 모두 null인 경우, 조건 없이 모두 조회
    List<Member> result = searchMember1(usernameParam,ageParam);

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

    assertThat(result.size()).isEqualTo(1);
}

private List<Member> searchMember1(String usernameCond, Integer ageCond) {

    BooleanBuilder builder = new BooleanBuilder();

    // 파라미터가 null이 아닌 경우 builder에 각 조건을 붙여줌
    if (usernameCond != null) {
        builder.and(member.username.eq(usernameCond));
    }
    if (ageCond != null) {
        builder.and(member.age.eq(ageCond));
    }

    return queryFactory
            .select(member)
            .from(member)
            .where(builder)
            .fetch();
}

위의 테스트 코드에서는 searchMember1() 메서드를 이용해서 member를 조회하고 있다. 해당 메서드를 뜯어보자면 내부에 BooleanBuilder 객체가 존재하며, 해당 객체의 사용을 보면 다음과 같다.

if (usernameCond != null) {
    builder.and(member.username.eq(usernameCond));
}

if (ageCond != null) {
    builder.and(member.age.eq(ageCond));
}

 만일 매개변수 usernameCond, ageCond가 null이 아닌 경우 builder에 각각 eq 연산자 비교문을 붙여주고 있다.

그리고 그렇게 비교 연산자가 붙은 builder는 where 문의 비교 연산자로 들어가서 조건식으로서 사용된다.

 

따라서 selectMember1( )은 매개변수로 들어온 값을 이요해서 Member 엔티티를 조회하는 메서드인 것이다. 또한 만일 매개변수가 null인 경우에는 해당 조건을 배제한 채로 조회를 진행한다. 따라서 usernameCond와 ageCond가 모두 null 이라면 조회를 할 때 where 문의 조건식이 존재하지 않아 모든 Member 엔티티를 조회한다.

 

결국 BooleanBuilder 는 where 문의 내부 인자로 사용해, 동적 쿼리를 만드는데 사용할 수있다.  

 

Where 다중 파라미터 사용

 이 경우에도 BooleanBuilder 사용시와 크게 다르진 않다. 다만, 별도의 비교 메서드(usernameEq, ageEq)를 통해서 BooleanBuilder 없이 동적 쿼리를 사용한다.

@Test
public void dynamicQuery_WhereParam() throws Exception{

    String usernameParam = null;
    Integer ageParam = null;

    // searchMember2:
    // 1) 인자가 모두 들어간 경우 두 조건을 모두 만족하는 엔티티 조회
    // 2) 인자가 1개만 들어간 경우(나머지는 null인 경우) 들어간 조건으로만 엔티티 조회
    // 3) 인자가 모두 null인 경우, 조건 없이 모두 조회
    List<Member> result = searchMember2(usernameParam,ageParam);

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

    assertThat(result.size()).isEqualTo(4);
}
private List<Member> searchMember2(String usernameCond, Integer ageCond) {
    return queryFactory
            .selectFrom(member)
            .where(usernameEq(usernameCond),ageEq(ageCond))
            .fetch();
}
private Predicate usernameEq(String usernameCond) {
    if (usernameCond == null) {
        return null;
    }
    return member.username.eq(usernameCond);
}
private Predicate ageEq(Integer ageCond) {
    if (ageCond == null) {
        return null;
    }
    return member.age.eq(ageCond);
}

 where 문은 내부 인자로 null이 들어가는 경우, null은 그냥 무시하기 때문에 만일 usernameCond나 ageCond가 null인 경우 where 문의 인자로 null을 반환한다. usernameEq와 ageEq는 인자로 들어온 값이 null 인 경우 null을 반환하고, null이 아닌 경우 해당 인자와 동일한 조건의 member(조건식)를 반환한다.

 

 BooleanBuilder에 비해서 이런식으로 따로 메서드를 정의하는 것의 장점은 메서드간 조립이 자유롭다는 것이다. 이게 뭔 말이냐면,

지금 구현한 usernameEq( ), ageEq( )를 합쳐서 하나의 메서드 allEq( )를 구현하는 식으로 사용할 수 있다는 것이다.

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

private BooleanExpression usernameEq(String usernameCond) {
    // 생략
}

private BooleanExpression ageEq(Integer ageCond) {
    // 생략
}

private BooleanExpression allEq(String usernameCond, Integer ageCond) {
    if (usernameEq(usernameCond) == null) {
        if (ageEq(ageCond) == null) {
            return null;  //usernameCond, ageCond 둘 다 null.
        } else {
            return ageEq(ageCond); // usernameCond만 null
        }
    } else {
        if (ageCond == null) {
            return usernameEq(usernameCond); // ageCond만 null
        }
        return usernameEq(usernameCond).and(ageEq(ageCond)); // null 없음
    }
}

이게 null처리 때문에 메서드가 좀 지저분하긴 하다. 그래도 이런식으로 여러개의 메서드를 합쳐서 하나의 메서드를 사용하는 식으로 사용할 수 있다는 장점이 존재한다. 

return usernameEq(usernameCond).and(ageEq(ageCond));

또한 주의해야할 점이 하나 있는데 이런식으로 메서드들을 연결하기 위해선 해당 메서드들의 반환형이 BooleanExpression이 되어야한다. 따라서 현재 usernameEq()와 ageEq()의 반환형이 Predicate에서 BooleanExpression으로 변경되었다.

'Spring > Querydsl' 카테고리의 다른 글

Querydsl- 벌크 연산  (0) 2023.08.06
Querydsl- 프로젝션  (0) 2023.08.04
Querydsl- 조인  (0) 2023.07.27
Querydsl- 집합  (0) 2023.07.26
Querydsl- 정렬과 페이징  (0) 2023.07.25