728x90
1. 엔티티 매핑
- 객체와 관계형 데이터베이스 매핑하기 : 실제 설계적인 측면 (정적 측면), 객체와 db를 어떻게 연결하는지
- 영속성 컨텍스트(entity Manager의 1차 cache라고 봐도 무방 - spring에선 약간 다르다.) : JPA 내부 동작 방식 및 메커니즘
1.1. 엔티티 매핑 개요
- 객체외 테이블 매핑 : @Entity, @Table
- 필드와 컬럼 매핑 : @Column
- 기본 키 매핑 : @Id
- 연관관계 매핑 : @ManyToOne, @JoinColumn
1.2. 데이터베이스 스키마 자동 생성
1.2.1 자동 생성
- DDL을 애플리케이션 실행 시점에 자동 생성 - local에서 개발할 때 도움
- 테이블 중심 설계 -> 객체 중심 설계 가능해짐
- 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL 생성
- 이렇게 생성된 DDL은 개발 장비에서만 사용
- 운영서버에서 적절히 다듬은 후 사용할 수 있는데 그냥 하지 말 것
1.2.2. 속성
옵션 | 설명 |
create | 기존 table 삭제 후 다시 생성 |
create-drop | create와 같으나 tx 종료시점에 table drop, test 때 많이 사용 |
update | 변경분만 반영(필드 추가시에만 적용, 필드제거에 적용되면 안됨) |
validate | 엔티티와 테이블이 정상 매핑되었는지만 확인 - 다를 시 에러 발생 (권장) |
none | 사용 안함 - 직접 DDL script 작성 (권장) |
1.2.3. 주의
- 개발 초기 단계 : create || update
- 테스트 서버 : update || validate
- 스테이징과 운영서버 : validate || none
- (권장) 협동개발, test, 운영 모두 validate || none 해서 개발자가 직접 DDL SQL 스크립트를 작성 할 것
- 완전 혼자 사용하는 개발 서버에서만 create, update 사용 할 것
<!-- persistence.xml -->
<!-- 데이터베이스 스키마 자동 생성 : 개발 장비에서만 사용 권장-->
<property name="hibernate.hbm2ddl.auto" value="create"/>
<property name="hibernate.hbm2ddl.auto" value="create-drop"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
<property name="hibernate.hbm2ddl.auto" value="validate"/>
<property name="hibernate.hbm2ddl.auto" value="none"/>
- create 수행 예시
1.2.4. DDL 생성 기능
- nullable, length, unique 같은 제약 조건은 runtime에 영향을 주지 않는다.
- DDL 생성 기능은 DDL을 자동 생성할 때만 사용되고 JPA의 실행 로직에는 영향을 주지 않는다.
@Column(nullable = false, length = 10)
@Column(unique = false, length = 10)
@Table(uniqueConstraints = {@UniqueConstraint(name = "NAME_AGE_UNIQUE", columnNames = {"NAME", "AGE"})})
// column의 nullable, unique 제약조건은 이름이 random하기 나와서 잘 사용안하고
// Table처럼 name을 지정할 수 있는 방식을 사용하는 것을 권장한다.
2. 객체와 테이블 매핑
2.1. @Entity
- 정의
- @Entity가 붙은 클래스 == 엔티티 : JPA가 관리 (엔티티를 가지고 entityManger가 작업 수행)
- JPA를 사용해서 테이블과 매핑할 클래스는 @Entity가 필수
- 주의
- 기본 생성자가 필수
- final class, enum, interface, inner class는 @Entity 사용 불가
- 저장할 필드에 final 사용 불가
- 속성
- name
- JPA에서 사용할 엔티티 이름을 지정
- 기본값은 클래스 이름을 그대로 사용
- 같은 class 이름이 없으면 가급적 기본값을 사용
- name
2.2. @Table
- @Table은 엔티티와 매핑할 테이블 지정
- 속성
속성 | 기능 | default |
name | 매핑할 table 이름 | 엔티티 이름을 사용 |
catalog | db catalog 매핑 | |
schema | db schema 매핑 | |
uniqueConstraints (DDL) | DDL 생성 시 유니크 제약조건 생성 |
3. 필드와 컬럼 매핑
- 일반 회원과 관리자로 구분 : enum 사용
- 회원 가입일과 수정일 필요
- 회원 설명 field 추가 - 길이 제한 없음 : @Lob 이용
- Member Entity - 매핑 어노테이션 정리
어노테이션 | 설명 |
@Column | 컬럼 매핑 |
@Temporal | 날짜 타입 매핑 - date, time, timeStamp cf> localDate(Time) 부터 사용 X |
@Enumerated | enum 타입 매핑 |
@Lob | BLOB(string, char 빼고 다), CLOB (String, char 용) - 아주 큰 값 넣는 경우 |
@Transient | 특정 필드를 컬럼에 매핑하지 않음(매핑 무시) - memory 상에서만 계산하고 싶은 경우 |
package hellojpa;
import javax.persistence.*;
import java.time.LocalDate; // java8
import java.time.LocalDateTime; // java8
import java.util.Date; // java8 이전
// version1 - @Id 기본키, 필드와 컬럼 맵핑
@Entity
public class Member {
@Id
private Long id;
@Column(name = "name")
private String username;
private Integer age;
@Enumerated(EnumType.STRING)
private RoleType roleType;
// java8 이전 사람들은 date 밖에 없어서 @Temporal로 구분해줘야함
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
// java8 이후 부터는 LocalDate, LocalDateTime으로 충분히 구분 가능해서 @Temporal 필요 없음
private LocalDate testLocalDate;
private LocalDateTime testLocalDateTime;
@Lob
private String description;
}
package hellojpa;
public enum RoleType { USER, ADMIN }
3.1. @Column (제일 중요)
속성 | 설명 | 기본값 |
name | 필드와 매핑할 테이블의 컬럼 명 | 객체의 필드 이름 |
insertable, updatable | 등록, 변경 가능 여부 컬럼을 수정, 첨가 했을 때 db에 insert, update 할지 말지 결정 | true |
nullable(DDL) | null 값의 허용 여부를 설정한다. false로 설정하면 DDL 생성 시에 not null 제약조건이 붙는다. | |
unique(DDL) | @Table의 uniqueConstraints와 같지만 한 컬럼에 간단히 유니크 제약조건을 걸 때 사용 - 이름이 너무 지저분해서 사용 안함 |
|
columnDefinition (DDL) | 데이터베이스 컬럼 정보를 직접 줄 수 있다. (특정 db에 종속적인 값도 넣을 수 있다.) ex) varchar(100) default ‘EMPTY' |
필드의 자바 타입과 방언 정보를 사용 |
length(DDL) | 문자 길이 제약 조건, String 타입에만 사용 | 255 |
precision, scale(DDL) | BigDecimal 타입에서 사용한다(BigInteger도 사용할 수 있다). precision은 소수점을 포함한 전체 자 릿수를, scale은 소수의 자릿수 다. 참고로 double, float 타입에는 적용되지 않는다. 아주 큰 숫자나 정 밀한 소수를 다루어야 할 때만 사용한다. | precision=19, scale=2 |
3.2. @Enumerated
- java enum 타입을 매핑할 때 사용
- ORDINAL 절대 사용 말 것
속성 | 설명 | 기본값 |
value | EnumType.STRING : enum 이름을 데이터베이스에 저장 | EnumType.ORDINAL : enum 순서를 db에 저장 (Integer로 저장) |
- ORDINAL을 사용하면 안되는 이유
package hellojpa;
public enum RoleType { USER, ADMIN } // DB에 user는 0, admin은 1로 저장 된다.
// 향후 회원 정보가 추가됨
public enum RoleType { GUEST, USER, ADMIN } // DB에 gues가 0, user가 1로 저장 된다.
// 문제점 : 기존의 user는 0으로 저장되어 있고 향후 guset가 0으로 저장되기 때문에 0이 뭐를 의미하는지 알 수 없게 된다.
3.3. @Temporal
- 날짜 타입을 매핑할 때 사용
- java8 이후부터는 LocalDate, LocalDateTime을 사용할 때는 생략 가능
속성 | 설명 | 기본값 |
value | TemporalType.DATE: 날짜, 데이터베이스 date 타입과 매핑 (예: 2013–10–11) value TemporalType.TIME: 시간, 데이터베이스 time 타입과 매핑 (예: 11:11:11) TemporalType.TIMESTAMP: 날짜와 시간, 데이터베이스 timestamp 타입과 매핑 (예: 2013–10–11 11:11:11) |
3.4. @Lob
- 속성이 없다.
- 매핑 필드 타입이 문자면 CLOB 매핑, 나머지는 BLOB 매핑
- CLOB: String, char[], java.sql.CLOB
- BLOB: byte[], java.sql. BLOB
3.5. @Transient
- field 매핑에서 제외시킴
- db에 저장, 조회 다 안됨
- 주로 메모리상에서만 임시로 어떤 값을 보관하고 싶을 때 사용
4. 기본 키 매핑
- 직접 할당 : @Id만 사용 - 개발자가 pk를 직접 넣는 경우
- 자동 생성 : @GeneratedValue에 3가지 속성 넣어서 사용. default는 AUTO
- Identity : 데이터베이스에 위임. MYSQL
- Sequence : 데이터베이스 시퀀스 오브젝트 사용, Oracle
- @SequenceGenerator 필요
- Table : 키 생성용 테이블 사용, 모든 DB에서 사용
- @TableGenerator 필요
- @TableGenerator 필요
- AUTO: 방언에 따라 자동 지정, 기본값
- 전체 MEMBER code
package hellojpa;
import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
/* version5 - 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;
@Column(name = "name", nullable = false)
private String username;
public void setUsername(String username) {
this.username = username;
}
}
*/
/* version4 - Sequence 2 */
@Entity
@SequenceGenerator(
name = "MEMBER_SEQ_GENERATOR",
sequenceName = "MEMBER_SEQ",
// initialValue = 1, allocationSize = 1) // 1부터 시작하고 seq가 1씩 증가
initialValue = 1, allocationSize = 50) // 1부터 시작하고 seq가 50씩 증가 <최적화> - db와 네트워크 소통을 최소화 하기 위함
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "MEMBER_SEQ_GENERATOR") // 내가 만든 sequence를 사용하고 싶을 때 generator="" 속성 추가
private Long id;
@Column(name = "name", nullable = false)
private String username;
public Member() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
/* version3 - sequence 1
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Column(name = "name", nullable = false)
private String username;
getter,setter 생략
}
*/
/* version2 - identity
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name", nullable = false)
private String username;
public void setUsername(String username) {
this.username = username;
}
}
*/
/* version1 -@Id 기본키, 필드와 컬럼 맵핑
@Entity
public class Member {
@Id
private Long id;
@Column(name = "name")
private String username;
private Integer age;
@Enumerated(EnumType.STRING)
private RoleType roleType;
// java8 이전 사람들은 date 밖에 없어서 @Temporal로 구분해줘야함
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
// java8 이후 부터는 LocalDate, LocalDateTime으로 충분히 구분 가능해서 @Temporal 필요 없음
private LocalDate testLocalDate;
private LocalDateTime testLocalDateTime;
@Lob
private String description;
}*/
4.1. 직접 할당
package hellojpa;
import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
/* version1 - @Id 기본키, 필드와 컬럼 맵핑 */
@Entity
public class Member {
@Id
private Long id;
@Column(name = "name")
private String username;
private Integer age;
@Enumerated(EnumType.STRING)
private RoleType roleType;
// java8 이전 사람들은 date 밖에 없어서 @Temporal로 구분해줘야함
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
// java8 이후 부터는 LocalDate, LocalDateTime으로 충분히 구분 가능해서 @Temporal 필요 없음
private LocalDate testLocalDate;
private LocalDateTime testLocalDateTime;
@Lob
private String description;
}
4.2. 자동 할당
- @GeneratedValue (strategy = Auto)
- DB 방언에 맞춰서 생성 : Identity, Sequence, Talbe 중에서 선택
- 참고로 키 type으로 Identity는 String, Long 둘 다 사용 가능하지만 Sequence는 Long만 사용이 가능하다.
* AUTO 로 Sequence가 될 수 있기 때문에 AUTO도 Long 만 사용 가능하다.
package hellojpa;
import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
/* AUTO */
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(name = "name", nullable = false)
private String username;
public void setUsername(String username) {
this.username = username;
}
}
4.3. IDENTITY 전략 - 특징
- 기본 키 생성을 db에 위임
- 주로 MySQL, PostgreSQL, SQL Server, DB2 에서 사용 - AutoIncrement
- JPA는 보통 transaction commit 시점에 insert sql 실행이 된다.
- 하지만 identity 전략은 insert 시 Pk에 null을 넣은 상태로 sql을 db에 주고 db 내부에서 Pk 값을 만들어서 insert를 수행한다.
- 문제점
- 영속성 컨텍스트는 관리를 위해 pk가 무조건 존재해야 한다. 현재 pk를 알수 있는 시점이 db에 값이 실제로 들어갈 경우에만 알 수 있기 때문에 문제가 된다.
- 해결
- IDENTITY 전략에서만 예외적으로 em.persist() 시점에 즉시 INSERT SQL을 실행하고 DB에 식별자를 조회한다.
package hellojpa;
import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
/* version2 - identity */
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name", nullable = false)
private String username;
public void setUsername(String username) {
this.username = username;
}
}
4.4. SEQUENCE 전략 - 특징
- 데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트
- Oracle, PostgreSQL, DB2, H2 에서 사용
- log 상에서 create sequence라고 해서 sequnce Object를 생성한다.
- 사용방법
- @SequenceGenerator 사용 안하는 경우 : jpa가 생성해주는 hibernate_sequence를 사용
- @SequenceGenerator 사용하는 경우 : 내가 직접 sequence object를 사용 - 속성을 이용해서 성능상 이점을 얻을 수 있다.
- Member 엔티티
package hellojpa;
import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
/* version4 - Sequence 2 */
@Entity
@SequenceGenerator(
name = "MEMBER_SEQ_GENERATOR",
sequenceName = "MEMBER_SEQ",
// initialValue = 1, allocationSize = 1) // 1부터 시작하고 seq가 1씩 증가
initialValue = 1, allocationSize = 50) // 1부터 시작하고 seq가 50씩 증가 <최적화> - db와 네트워크 소통을 최소화 하기 위함
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "MEMBER_SEQ_GENERATOR") // 내가 만든 sequence를 사용하고 싶을 때 generator="" 속성 추가
private Long id;
@Column(name = "name", nullable = false)
private String username;
getter,setter 생략
}
/* version3 - sequence 1 */
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Column(name = "name", nullable = false)
private String username;
getter,setter 생략
}
- sequence
4.5. SEQUENCE 전략과 최적화
- 속성
속성 | 설명 | 기본값 |
name | 식별자 생성기 이름 | 필수 |
sequenceName | 데이터베이스에 등록되어 있는 시퀀스 이름 | hibernate_sequence |
initialValue | DDL 생성 시에만 사용 시퀀스 DDL을 생성할 때 처음 시작되는 숫자를 지정한다. |
1 |
allocationSize | 시퀀스 한번 호출에 증가하는 수 (성능 최적화에 사용) | 50 |
catalog, schema | 데이터베이스 catalog, schema 이름 |
- 최적화 핵심 속성 : initialValue, allocationSize
- 원리
- Identity때와 마찬가지로 영속성 컨텍스트에 값을 넣기 위해서 jpa가 sequence를 db로 부터 select해 오고 거기가 +1 한 값을 영속성 컨텍스트의 pk 값으로 가진다.
- 이 때, db에 값을 저장할 때 마다 db와 app 소통을 하게 되므로 성능 저하를 일으킨다.
- allocationSize로 미리 50 값을 가져와서 50까지 사용하고 50 을 넘어서게 되면 db에서 50을 더해서 sequence 값을 추가하고 마찬가지로 app에서 50 이후부터 100까지 sequence를 사용하면 된다.
= app 내부 메모리 상에서 sequence를 이용해서 db와 소통을 최소화 - 50이 아닌 충분히 큰 숫자로 늘려도 되지만 웹서버를 내리는 시점에 해당 값이 날라가기 때문에 구멍이 생겨서 보통 50으로 둔다.
- 웹서버 여러대가 동시에 호출해도 동시성 문제가 없다.
4.6. TABLE 전략
- 사용 권장 안함
- 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략
- 장점 : 모든 db에 적용가능 - identity와 sequence를 제공하는 db가 있고 아닌 db가 존재
- 단점 : 성능
- 속성
속성 | 설명 | 기본값 |
name | 식별자 생성기 이름 | 필수 |
table | 키생성 테이블명 | hibernate_sequence |
pkColumnName | 시퀀스 컬럼명 | sequence_name |
valueColumnName | 시퀀스 값 컬럼명 | next_val |
pkColumnValue | 키로 사용할 값 이름 | 엔티티 이름 |
initialValue | 초기 값, 마지막으로 생성된 값이 기준 | 0 |
allocationSize | 시퀀스 한번 호출에 증가하는 수 (성능 최적화에 사용) | 50 |
catalog, schema | 데이터베이스 catalog, schema 이름 | |
uniqueConstraint(DDL) | 유니크 제약 조건을 지정할 수 있다. |
package hellojpa;
import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
/* version5 - 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;
@Column(name = "name", nullable = false)
private String username;
public void setUsername(String username) {
this.username = username;
}
}
table
5. 권장하는 식별자 전략
- Long Type + 대체키 (UUID, Auto_increment, Sequence_ojbedt, 렌덤 값) + 키 생성전략 사용
6. 실전 예제 - 1. 요구사항 분석과 기본 매핑
6.1. 요구사항 분석
- 회원은 상품을 주문할 수 있따.
- 주문 시 여러 종류의 상품을 선택할 수 있다.
6.2. 기능 목록
- 회원 기능 - 회원 등록, 회원 조회
- 상품 기능 - 상품등록, 상품수정, 상품조회
- 주문 기능 - 상품주문, 주문내역조회, 주문취소
6.3. 도메인 모델 분석
6.4. 테이블 설계
6.5. 엔티티 설계와 매핑
- 문제점
- 객체 설계 방식이 아닌 테이블 설계에 맞추어 설계 되어있다.
- 테이블을 참조하지 않고 외래키를 객체에 그대로 가지고 온다.
- 객체 그래프 탐색이 불가능하다.
6.6. 코드
- https://github.com/taewan625/jpa-basic2 의 1st commit
- private repository
- 핵심 코드
package jpabook.jpashop.domain;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "ORDERS") // db 중 order가 예약어로 걸린 곳이 있기 때문에 orders로 table명을 지었다.
public class Order {
@Id @GeneratedValue
/* 핵심 시작 */
@Column(name = "ORDER_ID")
private Long id;
// 객체 지향적이지 않은 필드
@Column(name = "MEMBER_ID")
private Long memberId;
// 객체 지향적인 필드
private Member member;
public Member getMember() {
return member;
}
public void setMember(Member member) {
this.member = member;
}
/* 핵심 끝 */
private LocalDateTime orderDate; // spring에서는 필드명 그대로 나가지만, springboot에서는 order_date(default), ORDER_DATE로 자동으로 변경 해줌.
@Enumerated(value = EnumType.STRING)
private OrderStatus staus;
getter,setter 생략
}