Hibernateによる動的マッピング

1前書き

この記事では、 @ Formula @ Where @ Filter 、および @ Any のアノテーションを使用して、Hibernateの動的マッピング機能について説明します。

HibernateはJPA仕様を実装していますが、ここで説明されているアノテーションはHibernateでのみ利用可能で、他のJPA実装に直接移植することはできません。

2プロジェクト設定

機能を実証するには、Hibernate-coreライブラリとバッキングH2データベースだけが必要です。

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>5.2.12.Final</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.194</version>
</dependency>

hibernate-core ライブラリの現在のバージョンについては、https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22org.hibernate%22%20AND%20a%3A%22hibernateに進んでください-core%22[メイヴン中央]。

3 @ Formula を使用して計算列

他のプロパティに基づいてエンティティフィールドの値を計算したいとします。そのための1つの方法は、私たちのJavaエンティティで計算された読み取り専用フィールドを定義することです。

@Entity
public class Employee implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private long grossIncome;

    private int taxInPercents;

    public long getTaxJavaWay() {
        return grossIncome **  taxInPercents/100;
    }

}

明らかな欠点は、 ゲッターによってこの仮想フィールドにアクセスするたびに 再計算しなければならないことです。

データベースからすでに計算済みの値を取得する方がはるかに簡単です。これは @ Formula アノテーションを使って行うことができます。

@Entity
public class Employee implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private long grossIncome;

    private int taxInPercents;

    @Formula("grossIncome **  taxInPercents/100")
    private long tax;

}
  • @ Formula を使用すると、サブクエリを使用したり、ネイティブデータベース関数やストアドプロシージャを呼び出したり、基本的にこのフィールドのSQL select句の構文を壊さないようなことはすべて実行できます。

Hibernateは提供したSQLを解析して正しいテーブルとフィールドのエイリアスを挿入するのに十分スマートです。注意すべき注意点は、注釈の値は生のSQLであるため、マッピングがデータベースに依存する可能性があることです。

また、値はエンティティがデータベースから取得されたときに計算されることにも注意してください。したがって、エンティティを永続化または更新しても、エンティティがコンテキストから削除されて再びロードされるまで、値は再計算されません。

Employee employee = new Employee(10__000L, 25);
session.save(employee);

session.flush();
session.clear();

employee = session.get(Employee.class, employee.getId());
assertThat(employee.getTax()).isEqualTo(2__500L);

4 @ Where を使用したエンティティのフィルタリング

エンティティを要求するたびにクエリに追加の条件を付けたいとします。

たとえば、「ソフト削除」を実装する必要があります。つまり、エンティティはデータベースから削除されることはなく、 boolean フィールドで削除済みとしてマークされるだけです。

アプリケーション内の既存および将来のすべてのクエリには細心の注意を払う必要があります。この追加条件をすべてのクエリに提供する必要があります。幸い、Hibernateはこれを一箇所で行う方法を提供します。

@Entity
@Where(clause = "deleted = false")
public class Employee implements Serializable {

   //...
}

メソッドの @ Where アノテーションには、このエンティティに対するクエリまたはサブクエリに追加されるSQL句が含まれています。

employee.setDeleted(true);

session.flush();
session.clear();

employee = session.find(Employee.class, employee.getId());
assertThat(employee).isNull();

@ Formula アノテーションの場合と同様に、 生のSQLを扱っているので、エンティティをデータベースにフラッシュしてコンテキストから削除するまで @ Where 条件は再評価されません

それまでは、エンティティはコンテキスト内に留まり、クエリと id による検索でアクセス可能になります。

@ Where 注釈は、コレクションフィールドにも使用できます。削除可能な電話のリストがあるとします。

@Entity
public class Phone implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private boolean deleted;

    private String number;

}

次に、 Employee 側から、削除可能な phones のコレクションを次のようにマッピングできます。

public class Employee implements Serializable {

   //...

