Entity Mapping

뚝딱이·2022년 8월 28일
0

JPA

목록 보기
3/11

객체와 테이블 매핑

@Entity가 붙은 클래스는 JPA가 관리, 엔티티라 한다.
따라서 @Entity가 붙지 않으면 JPA가 관리하지 않는, 내가 그냥 마음대로 사용하는 클래스이다.

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

주의

  • 기본 생성자 필수(파라미터가 없는 public 또는 protected 생성자)
    jpa 스펙상 규정되어있다. JPA같은 걸 구현해서 쓰는 라이브러리들이 동적으로 뭘하거나, 리플렉션이나 다양한 기술을 써서 객체를 프록싱하고 등의 기술을 사용할 때 필요하기 때문이다.
  • final 클래스, enum, interface, inner 클래스 사용X
  • 저장할 필드에 final 사용 X

@Entity 속성정리

중요하지 않다.

name

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

@Entity(name=" ")으로 이름 지정이 가능하다. 하지만 쓰지 않는다.
근데 다른 패키지에 같은 이름의 클래스가 있을 땐, 쓰기도 한다. jpa가 구분하는 이름이다.

@Table

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

@Table 테이블을 다른 이름으로 매핑하고 싶어 축약으로 하고 싶을 때 @Table(name="MBR")
이런식이면 클래스 이름이 MEMBER여도 쿼리가 INSERT INTO MBR 같은 식으로 쿼리가 나간다.

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

JPA는 매핑정보(@Id,@Entity등)만 보면 어떤 쿼리를 만들어야할지 어떤 테이블인지 다 알 수 있다.
따라서 애플리케이션 로딩 시점에 DB테이블을 생성하는 기능도 제공한다. 물론 이 기능은 운영서버에선 사용하면 안되고, 혼자 로컬에서 개발할 때만 사용하도록 하자.
따라서 이 기능을 사용하면 애플리케이션 실행시점에서 테이블이 생성되게 해준다.

애플리케이션 로딩 시점에 CREATE문으로 일단 DB를 생성하고 시작할 수 있게 해준다.
장점 -> 원래는 테이블을 다 만들어놓고 객체개발 근데 JPA는 그럴 필요가 없다.
매핑을 다 해놓으면 애플리케이션 뜰 때 테이블을 다 만들어준다.

데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL 생성
오라클로 설정하면 varchar2로 생성

속성

creat : 기존 테이블 삭제후 다시 생성 항상 생성전 drop table Member if exists
DB에서 따로 테이블 만들지 않아도 됨 그리고 테이블의 변경사항이 생겨도(필드 추가) 그냥 Member만 고치면 알아서 변경사항을 반영해 (필드가 추가됨) 생성된다.

create-drop : 보통 테스트 페이지에서 깔끔하게 다 날려버리고 싶을 때 사용

update : 테이블 만들 때 drop 안함 그래서 Member 테이블을 만들었다가 age필드를 추가하고 싶을 때, 기존 테이블을 유지한 상태로 테이블을 변경하고 싶다면 update 사용
대신 삭제가 안됨 age필드를 Member클래스에서 삭제해도 그대로 남아있는다.

validate : 엔티티와 테이블이 정상 매핑되었는지만 확인
만약 필드 이름 다르게 하고 돌리면 생성된 테이블과 다르기 때문에 오류난다.

주의점

운영장비에는 절대 사용 하지말아야한다.

개발 초기 -> create update 로컬과 개발 서버
테스트 서버 -> update validate -> 개발서버 개발자 여러명이 같이 쓰는 중간 서버
절대 create금지다. 다른 개발자가 쓰던 테이블의 데이터들 다 날아가기 때문.

스테이징과 운영 서버는 validate 또는 none :
사실 그냥 안쓰는게 제일 best다.
validate까진 괜찮다.

왜 쓰지말아야할까
개발서버는 update정도는 편하겠지만 결국 운영같은경우에는 몇 천만건씩 있는데 Alter같은 걸 잘못치게 되면 시스템이 중단상태가 되버린다.
애플리케이션 로딩 시점에 시스템이 알아서 alter를 쳐준다는것자체가 위험하다.
테스트 서버나 개발서버 전부 모두 본인이 직접싸서 쓰는 것을 추천한다.

