SQL mapper & ORM/JPA

4. 엔티티 매핑

wooweee 2023. 10. 2. 22:31
728x90

1. 엔티티 매핑

  1. 객체와 관계형 데이터베이스 매핑하기 : 실제 설계적인 측면 (정적 측면), 객체와 db를 어떻게 연결하는지
  2. 영속성 컨텍스트(entity Manager의 1차 cache라고 봐도 무방 - spring에선 약간 다르다.) : JPA 내부 동작 방식 및 메커니즘

 

1.1. 엔티티 매핑 개요

  • 객체외 테이블 매핑 : @Entity, @Table
  • 필드와 컬럼 매핑 : @Column
  • 기본 키 매핑 : @Id
  • 연관관계 매핑 : @ManyToOne, @JoinColumn

 

1.2. 데이터베이스 스키마 자동 생성

1.2.1 자동 생성

  • DDL을 애플리케이션 실행 시점에 자동 생성 - local에서 개발할 때 도움
  • 테이블 중심 설계 -> 객체 중심 설계 가능해짐
  • 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL 생성
  • 이렇게 생성된 DDL은 개발 장비에서만 사용
  • 운영서버에서 적절히 다듬은 후 사용할 수 있는데 그냥 하지 말 것

 

1.2.2. 속성

옵션 설명
create 기존 table 삭제 후 다시 생성
create-drop create와 같으나 tx 종료시점에 table drop, test 때 많이 사용
update 변경분만 반영(필드 추가시에만 적용, 필드제거에 적용되면 안됨)
validate 엔티티와 테이블이 정상 매핑되었는지만 확인 - 다를 시 에러 발생 (권장)
none 사용 안함 - 직접 DDL script 작성 (권장)

 

1.2.3. 주의

  • 개발 초기 단계 : create || update
  • 테스트 서버 : update || validate
  • 스테이징과 운영서버 : validate || none

  • (권장) 협동개발, test, 운영 모두 validate || none 해서 개발자가 직접 DDL SQL 스크립트를 작성 할 것
  • 완전 혼자 사용하는 개발 서버에서만 create, update 사용 할 것

<!-- persistence.xml -->

<!-- 데이터베이스 스키마 자동 생성 : 개발 장비에서만 사용 권장-->
<property name="hibernate.hbm2ddl.auto" value="create"/>
<property name="hibernate.hbm2ddl.auto" value="create-drop"/> 
<property name="hibernate.hbm2ddl.auto" value="update"/> 
<property name="hibernate.hbm2ddl.auto" value="validate"/>
<property name="hibernate.hbm2ddl.auto" value="none"/>

 

  • create 수행 예시

create 옵션 예시

 

1.2.4. DDL 생성 기능

  • nullable, length, unique 같은 제약 조건은 runtime에 영향을 주지 않는다.
  • DDL 생성 기능은 DDL을 자동 생성할 때만 사용되고 JPA의 실행 로직에는 영향을 주지 않는다.

 

@Column(nullable = false, length = 10)
@Column(unique = false, length = 10)

@Table(uniqueConstraints = {@UniqueConstraint(name = "NAME_AGE_UNIQUE", columnNames = {"NAME", "AGE"})})

// column의 nullable, unique 제약조건은 이름이 random하기 나와서 잘 사용안하고
// Table처럼 name을 지정할 수 있는 방식을 사용하는 것을 권장한다.

 

 

dialect h2 MySQL

 

2. 객체와 테이블 매핑

2.1. @Entity

  • 정의
    • @Entity가 붙은 클래스 == 엔티티 : JPA가 관리 (엔티티를 가지고 entityManger가 작업 수행)
    • JPA를 사용해서 테이블과 매핑할 클래스는 @Entity가 필수

 

  • 주의
    • 기본 생성자가 필수
    • final class, enum, interface, inner class는 @Entity 사용 불가
    • 저장할 필드에 final 사용 불가

 

  • 속성
    • name
      • JPA에서 사용할 엔티티 이름을 지정
      • 기본값은 클래스 이름을 그대로 사용
      • 같은 class 이름이 없으면 가급적 기본값을 사용

 