    @OneToMany
    @JoinColumn(name = "employee__id")
    @Where(clause = "deleted = false")
    private Set<Phone> phones = new HashSet<>(0);

}

違いは、 Employee.phones コレクションは常にフィルタリングされることですが、直接クエリを使用して、削除されたものも含め、すべての電話を取得できるということです。

employee.getPhones().iterator().next().setDeleted(true);
session.flush();
session.clear();

employee = session.find(Employee.class, employee.getId());
assertThat(employee.getPhones()).hasSize(1);

List<Phone> fullPhoneList
  = session.createQuery("from Phone").getResultList();
assertThat(fullPhoneList).hasSize(2);

5 @ Filter によるパラメータ化フィルタリング

@ Where アノテーションの問題点は、パラメータなしで静的クエリを指定することしかできず、必要に応じて無効化または有効化できないことです。

@ Filter アノテーションは @ Where と同じように機能しますが、セッションレベルで有効または無効にしたり、パラメータ化することもできます。

5.1. @ Filter を定義する

@ Filter のしくみを説明するために、まず Employee エンティティに次のフィルタ定義を追加しましょう。

@FilterDef(
    name = "incomeLevelFilter",
    parameters = @ParamDef(name = "incomeLimit", type = "int")
)
@Filter(
    name = "incomeLevelFilter",
    condition = "grossIncome > :incomeLimit"
)
public class Employee implements Serializable {

@ FilterDef アノテーションは、クエリに参加するフィルタ名とそのパラメータのセットを定義します。パラメータの型は、Hibernate型の1つの名前です(https://docs.jboss.org/hibernate/orm/5.2/javadocs/org/hibernate/type/Type.html[Type]、https://docs.jboss.org/hibernate/orm/5.2/javadocs/org/hibernate/usertype/UserType.html[UserType]またはhttps://docs.jboss.org/hibernate/orm/5.2/javadocs/org/hibernate/usertype/CompositeUserType.html[CompositeUserType])、この場合は int です。

@FilterDef アノテーションは、タイプまたはパッケージレベルのどちらにも配置できます。フィルタ条件自体は指定されていません(ただし、 defaultCondition__パラメータを指定できます)。

つまり、フィルタ(その名前とパラメータのセット)を1か所で定義してから、フィルタの条件を他の複数の場所で別々に定義できるということです。

これは @ Filter アノテーションを使って行うことができます。私たちの場合は、単純にするために同じクラスに入れています。条件の構文は、コロンが前に付いたパラメータ名を持つ生のSQLです。

5.2. フィルタリングされたエンティティへのアクセス

@からの @ Filter のもう1つの違いは、 @ Filter__はデフォルトでは有効になっていないことです。セッションレベルでそれを手動で有効にし、それにパラメータ値を提供する必要があります。

session.enableFilter("incomeLevelFilter")
  .setParameter("incomeLimit", 11__000);

データベースに次の3人の従業員がいるとします。

session.save(new Employee(10__000, 25));
session.save(new Employee(12__000, 25));
session.save(new Employee(15__000, 25));

次に、上記のようにフィルタを有効にした状態で、クエリを実行すると表示されるのは2つだけです。

List<Employee> employees = session.createQuery("from Employee")
  .getResultList();
assertThat(employees).hasSize(2);

有効フィルタとそのパラメータ値の両方が現在のセッション内でのみ適用されることに注意してください。フィルタが有効になっていない新しいセッションでは、3人の従業員全員が表示されます。

session = HibernateUtil.getSessionFactory().openSession();
employees = session.createQuery("from Employee").getResultList();
assertThat(employees).hasSize(3);

また、idによってエンティティを直接取得する場合、フィルタは適用されません。

Employee employee = session.get(Employee.class, 1);
assertThat(employee.getGrossIncome()).isEqualTo(10__000);

5.3. @フィルタ と第2レベルのキャッシング

高負荷のアプリケーションを使用している場合は、Hibernateの2次キャッシュを必ず有効にすることをお勧めします。これはパフォーマンス上の大きなメリットになります。

@ Filter アノテーションはキャッシュに対してうまく機能しないことに注意してください。

  • 2次キャッシュは、フィルタ処理されていないフルコレクションのみを保持します。そうでない場合は、フィルタを有効にして1つのセッションでコレクションを読み取り、フィルタを無効にしても別のセッションで同じキャッシュ済みフィルタコレクションを取得できます。

これが @ Filter アノテーションが基本的にエンティティのキ​​ャッシュを無効にする理由です。

6. @ Any を使用したエンティティ参照のマッピング

単一の @ MappedSuperclass に基づいていなくても、参照を複数のエンティティタイプのいずれかにマップしたい場合があります。それらは、異なる無関係のテーブルにもマップできます。これを @ Any アノテーションで実現できます。

私たちの例では、 私たちのパーシスタンスユニット のすべてのエンティティ、つまり Employee Phone に何らかの説明を付ける必要があります。これを行うためだけにすべてのエンティティを単一の抽象スーパークラスから継承するのは不合理です。

6.1. @ Any とのマッピング関係

これは、 Serializable を実装するエンティティへの参照(つまり、すべてのエンティティへの参照)を定義する方法です。

@Entity
public class EntityDescription implements Serializable {

    private String description;

    @Any(
        metaDef = "EntityDescriptionMetaDef",
        metaColumn = @Column(name = "entity__type"))
    @JoinColumn(name = "entity__id")
    private Serializable entity;

}

metaDef プロパティは定義の名前、 metaColumn はエンティティタイプを区別するために使用される列の名前です(単一テーブル階層マッピングの識別子列とは異なります)。

エンティティの id を参照する列も指定します。

  • この列は外部キーにはならないことに注意してください。** これは必要なテーブルを参照できるためです。

entity id__列も、一意ではありません。テーブルごとに異なる識別子が繰り返される可能性があるためです。

  • entity type / entity id ペアは、参照しているエンティティを一意に表すため、一意である必要があります。**

6.2. @ AnyMetaDef を使用した @ Any マッピングの定義

現時点では、Hibernateは entity type__列に含めることができるものを指定していないため、さまざまなエンティティタイプを区別する方法を知りません。

これを機能させるには、 @ AnyMetaDef アノテーションを使用してマッピングのメタ定義を追加する必要があります。それを置くのに最適な場所はパッケージレベルですので、他のマッピングで再利用することができます。

これは、 @ AnyMetaDef アノテーションを含む package-info.java ファイルがどのようになるかです。

@AnyMetaDef(
    name = "EntityDescriptionMetaDef",
    metaType = "string",
    idType = "int",
    metaValues = {
        @MetaValue(value = "Employee", targetEntity = Employee.class),
        @MetaValue(value = "Phone", targetEntity = Phone.class)
    }
)
package com.baeldung.hibernate.pojo;

ここでは、 entity type 列の型( string )、 entity id 列の型( int )、 entity type 列の許容値( "Employee" および "Phone" __)、および対応するエンティティ型を指定しました。

今、2台の電話を持つ従業員がこのように記述されているとします。

Employee employee = new Employee();
Phone phone1 = new Phone("555-45-67");
Phone phone2 = new Phone("555-89-01");
employee.getPhones().add(phone1);
employee.getPhones().add(phone2);

無関係なタイプが異なっていても、3つすべてのエンティティに記述メタデータを追加できます。

EntityDescription employeeDescription = new EntityDescription(
  "Send to conference next year", employee);
EntityDescription phone1Description = new EntityDescription(
  "Home phone (do not call after 10PM)", phone1);
EntityDescription phone2Description = new EntityDescription(
  "Work phone", phone1);

7. 結論

この記事では、生のSQLを使用してエンティティマッピングを微調整できるようにするHibernateのアノテーションのいくつかを調べました。

この記事のソースコードはhttps://github.com/eugenp/tutorials/tree/master/persistence-modules/hibernate5[GitHubで利用可能]です。