ddl 생성기능

@Table에서 이름을 name=""으로 바꾸는 것은 runtime에 영향을 줌 insert쿼리나 update쿼리등
DDL 생성 기능(유니크 제약 조건, 길이 제한 등)은 DDL을 자동 생성할 때만 사용되고 JPA의 실행 로직에는 영향을 주지 않는다

  • 제약조건 추가: 회원 이름은 필수, 10자 초과X
    @Column(nullable = false, length = 10)

  • 유니크 제약조건 추가
    @Table(uniqueConstraints = {@UniqueConstraint( name = "NAME_AGE_UNIQUE", columnNames = {"NAME", "AGE"} )})

DDL 생성 기능은 DDL을 자동 생성할 때만 사용되고 JPA의 실행 로직에는 영향을 주지 않는다.


매핑 어노테이션 정리

@Column

insertable, updatable : 등록, 변경 가능 여부/ 변경이 불가능하게 하려면 기본이 true이기 때문에 false로 지정한다.
nullable : 기본이 true 따라서 false로 하면 notnull제약조건이 붙는다. 자주 사용한다.
unique : 잘 쓰지 않는다. alter로 unique제약조건을 걸어주긴 하는데 이름을 랜덤값으로 붙여주기 때문에 이름반영이 어렵다.

따라서 랜덤값으로 이름이 생성되는게 싫다면 @Table 어노테이션에 unique 제약조건을 걸어주면된다.

columnDefinition : 데이터베이스 컬럼 정보를 직접 줄 수 있음

precision : 아주 큰 숫자나 소수점을 사용할 때사용한다.

@Enumerated

@Enumerated 주의사항

기본이 ORDINARY임 사용하지 않으므로 STRING으로 꼭 명시해줘야함

public enum RoleType {
USER, ADMIN
}
의 ENUM이 있을 때 ORDINARY를 사용하거나 명시하지 않는다면,
member.setRoleType(RoleType.ADMIN)을 하면 1이 저장된다. 순서가 저장되기 때문이다
마찬가지로 user로 set하고 persist, commit으로 db에 저장하면 0이 저장된다

그럼 왜 쓰면 안되나
위의 RoleType에 GUEST를 추가해다고 해보자.

public enum RoleType {
GUEST,USER, ADMIN
}
그리고 GUEST로 저장하면 이젠 GUEST가 0이다 .
하지만 DB를 확인해보면 예전에 저장되어있던 ADMIN-1, USER-0은 그대로이다. 이는 굉장한 문제를 야기한다.

@Temporal

날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용
참고: LocalDate, LocalDateTime을 사용할 때는 생략 가능(최신 하이버네이트 지원)

@Lob

데이터베이스 BLOB, CLOB 타입과 매핑
• @Lob에는 지정할 수 있는 속성이 없다.
• 매핑하는 필드 타입이 문자면 CLOB 매핑, 나머지는 BLOB 매핑
CLOB: String, char[], java.sql.CLOB
BLOB: byte[], java.sql. BLOB

@Lob
private String

같은 경우 데이터 베이스 생성 쿼리를 보면
description clob으로 clob타입으로 매핑된 것을 볼 수 있다.

@Trasient

필드 매핑X
• 데이터베이스에 저장X, 조회X
• 주로 메모리상에서만 임시로 어떤 값을 보관하고 싶을 때 사용
• @Transient
private Integer temp;


기본 키 매핑

어노테이션

@Id와 @GeneratedValue가 있다.

@id : 코드성 데이터를 조합해서 직접 id를 만들어서 할당할때 사용, 내가 직접 setting

RDB를 사용할 경우 보통 ORACLE의 경우 sequence , DB에서 자동으로 GENERATE 해주는 값
MYSQL 계열의 경우는 AUTO INCREMENT , 값을 NULL로 집어넣으면 값을 알아서 순차적으로 올라가도록 세팅해주는것이다.

