BackEnd/ORM(JPA)

고급 매핑

상속관계 매핑

더보기
  • 관계형 데이터베이스는 상속 관계 X
  • 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사
  • 상속관계 매핑: 객체의 상속과 구조와 DB의 슈퍼타입 서브타입 관계를 매핑
  • 슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법
  • 각각 테이블로 변환 -> 조인 전략
  • 통합 테이블로 변환 -> 단일 테이블 전략
  • 서브타입 테이블로 변환 -> 구현 클래스마다 테이블 전략

 

주요 어노테이션

  • @Inheritance(starategy=InheritanceType.XXX)
    • JOINED: 조인 전략
    • SINGLE_TABLE: 단일 테이블 전략
    • TABLE_PER_CLASS: 구현 클래스마다 테이블 전략
  • @DiscriminatorColumn(name="DTYPE") (default column name => DTYPE)
  • @DiscriminatorValue("XXX") (default = entity name)

 

조인 전략

  • 장점
    • 테이블 정규화
    • 외래키 참조 무결성 제약조건 활용가능
    • 저장공간 효율화
  • 단점
    • 조회시 조인을 많이 사용, 성능 저하
    • 조회 쿼리가 복잡함
    • 데이터 저장시 INSERT SQL 2번 호출

 Example Codes

package hellojpa;

import javax.persistence.*;

@Entity
@Inheritance(strategy = InheritanceType.JOINED)	// JOIN전략을 사용할때
@DiscriminatorColumn // DTYPE 컬럼을 ITEM Table에 추가
public class Item {

    @Id @GeneratedValue
    private Long id;

    private String name;

    private int price;
    
    // getters / setters
}
@Entity
@DiscriminatorValue("A") // 상속받는 subType 테이블의 이름을 A로 변경 | 기본값은 entity name
public class Movie extends Item{

    private String director;
    private String actor;

    // getters / setters
}

 

단일 테이블 전략

  • 장점
    • 조인이 필요 없으므로 일반적으로 조회 성능이 빠름
    • 조회 쿼리가 단순함
  • 단점
    • 자식 엔티티가 매핑한 컬럼은 모두 null 허용
    • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 상황에 따라서 조회 성능이 오히려 느려질 수 있다.

 

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
public class Item {

    @Id @GeneratedValue
    private Long id;

    private String name;

    private int price;

    // getters / setters
}

 

 

 

구현 클래스마다 테이블 전략

 

  • 이 전략은 데이터베이스 설계자와 ORM 전문가 둘 다 추천X
  • 장점
    • 서브 타입을 명확하게 구분해서 처리할 때 효과적
    • not null 제약조건 사용 가능
  • 단점
    • 여러 자식 테이블을 함께 조회할 때 성능이 느림(UNION SQL 필요)
    • 자식 테이블을 통합해서 쿼리하기 어려움
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@DiscriminatorColumn // <- 불필요 어짜피 별개 테이블
public abstract class Item {

    @Id @GeneratedValue
    private Long id;

    private String name;

    private int price;
    
    // getters / setters
}

 

 

@MappedSuperclass

더보기
  • 공통 매핑 정보가 필요할 때 사용(id, name)
  • 상속관계 매핑X
  • 엔티티X, 테이블과 매핑X
  • 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공
  • 조회, 검색 불가(em.find(BaseEntity) 불가)
  • 직접 생성해서 사용할 일이 없으므로 추상 클래스 권장
  • 테이블과 관계 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할
  • 주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용
  • 참고: @Entity 클래스는 엔티티나 @MappedSuperclass로 지정한 클래스만 상속 가능
@MappedSuperclass
public abstract class BaseEntity {

    // mapping 정보를 위한 Superclass 공통 field를 추가하기 위해 사용
    private String createdBy;
    private LocalDateTime createdDate;
    private String lastModifiedBy;
    private LocalDateTime lastModifiedDate;

	// getters / setters   
}

 

 

복합 키와 식별 관계 매핑

더보기

식별 관계 vs 비식별 관계

데이터베이스 테이블 사이에 관계는 외래 키가 기본 키에 포함되는지 여부에 따라 식별 관계와 비식별 관계로 구분한다. 두 관계의 특징을 이해하고 각각을 어떻게 매핑하는지 알아보자.

식별관계(Identifying Relationship)

부모 테이블의 기본 키를 내려 받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 관계

비식별 관계(Non-Identifying Relationship) 

