PK를 나타내기 위해 @Id 를 사용하고, 생성 전략을 정의하기 위해 @GeneratedValue 를 사용한다.
이 두 개의 어노테이션을 가지고 기본키 생성 전략을 알아보고자 한다.
@Id
- 엔티티 클래스에서 해당 필드가 데이터베이스의 기본 키 역할을 한다는 것을 명시한다.
- 이 어노테이션이 지정된 필드는 엔티티의 고유 식별자로 사용되며, 데이터베이스에서 레코드를 고유하게 식별하는 데 사용된다
public class Member {
@Id @Column(name = "member_id") // 컬럼명 따로 지정
private Long id;
}
@Column 을 활용하여 테이블의 PK 컬럼을 따로 지정할 수도 있다. 이때, 컬럼명을 따로 지정하지 않으면, 관례에 따라 매핑되는 테이블 컬럼명은 camelCase로 작성된 필드명을 snake_case로 바뀐 테이블을 컬럼을 찾아서 매핑시켜 준다. ex) memberId -> member_id
위처럼 @Id 로 식별자 필드와 테이블의 PK를 매핑만 시켜놓으면, 식별자로 사용될 값을 일일이 수동으로 넣어줘야 하는 불편함이 있다. 즉, persist() 메서드를 호출하기 전에 애플리케이션에서 직접 식별자 값을 할당해야 하며, 식별자 값이 없을 경우 에러를 발생시킨다. 아래 예제를 보자.
예제
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "todos")
public class Todo {
@Id
private Long id;
protected Todo() {
}
public Todo(final Long id) {
this.id = id;
}
public Long getId() {
return id;
}
}
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.time.LocalDateTime;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
class TodoRepositoryTest {
@Autowired
private TodoRepository todoRepository;
@DisplayName("@Id만 있을때 저장 테스트")
@Test
void idAnnotationTest() {
final Todo todo = new Todo(null);
final Todo save = todoRepository.save(todo);
assertThat(save.getId()).isNotNull();
}
}
해당 테스트를 실행한다면 IdentifierGenerationException 에러가 발생한다. 이는 ID 생성을 하려고 할 때 실패해서 발생하는 에러이다.
그렇다면 ID를 지정하면 어떻게 될까?
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.time.LocalDateTime;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
class TodoRepositoryTest {
@Autowired
private TodoRepository todoRepository;
@DisplayName("@Id만 있을때 저장 테스트")
@Test
void idAnnotationTest() {
final Todo todo = new Todo(1L);
final Todo save = todoRepository.save(todo);
assertThat(save.getId()).isNotNull();
}
}
해당 테스트가 정상적으로 통과되는 것을 확인할 수 있다. 이것을 바로 직접 할당이라고 생각하면 된다.
위의 불편함은 @GeneratedValue 를 사용하면 이를 해결할 수 있다.
@GeneratedValue 를 사용하면 식별자 값을 자동 생성 시켜줄 수 있다. 총 3가지 전략이 있고, JPA에게 전략 선택을 위임하는 옵션인 AUTO 옵션을 포함해 총 4가지 옵션이 존재한다.
@GeneratedValue - IDENTITY 전략
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
기본 키 생성을 데이터베이스에 위임한다. 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용한다. ( ex) MySQL의 AUTO_INCREMENT)
JPA는 보통 트랜잭션이 commit 되는 시점에 쓰기 지연 저장소에 모아놓은 SQL을 한 번에 DB로 전송하여 실행한다. 이렇게 해야 애플리케이션과 DB 사이에 네트워크를 오가는 횟수가 줄어들고 성능면에서 이득을 볼 수 있기 때문이다.
이처럼 JPA는 PK 값을 알아야 관리할 수 있다. 하지만 캐시에도 저장되어있지 않고(영속성 컨텍스트로 엔티티를 관리하려면 1차 캐시에 Id 값을 key 값으로 들고 있어야 한다.), DB에서 조회해 올 것도 없는데 어떻게 PK를 자동 생성해 줄 수 있는걸까?
트랜잭션 commit 시점에 SQL을 실행하는 보통의 경우와 달리 IDENTITY만 예외적으로 persist 호출 시점에서 insert가 발생한다. 그리고 그 PK 값을 캐시에 저장해 준다. 이러한 이유로 PK를 자동 생성해 줄 수 있는 것이다.
하지만 이처럼 IDENTITY는 persist 호출 시점에 쿼리가 날아가기 때문에 한 번에 쿼리를 날리는 작업은 불가능하다.
@GeneratedValue - SEQUENCE 전략
@Entity
@TableGenerator(
name = "MEMBER_SEQ_GENERATOR",
table = "MY_SEQUENCES",
pkColumnValue = “MEMBER_SEQ", allocationSize = 1)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.TABLE,
generator = "MEMBER_SEQ_GENERATOR")
private Long id;
}
데이터베이스의 특별한 오브젝트 시퀀스를 사용하여 기본키를 생성한다. DB Sequence란 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트이다. 이 전략은 sequence를 지원하는 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용할 수 있다.
sequenceName 으로 sequence를 분리하여 지정할 수 있고, allocationSize로 한 번에 사용할 sequence 덩어리 사이즈를 정해서 최적화할 수 있다.
속성 | 설명 | 기본값 |
name | 식별자 생성기 이름 | 필수 |
sequenceName | 데이터베이스에 등록되어 있는 시퀀스 이름 | hibernate_sequence |
initialValue | DDL 생성 시에만 사용됨, 시퀀스 DDL을 생성할 때 처음 시작하는 수로 기본값은 1이다. | 1 |
allocationSize | 시퀀스 값을 한 번에 여러 개 미리 가져와 성능을 최적화하는 설정으로, 시퀀스를 호출할 때마다 지정된 크기만큼 시퀀스 값이 증가한다. | 50 |
catalog, schema | 데이터베이스 catalog, schema 이름 |
@GeneratedValue - TABLE 전략
@Entity
@TableGenerator(
name = "MEMBER_SEQ_GENERATOR",
table = "MY_SEQUENCES",
pkColumnValue = “MEMBER_SEQ", allocationSize = 1)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.TABLE,
generator = "MEMBER_SEQ_GENERATOR")
private Long id;
}
키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내 내는 전략이다.
모든 데이터베이스에 적용 가능하나, 성능적인 손해가 있어서 잘 쓰지 않는다.
@GeneratedValue - AUTO 전략
hibernate.dialect에 설정된 DB 방언 종류에 따라, hibernate가 자동으로 전략을 선택하게끔 위임한다.
@Id @GeneratedValue(strategy = GenerationType.AUTO)
private Long id
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
하지만 hibernate를 맹신해서는 안된다. MySQL의 경우 AUTO로 설정하면 Identity 전략을 취할 것이라 생각하기 쉽지만 Hibernate 5부터 MySQL에서의 GenerationType.AUTO는 IDENTITY가 아닌 TABLE을 기본 시퀀스 전략으로 가져간다. 아래 흐름을 보자.
AUTO - 생성 전략 결정 흐름
AUTO는 Data Type을 기준으로 진행한다. 아래와 같이 Numeral 이라면 new_generator 를 검사하는데 기본값이 TRUE 이기 때문에 SequenceStyleGenerator로 간다. 그 이후 MySQL이라면 sequence를 지원하지 않기 때문에 TABLE Generator로 가게 된다.
이처럼 추후 DBMS 종류 변경을 고려해 그냥 AUTO로 지정하는 경우가 있는데, 버전에 따라 선택되는 전략이 달라질 수 있으므로, 직접 DBMS에 맞는 전략을 지정해 주는 게 좋을 듯하다.
JPA와 MySQL 사용 시, GenerationType은 IDENTITY로 지정해 주자!
출처 / 참고
https://devcamus.tistory.com/16
JPA 기본키 생성 전략, @GeneratedValue 사용시 주의점
JPA로 테이블과 엔티티를 매핑할 때, 식별자로 사용할 필드 위에 @Id 어노테이션을 붙여 테이블의 Primary Key와 연결 시켜줘야한다. 이 때, 컬럼 명을 따로 지정하지 않으면, 관례에 따라 매핑되는
devcamus.tistory.com
@Id / @GeneratedValue에 대해 알아보자
@Id / @GeneratedValue에 대해 알아보자 PK를 나타내기 위해 @Id 어노테이션을 사용하며, 생성 전략을 정의하기 위해 @GeneratedValue 를 사용한다. 해당 어노테이션에 대해서 알아보도록 하자. @Id package javax
rutgo-letsgo.tistory.com
'JPA' 카테고리의 다른 글
[JPA] 연관 관계 매핑에 대하여 (2) | 2024.09.04 |
---|---|
API 개발 고급 - 지연 로딩과 조회 성능 최적화 (0) | 2023.05.29 |
API 개발 고급 - 준비 (0) | 2023.05.29 |
API 개발 기본 (0) | 2023.05.29 |
웹 계층 개발 (0) | 2023.05.21 |