이와 같이 값을 자동으로 할당해주는 방법 -> @GeneratedValue

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

Identity 특징

GenerationType을 auto로 해놓으면 데이터베이스 방언을 알아서 처리해줌
auto는 방언에 맞춰 아래의 세가지 중에 한가지를 선택해줌

  • 기본 키 생성을 데이터베이스에 위임
  • 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용
    (예: MySQL의 AUTO_ INCREMENT)
  • JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL 실행
  • AUTO_ INCREMENT는 데이터베이스에 INSERT SQL을 실행
    한 이후에 ID 값을 알 수 있음
  • IDENTITY 전략은 em.persist() 시점에 즉시 INSERT SQL 실행
    하고 DB에서 식별자를 조회

SEQUENCE 전략

데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한
데이터베이스 오브젝트(예: 오라클 시퀀스)
• 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용
sequence : db에 있는 sequence 오브젝트를 통해서 값을 generating 하는 것

@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; 

@SequenceGenerator

Table 전략

키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략
increment든 sequence든 다 적용가능하지만, 성능이 좀 떨어진다.

운영에서 쓰기엔 좀 부담임

매핑

create table MY_SEQUENCES ( 
 sequence_name varchar(255) not null, 
 next_val bigint, 
 primary key ( sequence_name ) 
)
@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; 

@TableGenerator

initalValue와 allocationSize가 중요

권장하는 식별자 전략

기본 키 제약 조건에 대해 생각해보자. null 아니면서 유일하고 변하면 안된다.
변하면 안되는게 어려움 전체 수명 동안 변화하지 않는게 어렵고 이런것을 찾는 것 또한 어렵다.
자연키 : 비즈니스적으로 의미있는 키 - 주민번호 등
대리키를 사용하자. generatevalue

주민 등록 번호도 기본 키로 적절하지 않음 - 나라에서 주민 번호를 관리 하지 말라해서 봤더니 pk가 다 주민 번호 - pk를 주민번호로 쓰니 이 테이블을 사용하는 다른 모든 테이블에 영향이 감 왜냐면 거기 fk가 주민번호이기 때문.
따라서 변경하는데 어마무시한 비용이 든다

권장 : Long + 대체키 (uuid등) + 키 생성전략 사용

identity 전략

id에 값을 넣지 않고 db에 insert를 해야한다. db에서 null로 날아오면 값을 세팅한다.
뭐가 문제일까 ? id값을 알 수있는 시점이 db에 값이 세팅될때인 것이 문제가 된다.
영속성 컨텍스트에서 관리되려면 pk값이 있어야한다. 근데 db에 들어가야 pk를 알 수 있다.
영속 상태가 됐다는것은 1차 캐시에 존재한다는 뜻이다. 하지만 1차 캐시에 넣으려면 pk알아되는데 알 방법이 없는 것이다.
그래서 identity 전략에서만 예외적으로, persist를 호출한 시점에 바로 db에 insert쿼리를 날린다. 커밋 시점 아님 !

그래서 db에 값이 들어가면 jpa가 내부적으로 select해서 pk가져온다.
select 쿼리가 나가는게 보여야하는 거 아닌가 ? 내부적으로 짜져있기 때문에 insert하는 시점에 id를 바로 알 수 있다.

한 트랜잭션안에서 insert 쿼리가 네트워크를 여러번 탄다고 해서 성능에 그렇게 큰 영향을 끼치진 않는다.

sequence는 1부터 시작하고 1씩 올라간다.
create sequence MEMBER_SEQ start with 1 increment by 1

em.persist를 하면 영속 컨텍스트에 올라가야되는데 영속 컨텍스트에 가려면 pk가 있어야한다. 따라서 먼저 시퀀스를 가져와야한다.
그래서 MEMBER_SEQ에서 먼저 가져온다.
그래서 em.persist하면 call next value for MEMBER_SEQ 로그가 찍히는 걸 볼 수 있다.
MEMBER_SEQ 의 다음 값을 가져와 지금 가져와야할 PK값을 가져오는 것이다.
따라서 가져와서 영속성 컨텍스트에 넣는다.
쿼리는 안날아가고 SEQ값만 가져오는것이다.
따라서 SEQUENCE는 버퍼링 방법이 가능하다.

