(JPA) – 엔터티 상속 매핑

엔티티 상속 매핑

관계형 데이터베이스에서 더 작은 그룹으로 나누어 상위 속성 간에 관리해야 하는 속성이 있을 때 상위 유형 또는 하위 유형 단위로 상위 하위 유형 테이블을 설계하는 것이 한 가지 방법입니다.

이를 지원하기 위해, JPA는 객체 상속을 사용합니다.

@Inheritance상위 하위 유형 관계를 설계하는 데 사용할 수 있습니다.

1. 주요 사항

1-1. @유산

상위 엔티티(상위 유형)에 사용되는 주석으로 상속 전략 선택당신은 할 수 있습니다.

public @interface Inheritance {

    /** The strategy to be used for the entity inheritance hierarchy. */
    InheritanceType strategy() default SINGLE_TABLE;
}

예)

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@Getter
public abstract class Item {

    @Id @GeneratedValue
    @Column(name = "item_id")
    private Long id;
    private String name;
    private int price;
    private int stockQuantity;
}

엔터티 상속 계층(수퍼 하위 유형 관계)에 대해 세 가지 전략을 사용할 수 있습니다.

  • SINGLE_TABLE (기본값) – 클래스 계층 구조 전략당 단일 테이블
  • TABLE_PER_CLASS – 각 구현 엔터티 클래스에 대한 테이블 전략
  • JOINED – 상위 엔터티 클래스에 연결된 각 하위 클래스에 대한 테이블 전략
public enum InheritanceType { 

    /** A single table per class hierarchy. */
    SINGLE_TABLE, 

    /** A table per concrete entity class. */
    TABLE_PER_CLASS, 

    /** 
     * A strategy in which fields that are specific to a 
     * subclass are mapped to a separate table than the fields 
     * that are common to the parent class, and a join is 
     * performed to instantiate the subclass.
     */
    JOINED 
}

1-2. @DiscriminatorColumn

알려진 상위 엔터티의 하위 엔터티 구별 열 설명을 추가합니다.

기본 열 이름 DTYPEname 속성을 사용하여 열 이름을 변경할 수 있습니다.

@Entity
@Getter
@NoArgsConstructor
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "ITEM_TYPE") // 구분 컬럼 ITEM_TYPE 추가
public abstract class Item {

    @Id
    @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;
    private String name;
    private int price;
    private int stockQuantity;

    public Item(String name, int price, int stockQuantity) {
        this.name = name;
        this.price = price;
        this.stockQuantity = stockQuantity;
    }
}


1-3. @DisciminatorValue

분배하다 컬럼에 저장된 subentity의 이름을 변경할 수 있는 주석보지 않았다.

해당 주석을 사용하지 않는 경우 기본적으로 하위 엔터티 이름이 저장됩니다.

@Entity
@DiscriminatorValue(value = "M")
@NoArgsConstructor
public class Movie extends Item{

    private String director;
    private String actor;

    public Movie(String name, int price, int stockQuantity, String director, String actor) {
        super(name, price, stockQuantity);
        this.director = director;
        this.actor = actor;
    }
}


2. 엔터티 상속 계층 구조 전략

2-1.JOINED(조인 전략)

조인 전략은 공용 필드가 상위 엔티티(수퍼 유형)에 의해 관리되고 하위 엔티티(하위 유형)가 상위 유형에 연결되는 전략입니다.

보지 않았다.

조인 정책을 사용하여 저장 공간을 효율적으로 사용하고 관계에서 생성된 복잡한 참조 무결성 규칙을 적용합니다.

그러나 단점은 검색 시 조인을 사용하고 저장 시 삽입 쿼리를 두 번 수행해야 하므로 성능이 저하된다는 점이다.


예)

// 부모 엔티티
@Entity
@Getter
@NoArgsConstructor
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public abstract class Item {

    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;
    private String name;
    private int price;
    private int stockQuantity;

    public Item(String name, int price, int stockQuantity) {
        this.name = name;
        this.price = price;
        this.stockQuantity = stockQuantity;
    }
}

