[김영한 - 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-dropcreate 속성에서 종료 시점에 테이블 DROP
update변경된 내역만 반영
validate엔티티와 테이블이 정상 매핑되었는지만을 확인
none아무것도 하지 않음

validation 속성에 기존의 없던 컬럼을 추가했을 경우

image

운영상에서는 어떻게 ?

환경허용되는 속성
개발 초기 단계create, update
테스트 서버update, validate
스트에징과 운영서버validate, none
  • 운영환경에서는 절대 create, create=drop, update 을 사용하면 안된다 !!

가급적이면 개발서버에서도 update는 쓰지 않는게 좋다

만일 시스템상에서 자동으로 DDL이 나가면 락이 걸리게 된다.. 정말 주의해야 한다

DDL 생성 기능

@Column(nullable = false, length = 10)
  • 제약조건 추가 : 회원 이름은 필수, 10자 초과 (X)

image

  • 유니크 제약 조건 추가
@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값이 읽히지 않음, 순서 변경시 의미 상실)

날짜와 시간

  • LocalDate, LocalDateTime, LocalTime은 어노테이션 없이 사용한다

  • Date, Calendar의 경우는 아래의 어노테이션을 사용한다

    • @Temporal(TemporalType.DATE)
      • TemporalType.DATE
      • TemporalType.TIME
      • TemporalType.TIMESTAMP

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하는게 이 전략에선 불가능하다

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 어노테이션으로 외래키 제약 조건에 이름을 줄 수 있는가?

image

자동 생성된 외래키 제약 조건 이름이 불친절한 경우인데…

이 경우에 대해서는 직접 DDL문을 작성해야한다고 한다.

https://stackoverflow.com/questions/6608812/jpa-give-a-name-to-a-foreign-key-on-db




© 2020.12. by 따라쟁이

Powered by philz