JPA 클리어(clear)

2 분 소요


RECOMMEND POSTS BEFORE THIS

0. 들어가면서

JPA 플러쉬(flush)에 대한 글을 작성하면서 마주친 현상과 이를 해결하기 위한 방법을 정리했다. JPQL(Java Persistence Query Language) 쿼리는 즉시 실행한다. 때문에 엔티티 매니저(entity manager)의 영속성 컨텍스트(persistence context)에서 관리 중인 엔티티들과 데이터가 서로 다른 모습을 가질 수 있다. 간단한 테스트 코드를 통해 현상을 살펴보고, 왜 발생했는지 관련된 내용을 정리했다.

1. Problem

다음과 같은 문제가 발생했다.

  • JPQL 쿼리를 사용해 업데이트를 수행한다.
  • 업데이트 대상 엔티티를 다시 조회 후 상태를 확인했지만, 업데이트가 반영되지 않은 엔티티가 조회된다.
package blog.in.action.clear;

import blog.in.action.entity.Member;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.PersistenceUnit;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;

@Log4j2
@DataJpaTest
public class ClearTest {

    @PersistenceUnit
    private EntityManagerFactory factory;

    void transaction(Consumer<EntityManager> consumer) {
        EntityManager entityManager = factory.createEntityManager();
        EntityTransaction transaction = entityManager.getTransaction();
        transaction.begin();
        try {
            consumer.accept(entityManager);
        } catch (Exception ex) {
            throw ex;
        } finally {
            transaction.rollback();
            entityManager.close();
        }
    }

    @Test
    public void without_clear() {
        transaction(entityManager -> {
            Member member = Member.builder()
                    .id("Junhyunny")
                    .name("Junhyun Kang")
                    .contacts(Arrays.stream(new String[]{"kang3966@naver.com"}).collect(Collectors.toList()))
                    .build();
            entityManager.persist(member);
            member.appendContact("010-1234-1234");


            String updateQuery = "update Member m set m.contacts = '010-4321-4321' where m.id = 'Junhyunny'";
            int result = entityManager.createQuery(updateQuery).executeUpdate();


            String selectQuery = "select m from Member m where m.name = 'Junhyun Kang'";
            Member resultMember = entityManager.createQuery(selectQuery, Member.class).getSingleResult();
            List<String> contacts = resultMember.getContacts();
            assertThat(result, equalTo(1));
            assertThat(contacts.size(), equalTo(2));
            assertThat("kang3966@naver.com", equalTo(contacts.get(0)));
            assertThat("010-1234-1234", equalTo(contacts.get(1)));
        });
    }
}

테스트에서 실행된 SQL 로그를 보면 보면 다음과 같다.

  • JPQL 업데이트 쿼리를 수행하기 전에 엔티티 매니저와 관리 중인 엔티티들을 모두 플러시(flush)한다.
  • JPQL 업데이트 쿼리를 수행한다.
  • 이름이 Junhyun Kang인 데이터를 조회한다.
Hibernate: insert into tb_member (contacts, name, id) values (?, ?, ?)
Hibernate: update tb_member set contacts=?, name=? where id=?
Hibernate: update tb_member set contacts='010-4321-4321' where id='Junhyunny'
Hibernate: select member0_.id as id1_0_, member0_.contacts as contacts2_0_, member0_.name as name3_0_ from tb_member member0_ where member0_.name='Junhyun Kang'

2. Cause

테스트 코드의 검증(assert)을 봤을 때도 업데이트 결과는 1건으로 정상 처리됐다. 정상적으로 업데이트한 이후 재조회하였음에도 엔티티에 데이터 변경 내용이 반영되지 않았다. 이러한 현상이 발생하는 이유는 영속성 컨텍스트 때문이다.

  • 엔티티 매니저는 내부적으로 영속성 컨텍스트를 1차 캐시로 사용한다.
  • 영속성 컨텍스트에서 관리되는 엔티티가 존재한다면 이를 재사용한다.

3. Solve the problem

이 문제를 해결하려면 엔티티 매니저의 clear 메서드를 사용해야 한다. clear 메서드는 엔티티 매니저가 관리 중인 엔티티들을 영속성 컨텍스트에서 모두 비우는 작업을 수행한다. clear 메서드를 사용하면 다음과 같은 테스트 결과를 얻을 수 있다.

  • JPQL 쿼리를 사용해 업데이트를 수행한다.
  • 업데이트 대상 엔티티를 다시 조회 후 상태를 확인하면, 업데이트가 반영된 엔티티가 조회된다. 연락처가 변경된 휴대폰 번호 1개임을 확인할 수 있다.
    @Test
    public void with_clear() {
        transaction(entityManager -> {
            Member member = Member.builder()
                    .id("Junhyunny")
                    .name("Junhyun Kang")
                    .contacts(Arrays.stream(new String[]{"kang3966@naver.com"}).collect(Collectors.toList()))
                    .build();
            entityManager.persist(member);
            member.appendContact("010-1234-1234");
            String updateQuery = "update Member m set m.contacts = '010-4321-4321' where m.id = 'Junhyunny'";
            int result = entityManager.createQuery(updateQuery).executeUpdate();


            entityManager.clear();


            String selectQuery = "select m from Member m where m.name = 'Junhyun Kang'";
            Member resultMember = entityManager.createQuery(selectQuery, Member.class).getSingleResult();
            List<String> contacts = resultMember.getContacts();
            assertThat(result, equalTo(1));
            assertThat(contacts.size(), equalTo(1));
            assertThat("010-4321-4321", equalTo(contacts.get(0)));
        });
    }

테스트에서 실행된 SQL 로그를 보면 다음과 같다.

  • JPQL 업데이트 쿼리를 수행하기 전에 엔티티 매니저와 관리 중인 엔티티들을 모두 플러시(flush)한다.
  • JPQL 업데이트 쿼리를 수행한다.
  • 이름이 Junhyun Kang인 데이터를 조회한다.
Hibernate: insert into tb_member (contacts, name, id) values (?, ?, ?)
Hibernate: update tb_member set contacts=?, name=? where id=?
Hibernate: update tb_member set contacts='010-4321-4321' where id='Junhyunny'
Hibernate: select member0_.id as id1_0_, member0_.contacts as contacts2_0_, member0_.name as name3_0_ from tb_member member0_ where member0_.name='Junhyun Kang'

TEST CODE REPOSITORY

REFERENCE

댓글남기기