DB와 프로그램 사이에 공간이 있는데 이 공간이 영속성 컨텍스트이다.
엔티티의 생명주기
@GetMapping("/practice1")
fun practice1() {
val emf = Persistence.createEntityManager("hi")
val em = emf.createEntityManager()
// 객체를 생성한 상태 (비영속)
val member = Member()
member.id = 101L
member.title = "테스트"
val transaction = em.transaction
transaction.brgin()
println("BEFORE")
em.persist(member) // 객체를 저장한 상태(영속)
println("AFTER")
transaction.commit()
em.close()
emf.close()
실행 순서: BEFORE -> AFTER -> insert member
println("BEFORE") 보다 먼저 트랜잭션이 시작되면서 DB 와의 커넥션이 열렸지만,
member를 1차 캐시에 올려놓은 이후지만,
println("AFTER") 프린트문 출력 이후에 커밋이 되면서 member 엔티티를 영속성 컨텍스트에 저장하는 순서가 제일 마지막이 되었다.
=> member가 1차 캐시에 가만히 있다가 커밋을 만나면 DB로 들어가게 된다.
@GetMapping("/practice2")
fun practice2(){
// EntityManager 생성
val emf = Persistence.createEntityManagerFactory("hello")
val em = emf.createEntityManager()
//객체를 생성한 상태(비영속)
val member = Member()
member.id = 1001L
member.title = "회원"
val transaction = em.transaction
transaction.begin()
//1차 캐시에 저장됨
em.persist(member) // insert
//3번 멤버 조회
val member1 = em.find(Member().javaClass, 103L) // select
println("조회 결과 : " + member1.title) // println
transaction.commit()
em.close()
emf.close()
}
실행순서: select member1 -> insert member
1001번의 회원을 1차 캐시에 저장했다.
em.find 로 103번의 멤버를 조회하게 되어 103번의 member1을 1차 캐시에 올려 먼저 찾은 다음,
100번의 member가 1차 캐시에 다시 올려놓아지고 커밋시 DB에 저장된다.(이 일을 모두 엔티티 매니저가 한다)
@GetMapping("/practice3")
fun practice3(){
// EntityManager 생성
val emf = Persistence.createEntityManagerFactory("hello")
val em = emf.createEntityManager()
val transaction = em.transaction
transaction.begin()
//103번 멤버 조회
val member1 = em.find(Member().javaClass, 103L)
//103번 멤버 조회
val member2 = em.find(Member().javaClass, 103L)
println("비교 결과 : " + (member1 === member2))
transaction.commit()
em.close()
emf.close()
}
두개는 같은 것일까? 같은 것이 맞다.
1차 캐시에 있던 103번의 Member를 1차 캐시에서 또 조회하게 된다. 성능상 이점이 있다.
@GetMapping("/practice4")
fun practice4(){
// EntityManager 생성
val emf = Persistence.createEntityManagerFactory("hello")
val em = emf.createEntityManager()
val transaction = em.transaction
transaction.begin()
val member1 = Member()
member1.id = 10000L
member1.title = "회원님1"
val member2 = Member()
member2.id = 10001L
member2.title = "회원님2"
// 이 때 Insert 쿼리를 보내게 될까?
em.persist(member1)
em.persist(member2)
println("=============================")
transaction.commit()
em.close()
emf.close()
}
실행순서: println("=============================") -> insert member1 -> insert member2
영속성 컨텍스트 내 쓰기 지연 SQL 저장소가 있는데,
엔티티 매니저는 transaction을 commit하기 직전까지 내부 쿼리 저장소인 쓰기 지연 SQL 저장소에 쿼리문을 원기옥처럼 모아놓는다.
트랜잭션 커밋시 이 쿼리들이 DB로 한번에 뿌려진다.
트랜잭션이 커밋될때 한꺼번에 데이터베이스에 적용된다.
이것을 쓰기 지연 (Transactional write-behind)이라고 한다.
@GetMapping("/practice5")
fun practice5(){
// EntityManager 생성
val emf = Persistence.createEntityManagerFactory("hello")
val em = emf.createEntityManager()
val transaction = em.transaction
transaction.begin()
//103번 멤버 조회
val member1 = em.find(Member().javaClass, 103L)
member1.title = "바뀔까"
transaction.commit()
em.close()
emf.close()
}
실행순서: 103번의 Member 조회 -> 데이터 변경 -> 커밋 시 적용됨
업데이트 쿼리문이 자동생성돼 member1의 title 이 '바뀔까'로 바뀐다.
이 때 Dirty Checking(변경감지)이 이용된다.
103번 Member 엔티티가 엔티티 매니저에 의해 1차 캐시에 올려지면서 영속성 컨텍스트에 들어올 때 JPA는 최초의 엔티티 상태를 복사해 스냅샷에 저장해놓는다.
트랜잭션 커밋시 엔티티 매니저가 내부에서 먼저 flush()를 호출한다.
flush(): 영속성 컨텍스트의 변경내용을 DB와 동기화, 싱크를 맞추는 역할.
엔티티와 최초의 스냅샷이 다르다면 변경내용(member1.title = "바뀔까")을 Update 쿼리로 자동생성해 쓰기 지연 SQL 저장소에 저장한다.
쓰기 지연 저장소의 업데이트 쿼리가 발동돼 엔티티의 변경사항이 DB에 적용되고 트랜잭션이 끝나고 커밋된다.