Java - SpringJPA

Spring JPA (4) - JPA 개념 잡기 : 객체-테이블 매핑, 필드와 칼럼, 기본키 매핑

TerianP 2022. 10. 2.
728x90

1. 객체와 테이블 매핑

1) @Entity

- @Entity 가 붙은 클래스는 JPA 가 관리하며 엔티티, 엔티티 클래스 라고 한다.

- JPA 를 사용해서 테이블과 매핑 할 클래스는 @Entity 가 필수이다

- 이때 기본 생성자는 필수!!! : 파라미터가 없는 pulbic 또는 protected 생성자가 필요하다 => JPA 에서 사용하는 객체를 프로싱하거나 하는 등등의 기술들이 있는데 이때문에 기본 생성자가 필수라고한다

- final 클래스, enum, interface, inner 클래스 사용X

- 저장할 필드에 final 사용 X 

핵심 Point!!
DB 테이블과 매핑하기 위한 클래스 ( 보통 DTO ) 에 @Entity 어노테이션을 붙인다. 이때 PK 에 해당하는 변수에는 @Id 어노테이션을 붙여야 한다

2) @Table

- DB 테이블과 매핑 후 데이터를 가져와 저장하기 위한 클래스에 붙이는 어노테이션
- 이때 Entity 에 name 파라미터를 사용할 수 있는데 이 파라미터로 엔티티 이름을 지정할 수 있다 => 기본값은 클래스명

속성 기능 기본값
name 매핑할 테이블 이름 엔티티 이름을 사용
catalog DB catalog 매핑  
schema DB schema 매핑  
uniqueConstraints DDL 생성 시 유니크 제약 조건 생성  
/*
*  DB 테이블과 매핑하기 위한 클래스 ( 보통 DTO ) 에 @Entity 어노테이션을 붙인다
*  이때 PK 에 해당하는 변수에는 @Id 어노테이션을 붙여야 한다
*
* */

// DB 테이블과 매핑 후 데이터를 가져와 저장하기 위한 클래스에 붙이는 어노테이션
// 이때 Entity 에 name 파라미터를 사용할 수 있는데 이 파라미터로 엔티티 이름을 지정할 수 있다 => 기본값은 클래스명
@Entity(name="Member")
@Table(name="Member") // @Table 은 엔티티와 매핑할 테이블을 지정한다
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Member {

    @Id // PK에 해당하는 변수
    private Long id;
    private String name;

 

2. DB 스키마 자동 생성 -> application.properties 혹은 xml 에서 설정

- DDL 을 애플리케이션 실행 시점에 자동 생성!! => 테이블 중심 -> 객체 중심

- 데이터베이스 방언을 활용해서 DB 에 맞는 적절한 DDL 을 생성 가능하다

- 단!! 이렇게 자동으로 생성된 DDL 은 개발 장비에서만 사용하는 것이 적절하다. DDL 정도는 직접 만들자

- DB 스키마 자동 생성은 application properties 또는 xml 에서 설정 가능하다. 이때 value 에 해당하는 속성값들은 create, create-drop, update, vaildate, none 까지 총 5가지가 있다

속성 값 설명 사용처
create 기존 테이블 삭제 후 다시 생성 drop+create 주로 개발 초기 단계
create-drop create + 애플리케이션 종료 시 테이블 drop 주로 개발 초기 단계
update 변경된 부분만 반영  테스트 서버
vaildate 엔티티와 테이블이 정상 매핑되었는지만 확인 => 만약 정상 매핑 아니라면 에러!! 스테이징과 운영 서버
none 사용하지 않음 스테이징과 운영서버(운영때는 그냥 얘만 사용하자)

 

<!-- 데이터 베이스 스키마 자동생성 : value 속성 -->
<!--
    create : 기존 테이블 삭제 후 다시 생성 drop + create
    create-drop : create 와 같으나 애플리케이션 종료 시점에 테이블 drop
    update : 변경된 부분만 반영 => 특히 운영 DB 에 사용하면 클남
    vaildate : 엔티티와 테이블이 정상 매핑되었는지만 확인 => 매핑 안되면 에러남
    none : 사용하지 않음
-->
<property name="hibernate.hbm2ddl.auto" value="create" />

 

3. 필드와 컬럼 매핑

- @Column 등 여러 어노테이션을 사용해서 테이블과 엔티티의 변수를 매핑시킬 수 있다

- 이 부분은 바로 코드로 알아보자. 공부하기 위한 요구사항은 아래와 같다

요구사항
1. 회원은 일반 회원과 관리자로 구분한다
2. 회원 가입일과 수정일이 있어야한다
3. 회원을 설명할 수 있는 필드가 있어야 한다. 해당 필드에는 길이 제한이 없다
@Entity(name="NMember")
@Table(name="Member")
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Member {

    @Id
    private Long id; // pk

    // 컬럼명과 변수명이 다를 경우 name 파라미터로 지정 가능
    @Column(name="name", nullable = false)
    private String userName; // 이름

    private Integer age; // 나이

    @Enumerated(EnumType.STRING) // Enum 타입의 값이 들어가는 경우 사용하는 어노테이션
    private RoleType roleType; // 역할

    // 날짜, 시간, 날짜+시간 의 정보를 매핑할때 사용하는 어노테이션
    // 참고로 Enum 타입
    @Temporal(TemporalType.TIMESTAMP)
    private Date createDate; // 생성 날짜

    // @Temporal 을 사용하거나 아래처럼 LocalDate, LocalDateTime 을 사용해도 무방하다
    private LocalDateTime updateDate; // 수정 날짜

    @Lob
    // varchar 를 넘어서는 엄~~청나게 많은 데이터가 들어 갈 수 있는 타입
    // String 에 달려있는 경우 CLOB
    private String description; // 유저 설명

    @Transient // DB 와 매핑하고 싶지 않은 엔티티 변수에 달림 => DB 매핑 X , 컬럼 생성 X
    private  String tmp;
}

DDL 자동 생성


4. 매핑 어노테이션과 column 어노테이션 정리

- 엔티티-DB 매핑에 사용되는 어노테이션과 column 에 사용되는 어노테이션을 정리해보자

 

1) 매핑 어노테이션

