1. 概要

リレーショナルデータベースには、クラス階層をデータベーステーブルにマップする簡単な方法がありません。

これに対処するために、JPA仕様はいくつかの戦略を提供します。

  • MappedSuperclass –親クラス、エンティティにすることはできません
  • 単一のテーブル–共通の祖先を持つ異なるクラスのエンティティが単一のテーブルに配置されます。
  • 結合されたテーブル–各クラスにはテーブルがあり、サブクラスエンティティをクエリするには、テーブルを結合する必要があります。
  • クラスごとのテーブル–クラスのすべてのプロパティがテーブルにあるため、結合は必要ありません。

各戦略により、データベース構造が異なります。

エンティティの継承とは、スーパークラスをクエリするときに、ポリモーフィッククエリを使用してすべてのサブクラスエンティティを取得できることを意味します。

HibernateはJPA実装であるため、上記のすべてと、継承に関連するHibernate固有の機能がいくつか含まれています。

次のセクションでは、利用可能な戦略について詳しく説明します。

2. MappedSuperclass

MappedSuperclass 戦略を使用すると、継承はクラスでのみ明らかになり、エンティティモデルでは明らかになりません。

親クラスを表すPersonクラスを作成することから始めましょう。

@MappedSuperclass
public class Person {

    @Id
    private long personId;
    private String name;

    // constructor, getters, setters
}

このクラスには@Entityアノテーションがないことに注意してください。これは、それ自体がデータベースに永続化されないためです。

次に、Employeeサブクラスを追加しましょう。

@Entity
public class MyEmployee extends Person {
    private String company;
    // constructor, getters, setters 
}

データベースでは、これは、サブクラスの宣言されたフィールドと継承されたフィールドの3つの列を持つ1つのMyEmployeeテーブルに対応します。

この戦略を使用している場合、祖先に他のエンティティとの関連付けを含めることはできません。

3. 単一のテーブル

シングルテーブルストラテジーは、クラス階層ごとに1つのテーブルを作成します。明示的に指定しない場合、JPAはデフォルトでこのストラテジーも選択します。

スーパークラスに@Inheritanceアノテーションを追加することで、使用する戦略を定義できます。

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class MyProduct {
    @Id
    private long productId;
    private String name;

    // constructor, getters, setters
}

エンティティの識別子もスーパークラスで定義されます。

次に、サブクラスエンティティを追加できます。

@Entity
public class Book extends MyProduct {
    private String author;
}
@Entity
public class Pen extends MyProduct {
    private String color;
}

3.1. 弁別器の値

すべてのエンティティのレコードが同じテーブルにあるため、Hibernateにはそれらを区別する方法が必要です。

デフォルトでは、これは、エンティティの名前を値として持つDTYPEと呼ばれる識別子列を介して行われます。

弁別子列をカスタマイズするには、@DiscriminatorColumnアノテーションを使用できます。

@Entity(name="products")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="product_type", 
  discriminatorType = DiscriminatorType.INTEGER)
public class MyProduct {
    // ...
}

ここでは、MyProductサブクラスエンティティをproduct_typeというinteger列で区別することを選択しました。

次に、各サブクラスレコードがproduct_type列に対してどのような値を持つかをHibernateに通知する必要があります。

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

Hibernateは、注釈が取ることができる他の2つの事前定義された値を追加します— nullおよびnotnull

  • @DiscriminatorValue( “null”)は、ディスクリミネーター値のないすべての行が、このアノテーションを使用してエンティティクラスにマップされることを意味します。 これは、階層のルートクラスに適用できます。
  • @DiscriminatorValue( “not null”) –エンティティ定義に関連付けられているもののいずれとも一致しないディスクリミネーター値を持つ行は、このアノテーションを使用してクラスにマップされます。

列の代わりに、Hibernate固有の @DiscriminatorFormula アノテーションを使用して、差別化値を判別することもできます。

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

この戦略には、親エンティティをクエリするときに1つのテーブルにアクセスするだけでよいため、ポリモーフィッククエリのパフォーマンスという利点があります。

一方、これは、サブクラスエンティティのプロパティにNOTNULL制約を使用できなくなったことも意味します。

4. 参加テーブル

この戦略を使用して、階層内の各クラスがそのテーブルにマップされます。すべてのテーブルに繰り返し表示される唯一の列は識別子であり、必要に応じてそれらを結合するために使用されます。

この戦略を使用するスーパークラスを作成しましょう。

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Animal {
    @Id
    private long animalId;
    private String species;

    // constructor, getters, setters 
}

次に、サブクラスを簡単に定義できます。

@Entity
public class Pet extends Animal {
    private String name;

    // constructor, getters, setters
}

両方のテーブルには、animalId識別子列があります。

Pet エンティティの主キーには、親エンティティの主キーに対する外部キー制約もあります。

この列をカスタマイズするために、@PrimaryKeyJoinColumnアノテーションを追加できます。

@Entity
@PrimaryKeyJoinColumn(name = "petId")
public class Pet extends Animal {
    // ...
}

この継承マッピング方法の欠点は、エンティティの取得にテーブル間の結合が必要になることです。これにより、多数のレコードのパフォーマンスが低下する可能性があります。

親クラスは関連するすべての子と結合するため、親クラスにクエリを実行すると結合の数が多くなります。したがって、レコードを取得する階層の上位にあるほど、パフォーマンスが影響を受ける可能性が高くなります。

5. クラスごとのテーブル

クラスごとのテーブル戦略は、各エンティティをそのテーブルにマップします。このテーブルには、継承されたものを含む、エンティティのすべてのプロパティが含まれています。

結果のスキーマは、@MappedSuperclassを使用したものと似ています。 ただし、Table per Classは実際に親クラスのエンティティを定義し、結果として関連付けとポリモーフィッククエリを可能にします。

この戦略を使用するには、@Inheritanceアノテーションを基本クラスに追加するだけです。

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Vehicle {
    @Id
    private long vehicleId;

    private String manufacturer;

    // standard constructor, getters, setters
}

次に、標準的な方法でサブクラスを作成できます。

これは、継承せずに各エンティティを単にマッピングすることとそれほど違いはありません。 基本クラスを照会すると、バックグラウンドで UNION ステートメントを使用して、すべてのサブクラスレコードも返すという違いが明確になります。

UNIONを使用すると、この戦略を選択するときにパフォーマンスが低下する可能性もあります。もう1つの問題は、IDキーの生成を使用できなくなることです。

6. ポリモーフィッククエリ

前述のように、基本クラスをクエリすると、すべてのサブクラスエンティティも取得されます。

JUnitテストでこの動作を実際に見てみましょう。

@Test
public void givenSubclasses_whenQuerySuperclass_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()).hasSize(2);
}

この例では、2つのBookオブジェクトとPenオブジェクトを作成し、それらのスーパークラス MyProduct を照会して、2つのオブジェクトを取得することを確認しました。

Hibernateは、エンティティではないがエンティティクラスによって拡張または実装されているインターフェイスまたは基本クラスをクエリすることもできます。

@MappedSuperclassの例を使用してJUnitテストを見てみましょう。

@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で定義に追加するだけで済みます。

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

この場合、 Items を照会すると、Bagレコードは返されません。

7. 結論

この記事では、Hibernateで継承をマッピングするためのさまざまな戦略を示しました。

例の完全なソースコードは、GitHubにあります。