4장. 엔티티 매핑
(1) @Entity, @Table
@Entity
JPA를 사용해서 테이블과 매핑할 클래스는 @Entity
어노테이션을 필수로 붙여야 한다.
@Entity
가 붙은 클래스는 JPA가 관리한다.
속성
1. name
- JPA에서 사용할 엔티티 이름 지정. 보통 기본값인 클래스 이름을 사용한다.
- 만약 다른 패지키에 이름이 같은 엔티티 클래스가 있다면 이름을 지정해서 충돌하지 않도록 해야 한다.
@Entity 적용 시 주의 사항
- 기본 생성자는 필수이다.(파라미터가 없는 public 또는 protected 생성자)
final
클래스,enum
,interface
,inner
클래스에는 사용할 수 없다.- 저장할 필드에 final을 사용하면 안된다.
public Member() {} // 기본 생성자
public Member(String name) {
this.name = name;
}
자바는 생성자가 하나도 없으면 파라미터가 없는 기본 생성자를 자동으로 만들지만, 생성자가 하나 이상 존재하면 기본 생성자를 자동으로 만들지 않는다.
이때는 기본 생성자를 직접 만들어야 한다.
왜냐면 JPA가 엔티티 객체를 생성할 때 기본 생성자를 사용하기 때문이다.
@Table
엔티티와 매핑할 테이블을 지정한다. 생략 시 매핑한 엔티티 이름을 테이블 이름으로 사용한다.
@Entity
@Table(name="MEMBER")
public class Member {
...
}
(2) 다양한 매핑 사용
import javax.persistence.*;
import java.util.Date;
@Entity
@Table(name="MEMBER")
public class Member {
@Id
@Column(name = "ID")
private String id;
@Column(name = "NAME", nullable = false, length = 10) //추가
private String username;
private Integer age;
@Enumerated(EnumType.STRING)
private RoleType roleType;
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
@Lob
private String description;
@Transient
private String temp;
//Getter, Setter
...
}
public enum RoleType {
ADMIN, USER
}
코드 설명
1. roleType : 자바의 enum을 사용해서 회원 타입을 구분. 자바의 enum을 사용하려면 @Enumerated 어노테이션으로 매핑.
2. createDate, lastModifiedDate : 자바의 날짜 타입은 @Temporal을 사용해서 매핑
3. description : 회원을 설명하는 필드는 길이 제한이 없다. 데이타베이스 VARCHAR 타입 대신에 CLOB 타입으로 저장. @Lob를 사용하면 CLOB, BLOB 타입을 매핑할 수 있다.
오라클 기준 LOB 설명
- CLOB(Character Large OBject) : 텍스트 형태 파일 크기를 4GB까지 지원
- BLOB(Binary Large OBject) : 이진파일(이미지 등) 형태의 파일 크기를 4GB까지 지원
(3) 데이터베이스 스키마 자동 생성
JPA는 데이터베이스 스키마를 자동으로 생성하는 기능을 지원 -> 클래스의 매핑 정보를 보면 어떤 테이블에 어떤 칼럼을 사용하는지 알 수 있다.
어플리케이션 실행 시점에 데이터베이스 테이블을 자동으로 생성한다.
<property name="hibernate.hbm2ddl.auto" value="create" />
콘솔에 실행되는 DDL을 출력한다.
<property name="hibernate.show_sql" value="true" />
출력 예제
Hibernate:
INFO: HHH000227: Running hbm2ddl schema export
drop table MEMBER if exists
Hibernate:
create table MEMBER (
ID varchar(255) not null,
age integer,
createdDate timestamp,
description clob,
lastModifiedDate timestamp,
roleType varchar(255),
NAME varchar(10) not null,
primary key (ID)
)
Hibernate:
alter table MEMBER
add constraint NAME_AGE_UNIQUE unique (NAME, age)
1월 11, 2016 1:22:35 오후 org.hibernate.tool.hbm2ddl.SchemaExport execute
INFO: HHH000230: Schema export complete
findMember=지한, age=20
Hibernate:
/* insert jpabook.start.Member
*/ insert
into
MEMBER
(age, createdDate, description, lastModifiedDate, roleType, NAME, ID)
values
(?, ?, ?, ?, ?, ?, ?)
Hibernate:
/* update
jpabook.start.Member */ update
MEMBER
set
age=?,
createdDate=?,
description=?,
lastModifiedDate=?,
roleType=?,
NAME=?
where
ID=?
Hibernate:
/* select
m
from
Member m */ select
member0_.ID as ID1_0_,
member0_.age as age2_0_,
member0_.createdDate as createdD3_0_,
member0_.description as descript4_0_,
member0_.lastModifiedDate as lastModi5_0_,
member0_.roleType as roleType6_0_,
member0_.NAME as NAME7_0_
from
MEMBER member0_
members.size=1
Hibernate:
/* delete jpabook.start.Member */ delete
from
MEMBER
where
ID=?
- 자동 생성되는 DDL은 지정한 데이터베이스 방언에 따라 달라진다.
- 스키마 자동 생성 기능이 만든 DDL은 운영환경에서 사용할 만큼 완벽하지 않다.
- => 따라서 개발 환경에서 사용하거나 매핑 시 참고하는 용도로 사용한다.
스키마 자동 생성 옵션
옵션 | 설명 |
---|---|
create | 기존 테이블을 삭제하고 새로 생성. DROP + CREATE |
create-drop | create 속성에 추가로 애플리케이션 종료 시 생성한 DDL을 제거. DROP + CREATE + DROP |
update | 데이터베이스 테이블과 엔티티 매핑정보를 비교해서 변경 사항만 수정 |
validate | 데이터베이스 테이블과 엔티티 매핑정보를 비교해서 차이가 있으면 경고를 남기고 애플리케이션을 실행하지 않는다. 이 설정은 DDL을 수정하지 않는다. |
none | 자동 생성 기능을 사용하지 않을 때 유효하지 않은 옵션값 |
이름 매핑 전략 변경
테이블 명이나 컬럼 명이 생략되면 자바의 카멜케이스 표기법
을 언더스코어 표기법
으로 매핑한다.
<property name="hibernate.ejb.naming_strategy" value="org.hibernate.cfg.ImprovedNamingStrategy" />
(4) DDL 생성 기능
DDL 생성 기능으로 제약조건을 추가할 수 있다.
@Entity
@Table(name="MEMBER")
public class Member {
@Id
@Column(name = "ID")
private String id;
@Column(name = "NAME", nullable = false, length = 10) //추가
private String username;
...
}
// 생성된 DDL
create table MEMBER (
ID varchar(255) not null,
NAME varchar(10) not null,
...
primary key(ID)
)
nullable = false
: not null 제약조건 추가length = 10
: 크기를 지정
// 유니크 제약조건
@Entity(name="Member")
@Table(name="MEMBER", uniqueConstraints = {@UniqueConstraint( // 추가
name = "NAME_AGE_UNIQUE",
columnNames = {"NAME", "AGE"} )})
public class Member {
@Id
@Column(name = "ID")
private String id;
@Column(name = "NAME", nullable = false, length = 10) //추가
private String username;
...
}
// 생성된 DDL
ALTER TABLE MEMBER
ADD CONSTRAINT NAME_AGE_UNIQUE UNIQUE (NAME, AGE)
이런 기능들은 단지 DDL을 자동으로 생성할 때만 사용되고 JPA 실행 로직에는 영향을 주지 않는다.
따라서 스키마 자동 생성 기능을 사용하지 않고 직접 DDL을 만든다면 사용할 이유가 없다.
이 기능을 사용하면 애플리케이션 개발자가 엔티티만 보고도 손쉽게 다양한 제약조건을 파악할 수 있는 장점이 있다.
(5) 기본 키 매핑
@Entity
public class Member {
@Id
@Column(name = "ID")
private String id;
JPA가 제공하는 데이터베이스 기본 키 생성 전략
데이터베이스 벤더마다 기본 키 생성을 지원하는 방식이 다름
기본키 생성 전략 방식
1. 직접 할당 : 기본 키를 어플리케이션이 직접 할당
2. 자동 생성 : 대리 키 사용 방식
- IDENTITY : 기본 키 생성을 데이터베이스에 위임
- SEQUENCE : 데이터베이스 시퀀스를 사용해서 기본 키를 할당.
- TABLE : 키 생성 테이블을 만들어서 시퀀스처럼 사용한다. -> 모든 데이터베이스에서 사용 가능
기본키 생성 방법
- 기본 키를 직접 할당 :
@Id
만 사용 - 자동 생성 전략 사용 :
@GeneratedValue
추가 및 키 생성 전략 선택.
키 생성 전략 사용을 위한 속성 추가
<property name="hibernate.id.new_generator_mappings" value="true" />
기본 키 직접 할당 전략
// 기본 키 직접 할당
@Id
@Column(name = "id")
private String id;
Board board = new Board();
board.setId("id1"); // 기본 키 직접 할당
em.persist(board);
@Id 적용 가능한 자바 타입
- 자바 기본형
- 자바 래퍼형
- String
- java.util.Date
- java.sql.Date
- java.math.BigDecimal
- java.math.BigInteger
IDENTITY 전략
기본 키 생성을 데이터베이스에 위임하는 전략
주로 MySQL, PostgreSQL, SQL Server, DB2, H2 에서 사용
식별자를 데이터베이스가 자동으로 생성해주는 전략
@GeneratedValue
어노테이션을 사용하고 식별자 생성 전략을 선택한다.
@Entity
public class Board {
@Id
@GeneratedValues(strategy = GenerationType.IDENTITY) // 식별자 생성 전략 - 기본 키 값을 얻어오기 위해 데이터베이스를 추가로 조회
private Long id;
...
}
- 데이터베이스에 값을 저장하고 나서 기본 키 값을 구할 수 있을 때 사용.
em.persist()
호출 시INSERT SQL
을 즉시 데이터베이스에 전달한다. -> 쓰기 지연이 동작하지 않는다- 식별자를 조회해서 엔티티의 식별자에 할당한다.
- 엔티티에 식별자 값을 할당하려면 JPA는 추가로 데이터베이스를 조회해야 한다. 하지만 하이버네이트는 JDBC3에 추가된
Statement.getGeneratedKeys()
를 사용하여 데이터를 저장하는 동시에 생성된 기본 키 값도 얻어올 수 있다.
MySQL의 AUTO_INCREMENT 기능
CREATE TABLE BOARD {
ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY, // 기본 키 자동 생성
DATA VARCHAR(255)
};
INSERT INTO BOARD(DATA) VALUES('A'); // ID : 1
INSERT INTO BOARD(DATA) VALUES('B'); // ID : 2
SEQUENCE 전략
유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트
주로 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용.
데이터베이스 시퀀스를 통해 식별자를 조회하는 추가 작업이 필요하다. 따라서 데이터베이스와 2번 통신한다.
- 식별자를 구하기 위해 데이터베이스 시퀀스를 조회
SELECT BOARD_SEQ.NEXTVAL FROM DUAL
- 조회한 시퀀스를 기본 키 값으로 사용해 데이터베이스에 저장
INSERT INTO BOARD...
JPA는 시퀀스에 접근하는 횟수를 줄이기 위해 @SequenceGnerator.allocationSize
를 사용한다.
여기에 설정한 값만큼 한 번에 시퀀스 값을 증가시키고 나서 그만큼 메모리에 시퀀스 값을 할당한다.
ex) allocationSize 값이 50이면 시퀀스를 한 번에 50 증가시킨 다음 1 ~ 50 까지는 메모리에서 식별자를 할당한다.
그리고 51이 되면 시퀀스 값을 100으로 증가시킨 다음 51 ~ 100 까지 메모리에서 식별자를 할당한다.
=> 이러한 최적화 방법은 시퀀스 값을 선점하므로 여러 JVM이 동시에 동작해도 기본 키 값이 충돌하지 않는 장점이 있다.
하지만 데이터베이스에 직접 접근해서 데이터를 등록할 때 시퀀스 값이 한 번에 많이 증가한다는 점을 염두에 둬야한다.
INSERT
성능이 중요하지 않으면 allocationSize
값을 1로 설정하면 된다.
시퀀스 관련 SQL
CREATE TABLE BOARD (
ID BIGINT NOT NULL PRIMARY KEY,
DATA VARCHAR(255)
)
//시퀀스 생성
CREATE SEQUENCE BOARD_SEQ START WITH 1 INCREMENT BY 1;
시퀀스 매핑 코드
@Entity
@SequenceGenerator(
name = "BOARD_SEQ_GENERATOR",
sequenceName = "BOARD_SEQ", // 데이터베이스의 BOARD_SEQ 시퀀스와 매핑
initialValue = 1, // DDL 생성 시에만 사용, 처음 시작하는 수 지정. 기본값이 1
allocationSize = 1 ) // 시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용). 기본값이 50
public class Board {
@Id
@GeneraedValue(strategy = GenerationType.SEQUNCE,
generator = "BOARD_SEQ_GENERATOR")
private Long id;
}
시퀀스 사용 코드
private static void logic(EntityManager em) {
Board board = new Board();
em.persist(board);
System.out.println("board.id = " + board.getId());
}
em.persist()
를 호출하면 먼저 데이터베이스 시퀀스를 사용해 식별자를 조회한다.- 조회한 식별자를 엔티티에 할당 후, 엔티티를 영속성 컨텍스트에 저장한다.
- 트랜잭션을 커밋
- 플러시 발생 -> 엔티티를 데이터베이스에 저장
IDNETITY 전략은 먼저 엔티티를 데이터베이스에 저장한 후 식별자를 조회해서 엔티티의 식별자에 할당
주의
- SequenceGenerator.allocationSize 기본값이 50 이므로, 하나씩 증가시키려면 반드시 1로 설정해야 한다.
TABLE 전략
키 생성 전용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 컬럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략
테이블을 생성해 사용하므로 모든 데이터베이스에 적용할 수 있다.
시퀀스 대신에 테이블을 사용한다는 것만 제외하면 SEQUENCE 전략
과 내부 동작방식이 같다.
TABLE 전략 키 생성 테이블
create table MY_SEQUENCES (
sequence_name varchar(255) not null, // 시퀀스 이름
next_val bigint, // 시퀀스 호출마다 증가하는 수
primary key (sequence_name)
)
TABLE 전략 매핑 코드
@Entity
@TableGenerator(
name = "BOARD_SEQ_GENERATOR", // 식별자 생성기 이름
table = "MY_SEQUENCES", // 키 생성 테이블 명
pkColumnValue = "BOARD_SEQ", allocationSize = 1)
// 키로 사용할 값 이름 시퀀스 한 번 호출에 증가하는 수
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.TABLE,
generator = "BOARD_SEQ_GENERATOR")
private Long id;
}
TABLE 전략 매핑 사용 코드
private static void logic(EntityManger em) {
Board board = new Board();
em.persist(board);
System.out.println("board.id = " + board.getId());
}
MY_SEQUENCES
테이블에 값이 없으면 JPA가 값을 INSERT 하면서 초기화하므로 미리 넣어둘 필요는 없다.
값을 조회하면서 SELECT 쿼리
사용, 다음 값으로 증가시키기 위해 UPDATE 쿼리
사용
=> SEQUENCE
전략과 비교해서 데이터베이스와 한 번 더 통신하는 단점. -> 최적화하려면 @TableGenerator.allocationSize
를 사용
AUTO 전략
GenerationType.AUTO 는 선택한 데이터베이스 방언에 따라 IDENTITY
, SEQUENCE
, TABLE
전략 중 하나를 자동으로 선택
ex) 오라클 - SEQUENCE
, MySQL - IDENTITY
AUTO 전략 매핑 코드
@Entity
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
...
}
@GeneratedValue.strategy 의 기본값은 AUTO
장점
- 데이터베이스를 변경해도 코드를 수정할 필요가 없다.
- 키 생성 전략이 확정되지 않은 개발 초기 단계, 프로토타입 개발 시 편리하다.
SEQUENCE
, TABLE
전략 시
스키마 자동 생성 기능
을 사용하면 하이버네이트가 기본값을 사용해서 시퀀스나 키 생성용 테이블을 만들어 준다.
기본 키 매핑
영속성 컨텍스트는 엔티티를 식별자 값으로 구분하므로 엔티티를 영속 상태로 만들려면 식별자 값이 반드시 있어야 한다.
em.persist() 호출 직후 발생하는 일
1. 직접 할당
em.persist() 호출 전 애플리케이션에서 직접 식별자 값을 할당해야 한다. 만약 식별자 값이 없으면 예외 발생
2. SEQUENCE
데이터베이스 시퀀스에서 식별자 값을 획득한 후 영속성 컨텍스트에 저장.
3. TABLE
데이터베이스 시퀀스 생성용 테이블에서 식별자 값을 획득한 후 영속성 컨텍스트에 저장.
4. IDENTITY
데이터베이스에 엔티티를 저장해서 식별자 값을 획득한 후 영속성 컨텍스트에 저장
즉, 테이블에 데이터를 저장해야 식별자 값을 획득할 수 있다.
데이터베이스 기본 키의 조건
null
값은 허용하지 않는다.- 유일해야 한다.
- 변해서는 안된다.
테이블의 기본 키 선택 전략 2가지
- 자연 키(natural key)
- 비즈니스에 의미가 있는 키
- ex) 주민등록번호, 이메일, 전화번호
- 대리 키(surrogate key)
- 비즈니스와 관련 없는 임의로 만들어진 키. 대체 키로도 불린다.
- ex) 오라클 시퀀스, auto_increment, 키생성 테이블 사용
자연 키보다는 대리 키를 권장한다. -> 현실과 비즈니스 규칙은 쉽게 변하기 때문에
대리 키는 비즈니스와 무관한 임의의 값이므로 요구사항이 변경되어도 기본 키가 변경되는 일은 드물다.
대리 키를 기본 키로 사용하되, 주민등록번호나 이메일처럼 자연 키의 후보가 되는 컬럼들은
필요에 따라 유니크 인덱스를 설정해서 사용하는 것을 권장
미래까지 충족하는 자연 키를 찾기 쉽지 않다.
JPA는 모든 엔티티에 일관된 방식으로 대리 키 사용을 권장
(6) 필드와 컬럼 매핑: 레퍼런스
분류 | 매핑 어노테이션 | 설명 |
---|---|---|
필드와 컬럼 매핑 | @Column | 컬럼을 매핑한다. |
@Enumerated | 자바의 enum 타입을 매핑한다. | |
@Temporal | 날짜 타입을 매핑한다. | |
@Lob | BLOB, CLOB 타입을 매핑한다. | |
@Transient | 특정 필드를 데이터베이스에 매핑하지 않는다. | |
기타 | @Access | JPA가 엔티티에 접근하는 방식을 지정 |
@Column
기본값 = @Column(nullable = true)
하지만 자바 기본 타입일 때는 null
값을 입력할 수 없다.
따라서 자바 기본 타입을 DDL 로 생성할 때는 @Column(nullable = false)
를 추가해주는 것이 안전하다.
JPA는 이런 상황을 고려해서 DDL 생성 기능 사용 시 기본 타입에는 not null
제약 조건을 추가한다. 반면 Integer 같은 객체 타입이면 null 이 입력될 수 있어서 not null
제약조건을 설정하지 않는다.
=> 자바 기본 타입에는 @Column
어노테이션 자체를 생략(자바 기본 타입의 기본값은 not null)하거나 @Column(nullable = false)
까지 적어줘야한다!
@Enumerated
자바의 enum
타입을 매핑할 때 사용
@Enumerated(value = ~~)
value = EnumType.ORDINAL
: enum 순서를 데이터베이스에 저장 => 기본값value = EnumType.STRING
: enum 이름을 데이터베이스에 저장
// ex)
// enum 클래스
enum RoleType {
ADMIN, USER
}
// enum 이름으로 매핑
@Enumerated(EnumType.STRING)
private RoleType roleType;
member.setRoleType(RoleType.ADMIN); // -> DB에 문자 ADMIN으로 저장
1. EnumType.ORDINAL
enum에 정의도니 순서대로 ADMIN
은 0, USER
는 1 값이 데이터베이스에 저장
- 장점 : 데이터베이스에 저장되는 데이터 크기가 작다.
- 단점 : 이미 저장된
enum
의 순서를 변경할 수 없다. (중간에 새로운게 추가되면 뒤에꺼 다 1칸씩 밀려야함)
2. EnumType.STRING
enum 이름 그대로 ADMIN
은 'ADMIN', USER
는 'USER'라는 문자로 데이터베이스에 저장된다.
- 장점 : 저장된 enum의 순서가 바뀌거나 enum이 추가되어도 안전하다.
- 단점 : 데이터베이스에 저장되는 데이터 크기가
ORDINAL
에 비해서 크다.
@Temporal
날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용
생략 시 timestamp
로 정의
@Temporal(value = ~~)
value = TemporalType.DATE
: 날짜, 데이터베이스 date 타입과 매핑 (ex 2021-05-11)value = TemporalType.TIME
: 시간, 데이터베이스 time 타입과 매핑 (ex 12:54:23)value = TemporalType.TIMESTAMP
: 날짜와 시간, 데이터베이스 timestamp 타입과 매핑 (ex 2021-05-11 12:54:23)
자바 Date 타입에는 년월일 시분초,
데이터베이스에는 date(날짜), time(시간), timestamp(날짜와 시간) 세 가지 타입이 별도로 존재
@Temporal(TemporalType.DATE)
private Date date; // 날짜
@Temporal(TemporalType.TIME)
private Date time; // 시간
@Temporal(TemporalType.TIMESTAMP)
private Date timestamp; // 날짜와 시간
//==생성된 DDL==//
date date,
time time,
timestamp, timestamp,
@Transient
이 필드는 매핑하지 않는다. => 데이터베이스에 저장하지 않고 조회하지도 않는다.
객체에 임시로 어떤 값을 보관하고 싶을 때 사용.
@Transient
private Integer temp;
(7) 정리
1. 데이터베이스 스키마 자동 생성 기능 -> 엔티티 객체를 먼저 만들고 테이블은 자동으로 생성
2. JPA는 다양한 기본 키 매핑 전략을 지원
- 직접 할당
- 데이터베이스가 제공하는 기본 키를 사용하는
SEQUENCE
,IDENTITY
,TABLE
전략