제육's 휘발성 코딩
반응형

엔티티 매핑 종류

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

객체와 테이블 매핑

@Entity

  • @Entity가 붙은 클래스는 JPA가 관리하는 클래스로 엔티티라고 부른다. 즉, 테이블과 매핑할 클래스는 반드시 @Entity를 적용해야 한다.
  • 기본 생성자 필수 (파라미터가 없는 public 또는 protected)
  • final 클래스, enum, interface, inner 클래스 사용할 수 없다.
  • 저장할 필드에 final 키워드를 사용할 수 없다.

@Table

sec4  사진1

  • 기본 값은 클래스 이름을 그대로 사용한다.
  • 유니크 제약 조건 추가 : @Table(uniqueConstraints = {@UniqueConstraint( name = "NAME_AGE_UNIQUE", columnNames = {"NAME", "AGE"} )})

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

sec4  사진2

  • 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 클래스 재구성

매핑 애노테이션 정리

sec4  사진3

@Column

sec4  사진4

  • 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를 맞추기 위해 한번 더 호출하고 이후는 메모리에서 시퀀스값을 가져온다.

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) + 키 생성전략 사용

실전 예제

image

요구사항 분석

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

도메인 모델 분석

  • 회원과 주문의 관계 : 회원은 여러 번 주문할 수 있다. 일대다 관계
  • 주문과 상품의 관계 : 주문할 때 여러 상품을 선택할 수 있고, 상품은 여러 주문을 할 수 있다. 즉, 다대다 관계이므로 중간에 주문상품이라는 모델을 만들어서 일대다, 다대일로 풀어내자.

테이블 설계

image

엔티티 설계와 매핑

image

프로젝트 생성

image

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 프로그래밍 - 기본편)를 토대로 정리한 내용입니다.

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^