그러면 네트워크를 계속 왔다갔다 해야되지 않냐 성능상 좋지 않지 않냐 그냥 다녀올바엔 INSERT로 날리는게 낫지 않냐 라는 고민을 하게 될 수있다.

이때 우리가 사용할 수 있는것이 allocationSize=50이다.

em.persist(memberA)
em.persist(memberB)
em.persist(memberC)

를 SEQUENCE 방식으로 한다고 가정하자.
그럼 persist할 때 마다 call next value를 해야하니 네트워크를 타야된다.
그래서 allocationSize=50를 이용해 50개를 미리 땡겨오는 것이다.

next value를 할 때 미리 50개 사이즈를 올려놓고, db에 미리 50개를 올려놓고 메모리에서 1씩 사용하고, 다쓰면 next call을 한번 호출 한다.
그럼 또 50개를 미리 올려놓는다. 이러면 이미 db엔 100번대가 되어있다. 그러면 나는 51번 부터 다시 메모리에서 1씩 사용한다.
여러 웹서버를 사용해도 동시성 이슈 없이 사용가능하다.

처음 allocationSize를 설정하고 persist를 호출하면 call next value가 두번 호출된다.
처음에는 50개씩 써야되는데 호출해봤더니 1인거
그래서 문제가 있나 하고 한번 더 호출 한거임 그래서 51이됨
한번은 그냥 더미로 호출한거임
em.persist(memberA)//1, 51 db
em.persist(memberB)//memory
em.persist(memberC)//memory

이렇게 되면 next call 2번 호출

이론적으론 allcationSize를 많이 크게 하면 좋은데 웹서버를 내릴때 이게 다 날아간다.
그럼 구멍이 생긴다. 구멍이 생겨도 괜찮긴 한데 낭비다
Table 전략도 마찬가지다.

미리 값을 올려두는 방식이기 때문에 웹서버 10대가 동시에 호출하면 값이 쭉올라감
자기가 미리 숫자를 확보하기 때문에 상관이 없음
동시성 문제 없음


간단한 쇼핑몰 예제

요구사항

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

기능 목록

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

도메인 모델 분석

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

테이블 설계

id가 직접적으로 들어가있다.

엔티티 설계와 매핑

작성 코드

Member

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")//대문자 소문자는 회사의 RULE에 따라
    private Long id;
    private String name;
    private String city;
    private String street;
    private String zipcode;

    /**
     * getter setter를 꼭 다 만들 필요는 없음
     * getter는 가급적 만드는 게 좋고 setter는 고민을 좀 해봐야함
     * setter는 유지보수 측면에서 별로이기 때문에 생성자를 통해 set하는 식으로
     * @return
     */
    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

@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

@Entity
@Table(name = "ORDERS") //ORDER로 했을 때 안되는 DB도 있음
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;
    }
}

OrderItem

@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

public enum OrderStatus {
    ORDER, CANCEL
}

문제점

잘 보면 이상한게 있음
order에서
Order order = em.find(Order.class, 1L)
Long memberId= order.getMemberId();
Member member=em.find(Member.clas, memberId);

id가 1L인 상품을 주문한 member를 찾고 싶을 땐 위와 같이 해야한다.
하지만 이는 객체지향 스럽지 못하다.

객체 지향스러운 코드가 되려면
Member findMember = order.getMember()
가 되어야할 것이다.
왜냐면 객체지향에선 참조로 값들을 쭉쭉 찾아갈 수 잇어야하기 때문이다.

우리가 한 설계는 관계형 db에 맞춘 설계이다.

따라서 참조값을 가져와야되는데 테이블의 외래키를 객체에 그대로 가져온다.
id만 가지고 있어서 참조가 다 끊기고 있다.
예를 들어 order안에 memberid만 가지고 있는게 아니라 member가 있어야한다.


출처 : 자바 ORM 표준 JPA 프로그래밍 - 기본편

profile
백엔드 개발자 지망생

0개의 댓글