2.2. @Table

  • @Table은 엔티티와 매핑할 테이블 지정

 

  • 속성
속성 기능 default
name 매핑할 table 이름 엔티티 이름을 사용
catalog db catalog 매핑  
schema db schema 매핑  
uniqueConstraints (DDL) DDL 생성 시 유니크 제약조건 생성  

 

 

3.  필드와 컬럼 매핑

  • 일반 회원과 관리자로 구분 : enum 사용
  • 회원 가입일과 수정일 필요
  • 회원 설명 field 추가 - 길이 제한 없음 : @Lob 이용

 

  • Member Entity - 매핑 어노테이션 정리
어노테이션 설명
@Column 컬럼 매핑
@Temporal 날짜 타입 매핑 - date, time, timeStamp  cf> localDate(Time) 부터 사용 X
@Enumerated enum 타입 매핑
@Lob BLOB(string, char 빼고 다), CLOB (String, char 용) - 아주 큰 값 넣는 경우
@Transient 특정 필드를 컬럼에 매핑하지 않음(매핑 무시) - memory 상에서만 계산하고 싶은 경우

 

package hellojpa;

import javax.persistence.*;
import java.time.LocalDate; // java8
import java.time.LocalDateTime; // java8
import java.util.Date; // java8 이전

// version1 - @Id 기본키, 필드와 컬럼 맵핑
@Entity
public class Member {
    @Id
    private Long id;
    @Column(name = "name")
    private String username;
    private Integer age;
    @Enumerated(EnumType.STRING)
    private RoleType roleType;

    // java8 이전 사람들은 date 밖에 없어서 @Temporal로 구분해줘야함
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;

    // java8 이후 부터는 LocalDate, LocalDateTime으로 충분히 구분 가능해서 @Temporal 필요 없음
    private LocalDate testLocalDate;
    private LocalDateTime testLocalDateTime;

    @Lob
    private String description;
}

 

package hellojpa;

public enum RoleType { USER, ADMIN }

 

 

3.1. @Column (제일 중요)

속성 설명 기본값
name 필드와 매핑할 테이블의 컬럼 명 객체의 필드 이름
insertable, updatable 등록, 변경 가능 여부 컬럼을 수정, 첨가 했을 때 db에 insert, update 할지 말지 결정 true
nullable(DDL) null 값의 허용 여부를 설정한다. false로 설정하면 DDL 생성 시에 not null 제약조건이 붙는다.  
unique(DDL) @Table의 uniqueConstraints와 같지만 한 컬럼에 간단히 유니크 제약조건을 걸 때 사용
- 이름이 너무 지저분해서 사용 안함
 
columnDefinition (DDL)  데이터베이스 컬럼 정보를 직접 줄 수 있다. 
(특정 db에 종속적인 값도 넣을 수 있다.)
ex) varchar(100) default ‘EMPTY' 
필드의 자바 타입과 방언 정보를 사용
length(DDL) 문자 길이 제약 조건, String 타입에만 사용 255
precision, scale(DDL) BigDecimal 타입에서 사용한다(BigInteger도 사용할 수 있다). precision은 소수점을 포함한 전체 자 릿수를, scale은 소수의 자릿수 다. 참고로 double, float 타입에는 적용되지 않는다. 아주 큰 숫자나 정 밀한 소수를 다루어야 할 때만 사용한다. precision=19, scale=2

 

3.2. @Enumerated

  • java enum 타입을 매핑할 때 사용
  • ORDINAL 절대 사용 말 것
속성 설명 기본값
value EnumType.STRING : enum 이름을 데이터베이스에 저장 EnumType.ORDINAL : enum 순서를 db에 저장 (Integer로 저장)

 

  • ORDINAL을 사용하면 안되는 이유
package hellojpa;

public enum RoleType { USER, ADMIN } // DB에 user는 0, admin은 1로 저장 된다.

// 향후 회원 정보가 추가됨
public enum RoleType { GUEST, USER, ADMIN } // DB에 gues가 0, user가 1로 저장 된다. 

// 문제점 : 기존의 user는 0으로 저장되어 있고 향후 guset가 0으로 저장되기 때문에 0이 뭐를 의미하는지 알 수 없게 된다.

 

