앞서 우리는 JPA가 어떻게 동작을 하는지를 배웠었다.
"엔티티를 영구 저장하는 환경" 으로 번역해볼 수 있겠다.
그럼 아래 코드를 보면 우리는 " 아~ entity 객체를 db에 저장하는 거구나~ " 라고 이해할 수 있다.
EntityManager.persis(entity);
그런데 사실은 DB에 저장한다기보다는 영속성 컨텍스트를 통해서 해당 entity를 영속화 한다는 뜻이다.
정확히는 이 .persist() 메서드는 사실 DB에 저장하는 것이 아닌, 영속성 컨텍스트 라는 곳에 저장한다는 것이다.
영속성 컨텍스트는 논리적인 개념이다.
그리고 엔티티 매니저를 통해 영속성 컨텍스트에 접근한다.
쉽게 말하면 엔티티 매니저 안에 눈에 보이지 않는 공간이 생긴다고 이해하면 된다.
영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
Member member = new Member();
member.setId("member1")
...
이런식으로 생성만 한 상태
영속성 컨텍스트에 관리되는 상태
Member member = new Member();
member.setId("member1");
member.setUsername(“회원1”);
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
//객체를 저장한 상태(영속)
System.out.println("=== before ===");
em.persist(member);
System.out.println("=== after ===");
그리고 우리는 앞서 .persist()
를 하면 DB에 저장이 되는 것 처럼 이해했지만 사실을 그렇지 않다.
그것은 앞뒤에 로그를 찍어보면 알 수 있다. 아마 qeury 문이 before, after 사이에 날아가진 않을 것이다.
=> 바로 트랜잭션을 .commit()
하는 시점에 영속성 컨텍스트에 있는 db의 쿼리가 날아가게 된다.
영속성 컨텍스트에 저장되었다가 분리된 상태, 즉 영속성 컨텍스트에서 다시 지우는 것.
em.detach(member);
위 코드처럼 .detach() 메서드를 사용하면 회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태가 된 것이다.
준영속 상태는 영속에서 -> 준영속으로 넘어가는 것이다.
준영속 상태는 영속성 컨텍스트가 제공하는 기능( 영속성 컨텍스트의 이점 1~5 번 )을 사용 못한다.
그리고 이렇게 된다면 영속석 컨텍스트에 존재하지 않기 때문에 나중에 .commit() 해도 반영이 안 된다.
삭제된 상태
//객체를 삭제한 상태(삭제)
em.remove(member);
앞서 설명했 듯 영속성 컨텍스트는 DB와 애플리케이션 간에 있는 "뭔가" 다.
보면 entityManager 가 Persistance Context라고 되어있는데 사실 이 둘의 차이는 미묘하게 존재하나 여기서는 일단 동일하다고 생각해도 될 듯 하다.
1차 캐시 내부는 마치 Map 객체처럼 되어있고 key 는 우리가 entity에서 지정한 @Id가, value는 Entity 그 자체가 된다.
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
//1차 캐시에 저장됨
em.persist(member);
//1차 캐시에서 조회
Member findMember = em.find(Member.class, "member1");
위 사진을 코드로 구현한 것이다.
즉, find 메서드를 갈겼을 때 바로 DB로 가는 것이 아닌, 우선 1차 캐시를 먼저 뒤지고 없다면 DB를 조회하는 것이다.
그런데 아마 실제로 1차 캐시가 사용될 일은 거의 없을 것이다.
1차 캐시 즉, entityManger( em ) 은 사용자의 요청이 들어와서 비즈니스 로직이 끝나면 함께 사라지는, 아주 찰나의 순간, Transaction과 수명이 같기 때문에 여러명의 고객이 사용하는 캐시는 아니다.
참고로 2차 캐시가 애플리케이션 전체에서 공유하는 캐시이다.
Member member = new Member();
member.setId(2L);
member.setName("Hello B");
em.persist(member);
Member findMember = em.find(Member.class, 2L);
즉, 위 코드는 1차캐시에서 가져오기 때문에 select 쿼리가 한 번도 안 나간다는 것이다.
그리고 DB에서 가져온 값 역시 1차 캐시에 저장한다.
아래 사진은 DB에서 가져온 것이다.
Member findMember = em.find(Member.class, 2L);
Member findMember2 = em.find(Member.class, 2L);
그래서 위 코드를 실행한다면 실제 db에서 조회하는 select문은 단 한 번 나간다.
그리고 아 영속 엔티티의 동일성 보장이라는 것은
Member findMember = em.find(Member.class, 2L);
Member findMember2 = em.find(Member.class, 2L);
System.out.println(findMember == findMember2); //동일성 비교 true
1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다는 것이다.
즉, 마치 java collection 에서 가져온 것 처럼 객체 지향적으로 동작하게 도와준다는 것이다.
Member member1 = new Member();
member1.setId(1L);
member1.setName("Hello A");
Member member2 = new Member();
member2.setId(2L);
member2.setName("Hello B");
em.persist(member1);
em.persist(member2);
tx.commit();
그리고 commit 할 때 flush 가 되며 실제 DB에 저장이 된다.
참고로 jdbc, hibernate 모두 batch size 라는 옵션이 있는데
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2" xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="hello">
<properties>
<!-- 필수 속성 -->
<property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="jakarta.persistence.jdbc.user" value="sa"/>
<property name="jakarta.persistence.jdbc.password" value=""/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<!-- 옵션 -->
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
<property name="hibernate.jdbc.batch_size" value="10"/>
<property name="hibernate.hbm2ddl.auto" value="create" />
</properties>
</persistence-unit>
</persistence>
해당 옵션만큼 모아서 DB에 한방에 모아둔 query를 날린다.
즉, 마치 java의 Buffer 처럼 동작하는 것이다.
마치 java collection 을 다루듯 그냥 set 메서드로 수정만 해주면 된다.
즉, update query를 따로 날리지 않아도 된다. 이는 영속성 컨텍스트에 답이 있다.
여기서 snapshot은 값을 읽어온 최초의 상태를 떠둔다음 저장한 것이다.
그래서 싹 비교하여 달라진 점이 있다면 해당 부분은 update 한 후 db에 한 큐에 저장하는 것이다.
그리고 위 사진에서 볼 수 있듯, .find() 메서드로 가져와서 영속성 컨텍스트에 존재하지 않는다면 등록하여 jpa 가 관리할 수 있도록 해버린다.
플러시는 영속성 컨텍스트의 변경 내용을 DB에 반영하는 것이다.
보통 DB transaction이 커밋될 때 flush가 일어나는데 이는 우리가 샇아놨던 쓰기 지연 SQL 저장소에 모여있는 sql에 한 큐에 날아가는 것이다.
우선 flush를 하고 난 후 DB transaction을 commit 하는 것이다.
이 말은 commit 하기 전까지는 query문은 터미널에서 확인할 수 없다는 것이다.
아마 쓰일일은 거의 없을 것이다.
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
// 비영속 상태
Member member = new Member();
member.setId(2L);
member.setName("Hello B");
// 영속상태 ( 이때 DB에 저장되진 않음 )
System.out.println("=== before ===");
em.persist(member);
System.out.println("=== after ===");
Member findMember = em.find(Member.class, 2L);
Member findMember2 = em.find(Member.class, 2L);
System.out.println(findMember == findMember2);
findMember.setName("Hello JPA");
// jpql 을 날림으로써 자동 flush~
List<Member> result = em.createQuery("select m from Member as m", Member.class)
.setFirstResult(1)
.setMaxResults(10)
.getResultList();
System.out.println("result = " + result);
System.out.println("========== 절취선 =========");
// 이때 sql을 보냄.
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
보면 jpql 쿼리를 주석을 해제여부에 따라 순서가 바뀐다.
.flush()
라 하여 1차 캐시가 전부 지워지는게 아니고 오직 " 쓰기 지연 SQL 저장소 "에 있는 sql문들이 그냥 db에 반영되는 것이다.em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
//중간에 JPQL 실행
query = em.createQuery("select m from Member m", Member.class);
List<Member> members= query.getResultList();
문제는 위와같은 코드에서 발생한다. query를 실행을 하고 getResult를 하면 값이 안 들어와버리는 불상사가 발생하다. 따라서 jpql 쿼리를 날릴 땐 flush가 발생하여 db에 반영을 해준다.
em.setFlushMode(FlushModeType.COMMIT)
flush 정리
1. 영속성 컨텍스트를 비우지 않음
2. 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화
3. 트랜잭션이라는 작업 단위가 중요 이게 있기 때문에 flush 매커니즘이 동작함.
-> 걍 커밋 직전에만 동기화 하면 됨