기본키 생성 전략에 따른 JPA 의 기본 동작 차이.
IDENTITY
IDENTITY 전략에서는 PK (auto_increment) 값이 Insert 쿼리가 DB에 COMMIT 되는 시점에 확정된다.
문제 상황
영속성 컨텍스트를 가진 JPA 입장에서 어떤 트랜잭션 내에서 COMMIT 하기 전에 PK (ID) 를 사용해야 한다면 어떻게 해야할까?
em.persist(entity) 시점에
INSERT 쿼리를 실행한다.
(아직 COMMIT 전)
Member.java
@Entity
@Table(name = "member")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
@Column(name = "member_name", updatable = true, nullable = false)
private String name;
public static Member registerMember(String name) {
var member = new Member();
member.name = name;
return member;
}
}
MemberTest.java
class MemberTest {
private final EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa-practice");
private EntityManager em;
@Test
void strategyIdentityTest() {
var tx = getEntityTransaction();
try {
tx.begin();
var member1 = Member.registerMember("제이스");
System.out.println("=============================");
em.persist(member1);
System.out.println("=============================");
assertThat(member1.getId()).isEqualTo(1L);
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
}
}
실행 결과
=============================
15:07:13.566 [Test worker] DEBUG org.hibernate.engine.spi.ActionQueue - Executing identity-insert immediately
15:07:13.577 [Test worker] DEBUG org.hibernate.SQL -
/* insert com.example.jpapractice.domain.member.Member
*/ insert
into
member (member_id, age, create_date, description, last_modified_date, member_name, role_type)
values
(default, ?, ?, ?, ?, ?, ?)
Hibernate:
/* insert com.example.jpapractice.domain.member.Member
*/ insert
into
member (member_id, age, create_date, description, last_modified_date, member_name, role_type)
values
(default, ?, ?, ?, ?, ?, ?)
15:07:13.590 [Test worker] DEBUG org.hibernate.id.IdentifierGeneratorHelper - Natively generated identity: 1
=============================
em.persist(member) 하자마자 INSERT 쿼리가 실행된다.
물론 곧바로 영속성 컨텍스트에 써서 등록한다. (1차 캐싱상태 완료)
IDENTITY PK를 갖는 엔티티 클래스의 em.persist() 내부 동작 순서
- INSERT 쿼리를 일단 실행한다.
(완전히 COMMIT 하진 않는다.) - 생성된 IDENTITY 값을 내부적으로 가져온다.
- 영속성 컨텍스트에 가져온 PK값을 ID로 삼아 객체를 등록한다.
- 트랜잭션을 COMMIT 하여 영구 반영한다.
SEQUENCE
별도로 sequence 라고하는 object 가 DB 내에 생성된다.
em.persist 를 호출하는 시점에 타겟팅할 sequence 와 값만 불러온다.
Member.java
@Entity
@Table(name = "member")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "member_id")
private Long id;
@Column(name = "member_name", updatable = true, nullable = false)
private String name;
public static Member registerMember(String name) {
var member = new Member();
member.name = name;
return member;
}
}
생성된 테이블 DDL
테이블 정보와 생성된 시퀀스를 모두 확인할 수 있다.
create table MEMBER
(
MEMBER_ID BIGINT auto_increment
primary key,
AGE INTEGER
);
-- auto-generated definition
create sequence MEMBER_SEQ
increment by 50;
MemberTest.java
class MemberTest {
private final EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa-practice");
private EntityManager em;
@Test
void strategySequenceTest() {
var tx = getEntityTransaction();
try {
tx.begin();
var member1 = Member.registerMember("제이스");
System.out.println("=============================");
em.persist(member1);
System.out.println("=============================");
assertThat(member1.getId()).isEqualTo(1L);
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
}
private EntityTransaction getEntityTransaction() {
this.em = emf.createEntityManager();
return em.getTransaction();
}
}
실행 결과
member.getId() 에서 SELECT 를 통해 결정된 시퀀스 값을 끌고온다.
마지막으로 INSERT 쿼리가 실행된다.
=============================
14:53:05.757 [Test worker] DEBUG org.hibernate.SQL -
select
next value for member_seq
Hibernate:
select
next value for member_seq
14:53:05.759 [Test worker] DEBUG org.hibernate.id.enhanced.SequenceStructure - Sequence value obtained: 1
14:53:05.761 [Test worker] DEBUG org.hibernate.event.internal.AbstractSaveEventListener - Generated identifier: 1, using strategy: org.hibernate.id.enhanced.SequenceStyleGenerator
=============================
/* insert com.example.jpapractice.domain.member.Member
*/ insert
into
member (age, create_date, description, last_modified_date, member_name, role_type, member_id)
values
(?, ?, ?, ?, ?, ?, ?)
em.persist 해도
곧바로 ISNERT 쿼리가 실행되지 않는다.
SEQUENCE PK를 갖는 엔티티 클래스의 em.persist() 내부 동작 순서
- DB에서 지정된 next SEQUENCE 값을 찾아온다.
- 영속성 컨텍스트에 객체를 등록한다.
- INSERT 쿼리를 실행한다.
- COMMIT 한다.
allocationSize 가 50이 Default 인 이유 - 동시성 Issue
⚠️ 추후 내용 추가 예정
allocationSize 만큼 DB에는 미리 올려놓고 메모리에서는 1씩 쓰는 것.
여러 웹서버가 있어도 동시성 이슈 없이 다양한 문제를 해결할 수 있게한다.
✒️ TIP
`em.persist()` 시점에 즉시 INSERT 쿼리를 실행하는 하는 IDENTITY 보다 쿼리를 버퍼링하여 지연 실행하는 SEQUENCE 전략이 좋아보일 수 있으나, 버퍼링 전략에 대한 성능 차이는 미미하다.
JDBC Batch 같은 측면에서 보면 Network I/O 가 줄어들기 때문에 SEQUENCE 가 낫다.
상상해보라. IDENTITY 를 쓰면 총 50개의 INSERT 쿼리를 실행하고자 할 때 50번 각각의 INSERT 실행이 필요하다. (다음 PK 값을 생성하기 위해) 반면에 SEQUENCE 는 JVM 메모리 내에서 가상의 번호로 allocationSize 를 갖고 JDBC Batch 연산이 가능하므로 최적화에 용이하다.
📝 Conclusion
INSERT Batch 처리가 필요하다면 SEQUENCE를 권장하고
그 외에는 IDENTITY 를 사용해도 좋다.
It is important to realize that using IDENTITY columns imposes a runtime behavior where the entity row
must be physically inserted prior to the identifier value being known.
There is yet another important runtime impact of choosing IDENTITY generation: Hibernate will not be able to batch INSERT statements for the entities using the IDENTITY generation.
The importance of this depends on the application-specific use cases. If the application is not usually creating many new instances of a given entity type using the IDENTITY generator, then this limitation will be less important since batching would not have been very helpful anyway.
- Hibernate docs
🔗 Reference
Hibernate ORM 6.1.6.Final User Guide
Fetching, essentially, is the process of grabbing data from the database and making it available to the application. Tuning how an application does fetching is one of the biggest factors in determining how an application will perform. Fetching too much dat
docs.jboss.org
[JPA] 식별자 할당 SEQUENCE(시퀀스) 사용 전략
JPA 식별자 JPA는 엔티티들을 논리적인 공간인 영속성 컨텍스트에서 관리하는데, 엔티티를 구분할 수 있는 식별자가 필요합니다. 식별자가 되는 필드는 엔티티 클래스의 @Id 애노테이션을 통해 지
dololak.tistory.com
'JVM > JPA' 카테고리의 다른 글
| [JPA] 상속관계 매핑 (0) | 2023.01.13 |
|---|---|
| [JPA] 1:1 연관관계 설정 (0) | 2023.01.12 |
| [JPA] JPA 는 왜 등장했을까? (0) | 2023.01.07 |
| [JPA] API 생성시 Entity 를 반환하지 말자. (0) | 2022.12.29 |