3.3. @Temporal

  • 날짜 타입을 매핑할 때 사용
  • java8 이후부터는 LocalDate, LocalDateTime을 사용할 때는 생략 가능
속성  설명 기본값
value TemporalType.DATE: 날짜, 데이터베이스 date 타입과 매핑
(예: 2013–10–11) value

TemporalType.TIME: 시간, 데이터베이스 time 타입과 매핑
(예: 11:11:11)

TemporalType.TIMESTAMP: 날짜와 시간, 데이터베이스 timestamp 타입과 매핑
(예: 2013–10–11 11:11:11)
 

 

3.4. @Lob

  • 속성이 없다.
  • 매핑 필드 타입이 문자면 CLOB 매핑, 나머지는 BLOB 매핑
    • CLOB: String, char[], java.sql.CLOB
    • BLOB: byte[], java.sql. BLOB

 

3.5. @Transient

  • field 매핑에서 제외시킴
  • db에 저장, 조회 다 안됨
  • 주로 메모리상에서만 임시로 어떤 값을 보관하고 싶을 때 사용

 

 

4.  기본 키 매핑

  • 직접 할당 : @Id만 사용 - 개발자가 pk를 직접 넣는 경우

  • 자동 생성 : @GeneratedValue에 3가지 속성 넣어서 사용. default는 AUTO
    1. Identity : 데이터베이스에 위임. MYSQL
    2. Sequence : 데이터베이스 시퀀스 오브젝트 사용, Oracle
      • @SequenceGenerator 필요
    3. Table : 키 생성용 테이블 사용, 모든 DB에서 사용
      • @TableGenerator 필요

    4. AUTO: 방언에 따라 자동 지정, 기본값

 

  • 전체 MEMBER code
package hellojpa;

import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;

/* version5 - 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;
    @Column(name = "name", nullable = false)
    private String username;

    public void setUsername(String username) {
        this.username = username;
    }
}
 */

/* version4 - Sequence 2 */
@Entity
@SequenceGenerator(
        name = "MEMBER_SEQ_GENERATOR",
        sequenceName = "MEMBER_SEQ",
        // initialValue = 1, allocationSize = 1) // 1부터 시작하고 seq가 1씩 증가
        initialValue = 1, allocationSize = 50) // 1부터 시작하고 seq가 50씩 증가 <최적화> - db와 네트워크 소통을 최소화 하기 위함
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE,
            generator = "MEMBER_SEQ_GENERATOR") // 내가 만든 sequence를 사용하고 싶을 때 generator="" 속성 추가
    private Long id;
    @Column(name = "name", nullable = false)
    private String username;

    public Member() {
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}


/* version3 - sequence 1
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
    @Column(name = "name", nullable = false)
    private String username;

    getter,setter 생략
}
*/

/* version2 - identity
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "name", nullable = false)
    private String username;

    public void setUsername(String username) {
        this.username = username;
    }
}
*/

/* version1 -@Id 기본키, 필드와 컬럼 맵핑
@Entity
public class Member {
    @Id
    private Long id;
    @Column(name = "name")
    private String username;
    private Integer age;
    @Enumerated(EnumType.STRING)
    private RoleType roleType;

    // java8 이전 사람들은 date 밖에 없어서 @Temporal로 구분해줘야함
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;

    // java8 이후 부터는 LocalDate, LocalDateTime으로 충분히 구분 가능해서 @Temporal 필요 없음
    private LocalDate testLocalDate;
    private LocalDateTime testLocalDateTime;

    @Lob
    private String description;
}*/

 

4.1. 직접 할당

package hellojpa;

import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;

/* version1 - @Id 기본키, 필드와 컬럼 맵핑 */
@Entity
public class Member {
    @Id
    private Long id;
    @Column(name = "name")
    private String username;
    private Integer age;
    @Enumerated(EnumType.STRING)
    private RoleType roleType;

    // java8 이전 사람들은 date 밖에 없어서 @Temporal로 구분해줘야함
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;

    // java8 이후 부터는 LocalDate, LocalDateTime으로 충분히 구분 가능해서 @Temporal 필요 없음
    private LocalDate testLocalDate;
    private LocalDateTime testLocalDateTime;

