소스
https://github.com/rkwhr0010/jpa
- 객체 테이블 매핑
@Entity, @Table - 기본 키 매핑
@Id - 필드 컬럼 매핑
@Column - 연관고나계 매핑
@ManyToOne(외 다수), @JoinColumn
매핑은 XML이나 애노테이션 둘 중 하나로 기술한다.
요즘은 거의 애노테이션만 사용하는 추세다.
@Entity
필수 어노테이션, 이 값을 기준으로 JPA가 관리할 엔티티를 식별한다.
필수 규칙
- protect 범위 이상 기본 생성자 필수
- final 클래스, enum, interface, inner 클래스에 사용 불가
- 저장 필드에 final 사용 불가
기본 생성자 규칙은 쉽게 실수 할 수 있는 부분이다.
@Entity public class Member { @Id @Column(name = "MEMBER_ID") private Long id; private String name; // 생성자가 아무 것도 없으면, 컴파일러가 기본 생성자를 생성해준다. // 이렇게 생성자를 만들면, 컴파일러는 기본 생성자를 안만들어 준다. public Member(Long id, String name, Team team) { this.id = id; this.name = name; this.team = team; } |
다른 곳에서도 기본 생성자는 필수 있 때가 있다. 무조건 기본 생성자는 만들어도 나쁠 건 없다.
@Table
엔티티와 테이블을 매핑할 때 사용한다.
생략이 가능하다. 생략 시 @Entity 붙은 클래스 이름과 같은 테이블에 매핑한다.
속성 | 기능 | 기본값 |
name | 매핑할 테이블 이름 | 엔티티 이름 |
catalog | catalog 있는 DB에서 catalog 매핑 | |
schema | schema 있는 DB에서 schema 매핑 | |
uniqueConstraints | 유니크 제약조건을 추가한다. 자동 DDL 생성에서 만 사용한다. 실제 실무에선 Table을 직접 만드므로 사용할 일이 없다. |
매핑 정보
import java.util.Date; import jakarta.persistence.*; @Entity @Table(name = "MEMBER") public class Member { @Id @Column(name = "ID") private String id; @Column(name = "NAME") private String username; private Integer age; @Enumerated(EnumType.STRING) private RoleType roleType; @Temporal(TemporalType.TIMESTAMP) private Date createdDate; private Date lastModifiedDate; @Lob private String description; 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; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public RoleType getRoleType() { return roleType; } public void setRoleType(RoleType roleType) { this.roleType = roleType; } public Date getCreatedDate() { return createdDate; } public void setCreatedDate(Date createdDate) { this.createdDate = createdDate; } public Date getLastModifiedDate() { return lastModifiedDate; } public void setLastModifiedDate(Date lastModifiedDate) { this.lastModifiedDate = lastModifiedDate; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } } |
public enum RoleType { ADMIN, USER } |
DB 스키마 자동 생성
persistence.xml 설정 정보
<?xml version="1.0" encoding="utf-8"?> <persistence xmlns="https://jakarta.ee/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd" version="3.0"> <persistence-unit name="studyjpa" transaction-type="RESOURCE_LOCAL"> <properties> <!-- DB 연결 정보 --> <property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver" /> <property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:55555/jpa?characterEncoding=utf8" /> <property name="jakarta.persistence.jdbc.user" value="root" /> <property name="jakarta.persistence.jdbc.password" value="root" /> <property name="hibernate.hikari.poolName" value="pool" /> <property name="hibernate.hikari.maximumPoolSize" value="10" /> <property name="hibernate.hikari.minimumIdle" value="10" /> <property name="hibernate.hikari.connectionTimeout" value="1000" /> <!-- MySql 방언 --> <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect" /> <!-- SQL 출력 --> <!-- <property name="hibernate.show_sql" value="true" />--> <!-- SQL 이쁘게 출력 --> <property name="hibernate.format_sql" value="true" /> <!-- 주석도 함께 출력 --> <property name="hibernate.use_sql_comments" value="true" /> <!-- JPA 표준에 맞춘 새로운 키 생성 전략 사용 --> <property name="hibernate.id.new_generator_mappings" value="true" /> <!-- 실습에서만 사용할 것, @Entity에 따라 DDL 명령을 자동 실행해 준다. --> <property name="hibernate.hbm2ddl.auto" value="create" /> </properties> </persistence-unit> </persistence> |
hibernate.hbm2ddl.auto 이 옵션을 키면, @Entity 정보를 기반으로 DDL 을 생성한다.
DDL 은 hibernate.dialect 속성에 설정한 방언에 따라 그 DB에 맞게 생성된다.
DDL 자동 생성 기능은 개인적으로 학습할 때나, 신규 기능으로 테이블은 만들기 전에 테스트 용도로만 사용해야 한다.
잘못하면, 개발DB 운영 DB를 전부 날릴 수도 있다.
옵션 | 설명 |
create | 기존 테이블 삭제, 새로 생성 |
create-drop | create 기능 + 종료 시 생성된 DDL 제거 |
update | DB 테이블과 엔티티 비교 후 변경 분만 수정 |
validate | DB 테이블과 엔티티를 비교 후 다르면, 실행을 하지 않는다 |
none | 사용 안함, 속성 값을 지우는 것과 동일 |
이름 매핑 전략 변경
애플리케이션은 일반적으로 카멜케이스 표기법을 DB는 언더스코어 표기법을 많이 사용한다. 이를 자동으로 매핑해주는 속성이 있다.
과거 org.hibernate.cfg.NamingStrategy 속성으로 엔티티와 테이블 간 이름 매핑 전략을 설정했다. 현재는
hibernate.implicit_naming_strategy, hibernate.physical_naming_strategy 로 분리됐다.
implicit 애플리케이션 쪽, physical DB 쪽
분리가 됐다는 것은 과서 org.hibernate.cfg.NamingStrategy 설정이 두 개의 책임을 가졌다는 것을 의미한다. 현재는 제거된 속성이다.
https://docs.jboss.org/hibernate/orm/5.0/userguide/html_single/Hibernate_User_Guide.html#naming
DDL 생성 기능
어디까지나 자동 DDL 생성 기능을 활용한 것이다.
절대 운영이나 개발에선 사용하면 안된다.
유니크 제약조건 생성하기
주의할 것은 @Column(name = "NAME", nullable = false, length = 10) 에서 nullable, length는 DDL 자동 생성 시 사용되는 정보지 JPA 실행 로직에는 영향을 주지 않는다.
DDL 자동 생성 기능은 그냥 실제 개발할 땐 사용하지말고 개인적인 학습 용도로 사용하는 것이 좋다.
기본 키 매핑
- 직접할당
- 자동할당
자동 생성 방식이 많은 이유는 DB마다 키 생성 방식이 다르기 때문이다.
오라클 시퀀스, Mysql AUTO_INCREMENT 등…
SEQUENCE, IDENTITY는 DB에 의존적인 옵션이다
자동 키 생성 전략을 위해선 persistence.xml 파일에 아래와 같은 설정을 반드시 해야 한다.
키가 될 수 있는 타입
@Id
- 기본형
- 기본형의 래퍼 클래스
- String
- java.util.Date
- java.sql.Date
- java.math.BigDecimal
- java.math.BigInteger
2023.11 현재도 그대로다.
알 건 없지만, mysql 기준 enum도 된다.
IDENTITY 전략
키 생성을 DB에게 위임
Mysql
Oracle
이 전략은 키를 생성하기 위해 무조건 DB에 질의를 해야한다.
즉, 쓰기 지연 SQL에 저장되지 않고, 곧 바로 DB로 INSERT를 한다.
영속성 컨텍스트에 저장하기 위해선 무조건 식별자가 필수라는 것을 상기하자
SEQUENCE 전략
시퀀스를 사용하여 기본 키를 생성
시퀀스를 지원하는 DB에서 사용한다.
실습 중인 Mysql 은 시퀀스를 지원 안해 테이블이 만들어진다.
오라클로 테스트
persistence.xml
<?xml version="1.0" encoding="utf-8"?> <persistence xmlns="https://jakarta.ee/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd" version="3.0"> <persistence-unit name="studyjpa" transaction-type="RESOURCE_LOCAL"> <properties> <!-- DB 연결 정보 --> <property name="jakarta.persistence.jdbc.driver" value="oracle.jdbc.OracleDriver" /> <property name="jakarta.persistence.jdbc.url" value="jdbc:oracle:thin:@localhost:54321:XE" /> <property name="jakarta.persistence.jdbc.user" value="scott" /> <property name="jakarta.persistence.jdbc.password" value="tiger" /> <property name="hibernate.hikari.poolName" value="pool" /> <property name="hibernate.hikari.maximumPoolSize" value="10" /> <property name="hibernate.hikari.minimumIdle" value="10" /> <property name="hibernate.hikari.connectionTimeout" value="1000" /> <!-- Oracle 방언 --> <property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect" /> <!-- SQL 출력 --> <!-- <property name="hibernate.show_sql" value="true" />--> <!-- SQL 이쁘게 출력 --> <property name="hibernate.format_sql" value="true" /> <!-- 주석도 함께 출력 --> <property name="hibernate.use_sql_comments" value="true" /> <!-- JPA 표준에 맞춘 새로운 키 생성 전략 사용 --> <property name="hibernate.id.new_generator_mappings" value="true" /> <!-- 실습에서만 사용할 것, @Entity에 따라 DDL 명령을 자동 실행해 준다. --> <property name="hibernate.hbm2ddl.auto" value="create" /> <!-- <property name="hibernate.implicit_naming_strategy" value="jpa" />--> <!-- <property name="hibernate.physical_naming_strategy" --> <!-- value="org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy" />--> </properties> </persistence-unit> </persistence> |
오라클은 시퀀스를 지원해 생성된다.
다시 돌아와서 시퀀스 전략의 문제는 또 다시 영속성 컨텍스트 식별자 필수 제약과 연관있다.
영속성 컨텍스트에 저장되기 위해선 무조건 시퀀스가 있어야 한다.
따라서 영속성 컨텍스트에 저장하기 위한 시퀀스 조회를 수행하게 된다.
em.persist() 호출 시점에 시퀀스 조회
영속성 컨텍스트에 저장 후 INSERT 쿼리는 쓰기 지연 SQL에 저장된다.
이후 플러시 시점에 INSERT 쿼리가 실행된다.
결과적으로 DB와 두 번 통신하게 된다. 네트워크 통신은 가장 큰 병목 시점이다.
또 다른 문제로 시퀀스가 50씩 증가하는 증상은 아래와 같은 설정으로 변경할 수 있다.
명시적으로 @SequenceGenerator 어노테이션으로 시퀀스 생성기를 등록하고, @GeneratedValue.generator 속성으로 시퀀스 생성기를 선택한다.
@SequenceGenerator 속성
속성 | 기능 | 기본값 |
name | 시퀀스 생성기 이름 | 필수 |
sequenceName | DB 시퀀스 이름 | |
initialValue | 최초 생성 시 시작 값, 따라서 DDL 자동 생성 사용 시 사용하는 속성 | 1 |
allocationSize | 시퀀스 한 번 호출 시 증가하는 값 | 50 |
catalog | 카탈로그 지원하는 DB 카탈로그 | |
schema | 스키마 지원하는 DB 스키마 |
Caused by: org.hibernate.MappingException: The increment size of the [MEMBER_SEQ] sequence is set to [20] in the entity mapping while the associated database sequence increment size is [1]. at org.hibernate.id.enhanced.SequenceStyleGenerator.configure(SequenceStyleGenerator.java:218) at org.hibernate.id.factory.internal.StandardIdentifierGeneratorFactory.createIdentifierGenerator(StandardIdentifierGeneratorFactory.java:217) ... 19 more |
DDL 자동 생성 기능을 꺼도, allocationSize는 사용되는 유효한 값이다.
allocationSize 기본 값이 50 인 이유
시퀀스 전략은 INSERT 시 DB와 두 번 통신한다고 했다.
이를 줄이기 위한 최적화 방법으로 시퀀스는 50씩 증가시킨다.
한 번 시퀀스를 조회하고, 다음 번 시퀀스 증가 크기 만큼 메모리에서 시퀀스를 처리한다.
즉, 50 번 INSERT 마다 시퀀스를 한 번 조회한다.
Member member = null; for (int i = 0; i < 50; i++) { member = new Member(); member.setUsername("홍길동" + i); em.persist(member); } |
한 번의 시퀀스 조회로 50 까지 식별자를 메모리에서 할당한 것을 볼 수 있다.
보통은 allocationSize 값을 1로 주고 쓴다.
테이블 전략
키 생성 전용 테이블을 만들고 시퀀스를 흉내낸다.
테이블 전략도 allocationSize 속성 값으로 최적화를 진행할 수 있다.
로우 하나가 하나의 시퀀스 오브젝트를 흉내낸다.
@TableGenerator
속성 | 기능 | 기본값 |
name | 테이블 식별자 생성기 이름 | 필수 |
table | DB 테이블 명 | |
pkColumnName | 시퀀스 컬럼명, 시퀀스 오브젝트 이름이라고 봐도 된다. | |
valueColumnValue | 시퀀스 값 컬럼명 | |
initialValue | 초기 값 | 0 |
allocationSize | 한 번 호출 시 증가값 | 50 |
catalog, schema | ||
uniqueConstraints | 유니크 제약 조건 지정(테이블 이라 가능한 속성) |
AUTO 전략
선택한 DB 방언에 따라 TABLE, SEQUENCE, IDENTITY 중 하나를 자동으로 선택
참고로 기본 값은 AUTO다
참고
기본 키 매핑 정리
직접 할당 | DB 통신은 플러시 시점 한 번만 이뤄 진다. |
SEQUENCE | 시퀀스 채 번을 위해 persist() 시점에 DB 통신, 플러시 시점에 DB 통신 |
TABLE | 시퀀스와 똑같은 이유로 DB와 두 번 통신 |
IDENTITY | DB에게 위임 Mysql의 경우 무조건 데이터 저장시 키가 생성되기에 persit() 시점에 INSERT 가 이뤄진다. |
기본 키 조건
- null 불가
- 유일성
- 변하면 안됨
기본 키 선정 전략
- 자연 키
비즈니스 데이터를 사용한 키 - 대리 키
비즈니스 데이터와 관계 없는 임의 값
자동 생성 전략은 전부 대리키다.
될 수 있으면 대리 키 사용을 권장, 비즈니스는 언젠간 변하게 되어있다.
반면에 대리 키는 변할 여지가 없다.
대리 키를 기본 키로 잡고, 자연 키 후보들로 유니크 인덱스를 잡아서 사용하는 것을 권장한다.
필드와 컬럼 매핑
@Column | 컬럼 매핑 |
@Enumerated | 자바 열거형 매핑 |
@Temporal | 날짜 타입 매핑 |
@Lob | BLOB, CLOB 매핑 |
@Transient | 매핑 무시할 필드 |
@Column
속성 | 기능 | 기본값 |
name | 컬럼 매핑 | 필드 이름 |
insertable | 필드 저장 가능 여부, 읽기 전용으로 만들 때 가끔 사용 | true |
updatable | 필드 수정 가능 여부, 읽기 전용으로 만들 때 가끔 사용 | true |
table | 하나의 엔티티를 두 개 이상 테이블에 매핑할 때 사용 | 현재 클래스와 매핑된 테이블 |
nulable | DDL 생성 시 null 가능 여부 | true |
unique | DDL 생성 시 해당 컬럼의 유니크 제약 조건 설정 2개 이상 복합 유니크 제약은 클래스 레벨에서 @Table uniqueConstaints 를 사용해야 한다. 물론 하나도 가능 |
false |
length | DDL 생성 시 길이 제한, String 타입만 가능하다 | 255 |
precision | DDL 생성 시 BigDecimal, BigInteger 에서 사용, 소수점 포함 전체 자리수 | 38 |
scale | DDL 생성 시 BigDecimal에서 사용, 소수점 | 2 |
기본 값 참고
자바 기본형은 전부 자동으로 not null이 붙는다.
굳이 null이 가능하게 설정할 수도 있다.
하지만, 자바 단에서 기본값이 존재하기 때문에 DB에 null이 절대로 들어갈 수 없다.
이런 불일치 위험 때문에 기본형은 nullable = false 를 명시적으로 주는 것이 안전하다
아니면 기본형의 래퍼 클래스를 사용하도록 한다.
@Enumerated
자바 enum 열거형을 매핑할 때 사용한다.
기본 값이 ORDINAL 로 되어 있다.
무조건 STRING으로 사용한다.
이유는 간단하다. ORDINAL 는 말그대로 순서로 순서에 의존하게 된다.
의존성을 없앨 수 있는 선택지가 있는데 의존성을 굳이 가져가는 것은 좋지 않다.
@Temporal
날짜 타입을 매핑하기 위해 사용한다.
java.sql.Date는 년월일 정보만 담을 수 있다.
java.sql.Time은 시분초 정보만 담을 수 있다.
java.sql.Timestamp는 년월일시분초
꼭 해당 타입으로 매핑은 안 하더라도, 위 설정된 정보 만큼만 담긴다.
java.sql.* 패키지의 날짜 타입만 빼고, 어느 것이든 자기가 있는 사이트에서 사용하는 것을 사용하도록 한다.
@Temporal 생략해도 기본 동작이 "년월일시분초"라는 것만 알고 있으면 된다.
@Lob
DB 의 BLOB, CLOB 에 매핑
- CLOB
String, char[], java.sql.CLOB - BLOB
byte[], java.sql.BLOB
@Transient
매핑을 무시할 필드를 선정한다.
@Access
JPA가 엔티티 데이터의 접근하는 방식을 지정한다.
필드 접근 방식은 필드가 private 이여도 상관 없다.
@Access 가 명시적으로 없으면, @Id 위치로 접근 방식이 결정된다.
@Id가 필드에 있으므로, 필드 전략을 사용
서로 다른 전략을 사용할 수도 있다.
실전 예제
코드만 기술
@Entity @Data public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String name; private String city; private String strret; private String zipcode; } |
@Entity @Table(name = "ORDERS") @Data 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 @Table(name = "ORDER_ITEM") @Data public class OrderItem { @Id @GeneratedValue @Column(name = "ORDER_ITEM_ID") private Long id; @Column(name = "ITEM_ID") private Long itemId; @Column(name = "ORDER_ID") private Long orderId; private int orderPrice; private int count; } |
@Entity @Data public class Item { @Id @GeneratedValue @Column(name = "ITEM_ID") private Long id; private String name; private int price; private int stockQuantity; } |
객체지향설계는 객체가 맡은 역할과 책임을 가지고, 객체 간 참조를 통해 협업하도록 설계한다.
위 예제 코드는 객체 설계를 테이블에 맞춘 방법이다. 외래 키 부분이 그대로 데이터 타입인 것을 볼 수 있다. 테이블은 문제가 안되는 게 데이터로 조인을 하면 된다. 하지만 객체는 조인이 불가능하다. 연관된 객체의 참조가 있어야 한다.
'개발 > JPA' 카테고리의 다른 글
06 다양한 연관관계 매핑 (0) | 2024.04.14 |
---|---|
05 연관관계 매핑 기초 (1) | 2024.03.22 |
03 영속성 관리 (1) | 2024.02.19 |
02 JPA 소개 (0) | 2023.12.01 |
01 JPA 환경 설정 (0) | 2023.11.23 |