엔티티 매핑엔 다음과 같은 어노테이션이 사용된다.
객체와 테이블 매핑 : @Entity, @Table
필드와 컬럼 매핑 : @Column
기본 키 매핑 : @Id
연관관계 매핑 : @ManyToOne, @JoinColumn
객체와 테이블 매핑
@Entity
- @Entity가 붙은 클래스는 JPA가 관리하고, 엔티티라 한다.
- JPA를 사용해서 테이블과 매핑할 클래스는 @Entity가 필수이다.
- 주의
- 기본 생성자 필수(파라미터가 없는 public 또는 protected 생성자)
- final 클래스, enum, interface, inner 클래스는 사용할 수 없다.
- 저장할 필드에 final은 사용할 수 없다.
속성 : name
- JPA에서 사용할 엔티티 이름을 지정한다.
- 기본값 : 클래스 이름을 그대로 사용
- 같은 클래스 이름이 없으면 가급적 기본값을 사용하자.
@Table
@Table은 엔티티와 매핑할 테이블을 지정한다.
name : 매핑할 테이블 이름을 지정한다.
데이터베이스 스키마 자동 생성
JPA는 애플리케이션 로딩 시점에 DB테이블을 생성하는 것도 지원해준다. 이 자동 생성은 다음과 같은 이점을 지니고 있는데
1. 테이블 중심 설계를 객체 중심 설계로 바꿀 수 있다.
2. 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL을 생성한다.
이렇게 생성된 DDL은 개발 장비에서만 사용하도록 하자. 배포하기엔 적합하지 않다.
운영서버에서는 사용하지 않거나, 적절히 다듬은 후 사용하도록 하자
persistence.xml을 보면 다음과 같은 property가 있다.
<property name="hibernate.hbm2ddl.auto" value="create" />
create 상황일때 실행을 해보면
Hibernate:
drop table if exists Member
라는 결과가 나온다. 즉 기존 테이블을 삭제 후 다시 생성하는 것이다.
이런 property의 속성은 다음과 같은 것들이 있다.
create : 기존 테이블 삭제 후 다시 생성(DROP + CREATE)
create-drop : create와 같으나 종료 시점에 테이블 drop
update : 변경분만 반영(운영DB에는 사용하면 안됨)
validate : 엔티티와 테이블이 정상 매핑되었는지만 확인
none : 사용하지 않음
** 사용시 주의할 점 **
- 운영 장비에는 절대 create, create-drop, update를 사용하면 안된다.
- 개발 초기 단계는 create 또는 update
- 테스트 서버는 update 또는 validate
- 스테이징과 운영 서버는 validate 또는 none
으로 사용하도록 하자.
DDL 생성 기능
JPA의 실행로직, 런타임에 영향을 주지않으며, entity에 제약 조건을 추가할 수 있다.
@Column(nullable = false, length = 10)
private String username;
위처럼 컬럼에 회원 이름은 필수, 10자리 초과는 불가하다는 제약조건을 추가한다면
create table Member (
id bigint not null,
username varchar(10) not null,
primary key (id)
)
Member 테이블을 생성할 때 not null 제약조건과 크기 제약이 추가되게 된다.
필드와 컬럼 매핑
지금 만들어 놓은 Member에 요구사항을 추가해보자. 요구사항은 다음과 같다.
1. 회원은 일반 회원과 관리자로 구분해야 한다.
2. 회원 가입일과 수정일이 있어야 한다.
3. 회원을 설명할 수 있는 필드가 있어야 한다. 이 필드는 길이 제한이 없다.
public enum RoleType {
USER, ADMIN
}
@Entity
public class Member {
@Id // PK를 매핑
private Long id;
@Column(name = "name") //객체는 username이지만 DB에는 name으로 사용
private String username;
private Integer age; //Integer같은 타입도 사용 가능
@Enumerated(EnumType.STRING) //DB에는 enum타입이 없기에 Enumerated를 사용
private RoleType roleType;
@Temporal(TemporalType.TIMESTAMP) //Type은 세가지
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
@Lob //varchar를 넘어서는 큰 값 넣을때 LOB 사용
private String description;
public Member() {
}
}
Hibernate:
create table Member (
age integer,
createdDate timestamp(6),
id bigint not null,
lastModifiedDate timestamp(6),
name varchar(255),
roleType varchar(255) check (roleType in ('USER','ADMIN')),
description clob,
primary key (id)
)
실행시켜보면 위와 같은 결과가 도출된다.
매핑 어노테이션 정리
@Column : 컬럼 매핑
@Temporal : 날짜 타입 매핑
@Enumerated : enum 타입 매핑
@Lob : BLOB, CLOB 매핑
@Transient : 매핑을 하지 않을때 사용(DB랑 관계없이 구성하여 사용하고 싶을떄)
@Column
- name : 필드와 매핑할 테이블의 컬럼 이름
- insertable, updatable : 등록, 변경 가능 여부(기본값은 TRUE)
- nullable(DDL) : null 값의 허용 여부를 설정한다. false로 설정하면 DDL 생성 시 not null 제약 조건이 붙는다.
- unique(DDL) : @Table의 uniqueConstraints와 같지만 한 컬럼에 간단히 유니크 제약조건을 걸 때 사용한다.
- ColumnDefinition(DDL) : 데이터베이스 컬럼 정보를 직접 줄 수 있다.
- length(DDL) : 문자 길이 제약 조건, String타입에만 사용한다.
- precision, scale(DDL) : BigDecimal 타입에서 사용한다.
Enum 타입 사용 시 주의사항
Enum타입의 기본은 ORDINAL이다.
public @interface Enumerated {
EnumType value() default EnumType.ORDINAL;
}
Enum타입은 ORDINAL을 사용하지 말자
ORDINAL은 ENUM을 숫자순서로 DB에 입력하게 된다.
만약 enum타입이 변경되게 된다면
public enum RoleType {
GUEST, USER, ADMIN
}
DB에 저장이 꼬이게 된다. 따라서 ORDINAL이 아닌 STRING을 사용하여 enum에 문자 그대로 들어가게 하자.
AGE CREATEDDATE ID LASTMODIFIEDDATE NAME ROLETYPE DESCRIPTION
null | null | 2 | null | D | ADMIN | null |
null | null | 10 | null | C | USER | null |
기본 키 매핑
키본 키 매핑 어노테이션은 다음과 같다.
- @Id
- @GeneratedValue
@Entity
public class Member {
@Id // PK를 매핑
private String id;
@Column(name = "name") //객체는 username이지만 DB에는 name으로 사용
private String username;
public Member() {
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
멤버를 다음과 같이 설정하였다
Member member = new Member();
member.setId("ID_A");
member.setUsername("A");
// em.persist(member);
em.persist(member);
tx.commit();
이 상태로 실행을 하게 된다면 다음과 같은 DB 값을 확인할 수 있을 것이다.
ID NAME
ID_A | A |
@ID는 직접 할당하는 방식이고,
@GeneratedValue는 자동으로 할당해주는 방법이다.
@GeneratedValue
@GeneratedValue(strategy = GenerationType.IDENTITY)
GenerationType의 IDENTITY는 기본 키 생성을 데이터베이스에 위임한다.
주로 MySQL, SQL Server, DB2 등에서 사용한다.
@GeneratedValue(strategy = GenerationType.SEQUENCE)
SEQUENCE는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트이다.
오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용한다.
@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;
TABLE 전략은 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략이다. 모든 데이터베이스에 적용 가능하나, 성능이 약간 떨어지는 단점이 있다.
권장하는 식별자 전략
- 기본 키 제약 조건 : not null이어야 한다, 유일해야 한다, 변하면 안된다.
- 미래까지 이 조건을 만족하는 자연키는 찾기 어렵다. 대리키(대체키)를 사용하자.
- 예를 들어 주민등록번호도 기본 키로 적절하지 않다.
- 권장 : Long형 + 대체키 + 키 생성전략 사용
IDENTITY전략의 특징은
PK로 잡은 ID 값이 null인 상태에서 DB에 입력될 때 PK값이 들어간다는 점이다.
이렇게 되면 persist, commit에서 문제가 발생한다.
try {
Member member = new Member();
// member.setId(1L);
member.setUsername("B");
// em.persist(member);
System.out.println("===============");
em.persist(member);
System.out.println("===============");
tx.commit();
IDENTITY전략이기에 ID를 입력하지 않고, username만 B로 준 상태이다.
만약 기존 동작 방식과 동일하다면, commit하는 시점에의 ID값이 null이기 때문에 문제가 발생한다.
따라서 persist하는 시점에 즉시 insert SQL을 실행하고, DB에서 식별자를 조회한다.
만약 SEQUENCE나 다른 전략을 사용한다면 commit하는 시점에 쿼리가 날아가게 된다
SEQUENCE 전략의 특징은
실전 예제 - 1. 요구사항 분석과 기본 매핑
엔티티 매핑을 복잡한 예제를 통해 매핑해보자.
요구사항 분석
- 회원은 상품을 주문할 수 있다.
- 주문 시 여러 종류의 상품을 선택할 수 있다.
기능 목록
- 회원 기능
- 회원 등록
- 회원 조회
-상품 기능
- 상품 등록
- 상품 수정
- 상품 조회
- 주문 기능
- 상품 주문
- 주문 내역 조회
- 주문 취소
도메인 모델 분석
- 회원과 주문의 관계 : 회원은 여러 번 주문할 수 있다.(1:N)
- 주문과 상품의 관계 : 주문할 떄 여러 상품을 선택할 수 있다. 반대로 같은 상품도 여러 번 주문 될 수 있다. 주문 상품이라는 모델을 만들어서, 다대다 관걔를 일대다, 다대일 관계로 풀어낸다.
테이블로 설계하면 다음과 같다.
엔티티로 설계한다면 다음과 같이 설계될 것이다.
DB는 언더스코어("_")가 관례로 굳어져있고
자바는 낙타표기법이 관례이다.
그렇기에 애매한 것들은 @Column을 통해 직접 매핑해주는 것이 좋다.
위 설계를 바탕으로 코드를 만들어본다
Member
@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;
}
}
id의 DB 컬럼 명이 MEMBER_ID이므로 @Column(name = "MEMBER_ID")로 주었다.
ORDERS
@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;
private Member member;
public Member getMember() {
return member;
}
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;
}
}
public enum OrderStatus {
ORDER, CANCEL
}
ORDER는 특이하게 ORDER를 그대로 쓰지 않고 ORDERS로 표기하는데 SQL의 "ORDER BY"가 있기 때문에 꼬이지 않게 만들기 위해 테이블 명을 ORDERS로 하였다.
Enum 타입은 앞서 봤던 것처럼 STRING을 주었다. ORDINAL로 주면 DB가 개같이 꼬여버릴 수 있다.
ORDERITEM
@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;
}
}
ORDER_ITEM은 Order와 Item의 다대다 관계를 일대다, 다대일 관계로 풀어내기 위해 존재한다.
ITEM
@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;
}
}
마지막으로 ITEM이다.
그럼 테이블을 생성하기 위해 JpaMain 클래스를 만들어 실행해보자.
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();
}
}
Hibernate:
create table Item (
price integer not null,
stockQuantity integer not null,
ITEM_ID bigint not null,
name varchar(255),
primary key (ITEM_ID)
)
Hibernate:
create table Member (
MEMBER_ID bigint not null,
city varchar(255),
name varchar(255),
street varchar(255),
zipcode varchar(255),
primary key (MEMBER_ID)
)
Hibernate:
create table OrderItem (
count integer not null,
orderPrice integer not null,
ITEM_ID bigint,
ORDER_ID bigint,
ORDER_ITEM_ID bigint not null,
primary key (ORDER_ITEM_ID)
)
Hibernate:
create table ORDERS (
MEMBER_ID bigint,
ORDER_ID bigint not null,
orderDate timestamp(6),
member varbinary(255),
status varchar(255) check (status in ('ORDER','CANCEL')),
primary key (ORDER_ID)
)
만약 내가 order를 꺼내서 주문한 member를 찾고 싶다면 어떻게 해야할까?
Order order = em.find(Order.class, 1L);
Long memberId = order.getMemberId();
Member member = em.find(Member.class, memberId);
order를 꺼내고 주문한 사람의 Id를 꺼내고 id를 기반으로 member를 꺼내야 하는 복잡한 과정을 거치게 된다.
사실 이 코드는 객체 설계를 테이블 설계에 맞춘 방식이라 객체중심과는 적합하지 않다.
이 문제를 연관관계 매핑을 통해 해결해보도록 하자.
데이터 중심 설계의 문제점
- 현재 방식은 객체 설계를 테이블 설계에 맞춘 방식
- 테이블의 외래키를 객체에 그대로 가져옴
- 객체 그래프 탐색이 불가능
- 참조가 없으므로 UML도 잘못됨
-----> 해결 방법 : 연관관계 매핑을 통해 해결한다.
'공부 > JPA' 카테고리의 다른 글
다양한 연관관계 매핑 (0) | 2024.12.03 |
---|---|
연관관계 매핑 기초 (0) | 2024.12.02 |
JPA 영속성 관리 (1) | 2024.11.28 |
JPA 설정하기 (0) | 2024.11.26 |
JPA란? (0) | 2024.11.25 |