BackEnd/JPA

[JPA] QueryDSL 중급문법 - 2

샤아이인 2022. 5. 14.

내가 공부한 것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼 겸 상세히 기록하고 얕은 부분들은 가볍게 포스팅하겠습니다.

 

4. 동적 쿼리 - BooleanBuilder 사용

동적 쿼리를 해결하는 방식에는 2가지 방식이 존제한다.

  1. BooleanBuilder
  2. Where 다중 파라미터 사용

각각이 다 장단점이 있는 방식이다. 이에 대하여 알아보자.

 

▶ BooleanBuilder 를 사용하는 방법

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

    List<Member> result = searchMember1(usernameParam, ageParam);
    assertThat(result.size()).isEqualTo(1);
}

private List<Member> searchMember1(String usernameParam, Integer ageParam) {
    BooleanBuilder builder = new BooleanBuilder();
    if(usernameParam != null) {
        builder.and(member.username.eq(usernameParam));
    }

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

    return queryFactory
            .selectFrom(member)
            .where(builder)
            .fetch();
}

BooleanBuilder를 활용하는 searchMember1()이라는 메서드를 만들었다.

넘어오는 인자의 null 유무에 따라 조건이 동적으로 만들어 진다. 

 

위 코드의 경우 이름과 나이가 모두 null이 아니기 때문에 다음과 같은 SQL문이 생성되게 된다.

select
    member0_.member_id as member_i1_1_,
    member0_.age as age2_1_,
    member0_.team_id as team_id4_1_,
    member0_.username as username3_1_ 
from
    member member0_ 
where
    member0_.username=? 
    and member0_.age=?

 

만약 ageParam 에 null을 전달한다면 쿼리에 age 부분이 나오지 않게된다.

 

또한 usernameParam이 필수인 경우, BooleanBuilder에 인자로 초기값을 다음과 같이 전달할수도 있다.

BooleanBuilder builder = new BooleanBuilder(member.username.eq(usernameParam));

 

5. 동적 쿼리 - Where 다중 파라미터 사용

이 방법이 실무에 더 자주 사용하는 방식이다.

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

    List<Member> result = searchMember2(usernameParam, ageParam);
    assertThat(result.size()).isEqualTo(1);
}

private List<Member> searchMember2(String usernameParam, Integer ageParam) {
    return queryFactory
            .selectFrom(member)
            .where(usernameEq(usernameParam), ageEq(ageParam))
            .fetch();
}

private Predicate usernameEq(String usernameParam) {
    if(usernameParam != null) {
        return member.username.eq(usernameParam);
    }
    return null;
}

private Predicate ageEq(Integer ageParam) {
    if(ageParam != null) {
        return member.age.eq(ageParam);
    }
    return null;
}

searchMember2 메서드를 만들게 되었다.

where절이 핵심이다. 다음과 같다.

.where(usernameEq(usernameParam), ageEq(ageParam))

만약 usernameEq가 null을 반환한다면 다음과 같이 될것이다.

.where(null, ageEq(ageParam))

이때 null은 그냥 무시되어 버린다. 

 

이 방식은 searchMember2 메서드의 본문만 보면 어떠한 처리를 하는 쿼리인지 한눈에 들어온다는 장점이 있다.

생성되는 쿼리는 다음과 같다.

select
    member0_.member_id as member_i1_1_,
    member0_.age as age2_1_,
    member0_.team_id as team_id4_1_,
    member0_.username as username3_1_ 
from
    member member0_ 
where
    member0_.username=? 
    and member0_.age=?

추가로 다음과 같이 활용할수도 있다.

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

// 반환 타입이 BooleanExpression으로 변경
private BooleanExpression usernameEq(String usernameParam) {
    if(usernameParam != null) {
        return member.username.eq(usernameParam);
    }
    return null;
}

// 반환 타입이 BooleanExpression으로 변경
private BooleanExpression ageEq(Integer ageParam) {
    if(ageParam != null) {
        return member.age.eq(ageParam);
    }
    return null;
}

private BooleanExpression allEq(String usernameParam, Integer ageParam) {
    return usernameEq(usernameParam).and(ageEq(ageParam));
}

allEq()라는 메서드를 만들게 되었다. 기존의 usernameEq 와 ageEq를 재조립 하는 메서드를 만들수 있게 되었다.

