JPA를 공부할 때 가장 중요한게
객체와 관계형 데이터베이스를 매핑하는 것(Object Relational Mapping) 과
영속성 컨텍스트를 이해하는 것 이다.
두가지 개념은 꼭 알고 JPA를 활용하자.
영속성 컨텍스트는 JPA를 이해하는데 가장 중요한 용어이다.
"엔티티를 영구 저장하는 환경"이라는 뜻
EntityManager.persist(entity);
엔티티 매니저? 영속성 컨텍스트?
비영속(new/transient)
영속성 컨텍스트와 전혀 관계가 없는 상태
// 객체를 생성만 한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
영속(managed)
영속성 컨텍스트에 저장된 상태
엔티티가 영속성 컨텍스트에 의해 관리된다.
이때 DB에 저장 되지 않는다. 영속 상태가 된다고 DB에 쿼리가 날라가지 않는다.
트랜잭션의 커밋 시점에 영속성 컨텍스트에 있는 정보들이 DB에 쿼리로 날라간다.
// 객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
// 객체를 저장한 상태(영속)
em.persist(member);
준영속(detached)
영속성 컨텍스트에 저장되었다가 분리된 상태
// 회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(member);
삭제(removed)
삭제된 상태. DB에서도 날린다.
// 객체를 삭제한 상태
em.remove(member);
영속성 컨텍스트(엔티티 매니저)에는 내부에 1차 캐시가 존재한다.
엔티티를 영속성 컨텍스트에 저장하는 순간. 1차 캐시에
key : @Id로 선언한 필드 값, value : 해당 엔티티 자체로 캐시에 저장된다.
1차 캐시가 있으면 어떤 이점이있을까? 조회할 때 이점이 생긴다.
find()가 일어나는 순간, 엔티티 매니저 내부의 1차 캐시를 먼저 찾는다.
1차 캐시에 엔티티가 존재하면 바로 반환한다. DB 들리지 않는다.
주의! 1차 캐시는 글로벌하지 않다. 해당 스레드 하나가 시작할때 부터 끝날때 까지 잠깐 쓰는거다. 공유하지 않는 캐시다
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
...
// 1차 캐시에 저장됨
em.persist(member);
// 1차 캐시에서 조회
Member findMember = em.find(Member.class, "Member1");
1차 캐시에 데이터가 없다면? 데이터베이스에서 조회 한다.
영속 엔티티의 동일성을 보장한다.
1차 캐시 덕분에 member1을 두번 조회해도 다를 객체가 아니다. 같은 레퍼런스가 된다.
1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다.
Member a = em.find(Member.class, "Member1");
Member b = em.find(Member.class, "Member2");
Systen.out.println(a == b); // 동일성 비교 true
트랜잭션 내부에서 persist()
가 일어날 때,
엔티티들을 1차 캐시에 저장하고, 논리적으로 쓰기 지연 SQL 저장소 라는 곳에 INSERT 쿼리들을 생성해서 쌓아 놓는다.
DB에 바로 넣지 않고 기다린다.
언제 넣냐. commit()
하는 시점에 DB에 동시에 쿼리들을 쫙 보낸다.(쿼리를 보내는 방식은 동시 or 하나씩 옵션에 따라)
이렇게 쌓여있는 쿼리들을 DB에 보내는 동작이 flush()
이다.
flush()
는 1차캐시를 지우지는 않는다. 쿼리들을 DB에 날려서 DB와 싱크를 맞추는 역할을 한다.
실제로 쿼리를 보내고 나서, commit()
한다.
트랜잭션을 커밋하게 되면, flush() 와 commit()
두가지 일을 하게 되는 것이다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // 트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
// 이때까지 INSERT SQL을 데이터베이스에 보내지 않는다.
// 커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // 트랜잭션 커밋
persistence.xml에 아래와 같은 옵션을 줄 수 있다.
<property name="hibernate.jdbc.batch_size" value=10/>
엔티티 수정이 일어나면 update()나 persist()로 영속성 컨텍스트에 알려줘야 하지 않을까?
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin(); // 트랜잭션 시작
// 영속 엔티티 조회
Member memberA = em.find(Member.class, "memberA");
// 영속 엔티티 수정
memberA.setUsername("nj");
memberA.setAge(27);
//em.update(member) 또는 em.persist(member)로 다시 저장해야 하지 않을까?
transaction.commit(); // 트랜잭션 커밋
엔티티 데이터만 수정하면 끝이다. 데이터만 set하고 트랜잭션을 커밋하면 자동으로 업데이트 쿼리가 나간다.
어떻게 이게 가능할까?
변경 감지를 Dirty Checking이라고 한다.
사실은 1차 캐시에 저장할 때 동시에 스냅샷 필드도 저장한다.
그러고나서 commit()또는 flush()가 일어날 때 엔티티와 스냅샷을 비교해서, 변경사항이있으면 UPDATE SQL을 알아서 만들어서 DB에 저장한다.
update() 만들면 되지 왜 이렇게 복잡한 방법으로 처리하나...
따라서, 영속상태의 엔티티를 가져와서 값만 바꾸면 수정은 끝이다.
엔티티 수정시 기본적으로 전체 필드 다 업데이트, 변경된 필드만 반영 되도록 할 수도 있음. @DynamicUpdate
Member memberA = em.find(Member.class, "memberA");
em.remove(memberA); // 엔티티 삭제
em.flush() 로 직접호출
// 영속
Member member = new Member(200L, "A");
em.persist(member);
em.flush();
System.out.println("플러시 직접 호출하면 쿼리가 커밋 전 플러시 호출 시점에 나감");
transaction.commit();
트랜잭션 커밋시 플러시 자동 호출
JPQL 쿼리 실행하면 플러시 자동 호출
JPQL 쿼리 실행시 플러시가 자동으로 호출되는 이유는
아래와 같이 member1,2,3을 영속화한 상태에서. 쿼리는 안날라간 상태
JPQL로 SELECT 쿼리를 날리려고 하면 저장되어 있는 값이 없어서 문제가 생길 수 있다.
JPA는 이런 상황을 방지하고자 JPQL 실행 전에 무조건 flush()로 DB와의 싱크를 맞춘 다음에 JPQL 쿼리를 날리도록 설정 되어 있다.
그래서 아래의 상황에서는 JPQL로 멤버들을 조회할 수 있다.
em.persist(memberA);
em.persist(memberA);
em.persist(memberA);
// 중간에 JPQL 실행
query = em.createQuery("select m from Member m", Member.class);
List<Member> members = query.getResultList();
플러시가 일어나면 1차 캐시가 삭제될까?
em.setFlushMode(FlushModeType.COMMIT);
영속 상태
영속성 컨텍스트의 1차 캐시에 올라간 상태가 영속 상태이다. 엔티티 매니저가 관리하는 상태.
em.persist()로 영속성 컨텍스트에 저장한 상태도 영속 상태이지만,
em.find()로 조회를 할 때, 영속성 컨텍스트 1차 캐시에 없어서 DB에서 조회해와서 1차 캐시에 저장한 상태도 영속 상태다.
코드로 보면
Member member = em.find(Member.class, 150L);
member.setName("AAAAA");
transaction.commit();
준영속 상태 - 영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 상태(detached)
em.detach(member);
로 멤버를 영속성 컨텍스트에서 분리하고
트랜잭션을 커밋하면, 아무 일도 일어나지 않는다. JPA가 관리 하지 않는 객체가 된다.
실제로 아래에선 UPDATE 쿼리가 나가지 않는다.
Member member = em.find(Member.class, 150L);
member.setName("AAAAA");
em.detach(member);
transaction.commit();
영속성 컨텍스트가 제공하는 기능을 사용하지 못함. 쿼리 안나감.
준영속 상태로 만드는 방법
em.detach(entity) - 특정 엔티티만 준영속 상태로 전환
em.clear() - 영속성 컨텍스트를 완전히 초기화
Member member = em.find(Member.class, 150L);
member.setName("AAAAA");
em.clear();
Member member1 = em.find(Member.class, 150L);
transaction.commit();
em.close() - 영속성 컨텍스트를 종료