부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계

  • 필수적 비식별 관계(Mandatory) : 외래 키에 NULL을 허용하지 않는다. 연관 관계를 필수적으로 맺어야 한다.
  • 선택적 비식별 관계(Optional) : 외래 키에 NULL을 허용한다. 연관관계를 맺을지 말지 선택할 수 있다.

 

복합 키

 JPA는 복합 키를 지원하기 위해 @IdClass와 @EmbeddedId 2가지 방법을 제공한다.
@IdClass는 관계형 데이터베이스에 가까운 방법이다.
@EmbeddedId는 좀 더 객체지향에 가까운 방법이다.

 

@IdClass

복합 키 테이블은 비식별 관계고 PARENT는 복합 기본 키를 사용한다. (객체의 상속과는 무관) 

부모 클래스 Code

@Entity
public class Parent {
    
    @Id
    @Column(name = "PARENT_ID1")
    private String id1; // ParentId.id1과 연결
    
    @Id
    @Column(name = "PARENT_ID2")
    private String id2; // ParentId.id2와 연결
    
    private String name;
    
    // getters and setters
}

식별자 클래스 Code

public class ParentId implements Serializable {

    private String id1; // Parent.id1 매핑
    private String id2; // Parent.id2 매핑

    public ParentId() {}

    public ParentId(String id1, String id2) {
        this.id1 = id1;
        this.id2 = id2;
    }

    @Override
    public boolean equals(Object obj) {...}

    @Override
    public int hashCode() {...}
}
Parent parent = new Parent();
parent.setId("myId1");	// 식별자
parent.setId("myId2);	// 식별자
parent.setName("parentName");
em.persist(parent);

//복합키 조회 코드
ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);

자식 클래스 추가

@Entity
public class Child {

    @Id
    private String id;

    @ManyToOne
    @JoinColumns({@JoinColumn(name = "PARENT_ID1", referencedColumnName = "PARENT_ID1"),
    @JoinColumn(name = "PARENT_ID2", referencedColumnName = "PARENT_ID2")})
    private Parent parent;
    
    // getters and setters and constructor
}

 @IdClass는 다음 조건을 만족해야 한다.

  • 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다.
    • (Parent.id1 -> parent.id1, Parent.id2 -> parent.id2)
  • Serializable 인터페이스를 구현해야 한다.
  • equals, hashCode를 구현해야 한다.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public이어야 한다.

 

@EmbeddedId

@IdClass가 데이터베이스에 맞춘 방법이라면 @EmbeddedId는 좀 더 객체지향적인 방법

 

@Entity
public class Parent {

    @EmbeddedId
    private String id;
    private String name;

    // getters and setters
}

 @IdClass와는 다르게 @EmbeddedId를 적용한 식별자 클래스는 식별자 클래스에 기본 키를 직접 매핑한다.

@Embeddable
public class ParentId implements Serializable {
    
    @Column(name = "PARENT_ID1")
    private String id1;
    @Column(name = "PARENT_ID2")
    private String id2;

    // equals and hashCode 구현
}

 

 @EmbeddedId를 적용한 식별자 클래스는 다음 조건을 만족해야 한다.

  • @Embeddable 어노테이션을 붙여주어야 한다.
  • Serializable 인터페이스를 구현해야 한다.
  • equals, hashCode를 구현해야 한다.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public이어야 한다.

 

복합 키와 equals(), hashCode()

 

복합 키는 equals()와 hashCode()를 필수로 구현해야 한다.

ParentId id1 = new ParentId();
id1.setId1("myId1");
id1.setId2("myId2");

ParentId id2 = new ParentId();
id2.setId1("myId1");
id2.setId2("myId2");

id1.equals(id2) -> ?

 equals()를 적절히 오버라이딩했다면 참이지만 equals()를 적절히 오버라이딩하지 않았다면 결과는 거짓이다. 왜냐하면 자바의 모든 클래스는 기본으로 Object 클래스를 상속받는데 이 클래스가 제공하는 기본 equals()는 인스턴스 참조 값 비교인 == 비교 (동일성비교)를 하기 때문이다. 영속성 컨텍스트는 엔티티의 식별자를 키로 사용해서 엔티티를 관리한다. 그리고 식별자 비교에 equals()와 hashCode()를 사용한다. 고로 동등성(equals 비교)이 지켜지지 않으면 영속성 컨텍스트가 엔티티를 관리하는데 심각한 문제가 발생한다.

 

 @IdClass vs @EmbeddedId

각자 일장일단이 있다.

@EmbeddedId가 @IdClass와 비교해서 더 객체지향적이고 중복도 없어서 좋아보이긴 하나 특정 상황에 JPQL이 조금 더 길어질 수 있다.