    @Lob
    private String description;
}

 

4.2. 자동 할당

  • @GeneratedValue (strategy = Auto)
    • DB 방언에 맞춰서 생성 : Identity, Sequence, Talbe 중에서 선택
    • 참고로 키 type으로 Identity는 String, Long 둘 다 사용 가능하지만 Sequence는 Long만 사용이 가능하다.
      * AUTO 로 Sequence가 될 수 있기 때문에 AUTO도 Long 만 사용 가능하다.
package hellojpa;

import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;

/* AUTO */
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column(name = "name", nullable = false)
    private String username;

    public void setUsername(String username) {
        this.username = username;
    }
}

 

4.3. IDENTITY 전략 - 특징

  • 기본 키 생성을 db에 위임
  • 주로 MySQL, PostgreSQL, SQL Server, DB2 에서 사용 - AutoIncrement

  • JPA는 보통 transaction commit 시점에 insert sql 실행이 된다.
  • 하지만 identity 전략은 insert 시 Pk에 null을 넣은 상태로 sql을 db에 주고 db 내부에서 Pk 값을 만들어서 insert를 수행한다.

  • 문제점
    • 영속성 컨텍스트는 관리를 위해 pk가 무조건 존재해야 한다. 현재 pk를 알수 있는 시점이 db에 값이 실제로 들어갈 경우에만 알 수 있기 때문에 문제가 된다.
  • 해결
    • IDENTITY 전략에서만 예외적으로 em.persist() 시점에 즉시 INSERT SQL을 실행하고 DB에 식별자를 조회한다.

 

 

insert sql이 바로 날라간다.

package hellojpa;

import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;

/* version2 - identity */
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "name", nullable = false)
    private String username;

    public void setUsername(String username) {
        this.username = username;
    }
}

 

 

4.4. SEQUENCE 전략 - 특징

  • 데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트
  • Oracle, PostgreSQL, DB2, H2 에서 사용
  • log 상에서 create sequence라고 해서 sequnce Object를 생성한다.

 

  • 사용방법
    1. @SequenceGenerator 사용 안하는 경우 : jpa가 생성해주는 hibernate_sequence를 사용 
    2. @SequenceGenerator 사용하는 경우 : 내가 직접 sequence object를 사용 - 속성을 이용해서 성능상 이점을 얻을 수 있다.

 

  • Member 엔티티
package hellojpa;

import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;


/* version4 - Sequence 2 */
@Entity
@SequenceGenerator(
        name = "MEMBER_SEQ_GENERATOR",
        sequenceName = "MEMBER_SEQ",
        // initialValue = 1, allocationSize = 1) // 1부터 시작하고 seq가 1씩 증가
        initialValue = 1, allocationSize = 50) // 1부터 시작하고 seq가 50씩 증가 <최적화> - db와 네트워크 소통을 최소화 하기 위함
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE,
            generator = "MEMBER_SEQ_GENERATOR") // 내가 만든 sequence를 사용하고 싶을 때 generator="" 속성 추가
    private Long id;
    @Column(name = "name", nullable = false)
    private String username;

    getter,setter 생략
}


/* version3 - sequence 1 */
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
    @Column(name = "name", nullable = false)
    private String username;

    getter,setter 생략
}

 

  • sequence

@SequenceGenerator 사용 안하는 경우
@SequenceGenerator 사용하는 경우

 

 

4.5. SEQUENCE 전략과 최적화

  • 속성
