반응형
엔티티 매핑 종류
- 객체와 테이블 매핑 :
@Entity
,@Table
- 필드와 컬럼 매핑 :
@Column
- 기본 키 매핑 :
@Id
- 연관관계 매핑 :
@ManyToOne
,@JoinColumn
객체와 테이블 매핑
@Entity
@Entity
가 붙은 클래스는 JPA가 관리하는 클래스로 엔티티라고 부른다. 즉, 테이블과 매핑할 클래스는 반드시@Entity
를 적용해야 한다.- 기본 생성자 필수 (파라미터가 없는 public 또는 protected)
- final 클래스, enum, interface, inner 클래스 사용할 수 없다.
- 저장할 필드에 final 키워드를 사용할 수 없다.
@Table
- 기본 값은 클래스 이름을 그대로 사용한다.
- 유니크 제약 조건 추가 : @Table(uniqueConstraints = {@UniqueConstraint( name = "NAME_AGE_UNIQUE", columnNames = {"NAME", "AGE"} )})
데이터베이스 스키마 자동 생성
- DDL을 애플리케이션 실행 시점에 자동 생성
- 테이블 중심 -> 객체 중심
- 데이터베이스 방언을 통해 적절한 DDL 생성
- 운영 장비에는 절대 create, create-drop, update 사용하면 안된다. (데이터 변경 여지)
- 개발 초기 : create 또는 update
- 테스트 서버 : update 또는 validate
- 스테이징과 운영 서버는 validate 또는 none
필드와 컬럼 매핑
- 요구 사항
- 회원은 일반 회원과 관리자로 구분해야 한다.
- 회원 가입일과 수정일이 있어야 한다.
- 회원을 설명할 수 있는 필드가 있어야 한다. 이 필드는 길이 제한이 없다
@Entity
public class Member {
@Id
private Long id;
@Column(name = "name") // 객체는 username, 컬럼명은 name
private String username;
private Integer age;
@Enumerated(EnumType.STRING) // enum 타입 설정 , DB에는 기본적으로 없다고 봐야한다
private RoleType roleType;
@Temporal(TemporalType.TIMESTAMP) //날짜 타입
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
@Lob //큰 컨텐츠를 넣을 때
private String description;
public Member() {
}
}
- 요구사항 설정을 위한 Member 클래스 재구성
매핑 애노테이션 정리
@Column
- nullable은 (defalut : true)로 기본 설정되어 있다.
- 유니크 제약조건은 잘 사용하지 않는다. 유니크를 적용하면 이름이 반영하기 어려운 이상한 값이 출력된다. 그래서 클래스 레벨에서
@Table(unique~)
와 같은 방식을 사용한다.
@Enumerated
- EnumType.ORDINAL : enum 순서를 DB에 저장 - 순서대로 숫자로 들어감 (사용하지 않는다.)
- 숫자 마저도 새로운 enum이 들어가면 위치가 변경할 수 있음.
- EnumType.STRING : enum 이름을 DB에 저장
@Temporal
private LocalDate testLocalDate;
private LocalDateTime testLocalDateTime;
- 요즘 하이버네이트 버전은 LocalDate를 사용할 수 있다.
@Temporal
이런게 있다는 정도만 보고 넘어가자.
@Lob
- 큰 컨텐츠를 넣을 때 사용 (게시판의 게시글)
- 매핑하는 필드 타입이 문자면 CLOB, 나머지는 BLOB으로 자동 매핑해준다.
- CLOB : String, char[], java.sql.CLOB
- BLOB : byte[], java.sql.BLOB
기본 키 매핑 @Id
- 직접 할당 : @Id 만 사용
- 자동 생성 (@GenerateValue)
- IDENTITY : 데이터베이스에 위임, 예로 MYSQL의 AUTO_INCREAMENT 기능이 있다.
- SEQUENCE : 데이터베이스 시퀸스 오브젝트 사용, ORACLE
- @SequenceGenerator 필요
- TABLE : 키 생성용 테이블 사용, 모든 DB에서 사용
- @TableGenerator 필요
- AUTO : 방언에 따라 자동 지정, 기본값
IDENTITY 전략
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
- 기본 키 생성을 DB에 위임 , Id에 값을 넣으면 안된다.
- 주로 MYSQL, SQL Server, DB2에서 사용 (AUTO_INCREMENT)
- AUTO_ INCREMENT는 데이터베이스에 INSERT SQL을 실행 한 이후에 ID 값을 알 수 있음
System.out.println("===========");
em.persist(member);
System.out.println("===========");
- IDENTITY 전략은 em.persist() 시점에 즉시 INSERT SQL 실행 하고 DB에서 식별자를 조회한다. 즉, JPA 입장에선 영속성 컨텍스트의 1차 캐시에 키와 객체를 관리하는 데 키값을 모르게 된다. 그렇기 때문에 커밋시점이 아닌 시점에서 강제로 DB를 호출해서 키값을 가져와야 하는 문제가 있다.
=====
사이에서 쿼리문이 날라감, 즉 쓰기 지연 기능을 사용하지 못한다.
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;
- 데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트(예: 오라클 시퀀스)
- 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용
======
사이에 시퀸스값만 가지고 와서, 쓰기 지연 기능을 사용할 수 있다.- allocationSize 설정을 통해 해당 개수만큼 미리 시퀸스값을 갖고와서 , 그 범위 안은 메모리로 띄우기 떄문에 성능 문제는 없다고 볼 수 있다. (동시성 이슈가 없다.)
- 예 ) initialSize = 1, allocationSize = 50 -> (1,1) (51,2) (51,3) (51,4) - 50개를 미리 가져와서 그 후 메모리에서 갖고 온다. (DB SEQ, APP SEQ)로 보면 된다.(1,1) 에서
allocationSize
를 맞추기 위해 한번 더 호출하고 이후는 메모리에서 시퀀스값을 가져온다.
- 예 ) initialSize = 1, allocationSize = 50 -> (1,1) (51,2) (51,3) (51,4) - 50개를 미리 가져와서 그 후 메모리에서 갖고 온다. (DB SEQ, APP SEQ)로 보면 된다.(1,1) 에서
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;
- 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략이다.
- 모든 데이터베이스에 적용할 수 있는 장점이 있지만, 성능 문제가 있어서 잘 사용하지 않는다.
권장하는 식별자 전략
- 기본 키 제약 조건 : null 아니고 유일, 변하면 안된다.
- 미래까지 조건하는 자연키는 찾기 어렵다. 대체키를 사용하자.
- 예를 들어 국가에서 주민번호를 보관할 수 없도록 법적 규제할 가능성이 있으므로 주민등록번호도 기본 키로 적절하지 않다.
- 권장 : Long형 + 대체키(시퀸스 or UUID) + 키 생성전략 사용
실전 예제
요구사항 분석
- 회원은 상품을 주문할 수 있다.
- 주문 시 여러 종류의 상품을 선택할 수 있다.
도메인 모델 분석
- 회원과 주문의 관계 : 회원은 여러 번 주문할 수 있다. 일대다 관계
- 주문과 상품의 관계 : 주문할 때 여러 상품을 선택할 수 있고, 상품은 여러 주문을 할 수 있다. 즉, 다대다 관계이므로 중간에 주문상품이라는 모델을 만들어서 일대다, 다대일로 풀어내자.
테이블 설계
엔티티 설계와 매핑
프로젝트 생성
MAVEN과 이전의 XML 방식의 JPA와 H2 DB를 사용해서 프로젝트를 생성해보자.
pom.xml 설정
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>jpabook</groupId>
<artifactId>jpashop</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<!-- JPA 하이버네이트 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>5.3.10.Final</version>
</dependency>
<!-- H2 데이터베이스 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.199</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
</dependencies>
</project>
persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="hello">
<properties>
<!-- 필수 속성 -->
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="javax.persistence.jdbc.user" value="sa"/>
<property name="javax.persistence.jdbc.password" value=""/>
<property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/jpatest"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<!-- 옵션 -->
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="create" />
</properties>
</persistence-unit>
</persistence>
Member
package jpabook.jpashop.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
private String city;
private String street;
private String zipcode;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getZipcode() {
return zipcode;
}
public void setZipcode(String zipcode) {
this.zipcode = zipcode;
}
}
Item
package jpabook.jpashop.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
private int stockQuantity;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public int getStockQuantity() {
return stockQuantity;
}
public void setStockQuantity(int stockQuantity) {
this.stockQuantity = stockQuantity;
}
}
Order
package jpabook.jpashop.domain;
import javax.persistence.*;
import java.time.LocalDateTime;
@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 Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getMemberId() {
return memberId;
}
public void setMemberId(Long memberId) {
this.memberId = memberId;
}
public LocalDateTime getOrderDate() {
return orderDate;
}
public void setOrderDate(LocalDateTime orderDate) {
this.orderDate = orderDate;
}
public OrderStatus getStatus() {
return status;
}
public void setStatus(OrderStatus status) {
this.status = status;
}
}
- Enum 타입을 사용할 때 순서의 보장을 위해 ORDINAL 이 아닌, String 방식을 사용한다는 점을 기억하자.
OrderItem
package jpabook.jpashop.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class OrderItem {
@Id @GeneratedValue
@Column(name = "ORDER_ITEM_ID")
private Long id;
@Column(name = "ORDER_ID")
private Long orderId;
@Column(name = "ITEM_ID")
private Long itemId;
private int orderPrice;
private int count;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getOrderId() {
return orderId;
}
public void setOrderId(Long orderId) {
this.orderId = orderId;
}
public Long getItemId() {
return itemId;
}
public void setItemId(Long itemId) {
this.itemId = itemId;
}
public int getOrderPrice() {
return orderPrice;
}
public void setOrderPrice(int orderPrice) {
this.orderPrice = orderPrice;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}
OrderStatus
package jpabook.jpashop.domain;
public enum OrderStatus {
ORDER, CANCEL
}
JpaMain
package jpabook.jpashop;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
테이블 설계대로 엔티티를 매핑해서 만들어보았다. 하지만, RDB의 패러다임대로 따르다보니 다음과 같이 객체지향적이지 못한 문제점이 발생한다.
Order order = em.find(Order.class, 1L);
Long memberId = order.getMemberId();
Member member = em.find(Member.class, memberId);
- 주문한 회원을 조회하기 위해 주문 테이블에서 객체를 만들고, 그 객체에 해당하는 아이디 값을 다시 회원 테이블에서 조회해야한다. 즉, 객체 그래프 탐색이 불가능하므로, 객체지향관점에 맞지않다.
Member member = order.getMember();
- 다음과 같이 주문 테이블에서 멤버객체를 바로 꺼낼 수 있도록 설계해야한다. 즉, 주문 테이블에서 회원의 아이디 타입이
Long
이 아니라Member
타입을 가져야한다.
이 문제를 객체지향관점에서 해결하기 위한 연관관계 매핑에 대해 다음 포스팅에서 알아보자.
본 포스팅은 인프런 김영한님 강의(자바 ORM 표준 JPA 프로그래밍 - 기본편)를 토대로 정리한 내용입니다.
반응형