참고
복합 키에는 @GenerateValue를 사용할 수 없다. 복합 키를 구성하는 여러 칼럼 중 하나에도 사용할 수 없다.

 

복합 키 : 식별 관계 매핑

 

식별 관계에서 자식 테이블은 부모 테이블의 기본 키를 포함해서 복합 키를 구성해야 하므로 @IdClass나 @EmbeddedId를 사용해서 식별자를 매핑해야 한다.

부모, 자식, 손자까지 계속 기본키를 전달하는 식별 관계
//부모
@Entity
public class Parent {

    @Id @Column(name = "PARENT_ID")
    private String id;
    private String name;

    // getters and setters
}

// 자식
@Entity
@IdClass(ChildId.class)
public class Child {

    @Id
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    private Parent parent;

    @Id @Column(name = "CHILD_ID")
    private String childId;

    private String name;

    // getters and setters
}

// 자식ID
public class ChildId implements Serializable {

    private String parent;  // Child.parent 매핑
    private String childId; // Child.childId 매핑

    // equals, hashCode ...
}

// 손자
@Entity
@IdClass(GrnadChildId.class)
public class GrandChild {

    @Id
    @ManyToOne
    @JoinColumns({@JoinColumn(name = "PARENT_ID"),@JoinColumn(name = "CHILD_ID")})
    private Child child;

    @Id @Column(name = "GRNADCHILD_ID")
    private String id;
    private String name;

    // ...
}

// 손자 ID
public class GrandChildId implements Serializable {

    private ChildId child;  // GrnadChild.child 매핑
    private String id;  // GrandChild.id 매핑

    // equals, hashCode ...
}

 

 @EmbeddedId와 식별 관계 

 @EmbeddedId로 식별 관계를 구성할 때는 @MapsId를 사용해야 한다.

//부모
@Entity
public class Parent {

    @Id @Column(name = "PARENT_ID")
    private String id;
    private String name;
}

//자식
@Entity
public class Child {

    @EmbeddedId
    private ChildId id;
    
    @MapsId("parentId") //ChildId.parentId 매핑
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    public Parent parent;

    private String name;
    
}

// 자식 ID
@Embeddable
public class ChildId implements Serializable {

    private String parentId;    //@MapsId("parentId")로 매핑

    @Column(name = "CHILD_ID")
    private String id;

    // equals, hashCode ...
}

// 손자
@Entity
public class GrandChild {

   @EmbeddedId
   private GrandChildId id;

   @MapsId("child") //GrandChildId.childId 매핑
   @ManyToOne
   @JoinColumns({@JoinColumn(name = "PARENT_ID"),@JoinColumn(name = "CHILD_ID")})
   private Child child;

   private String name;

   // ...
}

// 손자 ID
@Embeddable
public class GrandChildId implements Serializable {

    private ChildId childId;  // @MapsId("childId")로 매핑
    @Column(name = "GRANDCHILD_ID")
    private String id;  // GrandChild.id 매핑

    // equals, hashCode ...
}
  • @IdClass와 다르게 @MapsId를 사용
  • @MapsId는 외래키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 의미
  • @MapsId의 속성 값은 @EmbeddedId를 사용한 식별자 클래스의 기본 키 필드를 지정하면 된다.

 

비식별 관계로 구현

식별 관계의 복합 키 코드와 비교하여 매핑도 쉽고 코드도 단순

복합키가 없으므로 복합 키 클래스를 만들지 않아도 된다.

//부모
@Entity
public class Parent {

    @Id @GeneratedValue
    @Column(name = "PARENT_ID")
    private String id;
    private String name;

    // getters and setters
}

//자식
@Entity
public class Child {

    @Id @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    public Parent parent;

    private String name;
    // ...
}

// 손자
@Entity
public class GrandChild {

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

   private String name;

   @ManyToOne
   @JoinColumn(name = "CHILD_ID")
   private Child child;
   
   // ...
}

 

일대일 식별 관계

 일대일 식별 관계는 자식 테이블의 기본 키 값으로 부모 테이블의 기본 키 값만 사용한다.

 

//부모
@Entity
public class Board {

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

    private String title;

    @OneToOne(mappedBy = "board")
    private BoardDetail boardDetail;
}

// 자식
@Entity
public class BoardDetail {

    @Id
    private Long boardId;

    @MapsId // BoardDetail.boardId 매핑
    @OneToOne
    @JoinColumn(name = "BOARD_ID")
    private Board board;