// 자식 엔티티
@Entity
@NoArgsConstructor
public class Movie extends Item{

    private String director;
    private String actor;

    public Movie(String name, int price, int stockQuantity, String director, String actor) {
        super(name, price, stockQuantity);
        this.director = director;
        this.actor = actor;
    }
}

참고로 Movie 엔터티를 저장하면 자동으로 Item 엔터티가 저장됩니다.

@Test
public void item_save_테스트() {
    movieRepository.save(
            new Movie("영화이름", 10000, 10, "감독이름", "배우이름")
    );
}

Hibernate: insert into item (name, price, stock_quantity, dtype, item_id) values (?, ?, ?, 'Movie', ?)
Hibernate: insert into movie (actor, director, item_id) values (?, ?, ?)


2-2. SINGLE_TABLE(단일 테이블 전략)

단일 테이블 전략은 단순히 상위 테이블에 있는 하위 엔터티의 모든 필드를 관리합니다.

보지 않았다.

테이블에 있는 쿼리로 인해 쿼리 성능이 빠르고 쿼리가 단순하지만 하위 엔터티의 필드는 null 값을 허용하므로 테이블에 저장된 엔터티가 많을 경우 성능 문제가 발생할 수 있습니다.


예)

// 부모 엔티티
@Entity
@Getter
@NoArgsConstructor
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
public abstract class Item {

    @Id
    @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;
    private String name;
    private int price;
    private int stockQuantity;

    public Item(String name, int price, int stockQuantity) {
        this.name = name;
        this.price = price;
        this.stockQuantity = stockQuantity;
    }
}

// 자식 엔티티
@Entity
@NoArgsConstructor
public class Movie extends Item{

    private String director;
    private String actor;

    public Movie(String name, int price, int stockQuantity, String director, String actor) {
        super(name, price, stockQuantity);
        this.director = director;
        this.actor = actor;
    }
}

동영상을 저장할 때 다음 쿼리가 실행됩니다.

@Test
public void item_테스트() {
    movieRepository.save(
            new Movie("영화이름", 10000, 10, "감독이름", "배우이름")
    );
}

Hibernate: insert into item (name, price, stock_quantity, actor, director, dtype, item_id) values (?, ?, ?, ?, ?, 'Movie', ?)


2-3. TABLE_PER_CLASS(엔티티 클래스 구현당 테이블 전략)

이 전략은 권장되지 않습니다.

ORM의 관점에서는 상속을 사용하지만 DB의 관점에서는 부모 엔터티의 모든 필드를 상속받은 각각의 자식 엔터티는 자식 테이블을 생성하고, 부모 테이블을 생성하지 않으며, DTYPE 테이블 관계가 없습니다.

따라서 ORM 개발자도 DB 개발자도 권장하지 않습니다.


예)

// 부모 엔티티
@Entity
@Getter
@NoArgsConstructor
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {

    @Id
    @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;
    private String name;
    private int price;
    private int stockQuantity;

    public Item(String name, int price, int stockQuantity) {
        this.name = name;
        this.price = price;
        this.stockQuantity = stockQuantity;
    }
}

// 자식 엔티티
@Entity
@NoArgsConstructor
public class Movie extends Item{

    private String director;
    private String actor;

    public Movie(String name, int price, int stockQuantity, String director, String actor) {
        super(name, price, stockQuantity);
        this.director = director;
        this.actor = actor;
    }
}

자식 엔터티를 저장하면 다음과 같이 구분할 수 있는 DTYPE이 없음을 알 수 있습니다.

따라서 특정 항목을 검색할 때는 모든 하위 테이블을 UNION으로 검색하기 때문에 성능이 좋지 않습니다.

@Test
public void item_테스트() {
    movieRepository.save(
            new Movie("영화이름", 10000, 10, "감독이름", "배우이름")
    );
    albumRepository.save(
            new Album("앨범 이름", 20000, 20, "가수이름", "1집")
    );
}