이와 같이 메서드가 재활용 가능하고, 쿼리의 가독성 또한 높혀주는 좋은 방식이다.

 

한가지 의문이 든다. 위 코드에서 다음 코드부분을 살펴보자.

private BooleanExpression allEq(String usernameParam, Integer ageParam) {
    return usernameEq(usernameParam).and(ageEq(ageParam));
}

만약 usernameEq()가 null을 반환하게 되면 메서드 체인으로 and()를 걸 수 없다. NullPointException이 발생할게 훤하다.

 

Null Safe한 코드를 어떻게 작성해야 할까?

 

다음은 기존의 BooleanExpression을 BooleanBuilder로 변경한 NullSafe한 코드이다.

private BooleanBuilder usernameEq(String usernameParam) {
    if(usernameParam != null) {
        return new BooleanBuilder(member.username.eq(usernameParam));
    }
    return new BooleanBuilder();
}

private BooleanBuilder ageEq(Integer ageParam) {
    if(ageParam != null) {
        return new BooleanBuilder(member.age.eq(ageParam));
    }
    return new BooleanBuilder();
}

private BooleanBuilder allEq(String usernameParam, Integer ageParam) {
    return usernameEq(usernameParam).and(ageEq(ageParam));
}

이를 람다를 혼용하면 다음과 같이 작성할수도 있다.

private BooleanBuilder usernameEq(String usernameParam) {
    return nullSafeBuilder(() -> member.username.eq(usernameParam));

}

private BooleanBuilder ageEq(Integer ageParam) {
    return nullSafeBuilder(() -> member.age.eq(ageParam));
}

public static BooleanBuilder nullSafeBuilder(Supplier<BooleanExpression> f) {
    try {
        return new BooleanBuilder(f.get());
    } catch (IllegalArgumentException e) {
        return new BooleanBuilder();
    }
}

 

6. 수정 삭제 벌크 연산

6-1) 벌크 업데이트 쿼리

다음 테스트 코드를 살펴보자.

@Test
@Commit
public void bulk_update_test() {
    Member member10 = new Member("member10", 40);
    em.persist(member10);

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

실행 후 H2를 통해 DB를 살펴보면 다음과 같다.

하지만 항상 벌크 연산에는 문제점이 한가지 있다.

JPQL에서도 그래왔듯, 벌크 연산은 영속성 컨텍스트에 있는 Entity를 무시하고 바로 DB에 쿼리가 나가기 때문에 영속성 컨텍스트의 데이터와 DB의 데이터가 달라지게 된다.

 

따라서 벌크 연산 후에는 다음 코드를 꼭 호출해주자.

em.flush()
em.clear()

 

6-2) 모든 회원나이 +1 하기

다음으로는 모든 회원의 나이를 +1 해주는 코드를 작성해 보자.

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

실행되는 쿼리는 다음과 같다.

update
    member 
set
    age=age+1

 

6-3) 모든 회원 삭제

@Test
public void bulk_delete_test() {
    queryFactory
            .delete(member)
            .where(member.age.gt(18))
            .execute();
}

실행되는 쿼리는 다음과 같다.

delete 
from
    member 
where
    age>18

 

7. SQL function 호출하기

username에서 member라는 단어를 m으로 변경하여 출력할 것 이다.

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

실행 결과는 기대한것처럼 잘 나오게 되었다.

만들어지는 SQL은 다음과 같다.

select
    replace(member0_.username, ?, ?) as col_0_0_ 
from
    member member0_

만약 사용자 함수를 등록해서 사용하려면 (예전에 JPA 기본편에서 언급한 내용)

H2Dialect를 상속받은 class를 만들고, 그거를 설정에 등록해서 사용해야 한다.

 

이번에는 문자를 소문자로 바꾸는 코드를 작성해 보자.

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

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

코드가 너무 복잡해 졌다...

실행되는 쿼리는 다음과 같다.

select
    member0_.username as col_0_0_ 
from
    member member0_ 
where
    member0_.username=lower(member0_.username)

하지만 소문자로 바꾸는 기능과 같이 자주 사용하는 몇가지 기능들은 QueryDsl이 내장하고 있다.

lower 같은 ansi 표준 함수들은 querydsl이 상당부분 내장하고 있다. 따라서 다음과 같이 처리해도 결과는 같다.

.where(member.username.eq(member.username.lower()))

댓글