어노테이션 설명
@Column 컬럼 매핑
@Temporal 날짜 타입 매핑, 아래 둘 중 하나로 사용 가능하다 -> Date, TimeStamp
- TemporalType ; 어노테이션 파라미터로 사용
- LocalDate, LocalDateTime : 변수 타입으로 사용
@Enumerated enum 타입 매핑
- EnumType.ODINAL : enum 순서(int 타입)를 DB 에 저장 => 기본값, 쓰지말자
- EnumType.STRING : enum 이름(String 타입)을 DB 에 저장
@Lob CLOB -> 문자 타입일 때 => String, char[]
BLOB -> CLOB 에 해당하지 않는 경우 전체
@Transient 특정 필드를 컬럼 매핑에서 빼고 싶을 때
- 주로 메모리 상에서만 사용하는 변수일때 사용

 

2) Column 어노테이션

속성 설명 기본값
name 필드와 매핑할 테이블의 컬럼명  
insertable, updateable 등록, 변경 가능 여부 -> false 인 경우 JPA 로는 등록, 변경이 불가능!! true
nullable(DDL) null 값의 허용 여부 설정, false 로 하는 경우 DDL 생성 시 not null 제약 조건이 붙는다 true
unique(DDL) @Table 의 uniqueConstraints 와 같지만 한 컬럼에 간단히 유니크 제약 조건을 걸 때 사용한다  
columnDefinition(DDL) DB 칼럼 정보를 줄 수 있다 필드의 자바 타입과 방언 정보 사용!!
length(DDL) 문자 길이 제약 조건 -> String 타입에만 사용 255
precision, scale(DDL) BigDecimal 타입에 사용!! => precision 은 소수점을 포함한 자리수 전체를, scale 는 소수 자리수를 의미한다.
참고로 double, float 타입에는 적용 XX, 아주 정밀한 소수를 다룰 때만 사용한다
 

 

5. 기본키 매핑 : Id && GeneratedValue

- 기본키를 숫자형으로 사용하는 경우 int 나 Integer 대신 Long 을 권장한다

- 기본키 매핑 방법 : @Id 만 사용

- @GeneratedValue 자동 생성 : @GeneratedValue 를 사용하면 PK 로 설정된 값을 자동으로 증가시켜야 하는 경우 사용한다 => Mysql 의 AUTO_INCREMENT 나 ORACLE 의 SEQUENCE 같은 느낌

- Auto_Increment 는 DB 에 insert SQL 을 실행한 이후에 ID 값을 알 수 있음

- @GeneratedValue 를 사용할 때 Strategy  파라미터를 사용하고 이에 해당하는 값을 넣어줘야 한다.

Strategy   설명
AUTO DB 에 맞게 알아서 자동으로 생성
- Oracle 이면 Sequence
- mysql : auto_increment
Indentity - 기본키 생성을 DB 에 위임 => Auto_Increment 같은 느낌
- initialValue : DDL 생성 시에만 사용됨, 시퀸스 dDL 을 생성할 때 처음 시작하는 수를 지정
- allocationSize : 시퀸스 한 번 호출에 증가하는 수
- em.persist() 시점에 insert sql 을 실행하고 DB에서 식별자를 조회함
===> sql 을 DB 에 넣고 나서 식별자 즉 PK 를 조회하기 때문에 아주 다른 애들과는 다르게 특이한 부분이 존재하는데 일반적으로 commit() 후 sql 이 DB 에 반영되는 것과는 다르게 얘만 persist 하는 순간 insert sql 을 실행함!!!
strategy = Sequence
추가적으로 @SequenceGenerator 를 사용해서 시퀸스 이름, 값 등을 지정 가능
- name : 식별자 생성 이름 - 필수
- generate [시퀸스명] : 어떤 시퀸스를 사용할 것인지 지정

- sequenceName : DB 에 등록되어 잇는 시퀸스 이름
- initialValue : DDL 생성 시에만 사용됨, 시퀸스 dDL 을 생성할 때 처음 시작하는 수를 지정
- allocationSize : 시퀸스 한 번 호출에 증가하는 수
- catalog, schema : DB catalog, schema 이름

