GenerationType
Enum 개요
기본키 생성 전략의 타입을 정의한다.
Enum 상수 요약
AUTO
(기본 값):persistence provider
가 특정한 데이터베이스에 따라 적절한 전략을 선택해야 함을 나타낸다.- Dialect 에 따라 자동 지정된다고 할 수 있다.
AUTO
생성 전략은 데이터베이스에 리소스가 있다고 기대할 수도 있고 혹은 새로운 리소스를 만들어내려 시도할 수도 있다.- 스키마 생성을 지원하지 않거나 스키마 리소스를 런타임에 생성할 수 없는 때에 어떻게 리소스를 생성하는지 벤더가 문서를 제공할 수도 있다.
IDENTITY
:persistence provider
가 데이터베이스 identity 컬럼을 사용하여 엔티티에 반드시 기본 키를 할당해야 함을 나타낸다.- 보통 데이터베이스에 키 설정을 위임하는 것이다.
SEQUENCE
:persistence provider
가 데이터베이스 sequence 를 사용하여 엔티티에 반드시 기본 키를 할당해야 함을 나타낸다.- 데이터베이스의 시퀀스 오브젝트를 이용하는 것이다.
TABLE
:persistence provider
가 유니크함을 보장하기 위해 기재된 테이블을 사용하여 엔티티에 반드시 기본키를 할당해야 함을 나타낸다.- 키 생성용 테이블을 사용하는 것이다.
AUTO
전략 세부 분석
AUTO
생성 전략을 해석하는 일은 전적으로persistence provider
에 달려있다.- 가장 처음으로 하는 일은 식별자(identifier) 타입이
UUID
인지 확인하고UUID
라면 UUID identifier를 적용하는 것이다. - 만일, 식별자 타입이 숫자(ex.
Long
,Integer
)라면,IdGeneratorStrategyInterpreter
를 적용한다.
IdGeneratorStrategyInterpreter
의 구현체
FallbackInterpreter
: 하이버네이트 5.0 부터 이 전략이 기본 전략이다. 이 전략에서AUTO
는SequenceStyleGenerator
로 해석된다. 만일, DB 에서 SEQUENCE 를 제공한다면 SEQUENCE 생성기가 사용되고, 지원하지 않는다면 TABLE 생성기가 대신 사용될 것이다.LegacyFallbackInterpreter
: 하이버네이트 5.0 이전에 사용되던 레거시 매커니즘이다.native
생성기를 사용하는데, 이는 NativeIdentifierGeneratorStrategy 를 따른다.
Sequence
사용 세부 분석
데이터베이스 시퀀스 기반 식별자 값 생성 구현을 위해, 하이버네이트는 org.hibernate.id.enhanced.SequenceStyleGenerator
식별자 생성기를 이용한다. SequenceStyleGenerator
는 테이블을 뒷단에 두어 시퀀스를 지원하지 않는 데이터베이스에 대해서도 동작이 가능하다. 이러한 점은 하이버네이트에게 여러 데이터베이스 사이에서의 뛰어난 이동성을 준다. (SEQUENCE 와 IDENTITY 중 선택하라는 것과 대비된다.) 이 때 사용되는 백업 스토리지는 사용자에게 완전히 투명하다.
이 생성기를 설정하기 위해 선호되는 방식은 JPA 에서 정의한 @SequenceGenerator
애노테이션을 사용하는 것이다. 시퀀스 생성을 요청하는 가장 간단한 형태이다. 하이버네이트는 이름이 정해지지 않은 시퀀스 정의에 대해서는 단일로 동작하며 암묵적으로 이름이 정해진 시퀀스(hibernate-sequence
) 를 사용할 것이다.
이름이 없는 시퀀스
@Entity(name = "Product")
public static class Product {
@Id
@GeneratedValue(
strategy = GenerationType.SEQUENCE
)
private Long id;
@Column(name = "product_name")
private String name;
//Getters and setters are omitted for brevity
}
@SequenceGenerator
애노테이션을 이용해 직접 데이터베이스 시퀀스 네임을 줄 수도 있다.
이름이 있는 시퀀스
@Entity(name = "Product")
public static class Product {
@Id
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "sequence-generator"
)
@SequenceGenerator(
name = "sequence-generator",
sequenceName = "product_sequence"
)
private Long id;
@Column(name = "product_name")
private String name;
}
@SequenceGenerator
애노테이션을 이용해 시퀀스 이름을 직접 설정하는 방법이다.
설정이 있는 시퀀스
@Entity(name = "Product")
public static class Product {
@Id
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "sequence-generator"
)
@SequenceGenerator(
name = "sequence-generator",
sequenceName = "product_sequence",
allocationSize = 5
)
private Long id;
@Column(name = "product_name")
private String name;
//Getters and setters are omitted for brevity
}
IDENTITY
컬럼 이용하기
하이버네이트는 IDENTITY
컬럼을 기반으로 하는 ID 값 생성을 구현하기 위해, 테이블에 INSERT
를 통해 ID 가 생성되어야 하는 org.hibernate.id.IdentityGenerator
아이디 생성기를 사용한다. 하이버네이트는 INSERT
로 생성된 값을 가져오는 3가지 방법을 이해한다.
- 하이버네이트가 JDBC 환경에서
java.sql.Statement#getGeneratedKeys
를 지원한다는 것을 인지하면, IDENTITY 에 의해 생성된 키를 추출하기 위해 이를 사용할 것이다. Dialect#supportsInsertSelectIdentity
가true
라면, 하이버네이트는 Dialect 에 맞는 INSERT+SELECT 문 문법을 사용할 것이다.- 위 두가지가 모두 안 된다면,
Dialect#getIdentitySelectString
에 의해 가리켜지는 가장 최근에 삽입된 IDENTITY 값을 요청할 것이다.
이 방법은 엔티티 row 가 반드시 ID 값이 알려지기 전에 물리적으로 insert 된다는 점에서 runtime behavior 를 부과한다는 것을 알아두는 것이 중요하다. 확장된 영속성 컨텍스트를 망칠 수 있다. 런타임 불일치성 때문에, 하이버네이트는 다른 형태의 ID 값 생성을 사용하기를 제안한다.
또 하나 알아둬야 할 중요한 것은 하이버네이트는 IDENTITY 생성을 사용하는 엔티티의 INSERT 에 대해 JDBC batching 을 할 수 없다. 이 중요성은 애플리케이션의 세부적인 유즈 케이스에 따라 달라진다. 만일 애플리케이션에서 IDENTITY 생성을 사용하는 엔티티를 많이 생성하지 않는다면, 배치가 그렇게 도움되지는 않을 것이므로 큰 영향이 없을 수 있다.
Table
ID 생성기 이용하기
많은 수의 엔티티에 대해 여러 개의 명명된 값 세그먼트들을 가지고 있을 수 있는 테이블을 정의하는 org.hibernate.id.enhanced.TableGenerator
를 기반으로하는 테이블 기반 ID 생성기를 사용할 수 있다.
주어진 테이블 생성기 (ex. hibernate_sequences
같은) 테이블이 여러 개의 ID 생성기 값들의 세그먼트를 가지고 있을 수 있다는 사실을 기반으로 한다.
@Entity(name = "Product")
public static class Product {
@Id
@GeneratedValue(
strategy = GenerationType.TABLE
)
private Long id;
@Column(name = "product_name")
private String name;
//Getters and setters are omitted for brevity
}
create table hibernate_sequences (
sequence_name varchar2(255 char) not null,
next_val number(19,0),
primary key (sequence_name)
)
만일 어떠한 테이블 이름도 주어지지 않는다면, hibernate_sequences
라는 암묵적인 이름을 따를 것이다.
추가적으로, javax.persistence.TableGenerator#pkColumnValue
도 명세되어 있지 않기 때문에, 하이버네이트는 hibernate_sequences
테이블에서도 기본 세그먼트(sequence_name='default'
)를 사용할 것이다.
@TableGenerator
애노테이션을 사용해 테이블 ID 생성기 세부사항을 설정할 수도 있다.
@Entity(name = "Product")
public static class Product {
@Id
@GeneratedValue(
strategy = GenerationType.TABLE,
generator = "table-generator"
)
@TableGenerator(
name = "table-generator",
table = "table_identifier",
pkColumnName = "table_name",
valueColumnName = "product_id",
allocationSize = 5 // 최적화를 위해 사용된다.
)
private Long id;
@Column(name = "product_name")
private String name;
//Getters and setters are omitted for brevity
}
allocationSize
는 식별자 값을 한 번에 몇개씩 생성할지에 대한 사이즈이다.
create table table_identifier (
table_name varchar2(255 char) not null,
product_id number(19,0),
primary key (table_name)
)
여기서 만일 3 개의 Product
엔티티를 추가한다면, 하이버네이트는 아래의 구문들을 만들어낼 것이다.
for ( long i = 1; i <= 3; i++ ) {
Product product = new Product();
product.setName( String.format( "Product %d", i ) );
entityManager.persist( product );
}
Product
데이터 3개를 추가하는 For 문을 수행한다.
select
tbl.product_id
from
table_identifier tbl
where
tbl.table_name = ? -- Product
for update
-- binding parameter [1] - [Product]
데이터를 사용하려 table_identifier
테이블에서 table_name
이 Product
이며, product_id
컬럼에 락을 건다. 락을 걸면, 다른 세션에서 접근할 수 없다. for update 관련 참고
insert
into
table_identifier
(table_name, product_id)
values
(?, ?) -- (Product, 1)
-- binding parameter [1] - [Product]
-- binding parameter [2] - [1]
table_identifier
테이블에 table_name
이 Product
이며, product_id
가 1
인 데이터를 삽입한다.
update
table_identifier
set
product_id= ?
where
product_id= ?
and table_name= ?
-- binding parameter [1] - [6]
-- binding parameter [2] - [1]
-- binding parameter [3] - [Product]
select
tbl.product_id
from
table_identifier tbl
where
tbl.table_name = ? for update
-- binding parameter [1] - [Product]
update
table_identifier
set
product_id= ?
where
product_id= ?
and table_name= ?
-- binding parameter [1] - [11]
-- binding parameter [2] - [6]
-- binding parameter [3] - [Product]
insert
into
Product
(product_name, id)
values
(?, ?)
-- binding parameter [1] as [VARCHAR] - [Product 1]
-- binding parameter [2] as [BIGINT] - [1]
insert
into
Product
(product_name, id)
values
(?, ?)
-- binding parameter [1] as [VARCHAR] - [Product 2]
-- binding parameter [2] as [BIGINT] - [2]
insert
into
Product
(product_name, id)
values
(?, ?)
-- binding parameter [1] as [VARCHAR] - [Product 3]
-- binding parameter [2] as [BIGINT] - [3]
UUID 생성기 이용하기
UUID
타입에 org.hibernate.id.UUIDGenerator
를 통해 UUID
생성을 지원한다.
기본 전략은 IETF RFC 4122 를 따르는 버전 4 (랜덤) 전략이다. 하이버네이트에서는 RFC 4122 버전 1 (시간 기반) 전략도 대안으로 제공한다.
@Entity(name = "Book")
public static class Book {
@Id
@GeneratedValue
private UUID id;
private String title;
private String author;
//Getters and setters are omitted for brevity
}
버전 1을 쓰려면 @GenericGenerator
애너테이션을 정의해야 한다. org.hibernate.id.uuid.CustomVersionOneStrategy
를 통해 아래와 같이 설정 가능하다.
@Entity(name = "Book")
public static class Book {
@Id
@GeneratedValue( generator = "custom-uuid" )
@GenericGenerator(
name = "custom-uuid",
strategy = "org.hibernate.id.UUIDGenerator",
parameters = {
@Parameter(
name = "uuid_gen_strategy_class",
value = "org.hibernate.id.uuid.CustomVersionOneStrategy"
)
}
)
private UUID id;
private String title;
private String author;
//Getters and setters are omitted for brevity
}
옵티마이저
DB 구조로부터 식별자 값을 각각 얻는 대부분의 하이버네이트 생성기는 이식 가능한 옵티마이저를 지원한다. 옵티마이저는 식별자를 생성하기 위해 DB 와 연결하는 부분을 최소화해준다.
None
아무런 옵티마이저도 적용하지 않는다. 매번 DB 와 통신한다.
Pooled-lo
create sequence m_sequence start with 1 increment by 20
와 같은 방식으로 적용 가능하다. 20 개의 사용 가능한 식별자를 미리 만들어놓는 것이다.
처음에 id 1을 얻어오고, 20까지 사용할 수 있음을 알 수 있다.
다음 호출에는 아이디 21이 결과로 나올 것이다. 이 때는 물론 21-40이 유효한 범위일 것이다.
"pooled-lo" 에서 "lo" 의 의미는 "pool low end" 일 것이다.
Pooled
Pooled-lo 와 흡사한데, 숫자를 위에서부터 받는다.
hilo, legacy-hilo
pool 을 만드는 커스텀 알고리즘을 정의한다. 사용은 권장되지 않는다.
애플리케이션은 자신만의 옵티마이저 전략을 사용할 수도 있다.
org.hibernate.id.enhanced.Optimizer
에 정의되어 있다.
@GenericGenerator
이용하기
@GenericGenerator
는 어떤 하이버네이트의 org.hibernate.id.IdentifierGenerator
구현과도 통합을 제공한다. 위에 언급된 ID 생성 전략과도 통합될 수 있다.
아래의 코드는 SEQUENCE
전략에 Pooled-lo
최적화를 사용했다.
@Entity(name = "Product")
public static class Product {
@Id
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "product_generator"
)
@GenericGenerator(
name = "product_generator",
strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator",
parameters = {
@Parameter(name = "sequence_name", value = "product_sequence"),
@Parameter(name = "initial_value", value = "1"),
@Parameter(name = "increment_size", value = "3"),
@Parameter(name = "optimizer", value = "pooled-lo")
}
)
private Long id;
@Column(name = "p_name")
private String name;
@Column(name = "p_number")
private String number;
//Getters and setters are omitted for brevity
}
이제 5개의 Product
엔티티를 저장한다면, 매 3번마다 Persistence Context
를 flush
할 것이다. 아래 코드는 예시이다.
for ( long i = 1; i <= 5; i++ ) {
if(i % 3 == 0) {
entityManager.flush();
}
Product product = new Product();
product.setName( String.format( "Product %d", i ) );
product.setNumber( String.format( "P_100_%d", i ) );
entityManager.persist( product );
}
CALL NEXT VALUE FOR product_sequence
INSERT INTO Product (p_name, p_number, id)
VALUES (?, ?, ?)
-- binding parameter [1] as [VARCHAR] - [Product 1]
-- binding parameter [2] as [VARCHAR] - [P_100_1]
-- binding parameter [3] as [BIGINT] - [1]
INSERT INTO Product (p_name, p_number, id)
VALUES (?, ?, ?)
-- binding parameter [1] as [VARCHAR] - [Product 2]
-- binding parameter [2] as [VARCHAR] - [P_100_2]
-- binding parameter [3] as [BIGINT] - [2]
CALL NEXT VALUE FOR product_sequence
INSERT INTO Product (p_name, p_number, id)
VALUES (?, ?, ?)
-- binding parameter [1] as [VARCHAR] - [Product 3]
-- binding parameter [2] as [VARCHAR] - [P_100_3]
-- binding parameter [3] as [BIGINT] - [3]
INSERT INTO Product (p_name, p_number, id)
VALUES (?, ?, ?)
-- binding parameter [1] as [VARCHAR] - [Product 4]
-- binding parameter [2] as [VARCHAR] - [P_100_4]
-- binding parameter [3] as [BIGINT] - [4]
INSERT INTO Product (p_name, p_number, id)
VALUES (?, ?, ?)
-- binding parameter [1] as [VARCHAR] - [Product 5]
-- binding parameter [2] as [VARCHAR] - [P_100_5]
-- binding parameter [3] as [BIGINT] - [5]
시작할 때 그리고 3번째 데이터를 추가할 때 CALL NEXT VALUE FOR product_sequence
가 일어난다. 6
번째 데이터를 추가한다면 또 CALL NEXT VALUE FOR product_sequence
가 일어날 것이다. 이러한 최적화가 DB 까지 오가는 시간을 줄여준다.
Derived Identifiers
JPA 2.0 에서 지원한다. many-to-one
혹은 one-to-one
관계로부터 ID 를 빌려온다.
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@NaturalId
private String registrationNumber;
public Person() {}
public Person(String registrationNumber) {
this.registrationNumber = registrationNumber;
}
//Getters and setters are omitted for brevity
}
@Entity(name = "PersonDetails")
public static class PersonDetails {
@Id
private Long id;
private String nickName;
@OneToOne
@MapsId
private Person person;
//Getters and setters are omitted for brevity
}
위의 예에서 PersonDetails
엔티티는 엔티티 식별자와 Person
엔티티와의 one-to-one
관계에 대해 id
컬럼을 사용한다. PersonDetails
엔티티 ID의 값은 부모 Person
엔티티의 ID 로 부터 "derived" 된다.
doInJPA(this::entityManagerFactory, entityManager -> {
Person person = new Person( "ABC-123" );
person.setId( 1L );
entityManager.persist( person );
PersonDetails personDetails = new PersonDetails();
personDetails.setNickName( "John Doe" );
personDetails.setPerson( person );
entityManager.persist( personDetails );
});
doInJPA(this::entityManagerFactory, entityManager -> {
PersonDetails personDetails = entityManager.find( PersonDetails.class, 1L );
assertEquals("John Doe", personDetails.getNickName());
});
@MapsId
애노테이션은 @EmbeddedId
ID 로 된 컬럼도 참조할 수 있다. 이전 예제는 @PrimaryKeyJoinColumn
을 통해서도 매핑될 수 있다.
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
@NaturalId
private String registrationNumber;
public Person() {}
public Person(String registrationNumber) {
this.registrationNumber = registrationNumber;
}
//Getters and setters are omitted for brevity
}
@Entity(name = "PersonDetails")
public static class PersonDetails {
@Id
private Long id;
private String nickName;
@OneToOne
@PrimaryKeyJoinColumn
private Person person;
public void setPerson(Person person) {
this.person = person;
this.id = person.getId();
}
//Other getters and setters are omitted for brevity
}
@MapsId
와 다르게, 애플리케이션 개발자는 ID 와 many-to-one
혹은 one-to-one
1 관계가 싱크가 맞는다는 것을 보장할 책임이 있다. PersonDetails#setPerson
메서드에서 이를 처리하고 있다.
@RowId
@RowId
애노테이션을 붙이고, ROWID(ex. 오라클) 에 의해 기록을 가져오는 것을 데이터베이스가 지원하면, 하이버네이트는 CRUD 연산에 대해 ROWID
의사 컬럼을 사용할 수 있다.
Product product = entityManager.find( Product.class, 1L );
product.setName( "Smart phone" );
SELECT
p.id as id1_0_0_,
p."name" as name2_0_0_,
p."number" as number3_0_0_,
p.ROWID as rowid_0_
FROM
Product p
WHERE
p.id = ?
-- binding parameter [1] as [BIGINT] - [1]
-- extracted value ([name2_0_0_] : [VARCHAR]) - [Mobile phone]
-- extracted value ([number3_0_0_] : [VARCHAR]) - [123-456-7890]
-- extracted ROWID value: AAAwkBAAEAAACP3AAA
UPDATE
Product
SET
"name" = ?,
"number" = ?
WHERE
ROWID = ?
-- binding parameter [1] as [VARCHAR] - [Smart phone]
-- binding parameter [2] as [VARCHAR] - [123-456-7890]
-- binding parameter [3] as ROWID - [AAAwkBAAEAAACP3AAA]
레퍼런스
'Java > 자바 잡지식' 카테고리의 다른 글
DTO (Data Transfer Object) 란? (0) | 2022.04.27 |
---|---|
Java EE 빈 검증 (Bean Validation) (0) | 2022.04.27 |
Java EE @GeneratedValue 공식문서 번역 정리 (0) | 2022.04.26 |
Lombok 을 사용할 때 주의해야 하는 점들 정리 (0) | 2022.04.23 |
스프링 @Bean 애노테이션 정리 (0) | 2022.04.23 |