    private String content;
    // ...
}

 

식별, 비식별 관계의 장단점

데이터베이스 설계 관점에서 다음 이유로 비식별 관계를 선호한다.

  • 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하며 자식 테이블의 기본 키 컬럼이 점점 늘어난다. 이는 조인시 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있다.
  • 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많다.
  • 비즈니스 요구사항은 시간이 지남에 따라 언젠가 변하는데 식별 관계의 자연 키 컬럼들이 자식에 손자까지 전파되면 변경이 힘들다.
  • 부모 테이블의 기본 키를 자식 테이블의 기본 키로 사용하는 식별 관계는 테이블 구조가 유연하지 못함

객체 관계 매핑 관점에서 다음과 같은 이유로 비식별 관계를 선호한다.

  • JPA에서 복합 키는 별도의 복합 키 클래스를 만들어 사용해야 함.
  • 비식별 관계의 기본 키는 주로 대리 키를 사용하는데 JPA는 @GenerateValue처럼 대리키 생성을 위한 편리한 방법을 제공한다.

식별 관계의 장점

  • 기본 키 인덱스를 활용하기 좋다(상위 테이블에서 정의해놓은 인덱스를 그대로 사용할 수 있다.)
    • ex) 부모 아이디가 A인 모든 자식 조회 / 부모 아이디가 A고 자식 아이디가 B인 자식 조회
ORM 신규 프로젝트 진행시 추천방법
(필수적[Not Null])비식별 관계 사용 / 기본 키는 Long 타입의 대리 키를 사용

 

조인 테이블

더보기

데이터베이스 테이블의 연관관계 설계 방법은 크게 2가지다.

 

  • 조인 컬럼 사용(외래 키)
  • 조인 테이블 사용(테이블 사용)

조인 컬럼 사용

외래 키에 null을 허용하는 선택적 비식별 관계는 회원과 사물함을 조인할 때 외부 조인(OUTER JOIN)을 사용해야 한다. 실수로 내부 조인을 사용하면 사물함과 관계가 없는 회원은 조회되지 않는다. 그리고 회원과 사물함이 아주 가끔 관계를 맺는다면 외래 키 값 대부분이 null로 저장되는 단점이 있다. 

 

조인 테이블 사용

 

이 방법은 조인 테이블이라는 별도의 테이블을 사용해서 연관관계를 관리한다. MEMBER와 LOCKER에는 연관 관계를 관리하기 위한 외래 키 컬럼이 없다. 대신 조인 테이블을 하나 추가해야 한다는 단점이 있다.

 

일대일 조인 테이블

 

일대다 조인 테이블

 

다대일 조인 테이블

 

다대다 조인 테이블

 

엔티티 하나에 여러 테이블 매핑

 

정리

 

실전예제 - 상속관계 매핑

더보기

요구사항 추가

  • 상품의 종류는 음반, 도서, 영화가 있고 이후 더 확장될 수 있다.
  • 모든 데이터는 등록일과 수정일이 필수다.

 

도메인 모델

 

도메인 모델 상세

 

테이블 설계

식별 관계 상속

@Entity
@Inheritance(strategy = InheritanceType.JOINED)	// 상속관계 부모
@DiscriminatorColumn	// DType 설정
public abstract class Item extends BaseEntity{

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

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

    @ManyToMany(mappedBy = "items")
    private List<Category> categories = new ArrayList<>();

    // getters and setters
}
@Entity
public class Book extends Item{

    private String author;
    private String isbn;

  // getters and setters
}

 

 공통 속성 상속을 위한 BaseEntity

@MappedSuperclass
public abstract class BaseEntity {

    // mapping 정보를 위한 Superclass 공통 field를 추가하기 위해 사용
    private String createdBy;
    private LocalDateTime createdDate;
    private String lastModifiedBy;
    private LocalDateTime lastModifiedDate;

    // getters and setters 
}

 

 

 


References By

참조 문헌 : ORM 표준 JPA 프로그래밍

 

https://incheol-jung.gitbook.io/docs/study/jpa/7

 

7장 고급 매핑 - Incheol's TECH BLOG

 

incheol-jung.gitbook.io

 

'BackEnd > ORM(JPA)' 카테고리의 다른 글

값 타입  (0) 2023.01.09
프록시와 연관관계 관리  (0) 2023.01.04
다양한 연관관계 매핑  (0) 2022.12.31
연관관계 매핑 기초  (0) 2022.12.30
엔티티 매핑  (0) 2022.12.29