-> 블로그 이전

[JPA] 벌크 연산

2022. 8. 11. 17:14Language`/JPA

벌크 연산

Entity를 수정하기 위해서는 [Dirty Checking, Merge]를 사용하고, 삭제하기 위해서는 [remove()]를 사용한다

 

1-1. 수정 (벌크 연산 X)

List<Member> memberList = new ArrayList<>();

for (long i = 1L; i <= 10L; i++) {
    Member findMember = em.find(Member.class, i);
    memberList.add(findMember);
}

// 멤버 나이 전부 100살로 변경
for (Member member : memberList) {
    member.setAge(100);
}
select
        member0_.member_id as member_i1_0_0_,
        member0_.age as age2_0_0_,
        member0_.birth as birth3_0_0_,
        member0_.team_id as team_id6_0_0_,
        member0_.type as type4_0_0_,
        member0_.username as username5_0_0_ 
    from
        member member0_ 
    where
        member0_.member_id=1
X 10개

update
        member 
    set
        age=100,
        birth='1999-01-17T00:00:00.000+0900',
        team_id=1,
        type='PLAYER',
        username='Avenus1' 
    where
        member_id=1
X 10개

 

1-2. 삭제 (벌크 연산 X)

List<Member> memberList = new ArrayList<>();

for (long i = 1L; i <= 10L; i++) {
    Member findMember = em.find(Member.class, i);
    memberList.add(findMember);
}

// 멤버 전원 삭제
for (Member member : memberList) {
    em.remove(member);
}
select
        member0_.member_id as member_i1_0_0_,
        member0_.age as age2_0_0_,
        member0_.birth as birth3_0_0_,
        member0_.team_id as team_id6_0_0_,
        member0_.type as type4_0_0_,
        member0_.username as username5_0_0_ 
    from
        member member0_ 
    where
        member0_.member_id=1
X 10개

delete 
    from
        member 
    where
        member_id=1
X 10개

이렇게 결과로 알 수 있듯이 하나하나 엔티티를 수정하거나 삭제하려면 시간도 너무 오래걸리고 쿼리도 개수만큼 DB로 날라가게 된다

>> 이럴때는 여러 건을 한번에 수정/삭제해주는 "벌크 연산"을 사용하면 된다

벌크 연산을 활용하려면 em.createQuery에 벌크 연산을 수행할 쿼리를 넣어주고 "executeUpdate()"를 호출해주면 된다

  • executeUpdate()의 Return Value는 "DB상에서 변화된 Tuple의 개수"이다

 

2-1. 수정 (벌크 연산 O)

String sql = "UPDATE Member m " +
             "SET m.age = 100";

int updateCount = em.createQuery(sql)
        .executeUpdate();

 

2-2. 삭제 (벌크 연산 O)

String sql = "DELETE FROM Member m " +
             "WHERE m.birth < '2000-01-01'";

int updateCount = em.createQuery(sql)
        .executeUpdate();

 

2-3. 삽입 (벌크 연산 O)

INSERT 벌크 연산은 JPA에서 공식적으로 제공해주는 기능이 아니라 "Hibernate"에서 따로 제공해주는 기능이다

String sql =
        "INSERT INTO OldMember(id, username, age, birth, type) " +
        "SELECT m.id, m.username, m.age, m.birth, m.type" +
        " FROM Member m" +
        " WHERE m.birth < '1991-01-01'";

int updateCount = em.createQuery(sql)
        .executeUpdate();

위의 코드는 "1991년 1월 1일" 이전에 태어난 멤버들을 "old_member Table"에 저장하는 코드이다

 

벌크 연산 주의점

벌크 연산을 할 때 가장 조심해야할 점은 "벌크 연산은 영속성 컨텍스트를 거치지 않고 바로 DB에 쿼리가 날라간다"라는 점이다

// 멤버 3명 저장
Member member1 = Member.createMemberWithNoTeam(
        "Avenus1", 
        23, 
        LocalDate.of(1999, 1, 17), 
        MemberType.PLAYER
);
Member member2 = Member.createMemberWithNoTeam(
        "Avenus2", 
        25, 
        LocalDate.of(1997, 3, 15), 
        MemberType.PLAYER
);
Member member3 = Member.createMemberWithNoTeam(
        "Avenus3", 
        21, 
        LocalDate.of(2001, 3, 10), 
        MemberType.MANAGER
);    

em.persist(member1);
em.persist(member2);
em.persist(member3);

위의 코드는 멤버 3명을 em.persist를 통해서 영속화하는 코드이다

em.persist 후 영속성 컨텍스트의 모습은 다음과 같을 것이다

PK 생성 전략을 IDENTITY로 가져갔기 때문에 예외적으로 em.persist를 하자마자 DB로 INSERT Query가 나가게 되었고 그에 따라서 DB에 3명의 멤버가 저장됨을 알 수 있다

 

여기서 "벌크 연산"을 활용해서 '1999년 1월 1일' 이후 멤버의 birth를 '2022년 8월 11일'로 바꿔보자

String sql =
        "UPDATE Member m " +
        "SET m.birth = '2022-08-11' " +
        "WHERE m.birth >= '1999-01-01'";

int updateCount = em.createQuery(sql)
        .executeUpdate();

벌크 연산 후 영속성 컨텍스트 & DB간에 멤버 정보들을 비교해보자

  • 간단한 비교를 위해서 birth만 명시

결과를 보면 영속성 컨텍스트 1차 캐시에 존재하는 멤버의 birth정보와 DB에 존재하는 멤버의 birth정보가 서로 다름을 확인할 수 있다

>> 왜냐하면 "벌크 연산"다이렉트로 DB에 쿼리가 날라가기 때문이다

이 다른 정보를 코드를 통해서 직접 확인해보자

// 3. DB - 영속성 컨텍스트간 데이터 비교
Member findAvenus1InPersistenceContext = em.find(Member.class, 1L);
em.clear();

Member findAvenus1InDB = em.createQuery(
        "SELECT m FROM Member m WHERE m.id = 1",
        Member.class
).getSingleResult();

Member findAvenus3InPersistenceContext = em.find(Member.class, 3L);
em.clear();

Member findAvenus3InDB = em.createQuery(
        "SELECT m FROM Member m WHERE m.id = 3",
        Member.class
).getSingleResult();

System.out.println("## Avenus1 ##");
System.out.println("-> 영속성 컨텍스트 정보 = " + findAvenus1InPersistenceContext);
System.out.println("-> DB 정보 = " + findAvenus1InDB);

System.out.println("\n## Avenus3 ##");
System.out.println("-> 영속성 컨텍스트 정보 = " + findAvenus3InPersistenceContext);
System.out.println("-> DB 정보 = " + findAvenus3InDB);
em.find()를 통해서 영속성 컨텍스트에서 엔티티를 조회하고 난 후 "em.clear()"를 하는 이유는 다음과 같다
>> 이미 영속성 컨텍스트에 DB에서 조회하려는 엔티티가 있는경우, "DB에서 조회한 엔티티 정보"를 버리고 영속성 컨텍스트에서 해당 엔티티를 가져온다. 따라서 em.clear()를 하지 않으면 [영속성 컨텍스트 - DB]간에 정보 비교가 불가능하기 때문에 em.clear()를 통해서 서로의 정보 비교가 가능하도록 한 것이다

이 때 영속성 컨텍스트에 있는 엔티티 vs DB에서 조회한 엔티티가 "같음"을 판별할 때는 [식별자]를 통해서 판별한다

이처럼 영속성 컨텍스트 - DB간에 birth 정보가 다름을 확인할 수 있다

 

벌크 연산 데이터 동기화 해결책

1. em.refresh()

벌크 연산 후, 해당 엔티티를 즉시 영속성 컨텍스트에서 꺼내서 사용해야 한다면 em.refresh()를 통해서 DB에서 다시 조회한 후에 사용하면 된다

Member findAvenus1InPersistenceContext = em.find(Member.class, 1L);
em.refresh(findAvenus1InPersistenceContext);

Member findAvenus3InPersistenceContext = em.find(Member.class, 3L);
em.refresh(findAvenus3InPersistenceContext);

 

2. 벌크 연산을 먼저 실행

Member findAvenus1InPersistenceContext = em.find(Member.class, 1L);
Member findAvenus3InPersistenceContext = em.find(Member.class, 3L);

String sql =
        "UPDATE Member m " +
                "SET m.birth = '2022-08-11' " +
                "WHERE m.birth >= '1999-01-01'";
em.createQuery(sql).executeUpdate();

벌크 연산을 조회한 후에 실행하게 된다면 조회한 결과와 DB의 데이터가 다름을 확인할 수 있다

 

String sql =
        "UPDATE Member m " +
                "SET m.birth = '2022-08-11' " +
                "WHERE m.birth >= '1999-01-01'";
em.createQuery(sql).executeUpdate();

Member findAvenus1InPersistenceContext = em.find(Member.class, 1L);
Member findAvenus3InPersistenceContext = em.find(Member.class, 3L);

벌크 연산을 "먼저"한 후에 조회를 하면 결과는 다음과 같다

이제 영속성 컨텍스트 - DB간에 데이터 정합성 문제가 없음을 확인할 수 있다

 

3. 벌크 연산 수행 후 "영속성 컨텍스트 초기화 = em.clear()"

Member findAvenus1InPersistenceContext = em.find(Member.class, 1L);
Member findAvenus3InPersistenceContext = em.find(Member.class, 3L);

String sql =
        "UPDATE Member m " +
                "SET m.birth = '2022-08-11' " +
                "WHERE m.birth >= '1999-01-01'";
em.createQuery(sql).executeUpdate();

Member findAvenus1InDB = em.createQuery(
        "SELECT m FROM Member m WHERE m.id = 1",
        Member.class
).getSingleResult();
Member findAvenus3InDB = em.createQuery(
        "SELECT m FROM Member m WHERE m.id = 3",
        Member.class
).getSingleResult();

em.createQuery를 통해서 멤버를 찾아올 때 "영속성 컨텍스트"에 이미 em.find를 통해서 찾아와진 멤버들이 존재하기 때문에 em.createQuery를 해도 select query가 날라가서 조회해도 영속성 컨텍스트에 이미 데이터가 존재하기 때문에 조회한 데이터는 버리고 영속성 컨텍스트의 데이터를 가져오게 된다

따라서 벌크 연산을 통해서 update된 데이터가 아닌 "영속성 컨텍스트에 존재하는 update되지 않은 엔티티"를 조회함을 알 수 있다

 

Member findAvenus1InPersistenceContext = em.find(Member.class, 1L);
Member findAvenus3InPersistenceContext = em.find(Member.class, 3L);

String sql =
        "UPDATE Member m " +
                "SET m.birth = '2022-08-11' " +
                "WHERE m.birth >= '1999-01-01'";
em.createQuery(sql).executeUpdate();
em.clear();

Member findAvenus1InDB = em.createQuery(
        "SELECT m FROM Member m WHERE m.id = 1",
        Member.class
).getSingleResult();
Member findAvenus3InDB = em.createQuery(
        "SELECT m FROM Member m WHERE m.id = 3",
        Member.class
).getSingleResult();

위의 코드에서 "벌크 연산"후에 em.clear()를 통해서 영속성 컨텍스트를 완전히 초기화하였다

따라서 em.clear()다음에 em.createQuery를 통해서 엔티티를 조회할 때 일단 DB에서 무조건 조회해오고, 그 다음에 영속성 컨텍스트에 동일한 엔티티가 없기 때문에 조회한 데이터 그대로를 반환한다

 

영속성 컨텍스트가 관리하는 것은 조회한 "엔티티"만 관리한다.
엔티티 타입이 아닌 "임베디드 값 타입, 기본 값 타입"을 조회할 경우 영속성 컨텍스트는 이들을 관리해주지 않는다

 

em.find() vs JPQL

em.find()는 영속성 컨텍스트에 조회하려는 엔티티가 존재할 경우 DB에 쿼리를 날리지 않고, 영속성 컨텍스트에 없는 경우에만 DB에 쿼리를 날린다

 

반면에 JPQL은 영속성 컨텍스트에 조회하려는 엔티티가 있든 없든 "무조건 DB에 쿼리를 날려서 먼저 조회를 한다"

그리고 "DB에서 조회한 엔티티"가 영속성 컨텍스트에 있다면 조회한 데이터를 버리고, 영속성 컨텍스트에 없다면 조회한 데이터를 반환한다