Post

Hibernate 상속 매핑

1. 매핑

관계형 데이터베이스는 클래스 계층을 데이터베이스 테이블에 매핑하는 간단한 방법이 없다.

이를 해결하기 위해 JPA 사양은 여러 가지 전략을 제공한다.

  • MappedSuperclass - 부모 클래스, 엔터티가 될 수 없다.

  • Single Table - 공통 조상을 가진 서로 다른 클래스의 엔터티가 단일 테이블에 배치된다.

  • Joined Table - 각 클래스에는 테이블이 있으며, 하위 클래스 엔터티를 쿼리하려면 테이블을 조인해야 한다.

  • 클래스별 테이블 - 클래스의 모든 속성은 해당 테이블에 있으므로 조인이 필요하지 않다.

각 전략은 서로 다른 데이터베이스 구조를 만들어낸다.

엔티티 상속은 슈퍼클래스에 대한 쿼리를 실행할 때 다형성 쿼리를 사용하여 모든 서브클래스 엔티티를 검색할 수 있음을 의미한다.

Hibernate는 JPA 구현이므로 위에서 언급한 모든 기능뿐 아니라 상속과 관련된 몇 가지 Hibernate 특정 기능도 포함한다.

2. MappedSuperclass

MappedSuperclass 전략을 사용하면 상속은 클래스에서만 명확해지고 엔터티 모델에서는 명확하지 않다.

부모 클래스를 나타내는 Person 클래스를 만든다.

1
2
3
4
5
6
7
8
9
@MappedSuperclass
public class Person {

    @Id
    private long personId;
    private String name;

    // constructor, getters, setters
}

이 클래스에는 더 이상 @Entity 주석이 없다. 이 클래스는 그 자체로 데이터베이스에 저장되지 않기 때문이다.

Employee 하위 클래스를 추가한다.

1
2
3
4
5
@Entity
public class MyEmployee extends Person {
    private String company;
    // constructor, getters, setters 
}

데이터베이스에서 이는 하위 클래스의 선언된 필드와 상속된 필드에 대한 세 개의 열이 있는 하나의 MyEmployee 테이블에 해당한다.

이 전략을 사용하면 조상은 다른 엔터티와의 연결을 포함할 수 없다.

3. Single Table

Single Table 전략은 각 클래스 계층에 대해 하나의 테이블을 생성한다. JPA는 명시적으로 지정하지 않으면 기본적으로 이 전략을 선택한다.

슈퍼클래스에 @Inheritance 주석을 추가하여 사용하려는 전략을 정의할 수 있다.

1
2
3
4
5
6
7
8
9
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class MyProduct {
    @Id
    private long productId;
    private String name;

    // constructor, getters, setters
}

엔터티의 식별자도 슈퍼클래스에 정의되어 있다.

그런 다음 하위 클래스 엔터티를 추가할 수 있다.

1
2
3
4
@Entity
public class Book extends MyProduct {
    private String author;
}
1
2
3
4
@Entity
public class Pen extends MyProduct {
    private String color;
}

1) 판별자 값

모든 엔터티에 대한 레코드가 동일한 테이블에 있으므로 Hibernate에서는 이를 구별할 방법이 필요하다.

기본적으로 이 작업은 엔터티 이름을 값으로 갖는 DTYPE 이라는 ​​판별자 열을 통해 수행된다.

판별자 열을 사용자 지정하려면 @DiscriminatorColumn 주석을 사용할 수 있다.

1
2
3
4
5
6
7
@Entity(name="products")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="product_type", 
  discriminatorType = DiscriminatorType.INTEGER)
public class MyProduct {
    // ...
}

여기서 product_type 이라는 정수 열을 기준으로 MyProduct 하위 클래스 엔터티를 구별한다.

product_type 열에 대해 각 하위 클래스 레코드가 어떤 값을 가질지 Hibernate에 알려줘야 한다.

1
2
3
4
5
@Entity
@DiscriminatorValue("1")
public class Book extends MyProduct {
    // ...
}
1
2
3
4
5
@Entity
@DiscriminatorValue("2")
public class Pen extends MyProduct {
    // ...
}

Hibernate는 주석이 취할 수 있는 두 가지 다른 미리 정의된 값(null 및 not null)을 추가한다.

  • @DiscriminatorValue("null") - 판별자 값이 없는 모든 행이 이 주석이 있는 엔터티 클래스에 매핑됨을 의미한다. 이는 계층 구조의 루트 클래스에 적용될 수 있다.

  • @DiscriminatorValue("not null") – 엔티티 정의와 연관된 어떤 것과도 일치하지 않는 판별자 값을 갖는 모든 행은 이 주석이 있는 클래스에 매핑된다. 열 대신 Hibernate 특정 @DiscriminatorFormula 주석을 사용하여 차별화 값을 결정할 수도 있다.

1
2
3
4
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorFormula("case when author is not null then 1 else 2 end")
public class MyProduct { ... }

이 방법은 부모 엔터티를 쿼리할 때 하나의 테이블에만 액세스하면 되므로 다형성 쿼리 성능의 이점이 있다.

반면에 이는 하위 클래스 엔터티 속성에 더 이상 NOT NULL 제약 조건을 사용할 수 없다는 것을 의미한다.

4. Joined Table

