객체-테이블 매핑: @Entity, @Table
필드-컬럼 매핑: @Column
기본키 매핑: @Id
연관관계 매핑: @ManyToOne,@JoinColumn => 다음 장
객체와 테이블 매핑
- @Entity
=> JPA를 사용해 테이블과 매핑할 클래스 / JPA의 관리 받음
기본 생성자 필수 (public 또는 protected)
final 클래스, enum, interface, inner 클래스 사용 X
저장할 필드에 final 사용 X
- 속성 (name) : JPA에서 사용할 엔티티 이름 지정
(기본값) 클래스 이름 그대로 사용 ex. Member
같은 클래스 이름이 없으면 가급적 기본값 사용
- @Table : 엔티티와 매핑할 테이블 지정
속성 | 기능 | 기본값 |
name | 매핑할 테이블명 | 엔티티 이름 |
catalog | DB catalog 매핑 | |
schema | DB schema 매핑 | |
uniqueConstraints(DDL) | DDL 생성시 유니크 제약조건 생성 |
데이터베이스 스키마 자동 생성
애플리케이션 실행 시점에 DDL을 자동 생성
테이블 중심 → 객체 중심
데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL 생성
이렇게 생성된 DDL은 개발 장비에서만 사용 (운영서버에서는 사용하지 않거나, 다듬은 후 사용)
- 속성 (hibernate.hbm2ddl.auto)
옵션 | 설명 |
create | 기존 테이블 삭제 후 다시 생성 (DROP + CREATE) |
create-drop | create와 같으나 종료 시점에 테이블 DROP |
update | 변경분만 반영 (운영DB에는 사용하면 안됨) |
validate | 엔티티-테이블이 정상 매핑되었는지만 확인 |
none | 사용하지 않음 |
- 주의
운영 장비 => 절대 create, create-drop, update 사용하면 안됨
개발 초기 단계 => create 또는 update
테스트 서버 => update 또는 validate (create 사용시 데이터 삭제됨)
스테이징, 운영 서버 => validate 또는 none
- DDL 생성 기능
1. 제약조건 추가
@Column(nullable = false, length = 10) //회원 이름은 필수, 10자 초과 X
2. 유니크 제약조건 추가
@Table(uniqueConstraints = {@UniqueConstraint( name = "NAME_AGE_UNIQUE", columnNames = {"NAME", "AGE"} )})
@Column(unique=true)
=> DDL 자동 생성에만 영향 O JPA의 실행 로직에는 영향 X
필드 - 컬럼 매핑
- 요구사항 추가
회원은 일반 회원과 관리자로 구분
+ 회원 가입일과 수정일 필드
+ 회원을 설명할 수 있는 필드 (길이 제한 X)
- Member
package hellojpa;
import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
@Entity
public class Member {
@Id
private Long id;
@Column(name = "name") //db 컬럼명은 name
private String username;
private Integer age;
@Enumerated(EnumType.STRING) //enum
private RoleType roleType;
@Temporal(TemporalType.TIMESTAMP) //날짜 타입 : DATE, TIME, TIMESTAMP
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
@Lob
private String description;
public Member(){}
}
- RoleType (enum)
package hellojpa;
public enum RoleType {
USER, ADMIN
}
- 매핑 어노테이션
- @Column : 컬럼 매핑
속성 | 설명 | 기본값 |
name | 필드와 매핑할 테이블의 컬럼 이름 | 객체 필드명 |
insertable, updatable | 등록, 변경 가능 여부 | TRUE |
nullable (DDL) | null 값 허용 여부 설정 false : DDL 생성시 not null 제약조건 생김 |
TRUE |
unique (DDL) | 한 컬럼에만 간단히 유니크 제약조건 검 => 제약조건명 랜덤 생성 (▲) cf. @Table의 uniqueConstraints |
|
columnDefinition (DDL) | 데이터베이스 컬럼 정보를 직접 줌 ex) varchar(100) default ‘EMPTY' |
필드의 자바타입, 방언 정보 사용 |
length (DDL) | 문자 길이 제약조건 String 타입에만 사용 |
255 |
precision, scale (DDL) | BigDecimal 타입에서 사용 +BigInteger precision: 소수점 포함한 전체 자릿수 scale: 소수의 자릿수 (double, float 타입에는 적용 X) 아주 큰 숫자, 정밀한 소수 다룰 때 사용 |
precision=19, scale=2 |
- @Temporal : 날짜타입 매핑
날짜 타입(java.util.Date, java.util.Calendar) 매핑할 때 사용
최근에는 하이버네이트 지원 => LocalDate, LocalDateTime 사용시 생략 가능
ex. private LocalDate date;
속성 | 설명 | 기본값 |
value | TemporalType.DATE : 날짜 DB의 date 타입과 매핑 ex. 2013–10–11 TemporalType.TIME : 시간 DB의 time 타입과 매핑 ex. 11:11:11 TemporalType.TIMESTAMP : 날짜.시간 DB의 timestamp 타입과 매핑 ex. 2013–10–11 11:11:11 |
- @Enumerated : enum타입 매핑
속성 | 설명 | 기본값 |
value | EnumType.ORDINAL : enum 순서를 DB에 저장 ex. 0 EnumType.STRING : enum 이름을 DB에 저장 ex. USER |
EnumType.ORDINAL |
ORDINAL => enum 값 추가 시 이전 값들과 혼동될 수 있으므로 쓰지 말기
- @Lob : BLOB, CLOB 매핑 (크기가 큰 문자열 / 바이트 정보)
지정할 수 있는 속성 無
매핑하는 필드 타입이 문자면 CLOB 매핑 / 나머지는 BLOB 매핑
- CLOB : String, char[], java.sql.CLOB
- BLOB : byte[], java.sql. BLOB
- @Transient : 특정 필드를 컬럼에 매핑 X (매핑 푸시)
데이터베이스에 저장 X, 조회 X
메모리상에서만 어떤 값을 임시 보관하고 싶을 때 사용
기본 키 매핑
@Id @GeneratedValue
- 기본키 매핑 방법
1. 직접 할당 : @Id 만 사용
2. 자동 생성 (@GeneratedValue) :
IDENTITY : 데이터베이스에 위임, MYSQL
SEQUENCE : 데이터베이스 시퀀스 오브젝트 사용, ORACLE => @SequenceGenerator 필요
TABLE : 키 생성용 테이블 사용, 모든 DB에서 사용 => @TableGenerator 필요
AUTO : (기본값) 방언에 따라 자동 지정
ex) @GeneratedValue(strategy=GenerationType.AUTO)
- IDENTITY 전략 => @GeneratedValue(strategy=GenerationType.IDENTITY)
기본키 생성을 데이터베이스에 위임
=> em.persist() 시점에 즉시 INSERT SQL 실행 (IDENTITY 사용시에만) → DB에서 식별자 조회
(IDENTITY 전략 사용시 db에 쿼리가 전송돼야 값 확인 가능하므로 해당 예외사항 가능하도록 돼있음)
∴ 모아서(buffering) INSERT 불가
주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용 ex) MySQL의 AUTO_ INCREMENT
AUTO_ INCREMENT => db에 INSERT SQL을 실행한 이후, ID 값을 알 수 있음
cf. JPA => 보통 트랜잭션 커밋 시점에 INSERT SQL 실행
- SEQUENCE 전략 => @GeneratedValue(strategy=GenerationType.SEQUENCE)
데이터베이스 시퀀스 => 유일한 값을 순서대로 생성하는 데이터베이스 오브젝트
오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용 ex) 오라클의 시퀀스
※ 자동 생성되는 id 데이터타입으로 Long 권장
@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;
※ @SequenceGenerator 속성
속성 | 설명 | 기본값 |
name | 식별자 생성기 이름 | 필수 |
sequenceName | db에 등록된 시퀀스 이름 | hibernate_sequence |
initialValue | DDL을 생성할 때 처음 시작할 숫자 (DDL 생성 시에만 사용) |
1 |
allocationSize | 시퀀스 한 번 호출에 증가하는 수 (성능 최적화에 사용) |
50 |
catalog, schema | db 내 catalog, schema 이름 |
※ allocationSize : 할당할 메모리 크기 => 성능 최적화
em.persist(member); // DB Sequence Call : 1, 51
//최초 persist() 시 2번 호출 => 1, 51 리턴 (시퀀스 시작값, 끝값)
em.persist(member); // Memory Call : 2
em.persist(member); // Memory Call : 3
em.persist(member); // Memory Call : 4
1. 최초 persist() 호출 => 데이터베이스의 시퀀스 nextValue를 두번 호출 → 시작값, 끝값 가져옴
2. 어플리케이션에서 시작값이 끝값이 될때까지 시퀀스를 메모리에서 할당해줌
3. 시퀀스를 끝값까지 전부 사용하면 다시 시퀀스 호출
=> jpa는 allocationSize로 다음 시작값 계산 (끝값 = 현재값 + allocationSize / 시작값 = 끝값 - (allocationSize - 1))
4. 2번, 3번 반복
=> 여러 Web Server 동시 호출 시에도 메모리에서 값을 가져와 사용하므로 동시성 문제 발생 X
※ DB의 시퀀스 증가값이 1이면 allocationSize도 1로 맞추기
- TABLE 전략 => @GeneratedValue(strategy=GenerationType.TABLE)
키 생성 전용 테이블 하나 생성, db 시퀀스를 흉내내는 전략
장점: 모든 데이터베이스에 적용 가능
단점: 성능
create table MY_SEQUENCES (
sequence_name varchar(255) not null,
next_val bigint,
primary key ( sequence_name )
)
@Entity
@TableGenerator(
name = "MEMBER_SEQ_GENERATOR", //tablegenerator명
table = "MY_SEQUENCES", //db 시퀀스명
pkColumnValue = “MEMBER_SEQ", //db 시퀀스명 컬럼 값
allocationSize = 1) //시퀀스 증가값
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "MEMBER_SEQ_GENERATOR") ////
private Long id;
...
}
※ 새로운 테이블 생성, 운영 상 부담스러움 => db마다 쓰는 전략 권장
- 권장하는 식별자 전략
기본키 제약 조건: null 아님, 유일, 불변
미래까지 이 조건을 만족하는 자연키는 찾기 어려움 => 대리키(대체키)를 사용
ex. 주민등록번호도 기본 키로 적절 X
권장: Long형 + 대체키 + 키 생성전략
실전 예제 - 1. 요구사항 분석과 기본 매핑
- 요구사항 분석
회원은 상품을 주문할 수 있다
주문 시 여러 종류의 상품을 선택할 수 있다
- 기능 목록
회원 기능 - 회원등록, 회원조회
상품 기능 - 상품등록, 상품수정, 상품조회
주문 기능 - 상품주문, 주문내역조회, 주문취소
- 도메인 모델 분석
회원-주문 관계: 회원은 여러 번 주문 가능 => 일대다
주문-상품 관계: 주문 시 여러 상품 선택 가능 & 한 상품도 여러 번 주문 가능
=> 주문상품 모델 추가 => 다대다 관계를 일다대, 다대일 관계로 풀어냄
- 테이블 설계
- 엔티티 테이블, 매핑
- 예제
제약조건, 인덱스 등 세부 설정도 클래스에 표시하는 것 권장 (작성 시 편리)
- 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/~/jpashop"/>
<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.jdbc.batch_size" value="10"/>
<property name="hibernate.hbm2ddl.auto" value="create" />
</properties>
</persistence-unit>
</persistence>
※ 정상 실행되는데 테이블이 생성이 안 되고 hibernate.properties not found 라는 메시지를 확인했다
property 중 create가 주석 처리된 게 원인이었음
- 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>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</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.200</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
</dependencies>
</project>
- Member
package jpabook.jpashop.domain;
import javax.persistence.*;
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.AUTO) //생략해도 AUTO
@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")
//예약어 오류(order by) 발생 가능하므로 orders로 테이블명 사용
public class Order {
@Id @GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@Column(name = "MEMBER_ID")
private Long memberId;
private LocalDateTime orderDate; //그대로 orderDate로 컬럼명 지정됨
// order_date로 변수명 설정하는 것이 편리할 수도
@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;
}
}
- OrderStatus (enum)
package jpabook.jpashop.domain;
public enum OrderStatus {
ORDER, CANCEL
}
- 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;
}
}
- JpaMain
package jpabook.jpashop;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.domain.Order;
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{
Order order = em.find(Order.class, 1L);
Long memberId = order.getMemberId();
Member member = em.find(Member.class, memberId);
//객체지향 X
tx.commit();
} catch (Exception e){
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
//
find 메소드 중복사용 대신 메인 메소드에서 바로 order.getMember(); 사용할 수 있도록 코드 작성해야 함
=> 연관관계 매핑 필요
//
create table Item (
ITEM_ID bigint not null,
name varchar(255),
price integer not null,
stockQuantity integer not null,
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 (
ORDER_ITEM_ID bigint not null,
count integer not null,
ITEM_ID bigint,
ORDER_ID bigint,
orderPrice integer not null,
primary key (ORDER_ITEM_ID)
)
Hibernate:
create table ORDERS (
ORDER_ID bigint not null,
MEMBER_ID bigint,
orderDate timestamp,
status varchar(255),
primary key (ORDER_ID)
)
- 데이터 중심 설계 문제점
현재 방식은 객체 설계를 테이블 설계에 맞춘 방식
테이블의 외래키를 객체에 그대로 가져옴
객체 그래프 탐색이 불가능
참조가 없으므로 UML도 잘못
'Programming > 스프링' 카테고리의 다른 글
[김영한 스프링 MVC 1] MVC 프레임워크 만들기 (0) | 2023.09.24 |
---|---|
[김영한 스프링 MVC 1] 서블릿, JSP, MVC 패턴 (0) | 2023.09.17 |
[김영한 스프링 MVC 1] 서블릿 (0) | 2023.08.13 |
[김영한 스프링 MVC 1] 웹 애플리케이션 이해 (0) | 2023.08.06 |
[스프링] 컨트롤러 파라미터 어노테이션 - 클라이언트 → 서버 (0) | 2023.08.05 |