[Querydsl] 18. 수정, 삭제 벌크 연산

민정·2023년 1월 10일

QueryDSL

목록 보기
18/18
post-thumbnail

✨ 수정, 삭제 벌크 연산

벌크 연산 : 쿼리 한번으로 대량의 데이터 수정

사용법

.queryFactory.update(Q타입 인스턴스).set(필드, 변경이후 값).where(조건).execute()

  • 반환 값 : 변경된 row 개수

코드 예시

예) 모든 사람 연봉 연상
엔티티 하나 하나 update 쿼리 날리기(by 더티체킹)보다는 한번의 update 쿼리로 처리하는 것이 효율적

/**
     * 수정, 삭제 벌크 연산
     *
     * 예) 모든 사람 연봉 연상: 엔티티 하나 하나 update 쿼리 날리기(by 더티체킹)보다는 한번의 update 쿼리로 처리하는 것이 효율적
     */
    @Test
    public void bulkUpdate() throws Exception {
        // member1 = 10살 -> 비회원
        // member2 = 20살 -> 비회원

        // 회원 중에서 나이가 28살보다 작은 회원의 이름을 "비회원"으로 변경
        long count = queryFactory // count = update의 영향을 받은 row의 수
                .update(member)
                .set(member.username, "비회원")
                .where(member.age.lt(28))
                .execute();

        // 벌크 연산 이후, 영속성 컨텍스트 초기화 필수!
        // 벌크 연산은 영속성 컨텍스트를 무시하고 DB에 바로 쿼리를 날리기 때문
        em.flush();
        em.clear();

        // 조회
        List<Member> result = queryFactory
                .selectFrom(member)
                .fetch();

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

update 쿼리 한개 나감

28살보다 나이가 어린 사람들의 username이 비회원으로 변경된 것을 확인할 수 있다.



✨ 벌크 연산 항상 조심

벌크 연산은 영속성 컨텍스트를 무시하고 DB에 바로 쿼리가 나간다.
즉, 벌크연산이후에는 DB의 상태영속성 컨텍스트의 상태가 달라진다는 것이다!

DB에서 데이터를 가져오더라도, 이미 영속성 컨텍스트에 존재하면 영속성 컨텍스트의 데이터를 유지한다.
-> 영속성 컨텍스트가 우선권을 가진다.

따라서 벌크 연산 이후에 영속성 컨텍스트를 초기화하고, 다시 DB에서 데이터를 가져와서 영속성 컨텍스트에 넣어줘야한다!!!

✅ 해결

벌크 연산 이후 무조건 영속성 컨텍스트 초기화 필수

✅ 영속성 컨텍스트 초기화 코드

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



🧐 궁금증) set으로 특정 엔티티의 이름 변경 이후, 벌크 수정 쿼리로 또 그 엔티티의 이름을 변경한 경우 충돌이 발생하지 않는가!?

em.flush()를 하면 쓰기 지연 저장소에 있던 SQL이 DB에 실제로 반영

그럼 예를 들어 member1의 이름을 setUsername()으로 변경하고, 벌크 update로 member1의 이름을 변경했을 경우, em.flush(), em.clear()를 하면 실제로 DB에는 어떻게 되는가?

일단 충돌이 발생하지 않나? 라고 생각했던 이유는
벌크 연산은 영속성 컨텍스트를 무시하고 DB에 바로 쿼리를 날린다기에 DB에 쿼리가 날라가는 순서가 아래와 같을 것이라고 예측했었다!
1. 벌크 update SQL
2. em.flush()로 인해서 엔티티 더티체킹 이후 SQL 쓰기 지연 저장소에 있던 엔티티 1개의 update SQL
-> 하지만 코드 상으로는 벌크 수정 쿼리가 setUsername()보다 나중이라서, 원했던 결과는 벌크 수정의 결과가 DB에 남아있길 바란다!

✅ 코드로 직접 테스트!

코드 순서

  1. setUsername("member111")
  2. queryFactory로 벌크 update
  3. em.flush(), em.clear()
@Test
    @Commit
    public void bulkUpdate() throws Exception {
        // member1 = 10살 -> 비회원
        // member2 = 20살 -> 비회원
        Member findMember = queryFactory
                .selectFrom(member)
                .where(member.username.eq("member1"))
                .fetchOne();

        System.out.println("findMember = " + findMember);
        
        findMember.setUsername("member111");

        // 회원 중에서 나이가 28살보다 작은 회원의 이름을 "비회원"으로 변경
        long count = queryFactory // count = update의 영향을 받은 row의 수
                .update(member)
                .set(member.username, "비회원")
                .where(member.age.lt(28))
                .execute();

        // 벌크 연산 이후, 영속성 컨텍스트 초기화 필수!
        // 벌크 연산은 영속성 컨텍스트를 무시하고 DB에 바로 쿼리를 날리기 때문
        em.flush();
        em.clear();

        // 조회
        List<Member> result = queryFactory
                .selectFrom(member)
                .fetch();

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

    }

✅ 결과

member111로 변경하는 update 쿼리가 먼저 나가고

그다음 벌크연산 update 발생해서

결국에는 DB에는 벌크 연산 update의 결과인 "비회원"이 member1의 username으로 저장되어있었다.

✅ 강의 QnA

강의 QnA에서 해당 궁금증을 가진 사람과 선생님의 답변을 확인할 수 있었다.
벌크 연산에 대한 질문
벌크성 수정 쿼리 질문

QnA 요약

JPQL 실행 직전에 자동으로 플러시가 호출 된다.
벌크 연산도 JPQL을 실행하는 것이기 때문에 플러시가 발생.
즉, 벌크 연산 전에 쓰기연산 저장소에 있던 Query들이 모두 DB에 적용된다는 것!



✨ 벌크 수정 : add, multiply

   /**
     * 벌크 연산 : add, multiply
     */
    @Test
    public void bulkAdd() throws Exception {
        long count = queryFactory
                .update(member)
                .set(member.age, member.age.add(1)) // minus 없음. add(-1)사용. 곱하기: multiply(x)
                .execute();

    }



✨ 벌크 삭제

사용법

queryFactory.delete(Q타입 인스턴스).where(조건).execute()

  • 반환 값 : 삭제된 row 수
    /**
     * 쿼리 한번으로 대량 데이터 삭제
     */
    @Test
    public void bulkDelete() throws Exception {
        long count = queryFactory
                .delete(member) // 삭제
                .where(member.age.gt(18)) // 18살 이상
                .execute();

    }



출처

김영한 강사님 - 인프런 실전! Querydsl

0개의 댓글