jpa merge 와 dirty checking

준영속 객체의 update를 위하여

  • 영속성 컨텍스트에서 관리를 하기 위해서는 특정 객체게 엔티티 매니저에 영속성으로 들어가도록 하거나(em.persist(obj)), 영속성 컨텍스트로 객체를 찾아오면 된다(em.find(Member.class, 1L)).
  • 만약 DB에서의 아이디(@Id)의 값을 알고, 그 값을 객체로 생성하였고, 이를 update 하려면 어떻게 해야할까?
  • 여기서 논의할 방법은 두 가지이다. merge병함과 dirty checking 더티 체킹이다.

병합

// 1) 회원을 등록
Member member = new Member();
member.setName("kim");
member.setAge(10);
em.persist(member);

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

long memberId = member.getId();

// 2) update를 위하여 객체를 생성하여 persist에 삽입
Member updateMember = new Member();
updateMember.setId(memberId);
updateMember.setAge(11);

try {
    em.persist(updateMember);
}catch (PersistenceException e) {
    e.printStackTrace();
    System.out.println("========= detached entity passed to persist ==========");
}

// 3) merge를 통해 update를 진행. 영속화된 객체는 리턴으로 받는다.

Member resultMember = em.merge(updateMember);
em.flush();
em.clear();

System.out.println("============ merge를 통해 update에 성공 ==============");

System.out.printf("원래 값 : %s, %d\n", member.getName(), member.getAge());
System.out.printf("merger로 업데이트 한 값 : %s, %d\n", resultMember.getName(), resultMember.getAge());
em.flush();
em.clear();

  • 2) update를 위하여 persist를 시도하면 예외가 나온다. @Id @GenerateValue 인 필드에 값이 있을 경우, 준영속(detached entity) 상태로 보기 때문이다.
  • 준영속 상태란, 영속되었으나 영속성 컨텍스트에서 관리하지 않는 객체를 의미한다.

  • 3)에서 merge를 통해 update를 성공했다.
  • merge에 삽입한 객체가 아닌, 리턴한 객체를 사용해야 한다. 이 객체는 영속성 컨텍스트가 관리한다.
  • 다만 merge는 문제를 가진다.
Hibernate: 
    /* update
        jpa9_merge.Member */ update
            Member 
        set
            age=?,
            name=? 
        where
            id=?
============ merge를 통해 update에 성공 ==============
원래 값 : kim, 10
merger로 업데이트 한 값 : null, 11
  • 위의 코드의 의도는 1년이 지나서 10살에서 11살로 업데이트하고 싶었다. 그래서 age만 set을 하였지만, 사실상 setName을 하지 않았으므로 그 값은 null 이 된다. null 역시 update 되어 DB에는 이름이 사라진다.
  • 비지니스 로직에서 엔티티의 전체 데이터를 update 하는 경우가 사실은 없다. 보통은 public void addAge(){ this.age++;} 라는 식의 매서드를 사용하며, 이것은 특정 필드에만 영항을 미친다. 그러나 merge는 모든 필드에 영향을 미치기 때문에, 사실상 사용하는 것이 위험하다.
  • 그래서 아래와 같은 더치체킹을 주로 사용한다.

더티체킹

Member member2 = new Member();
member2.setName("lee");
member2.setAge(22);
em.persist(member2);

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

Member updateMember2 = em.find(Member.class, member2.getId());
updateMember2.setAge(23);

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

Member resultMember2 = em.find(Member.class, member2.getId());


System.out.printf("원래 값 : %s, %d\n", member2.getName(), member2.getAge());
System.out.printf("터티체킹으로 업데이트 한 값 : %s, %d\n", resultMember2.getName(), resultMember2.getAge());
Hibernate: 
    /* update
        jpa9_merge.Member */ update
            Member 
        set
            age=?,
            name=? 
        where
            id=?
Hibernate: 
    select
        member0_.id as id1_1_0_,
        member0_.age as age2_1_0_,
        member0_.name as name3_1_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
원래 값 : lee, 22
터티체킹으로 업데이트 한 값 : lee, 23
  • 더티체킹은 영속 객체의 변경점을 엔티티 매니저가 flush 할 때 감지하는 방식이다.
  • setAge()를 할 경우, age 필드만 변경된다. 이를 감지하여 age만 변경한다.