[김영한 - JPA 기본] #4 엔티티 매핑
매핑 어노테이션들
- 객체와 테이블 :
매핑 대상 | 어노테이션 |
---|---|
객체와 테이블 | @Entity , @Table |
필드와 컬럼 | @Column |
기본키 | @Id |
연관관계 | @ManyToOne , @JoinColumn |
객체와 테이블
@Entity
가 붙은 클래스는 JPA가 관리하며, 엔티티라고 부른다기본 생성자 필수 (public, protected)
- (리플렉션 등의 기술을 사용하기 때문)
@Enttiy
- 속성: name
- JPA에서 사용할 엔티티 이름을 지정한다.
- 기본값: 클래스 이름을 그대로 사용
- 가급적 기본값을 사용한다.
@Table
- 엔티티와 매핑할 테이블 지정
속성 | 기능 설명 | 기본값 |
---|---|---|
name | 매핑할 테이블 이름 | 엔티티 이름을 사용 |
uniqueConstraints (DDL) | DDL 생성 시에 유니크 제약 조건 생성 |
데이터베이스 스키마 자동 생성
DDL을 애플리케이션 실행 시점에 자동 생성
정말 정말 주의해서 사용해야 한다
hibernate.hbm2ddl.auto
옵션 | 설명 |
---|---|
create | 기존 테이블 DROP 후 생성 (DROP -> CREATE) |
create-drop | create 속성에서 종료 시점에 테이블 DROP |
update | 변경된 내역만 반영 |
validate | 엔티티와 테이블이 정상 매핑되었는지만을 확인 |
none | 아무것도 하지 않음 |
validation
속성에 기존의 없던 컬럼을 추가했을 경우
운영상에서는 어떻게 ?
환경 | 허용되는 속성 |
---|---|
개발 초기 단계 | create , update |
테스트 서버 | update , validate |
스트에징과 운영서버 | validate , none |
- 운영환경에서는 절대
create
,create=drop
,update
을 사용하면 안된다 !!
가급적이면 개발서버에서도 update
는 쓰지 않는게 좋다
만일 시스템상에서 자동으로 DDL
이 나가면 락이 걸리게 된다.. 정말 주의해야 한다
DDL 생성 기능
@Column(nullable = false, length = 10)
- 제약조건 추가 : 회원 이름은 필수, 10자 초과 (X)
- 유니크 제약 조건 추가
@Table(uniqueConstraints = {
@UniqueConstraint(
name = "NAME_AGE_UNIQUE",
columnNames = {"NAME", "AGE"}
)
}
)
필드와 컬럼 매핑
@Id
private Long id;
// @Column(nullable = false, length = 4)
private String name;
private Integer age;
@Enumerated(EnumType.STRING)
private RoleType roleType;
@Temporal(TemporalType.DATE)
private Date createDate;
@Temporal(TemporalType.DATE)
private Date lastModifiedDate;
@Lob
private String description;
@Transient
private String temp;
컬럼 속성에 따른 다양한 매핑들
Enum
@Enumerated
에는 두가지 속성이 있다- EnumType.STRING
- enum 이름을 저장 (name())
- 반드시 이 속성을 사용해야 문제가 없다, 문제는 이게 기본값이 아니다
- EnumType.ORDINAL
- 순서를 저장 (0부터 시작)
- 이 속성을 사용했을 때 문제가 있다 (DB값이 읽히지 않음, 순서 변경시 의미 상실)
- EnumType.STRING
날짜와 시간
LocalDate
,LocalDateTime
,LocalTime
은 어노테이션 없이 사용한다Date
,Calendar
의 경우는 아래의 어노테이션을 사용한다- @Temporal(TemporalType.DATE)
- TemporalType.DATE
- TemporalType.TIME
- TemporalType.TIMESTAMP
- @Temporal(TemporalType.DATE)
Lob
대용량 데이터
CLob
- String, char[]
Blob
- byte[]
기본 키 매핑
기본 키 매핑 어노테이션
@Id @GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
기본 키 매핑 속성
직접 할당:
@Id
만 사용자동 생성 :
@GeneratedValue
- IDENTITY : 키 생성을 데이터베이스에 위임한다 ex) MySql - auto increment
- SEQUENCE : 데이터베이스 시퀀스 오브젝트 사용 ex) Oracle - oracle squence
- TABLE : 키 생성용 테이블 사용, 모든 DB에서 사용
- AUTO : 방언(Oracle, MySQL)에 따라 자동 지정, 기본값
IDENTITY 전략
- 키 생성을 데이터베이스에 위임한다 ex) MySql - auto increment
- 최초 키 생성시 null로 들어가고
DB에서 키를 할당한다- 이론상
persist context
에도null
이 들어갈것 같지만 사실은 DB에 저장하고 나서 내부적으로 JDBC 드라이버가 키를persist context
로 끌고 온다 - 때문에 버퍼를 이용하여 모아서 INSERT하는게 이 전략에선 불가능하다
- 이론상
- 최초 키 생성시 null로 들어가고
SEQUENCE 전략
- 데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트
- 예) 오라클 시퀀스
@Entity
@SequenceGenerator(
name = “MEMBER_SEQ_GENERATOR",
sequenceName = “MEMBER_SEQ", //매핑할 데이터베이스 시퀀스 이름
initialValue = 1,
allocationSize = 1
)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MEMBER_SEQ_GENERATOR")
private Long id;
...
SEQUENCE 전략 : 최적화
- 한번에 50개씩 할당하면 네트워킹 왕복을 덜하면서 pk를 갱신할 수 있다
예를들어 서로다른 thread가 transaction을 가진다고할 때.. 50개씩의 pk값을 올리고 알파벳 26개를 name에 기록할 경우 1애서 52까지의 key를 사용
- 실제로 테스트를 해본 결과
50개 + 50개가 할당되었고
키값은 1~52개의 번호가 할당되었다.
난 1~26, 51~76일 줄 알았는데 ㅠ 아니었다
테이블 전략
- 키를 관리하는 테이블을 생성한다
- 기본적으로 많이 사용하지는 않는다
- 키 테이블을 통해서 끌고 갈때 테이블 락이 걸릴 수 있다
권장하는 식별자 전략
기본 키 제약조건:
- NULL이 아니어야 하며,
- 유일하고,
- 변하면 안된다 (불변)
- 먼 미래까지 이 조건을 만족하는 자연키(주민번호 등)는 찾기 어렵다
- 예를들어 주민번호도 기본키로 적절하지 않다
- 영한쌤의 썰.. 주민번호를 사용하였는데 하루아침에 모두 대체키로 바꾸어야 했던 상황
- 비즈니스 개념상의 값을 키로 끌고 오는것은 좋지 않다 !!
- 권장 : Long형 + 대체키(시퀀스, UUID등) + 키 생성 전략
그외
int보다는 Integer, Integer보다는 Long형을 사용하는 것이 좋다
- int는 10억 정도의 숫자밖에 저장을 못하며
나중에 자료형을 바꾸는데 기회비용이 든다
아래 에러의 이유
PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: hellojpa.domain.Member
한참을 찾았는데 나의 경우는 PK
전략을 생성해서 발급을 JPA나 DB에 맡겼는데
VO객체를 생성할 떄 PK
를 수동으로 넣어주어서 생긴 에러였다
시퀀스 테이블 전략 테스트
50개씩 증가하는 시퀀스에 대해
한 쓰레드에 대하여 사용자 키를 25개씩 증가 시켜보았다
테스트에 사용할 쓰레드
public class EntityThread extends Thread {
private EntityManagerFactory emf;
private EntityManager em;
private EntityTransaction tx;
public EntityThread(EntityManagerFactory emf) {
this.emf = emf;
this.em = emf.createEntityManager();
this.tx = em.getTransaction();
}
public EntityThread() { // 쓰레드 마다 EntityManagerFactory 생성시
this.emf = Persistence.createEntityManagerFactory("hello");
this.em = emf.createEntityManager();
this.tx = em.getTransaction();
}
@Override
public void run() {
final int NUM_OF_MEMBER = 1;
tx.begin();
try {
for (int i = 1; i <= NUM_OF_MEMBER; i++) {
Member member = new Member("user " + i);
em.persist(member);
}
tx.commit();
} catch (Exception e) {
e.printStackTrace();
tx.rollback();
}
em.close();
}
}
실행 메서드
final int NUM_OF_THREAD_MAX =20;
EntityThread th[] = new EntityThread[NUM_OF_THREAD_MAX];
for (int i = 0; i < NUM_OF_THREAD_MAX; i++) {
// th[i] = new EntityThread(emf);
th[i] = new EntityThread();
th[i].start();
}
try {
for (int i = 0; i < NUM_OF_THREAD_MAX; i++) {
th[i].join();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
테스트 결과
- 500개의 유저 생성
- MEMBER_SEQ의 Current value: 501
난 시퀀스가 1001이 될 줄 알았는데.. 딱 500개만 생겼다 같은 영속성 컨텍스트를 공유해서 이런 현상이 발견된 것으로 보인다
테스트 변경
그래서 한번 테스트를 변경해 보았다
entityMangerFactory 자체를 각 쓰레드 마다 생성하는 것으로 변경해보았다 !
테스트 결과
- 생성된 유저 : 500개
- MEMBER_SEQ의 Current value: 1001
맞았다 !! 각각 다른 컨텍스트를 사용한다면 그 컨텍스트마다 시퀀스를 생성한다
예를 들어 WAS가 2개이고 각각 하나의 유저를 발급하면
- 생성된 유저 : 2개
- MEMBER_SEQ의 Current value: 51
이 된다고 볼 수 있다 !
16개의 쓰레드에서 1000개 유저 생성
- 이럴 일이 있을지는 모르겠지만 극단적으로 WAS가 16개 있고
동시에 1000명씩 유저를 생성한다고 하였을 떄를 가정하였다 allocationSize=2000 기준!
- 예상은 16000명의 회원이 생기고 current sequence는 32001이 되어있을 것이다 !
final int NUM_OF_MEMBER = 1000;
tx.begin();
try {
for (int i = 1; i <= NUM_OF_MEMBER; i++) {
Member member = new Member("user " + i);
em.persist(member);
}
...
3번 수행한 결과, 예상한 결과가 맞아 떨어졌다
쓰레드 동기화로 인한 이슈는 없었다 !
위와 같은 실험은 각각의 Persistence를 생성한다 그런데 옵션이 create
일 때는 테이블을 드랍 후 생성을 하기 때문에
ddl-option을 update
이하로 해주는 것이 좋다
내 팁 !
테이블, 시퀀스등을 지울때 create-drop 옵션으로 먼저 지워주면 수동으로 지우는것보다 편한다 !
실전 예제 - 1. 요구사항 분석과 기본 매핑
예제 코드
예제에 필요한 코드들이다 !
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpashop");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
tx.commit();
} catch (Exception e) {
System.out.println("rollback !! ");
tx.rollback();
e.printStackTrace();
}
em.close();
emf.close();
}
}
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
private String street;
private String zipcode;
@Column(name = "ORDER_ID")
private Long orderId;
// getters & setters ...
@Entity
@Table(name = "ORDERS")
public class Order {
@Id @GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@Column(name = "MEMBER_ID")
private Long memberId;
private LocalDateTime orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus status;
public enum OrderStatus {
ORDER, CANCEL;
}
@Entity
public class OrderItem {
@Id @GeneratedValue
@Column(name = "ORDER_ITEM_ID")
private Long id;
private Long orderId;
private Long itemId;
private int orderPrice;
private int count;
@Entity
public class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
private int stockQuantity;
영한님 노하우
예를들어 Member의 이름에 10글자 제한을 두고 싶다..
DB에 DDL로 컬럼을 수정하는 방법도 있지만
@Column(length = 10)
private String name;
위처럼 조건을 자바에 명시할 수도 있다
이 방식의 장점을 굳이 DB Client tool 을 매번 열어서 확인할 필요 없이
소스 코드상에서 확인할 수 있다는 장점이 있다 !
인덱스와 유일키도 마찬가지로
@Table
의 속성(indexes
, uniqueConstraint
)으로 표기할 수 있다
그외
부트+하이버네이트를 적용했을시에
orderStatus 와 같은 캐멀케이스 컬럼은 굳이 @Column
name 속성을 지정하지 않더라도 소문자 언더스코어(order_status)로 바꾸어준다
Table 의존적인 연관관계 : 객체지향적이지 못한 Domain 객체
Entity의 연관관계를 갖는 필드가 참조가 아닌 id를 의존한다면
객체지향적이지 못한 코드가 탄생한다
아래는 Member와 Order를 연관관계를 맺고 각각의 Entity를 저장하는 코드이다
Order order = new Order();
order.setStatus(OrderStatus.ORDER);
em.persist(order);
Long orderId = order.getId();
Member member = new Member();
member.setName("swcho");
member.setOrderId(orderId);
em.persist(member);
Long memberId = member.getId();
order.setMemberId(memberId);
tx.commit();
long vs Long
ID가 비어있는 상태를 나타내기 위해 null
을 이용한다.
또 객체를 최초 생성하는 시점에는 null을 허용해주어야 한다.
long은 null 을 표현할 수 없다. 0
자체는 실 ID를 표현하는 의미일 수 있으니 추천하지 않는다.
기선님 https://www.inflearn.com/questions/35759
영한님
https://www.inflearn.com/questions/128732
https://www.inflearn.com/questions/260916
https://www.inflearn.com/questions/273180
https://www.inflearn.com/questions/273180
https://www.inflearn.com/questions/111851
https://www.inflearn.com/questions/278198
JPA 어노테이션으로 외래키 제약 조건에 이름을 줄 수 있는가?
자동 생성된 외래키 제약 조건 이름이 불친절한 경우인데…
이 경우에 대해서는 직접 DDL문을 작성해야한다고 한다.
https://stackoverflow.com/questions/6608812/jpa-give-a-name-to-a-foreign-key-on-db