웹 애플리케이션에 엔티티 매니저 팩토리가 있다.(싱글톤)
만약 클라이언트에서 요청이오면 엔티티 매니저 팩토리에서 엔티티 매니저를 생성한다.
엔티티 매니저는 내부적으로 DB connection을 사용해서 DB를 사용하게 된다.
- JPA를 이해하는데 가장 중요한 용어
- 엔티티를 영구 저장하는 환경이라는 뜻
- EntityManager.persist(entity);
- 엔티티를 DB에 저장하는 것이 아니라 영속성 컨텍스트를 통해서 엔티티를 영속화 한다는 뜻이다.
- 영속성 컨텍스트는 논리적인 개념이다.
- 따라서 눈에 보이지 않는다.
- 엔티티 매니저를 통해서 영속성 컨텍스트에 접근한다.
영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
ex) Member member = new Member();
영속성 컨텍스트에 관리되는 상태
ex) EntityManager.persist(member);
영속성 컨텍스트에 저장되었다가 분리된 상태
ex) EntityManager.clear();
삭제된 상태
ex)EntityManager.remove(member);
//객체를 생성한 상태(비영속) Member member = new Member(); member.setId("member1"); member.setUsername("회원1");
final EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
final EntityManager em = emf.createEntityManager();
final EntityTransaction tx = em.getTransaction();
tx.begin();
try {
// 비영속
final Member member = new Member();
member.setId(100L);
member.setName("HelloJPA");
//영속
System.out.println("=== BEFORE ===");
em.persist(member);
System.out.println("=== AFTER ===");
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
코드를 보면 em.persist(member) 를 한다고 바로 DB에 저장되는 것이 아니라
영속성 컨텍스트에 먼저 저장한 후 트랜잭션에서 commit이 일어나야 DB에 저장되는걸 볼 수 있다.
//회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
em.detatch(member);
// 객체를 삭제한 상태(삭제)
em.remove(embmer);
준영속 상태는 영속성 컨텍스트에서 해당 객체를 꺼내진(분리시키는) 상태이다.
- 1차 캐시
- 동일성(identity) 보장
- 트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)
- 변경 감지(Dirty Checking)
- 지연 로딩(Lazy Loading)
//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
//엔티티를 영속 (1차 캐시에 저장됨)
em.persist(member);
//1차 캐시에서 조회
Member findMember = em.find(Member.class, "member1");
영속성 컨텍스트는 내부에 1차 캐시를 가지고 있다.
1차 캐시는 Map으로 되어있는데 DB의 PK가 키(Id)가 되고 객체(Entity)가 값이 된다.
만약 영속성 컨텍스트안에 있는 엔티티를 조회 할려고 할 때
JPA는 먼저 DB가 아닌 영속성 컨텍스트 안에 있는 1차 캐시에서 찾는다.
만약 1차 캐시에서 찾고 있는 엔티티가 있다면 그대로 반환한다.
Member findMember2 = em.find(Member.class, "member2");
만약 1차 캐시에 없는 member2를 조회할려고 한다면
JPA는 영속성 컨텍스트 안에 있는 1차 캐시에서 member2를 찾는다.
만약 1차 캐시에서 member2를 찾지 못했다면
DB에서 member2를 조회한다.
그리고 나서 DB에서 member2를 찾은 다음 영속성 컨텍스트의 1차 캐시안에 member2를 저장한다.
그 후에 member2를 반환한다.
만약에 다음에 다시 member2를 조회한다면 1차캐시에서 member2를 반환한다.
cf) EntityManager는 데이터베이스의 트랜잭션 단위로 만들어지기 때문에 트랜잭션이 종료된다면 같이 종료하게 된다.
즉 클라이언트에서 요청이 온 후에 요청을 완료하면 EntityManager(영속성 컨텍스트)는 사라지기 때문에 1차 캐시에 있는 내용이 전부 사라지게 된다.
때문에 영속성 컨텍스트의 1차 캐시는 아주 짧은 시간에만 일어나기 때문에 효과는 사실상 미비하다.
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a==b);
true
1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야 한다.
transcaction.begin();
//em.persist()를 하면 DB가 아닌 영속성 컨텍스트 안에 저장된다.
// 때문에 DB에 Insert Query가 나가지 않는다.
em.persist(memberA);
em.persist(memberB);
//트랜잭션 커밋을 하는 순간 DB에 Insert Query가 나간다.
transaction.commit();
영속성 컨텍스트 안에는 쓰기 지연 SQL 저장소도 있는데
만약 em.persist(memberA)를 실행하면
1차 캐시에 memberA 객체를 저장하고 Insert 쿼리를 생성해 쓰기 지연 SQL 저장소에 쌓아둔다.
그리고나서 em.persist(memberB)를 실행하면
1차 캐시에 memberB 객체를 저장하고 InserT 쿼리를 생성해 쓰기 지연 SQL 저장소에 쌓아둔다.
그리고 나서 transaction.commit()
트랜잭션을 커밋하는 시점에 쓰기 지연 저장소에 저장되어있는 Insert 쿼리들이 flush가 되면서 DB에 날라가게 된다.
그리고 나서 실제 데이터베이스 트랜잭션이 커밋이 된다.
하이버네이트에는 jdbc.batch_size 라는 옵션이 있는데
이 옵션을 사용하면 DB에 바로 커밋이 되는 것이 아니라 batch_size만큼 저장한다음 DB에 커밋을 날려 최적화 할 수 있다.
즉 모았다가 한번에 보내는 버퍼링 같은 기술을 쓸 수 있다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin();
// 영속 엔티티 조회
Member memberA = em.find(Member.class, "memberA");
// 영속 엔티티 수정
memberA.setUsername("changeName");
memberA.setAge(100);
// em.update(member) 같은 코드가 없어도 자동으로 변경을 감지해서 엔티티를 수정해준다.
transaction.commit();
JPA는 트랜잭션을 커밋하는 순간에 내부적으로 flush가 호출된다.
1차 캐시에는 Id와 Entity 외에도 스냅샷이라는게 있는데
스냅샷은 값을 최초에 영속성 컨텍스트 안에 들어온 시점(DB에서 처음 조회 했을 때 or 1차 캐시에 처음 저장 되었을 떄)을 스냅샷으로 남겨놓는다.
그리고나서 flush가 호출되면 엔티티와 스냅샷을 비교한다.
만약 비교해서 변경된 점을 발견한다면
Update쿼리를 쓰기 지연 SQL 저장소에 쌓아둔다.
그리고 나서 DB에 Update 쿼리를 날리고 트랜잭션을 커밋하게 된다.
이것을 변경감지라고 한다.
//삭제 대상 엔티티 조회
Member memberA = em.find(Member.class, "memberA");
//엔티티 삭제
em.remove(memberA);