이 방법을 사용하면 계층 구조의 각 클래스가 해당 테이블에 매핑된다. 모든 테이블에 반복적으로 나타나는 유일한 열은 식별자이며, 필요할 때 조인하는데 사용된다.

이 전략을 사용하는 슈퍼클래스를 만든다.

1
2
3
4
5
6
7
8
9
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Animal {
    @Id
    private long animalId;
    private String species;

    // constructor, getters, setters 
}

그러면 간단히 하위 클래스를 정의할 수 있다.

1
2
3
4
5
6
@Entity
public class Pet extends Animal {
    private String name;

    // constructor, getters, setters
}

두 테이블 모두 animalId 식별자 열이 있다.

Pet 엔터티의 기본 키에는 부모 엔터티의 기본 키에 대한 외래 키 제약 조건도 있다.

이 열을 사용자 지정하려면 @PrimaryKeyJoinColumn 주석을 추가할 수 있다.

1
2
3
4
5
@Entity
@PrimaryKeyJoinColumn(name = "petId")
public class Pet extends Animal {
    // ...
}

이 상속 매핑 방식의 단점은 엔터티를 검색하려면 테이블 간 조인이 필요하므로 레코드 수가 많은 경우 성능이 저하될 수 있다.

부모 클래스를 쿼리할 경우 모든 관련 자식과 조인하므로 조인 수가 더 많다. 따라서 레코드를 검색하려는 계층 구조가 높을수록 성능에 영향을 줄 가능성이 더 크다.

5. 클래스별 테이블

클래스별 테이블 전략은 각 엔터티를 해당 테이블에 매핑한다. 테이블에는 상속된 속성을 포함하여 엔터티의 모든 속성이 포함된다.

결과 스키마는 @MappedSuperclass를 사용한 스키마와 유사하다. 하지만 Table per Class는 실제로 부모 클래스에 대한 엔티티를 정의하여 결과적으로 연결 및 다형성 쿼리를 허용한다.

이 전략을 사용하려면 기본 클래스에 @Inheritance 주석만 추가하면 된다.

1
2
3
4
5
6
7
8
9
10
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Vehicle {
    @Id
    private long vehicleId;

    private String manufacturer;

    // standard constructor, getters, setters
}

그런 다음 표준적인 방법으로 하위 클래스를 만들 수 있다.

이는 상속 없이 각 엔터티를 매핑하는 것과 크게 다르지 않다. 기본 클래스를 쿼리할 때 구분이 명확해지며, 백그라운드에서 UNION 문을 사용하여 모든 하위 클래스 레코드도 반환한다.

UNION을 사용하면 이 전략을 선택할 때 성능이 저하될 수도 있다. 또 다른 문제는 더 이상 ID 키 생성을 사용할 수 없다는 것이다.

6. 다형성 쿼리

언급한 대로 기본 클래스를 쿼리하면 모든 하위 클래스 엔터티도 검색된다.

JUnit 테스트를 통해 이 동작이 실제로 어떻게 나타나는지 확인한다.

1
2
3
4
5
6
7
8
9
10
@Test
public void givenSubclasses_whenQuerySingleTableSuperclass_thenOk() {
    Book book = new Book(1, "1984", "George Orwell");
    session.save(book);
    Pen pen = new Pen(2, "my pen", "blue");
    session.save(pen);

    assertThat(session.createQuery("from MyProduct")
      .getResultList()).size()).isEqualTo(2);
}

예제에서 두 개의 Book과 Pen 객체를 생성한 다음, 그들의 슈퍼클래스인 MyProduct를 쿼리하여 두 개의 객체를 검색할 수 있는지 확인했다.

Hibernate는 엔티티가 아니지만 엔티티 클래스에 의해 확장되거나 구현되는 인터페이스나 기본 클래스를 쿼리할 수도 있다.

@MappedSuperclass 예제를 사용하여 JUnit 테스트를 확인한다.

1
2
3
4
5
6
7
8
9
10
@Test
public void givenSubclasses_whenQueryMappedSuperclass_thenOk() {
    MyEmployee emp = new MyEmployee(1, "john", "baeldung");
    session.save(emp);

    assertThat(session.createQuery(
      "from com.baeldung.hibernate.pojo.inheritance.Person")
      .getResultList())
      .hasSize(1);
}

이것은 @MappedSuperclass 이든 아니든 모든 슈퍼클래스나 인터페이스에 적용된다. 일반적인 HQL 쿼리와의 차이점은 Hibernate에서 관리하는 엔티티가 아니기 때문에 완전히 정규화된 이름을 사용해야 한다는 것이다.

이 유형의 쿼리에서 하위 클래스가 반환되는 것을 원하지 않으면 Hibernate @Polymorphism 주석을 정의에 EXPLICIT 유형으로 추가하기만 하면 된다.

1
2
3
@Entity
@Polymorphism(type = PolymorphismType.EXPLICIT)
public class Bag implements Item { ...}

이 경우, Items를 쿼리할 때 Bag 클래스가 명시적이기 때문에 Item 엔터티를 확인할 수 없다는 예외가 발생한다. 이러한 제한으로 인해 @Polymorphism(type = PolymorphismType.IMPLICIT)이 있는 다른 클래스를 정의해야 한다. Items를 쿼리할 때 해당 인터페이스를 구현하는 두 객체에서 레코드를 검색한다.

[출처 및 참고]

This post is licensed under CC BY 4.0 by the author.