속성 설명 기본값
name 식별자 생성기 이름 필수
sequenceName 데이터베이스에 등록되어 있는 시퀀스 이름 hibernate_sequence
initialValue DDL 생성 시에만 사용
시퀀스 DDL을 생성할 때 처음 시작되는 숫자를 지정한다.
1
allocationSize 시퀀스 한번 호출에 증가하는 수 (성능 최적화에 사용) 50
catalog, schema 데이터베이스 catalog, schema 이름  

 

  • 최적화 핵심 속성 : initialValue, allocationSize

  • 원리
    • Identity때와 마찬가지로 영속성 컨텍스트에 값을 넣기 위해서 jpa가 sequence를 db로 부터 select해 오고 거기가 +1 한 값을 영속성 컨텍스트의 pk 값으로 가진다.
    • 이 때, db에 값을 저장할 때 마다 db와 app 소통을 하게 되므로 성능 저하를 일으킨다.
    • allocationSize로 미리 50 값을 가져와서 50까지 사용하고 50 을 넘어서게 되면 db에서 50을 더해서 sequence 값을 추가하고 마찬가지로 app에서 50 이후부터 100까지 sequence를 사용하면 된다.
      = app 내부 메모리 상에서 sequence를 이용해서 db와 소통을 최소화

    • 50이 아닌 충분히 큰 숫자로 늘려도 되지만 웹서버를 내리는 시점에 해당 값이 날라가기 때문에 구멍이 생겨서 보통 50으로 둔다.
    • 웹서버 여러대가 동시에 호출해도 동시성 문제가 없다.

 

 

 

4.6. TABLE 전략

  • 사용 권장 안함
  • 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략
  • 장점 : 모든 db에 적용가능 - identity와 sequence를 제공하는 db가 있고 아닌 db가 존재
  • 단점 : 성능

 

  • 속성
속성 설명 기본값
name 식별자 생성기 이름 필수
table 키생성 테이블명 hibernate_sequence
pkColumnName 시퀀스 컬럼명 sequence_name
valueColumnName 시퀀스 값 컬럼명 next_val
pkColumnValue 키로 사용할 값 이름 엔티티 이름
initialValue 초기 값, 마지막으로 생성된 값이 기준 0
allocationSize 시퀀스 한번 호출에 증가하는 수 (성능 최적화에 사용) 50
catalog, schema 데이터베이스 catalog, schema 이름  
uniqueConstraint(DDL) 유니크 제약 조건을 지정할 수 있다.  

 

 

package hellojpa;

import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;

/* version5 - 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;
    @Column(name = "name", nullable = false)
    private String username;

    public void setUsername(String username) {
        this.username = username;
    }
}

 

table

 

 

5. 권장하는 식별자 전략

  • Long Type + 대체키 (UUID, Auto_increment, Sequence_ojbedt, 렌덤 값) + 키 생성전략 사용

 

 

6. 실전 예제 - 1. 요구사항 분석과 기본 매핑

6.1. 요구사항 분석

  • 회원은 상품을 주문할 수 있따.
  • 주문 시 여러 종류의 상품을 선택할 수 있다.

 

6.2. 기능 목록

  • 회원 기능 - 회원 등록, 회원 조회
  • 상품 기능 - 상품등록, 상품수정, 상품조회
  • 주문 기능 - 상품주문, 주문내역조회, 주문취소

 

6.3. 도메인 모델 분석

주문상품으로 n:m 관게 해소

 

 

6.4. 테이블 설계

 

6.5. 엔티티 설계와 매핑

  • 문제점
    1. 객체 설계 방식이 아닌 테이블 설계에 맞추어 설계 되어있다.
    2. 테이블을 참조하지 않고 외래키를 객체에 그대로 가지고 온다.
    3. 객체 그래프 탐색이 불가능하다.

 

 

6.6. 코드

 

  • 핵심 코드
package jpabook.jpashop.domain;

import javax.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "ORDERS") // db 중 order가 예약어로 걸린 곳이 있기 때문에 orders로 table명을 지었다.
public class Order {
    @Id @GeneratedValue

    /* 핵심 시작 */
    @Column(name = "ORDER_ID")
    private Long id;

    // 객체 지향적이지 않은 필드
    @Column(name = "MEMBER_ID")
    private Long memberId;

    // 객체 지향적인 필드
    private Member member;

    public Member getMember() {
        return member;
    }

    public void setMember(Member member) {
        this.member = member;
    }

    /* 핵심 끝 */

    private LocalDateTime orderDate; // spring에서는 필드명 그대로 나가지만, springboot에서는 order_date(default), ORDER_DATE로 자동으로 변경 해줌.

    @Enumerated(value = EnumType.STRING)
    private OrderStatus staus;

	getter,setter 생략
}