JPA

QueryDSL 동적 쿼리 및 Bulk 연산

빈코 2022. 11. 15. 13:25

지난 포스팅에서는 QueryDSL의 Projection에 대해 알아보았습니다. 이번 포스팅은 동적 쿼리를 BooleanBuilder 사용과 Where 다중 파라미터 사용방법에 대해 알아보고 이어서 다중 업데이트 및 삭제를 위한 벌크 연산에 대해 알아보려고 합니다😊

 

로고
동적 쿼리 & 벌크 연산

 

개요

예시 관계도

이전 포스팅에서 사용했던 엔티티들을 재사용할 예정입니다. 혹여 엔티티 클래스의 코드가 궁금하신 분들은 엔티티 설계 포스팅을 참고해주세요😄

 

📌 저는 BeforeEach 어노테이션을 활용해 가 데이터가 들어간 상황에서 테스트를 진행한 것입니다.

 

@BeforeEach
public void before() {
    queryFactory = new JPAQueryFactory(em);
    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);
}
MEMBER_ID NAME AGE TEAM_NAME
1 member1 10 teamA
2 member2 20 teamA
3 member3 30 teamB
4 member4 40 teamB

 

동적 쿼리📚

동적 쿼리란 상황에 따라 다른 문법의 SQL을 적용하는 것을 말합니다. 예를 들어 조회 로직을 구현할 때, 이름값이 들어오면 WHERE name = ${name}, 나이가 들어오면 WHERE age = ${age}로 쉽게 변경이 가능한 것이 동적 쿼리입니다.

 

이를 QueryDSL을 사용하여 해결하기 위해선 서두에서 언급한 BooleanBuilderWhere Param을 사용하여 해결 할 수 있습니다. 먼저 BooleanBuilder를 알아볼까요?

 

BooleanBuilder📙

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

    BooleanBuilder builder = new BooleanBuilder();
    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();
}

만약 Member를 이름과 나이 필드를 이용하여 검색하는 로직이 있다고 가정했을 때, 매개변수로 해당 값(이름, 나이)을 받고 null이 아닐 때만 BooleanBuilder에 조건문을 추가시킵니다.

 

원하는 조건을 다 넣어준 후 queryFactory.where 절에 해당 builder를 넣어주면 조건문이 완성이 됩니다. 해당 메서드의 테스트 코드를 작성해보면

 

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

    List<Member> result = searchMember(usernameParam, ageParam);

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

 

이름이 "member1"이면서 10살인 회원은 member1 밖에 없으므로 테스트를 성공하는 것을 확인할 수 있습니다. 또한 테스트 결과와 같이 원하는 대로 쿼리문이 전달된 것도 확인할 수 있었습니다.

 

테스트 결과
테스트 결과

 

Where 다중 파라미터📒

이전에 소개한 BooleanBuilder도 실무에서 많이 활용하지만, Where 다중 파라미터를 사용하면 메서드들을 다른 쿼리에서도 재활용 할 수 있으며 쿼리 자체의 가독성이 높아지는 장점이 있습니다.

 

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

    List<Member> result = searchMemberWithWhere(usernameParam, ageParam);

    Assertions.assertThat(result.size()).isEqualTo(2);
}


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

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

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

searchMemberWithWhere 메서드에서는 해당 where 조건문에 usernameEq 메서드와 ageEq 메서드를 넣었는데, 직관적으로 어떠한 조건인지 파악하기 쉽고, usernameEq, ageEq 메서드는 다른 로직에서도 재활용할 수 있습니다. 

 

한 가지 예시로, 이름과 나이가 모두 같은 allEq 메서드를 만들 수 있습니다😊

 

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

또한, return 값이 null일 경우 QueryDSL에서는 자동으로 해당 값을 무시하기 때문에 동적으로 쿼리 작성이 가능합니다.

 

Bulk 연산📘

JPA에서는 따로 update 메서드를 제공하지 않습니다. Dirty Checking(변경 감지)을 통해 수정 로직이 진행되는데, 이 부분에 대해서는 상당히 중요한 이야기여서 모르시는 분은 링크를 클릭하셔서 선수 학습을 진행해주시면 좋을 것 같습니다😄

 

Dirty Checking은 건별로 업데이트를 진행하기 때문에, 한번에 업데이트하는 로직은 Bulk 연산을 사용하는 것이 좋습니다. 만약 클라이언트 요구사항 중에 28살보다 작은 회원들의 이름을 "비회원"으로 수정해달라는 요청이 있었다고 가정해보겠습니다.

 

