[JPA] 자바 ORM 표준 JPA 프로그래밍 - 기본편_5. 엔티티 매핑

유진·2024년 8월 17일
0

ORM JPA 기본편

목록 보기
3/6
post-thumbnail

출처 : 인프런 > 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 듣고 작성한 글입니다.
강의 링크 : 자바 ORM 표준 JPA 프로그래밍 - 기본편

섹션 5. 엔티티 매핑

📘 객체와 테이블 매핑

📖 엔티티 매핑 소개

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

📖 @Entity

  • @Entity가 붙은 클래스는 JPA가 관리, 엔티티라 한다.
  • JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 필수
  • 📢 주의
    - 기본 생성자 필수 (파라미터가 없는 public 또는 protected 생성자)
    - final 클래스, enum, interface, inner 클래스 사용 X
    - 저장할 필드에 final 사용 X

📖 @Entity 속성 정리

  • 속성: name
    - JPA에서 사용할 엔티티 이름을 지정한다.
    - 기본값: 클래스 이름을 그대로 사용(예: Member)
    - 같은 클래스 이름이 없으면 가급적 기본값을 사용한다

📖 @Table

  • @Table은 엔티티와 매핑할 테이블 지정
@Entity
@Table(name = "MBR") // 매핑할 MBR 테이블 지정

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

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

  • DDL(Data Definition Language)을 애플리케이션 실행 시점에 자동 생성
  • 테이블 중심 -> 객체 중심
  • 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL 생성
  • 이렇게 생성된 DDL은 📢개발 장비에서만 사용
  • 생성된 DDL은 운영서버에서는 사용하지 않거나, 적절히 다듬은 후 사용
  • DDL(Data Definition Language)
    : 데이터 정의어란? 데이터베이스를 정의하는 언어이며, 데이터를 생성, 수정, 삭제하는 등의 데이터의 전체의 골격을 결정하는 역할을 하는 언어이다.
    - create : 데이터베이스, 테이블등을 생성
    - alter : 테이블을 수정
    - drop : 데이터베이스, 테이블을 삭제
    - truncate : 테이블을 초기화

📖 데이터베이스 스키마 자동 생성 - 속성

 <property name="hibernate.hbm2ddl.auto" value="create"/>

 @Id
private Long id;
private String name;
private int age; // age 컬럼 추가

컬럼을 추가할 때 일일이 컬럼을 추가 하는 것이 아니라 hibernate.hbm2ddl.auto가 알아서 컬럼을 추가해준다.

📖 데이터베이스 스키마 자동 생성 - 실습

  • 스키마 자동 생성하기 설정
  • 스키마 자동생성하기 실행, 옵션별 확인
  • 데이터베이스 방언 별로 달라지는 것 확인(varchar)
<property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect"/>

방언을 oracle로 설정시 가변 문자인 varchar2로 생성이 된다.

📖 📢 데이터베이스 스키마 자동 생성 - 주의

  • 📢 운영 장비에는 절대 create, create-drop, update 사용하면 안된다.
  • 개발 초기 단계는 create 또는 update
  • 테스트 서버는 update 또는 validate
    : 테스트 서버에서 create를 쓰면 데이터가 다 날라가는 문제가 생긴다.
  • 스테이징과 운영 서버는 validate 또는 none

✅ 결론 : 자기 로컬PC에서만 자유롭게 사용하고 여러 명이 쓰는 개발 서버, 스테이징, 운영 서버에는 가급적이면 쓰지 않는다!

📖 DDL 생성 기능

  • 제약조건 추가: 회원 이름은 필수, 10자 초과X
    - @Column(nullable = false, length = 10)
  • 유니크 제약조건 추가
    - @Table(uniqueConstraints = {@UniqueConstraint( name = "NAME_AGE_UNIQUE", columnNames = {"NAME", "AGE"} )})
  • DDL 생성 기능은 DDL을 자동 생성할 때만 사용되고 JPA의 실행 로직에는 영향을 주지 않는다.

📘 필드와 컬럼 매핑

📖 요구사항 추가

  1. 회원은 일반 회원과 관리자로 구분해야 한다.
  2. 회원 가입일과 수정일이 있어야 한다.
  3. 회원을 설명할 수 있는 필드가 있어야 한다. 이 필드는 길이 제한이 없다.

src\main\java\hellojpa\Member.java

package hellojpa;

import jakarta.persistence.*;
import java.util.Date;

@Entity
public class Member {

    // PK mapping
    @Id
    private Long id;

    // 객체에는 username, DB에는 name이라 쓰고 싶음 -> Column 속성 사용
    @Column(name = "name")
    private String username;

    private Integer age;