----------------------------------------------------------------------------------------
- sequence 전략의 경우 em.persist() 하는 시점에 시퀸즈 전략인 것을 확인 한 후 DB 에 만들어진 시퀸스에서 "call next value for 시퀸스명" 해서 다음 시퀸스 값을 가져온 후 영속성 컨텍스트로 저장함
- call next for 시퀸스명 할때마다 네트워크로 요청하기 때문에 성능 상 이슈가 발 생할 수 있다. 이를 해결하기 위해서 allocationSize 의 기본값이 50 으로 지정된다. 이렇게 하면 하나의 엔티티를 persist 했을 때 DB 에서의 next value 값은 51로 되고, 애플리케이션 메모리에는 50개가 쌓여있게 된다.  이후 메모리에 쌓인 50개 안의 숫자를 먼저 PK 로 사용한다.
==> 쉽게 생각하자면 50개를 미리 가불해서 당겨온 후 가불해서 당겨온 값부터 먼저 PK 로 사용한다. 가불해서 가져왔기 때문에 51이 될 때까지 DB 에 next value 하지 않는것은 덤
Table - 키 생성 전용 테이블을 하나 만들어서 사용!! => 마치 DB 시퀸스를 흉내내서 사용하는 전략
- 모든 DB 에 사용가능하나, 성능 이슈가 있다
// AUTO
@Entity(name="NMember")
@Table(name="Member")
public class Member {

    @Id
    // Auto 는 DB 에 맞춰서 자동으로
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id; // pk
    
    }
    
    
// Sequence
@Entity(name="NMember")
@Table(name="Member")
@SequenceGenerator(name = "member_seq_generator", sequenceName = "member_seq")
public class Member {

    // SEQUENCE 를 사용하는 경우 SequenceGenerator 를 추가적으로 사용해서 시퀸스 제너레이터명과 시퀸스명을 지정 가능하다
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "member_seq_generator")
    private Long seqId;
    
    }
    
    
// Table
@TableGenerator(name = "member_tableseq_generator", table = "my_seq",
                pkColumnName = "member_SEQ", allocationSize = 1)
public class Member {

    // Table 전략은 시퀸스 전용 테이블을 만들어서 사용하는 것 -> DB 에서 사용하는 시퀸스 테이블을 흉내내는 전략
    // 시퀸스 전략과 마찬가지로 제너레이터 파라미터에 TableGenerator name 을 사용한다
    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "member_tableseq_generator")
    private Long tableId;
    }

Table 전략은 TableGenerator 에서 지정한 table 파라미터의 값으로 테이블이 생성된다

 

권장하는 식별자 전략!!
- 기본 키 제약 조건 : not null, unique, 변하면 안된다
- 미래까지 이 조건을 만족하는 자연키는 찾기 어렵다
--> 권장 : Long 형 + 대체키 + 키 생성전략 사용

 

6. 실전 예제를 통해코드로 만들어보기

- Member, Orders, Order_tiem, item

요렇게 만들어보겠습니다!

1) MEMBER

package com.use.jpabasic.practice.domain;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Entity(name = "ShopMember")
@Table
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member {

    @Id
    @GeneratedValue // 전략 생략하면 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;
    }
}

 

2) ORDER

package com.use.jpabasic.practice.domain;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;

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

@Entity
@Table(name = "ORDERS")
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Order {

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

    @Column(name = "MEMBER_ID")
    private Long memberId;

    private LocalDateTime orderDate;

    // 주문 상태는 Enum 으로
    @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;
    }
}

 

3) ORDER_STATUS : Enum 클래스

package com.use.jpabasic.practice.domain;

public enum OrderStatus {
    ORDER, CANCEL;
}

 

4) ORDER_ITEM

package com.use.jpabasic.practice.domain;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Entity
@Table
@NoArgsConstructor
@AllArgsConstructor
@Builder
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;
}

 

5) ITEM

package com.use.jpabasic.practice.domain;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Entity
@Table
@NoArgsConstructor
@AllArgsConstructor
@Builder
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_id 가 1 인 Member 를 가져오기 위해서는 아래와 같이 Order 코드를 고치거나 find 를 2번 실행해서 가져와야 한다.

// Order_id = 1 인 Member 를 가져오기 위해서
// 일단 Order_id = 1 인 Order 객체를 가져오고 나서
Order order = em.find(Order.class, 1L);
// 다시 Member_ID 에 맞는 member 를 꺼내와야 한다
Member member = em.find(Member.class, order.getMemberId());

--------- 혹은 아래처럼 Order 쪽에 코드를 추가하던가 -----------
    private Member member;
    public Member getMember(){
        return this.member;
    }
    public void setMember(Member member){
        this.member = member;
    }

 

DB 데이터 중심 설계의 문제점
- 객체 설계를 테이블 설계에 맞춘 방식
- 테이블의 외래키를 객체에 그대로 가져옴 => 객체 그래프 탐색 불가능
- 참조가 없음으로 UML 도 잘못됨

 

댓글