JPA Clear

2 분 소요


RECOMMEND POSTS BEFORE THIS

0. 들어가면서

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

1. Problem

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

  • JQPL 쿼리를 사용해 업데이트를 수행합니다.
  • 업데이트 대상 엔티티를 재조회합니다.
  • 엔티티의 모습을 확인합니다.
    • 업데이트가 반영되지 않은 엔티티가 조회됩니다.
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)));
        });
    }
}
Test Result
  • 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 메소드를 사용하면 다음과 같은 테스트 결과를 얻을 수 있습니다.

  • JQPL 쿼리를 사용해 업데이트를 수행합니다.
  • 업데이트 대상 엔티티를 재조회합니다.
  • 엔티티의 모습을 확인합니다.
    • 업데이트가 반영된 엔티티가 조회됩니다.
    • 연락처가 변경된 휴대폰 번호 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)));
        });
    }
Test Result
  • 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

댓글남기기