    // 객체에는 enum 타입을 쓰고 싶음, but DB에는 enum 타입이 없음 -> @Enumerated 사용
    @Enumerated(EnumType.STRING)
    private RoleType roleType;

    // 날짜 타입 : DATE = 날짜, TIME = 시간, TIMESTAMP = 날짜 + 시간
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate; // 생성 일자

    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate; // 수정 일자

    // varchar를 넘어서는 큰 컨텐츠 -> Lob 사용
    @Lob
    private String description;

    public Member () {
    }

}

📖 매핑 어노테이션 정리


📘 기본 키 매핑

📖 기본 키 매핑 어노테이션

  • @Id
  • @GeneratedValue
@Id @GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

📖 기본 키 매핑 방법

  • 직접 할당: @Id만 사용
  • 자동 생성(@GeneratedValue)
    - IDENTITY: 데이터베이스에 위임, MYSQL
    - SEQUENCE: 데이터베이스 시퀀스 오브젝트 사용, ORACLE
    @SequenceGenerator 필요
    - TABLE: 키 생성용 테이블 사용, 모든 DB에서 사용
    @TableGenerator 필요
    - AUTO: 방언에 따라 자동 지정, 기본값

📖 IDENTITY 전략

@Entity
public class Member {
 	@Id
 	@GeneratedValue(strategy = GenerationType.IDENTITY)
 	private Long id;
}
  • 기본 키 생성을 데이터베이스에 위임 (내가 id값을 생성X, DB가 알아서 해)
  • 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용
    (예: MySQL의 AUTO_ INCREMENT)
  • JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL 실행
  • AUTO_ INCREMENT는 데이터베이스에 INSERT SQL을 실행한 이후에 ID 값을 알 수 있음
  • IDENTITY 전략은 em.persist() 시점에 즉시 INSERT SQL 실행하고 DB에서 식별자를 조회

📖 SEQUENCE 전략

@Entity
@SequenceGenerator(name = "member_seq_generator", 
sequenceName = "member_seq", // 매핑할 DB 시퀀스 이름
initialValue = 1, allocationSize = 1) 

public class Member {

    // PK mapping
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "member_seq_generator")
    private Long id;
    
    ...
}
  • 데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트(예: 오라클 시퀀스)
  • 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용

📖 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형 + 대체키 + 키 생성전략 사용

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

📖 요구사항 분석

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

📖 기능 목록

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

📖 도메인 모델 분석

  • 회원과 주문의 관계: 회원은 여러 번 주문할 수 있다. (일대다)
  • 주문과 상품의 관계: 주문할 때 여러 상품을 선택할 수 있다. 반대로 같은 상품도 여러 번 주문될 수 있다. 주문상품 이라는 모델을 만들어서 다대다 관계를 일다대, 다대일 관계로 풀어냄

📖 테이블 설계

📖 엔티티 설계와 매핑

📖 데이터 중심 설계의 문제점

  • 현재 방식은 객체 설계를 테이블 설계에 맞춘 방식
  • 테이블의 외래키를 객체에 그대로 가져옴
  • 객체 그래프 탐색이 불가능
  • 참조가 없으므로 UML도 잘못됨

src\main\java\jpabook\jpashop\JpaMain.java

// Order를 찾았음
Order order = em.find(Order.class, 1L);

// Order한 member를 찾고 싶음 -> getMemberId
Long memberId = order.getMemberId();

// MemberId를 찾은 다음 또 Member를 찾아야 함
Member member = em.find(Member.class, memberId);


// 위 코드는 객체 지향스럽지 않음
// Order에서 바로 Member를 바로 찾을 수 있어야 함
Member findMember = order.getMember();

src\main\java\jpabook\jpashop\domain\Member.java

package jpabook.jpashop.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.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;
    }
}

src\main\java\jpabook\jpashop\domain\Order.java

package jpabook.jpashop.domain;

import jakarta.persistence.*;

import java.time.LocalDateTime;

@Entity
@Table(name = "ORDERS") // DB에 "ORDER BY"기 예약어로 설정되어 있는 경우가 있어서
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;
    }
}

src\main\java\jpabook\jpashop\domain\OrderItem.java

package jpabook.jpashop.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.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;
    }
}

src\main\java\jpabook\jpashop\domain\Item.java

package jpabook.jpashop.domain;

import jakarta.persistence.Id;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;

@Entity
public class Item {

    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

    private String name;
    private int price;
    private int stockQuantity;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getStockQuantity() {
        return stockQuantity;
    }

    public void setStockQuantity(int stockQuantity) {
        this.stockQuantity = stockQuantity;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
}
profile
유진진입니덩

0개의 댓글