@Test
public void bulkUpdate(){

    // member1 = 10 -> 비회원
    // member2 = 20 -> 비회원
    // member3 = 30 -> 유지
    // member4 = 40 -> 유지

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

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

 

코드상 문제가 없지만, em.flush()em.clear() 메서드가 왜 들어갔을까요? JPA는 기본적으로 트랜잭션이 끝나기 전까지 영속성 컨텍스트에 쿼리문을 쌓아놓았다가 트랜잭션이 끝남과 동시에 DB에 전달하는 특징을 가집니다.

 

하지만 벌크연산은 DB에 바로 쿼리를 전달합니다. 만약, 업데이트한 후 다시 DB에서 조회하는 로직이 추가된다면 어떻게 될까요?

 

예상대로라면 DB에서 다시 값을 가져와서 member1과 member2의 이름은 "비회원"으로 가져올 거라 생각이 들지만, 여전히 member1, member2로 값을 가져옵니다.

 

이 문제는 영속성 컨텍스트와 DB값의 불일치 현상 때문입니다. 앞서 말씀드린 것처럼 벌크 연산을 통해 DB값은 바로 바뀌고, 영속성 컨텍스트에는 이전의 값이 남아있기 때문에 불일치 현상이 일어난 것이며, 이러한 불일치 현상을 해결하기 위해 영속성 컨텍스트를 모두 초기화시켜주는 em.flush()와 em.clear() 메서드가 들어간 이유입니다😀


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

모든 회원의 나이를 1씩 더하는 로직입니다. 반대로 1씩 빼는 로직은 minus 로직은 따로 없고 add() 메서드안에 -1을 넣어주시면 됩니다. 또한, 곱하기 같은 경우에는 multiply() 메서드를 사용하면 됩니다.

 

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

조건문에 해당하는 모든 회원을 삭제하는 로직은 delete 연산을 사용하시면 됩니다. 이 밖의 많은 메서드들을 제공하는데 API를 확인하시는 게 좋아 보이네요😄

 


👨‍👩‍👦‍👦 오픈채팅방 운영

취업을 준비하는 예비 개발자분들을 위한 질문&답변할 수 있는 공간을 만들었습니다. 취업과 이직을 하기 위해서 어떤 걸 중점적으로 준비해야 하는지부터 포트폴리오&이력서 작성법 등 다양한 질문들을 받고 답변을 드립니다. 참여하셔서 다양한 정보 얻고 가시면 좋을 것 같네요😁

 

참여코드 : 456456

https://open.kakao.com/o/gVHZP8dg

 

비전공 개발자 취업 준비방(질문&답변)

#비전공 #개발자 #취업 #멘토링 #부트캠프 #국비지원 #백엔드 #프론트엔드 #중소기업 #중견기업 #자바 #Java #sql

open.kakao.com

 


👨‍💻 전자책 출간

아울러 제가  🌟비전공자에서 2년만에 보안 전문 중견기업으로 이직 한 방법들을 정리한 전자책을 출간 하게 되었습니다. 어떤 걸 공부해야 하는지, 이직을 위해서 무엇을 준비해야 하는지, 제가 받았던 기술 면접 리스트 등 다양한 목차로 구성되어 있습니다. 또한, 구매 시 1:1 채팅을 이용하여 포트폴리오 첨삭을 도와드리고 있습니다. 🐕전자책으로 얻은 모든 수익은 유기견 센터 '팅*벨 입양센터'에 후원될 예정입니다. 관심 있으신 분들은 아래 링크를 참고해주세요😁

https://kmong.com/gig/480954

 

비전공개발자 2년만에 중견기업 들어간 방법 | 14000원부터 시작 가능한 총 평점 0점의 전자책, 취

0개 총 작업 개수 완료한 총 평점 0점인 Binco의 전자책, 취업·이직 전자책 서비스를 0개의 리뷰와 함께 확인해 보세요. 전자책, 취업·이직 전자책 제공 등 14000원부터 시작 가능한 서비스

kmong.com


 

 

마치며

지금까지 QueryDSL에서 제공하는 동적 쿼리와 Bulk 연산에 대해 알아보았습니다. 사실 설명할 부분이 크게 없을 정도로 상당히 직관적이지만, 접해보지 못했더라면 영영 몰랐을 만한 정보였던 것 같아요. 테스트 코드로 열심히 연습해서 실무에도 적용해보려 합니다😄

 

QueryDSL Series

1. QueryDSL 적용 및 예제 시리즈 시작

2. QueryDSL 조회 예제

3. QueryDSL 정렬 & 페이징 & 집합

4. QueryDSL 조인 예제

5. QueryDSL Projection

6. QueryDSL 동적 쿼리 & Bulk 연산(현재)

7. QueryDSL 실무 활용

 

Reference

실전! Querydsl

 

반응형

'JPA' 카테고리의 다른 글

QueryDSL 실무 활용  (0) 2022.11.18
QueryDSL Projection 결과 반환하는 방법  (0) 2022.11.14
QueryDSL 조인 사용 방법  (0) 2022.11.01
QueryDSL 정렬과 페이징 처리 및 집합 예시  (2) 2022.10.31
QueryDSL 사용하는 조회 예제  (0) 2022.10.28