Hibernateによる動的マッピング

Hibernateを使用した動的マッピング

1. 前書き

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

HibernateはJPA仕様を実装していますが、ここで説明する注釈はHibernateでのみ使用可能であり、他のJPA実装に直接移植できないことに注意してください。

2. プロジェクトのセットアップ

機能を示すために必要なのは、hibernate-coreライブラリとバッキングH2データベースのみです。


    org.hibernate
    hibernate-core
    5.2.12.Final


    com.h2database
    h2
    1.4.194

hibernate-coreライブラリの現在のバージョンについては、Maven Centralに進んでください。

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;
    }

}

明らかな欠点は、we’d have to do the recalculation each time we access this virtual field by the getterです。

データベースからすでに計算された値を取得する方がはるかに簡単です。 これは、@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を使用すると、サブクエリを使用したり、ネイティブデータベース関数やストアドプロシージャを呼び出したり、基本的にこのフィールドのSQLselect句の構文に違反しないことを実行したりできます。

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

また、the value is calculated when the entity is fetched from the databaseであることにも注意してください。 したがって、エンティティを永続化または更新する場合、エンティティがコンテキストから削除されて再度ロードされるまで、値は再計算されません。

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は1つの場所でこれを行う方法を提供します。

@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注釈の場合と同様に、since we’re dealing with raw SQL, the @Where condition won’t be reevaluated until we flush the entity to the database and evict it from the context

その時まで、エンティティはコンテキスト内にとどまり、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 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 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つ(TypeUserType、またはCompositeUserType)の名前であり、この場合はintです。

The @FilterDefアノテーションは、タイプレベルまたはパッケージレベルのいずれかに配置できます。 フィルタ条件自体は指定されていないことに注意してください(ただし、defaultConditionパラメータを指定することはできます)。

これは、フィルター(その名前とパラメーターのセット)を1つの場所で定義してから、他の複数の場所でフィルターの条件を異なる方法で定義できることを意味します。

これは、@Filterアノテーションを使用して実行できます。 この例では、簡単にするために同じクラスに入れています。 条件の構文は、パラメーター名の前にコロンが付いた生のSQLです。

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

@Filter@Whereのもう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 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. @Filterおよび第2レベルのキャッシング

高負荷のアプリケーションがある場合は、Hibernateの第2レベルのキャッシュを有効にする必要があります。これは、パフォーマンスを大幅に向上させる可能性があります。 the @Filter annotation does not play nicely with caching.

The second-level cache only keeps full unfiltered collections。 そうでない場合は、フィルターを有効にして1つのセッションでコレクションを読み取り、フィルターを無効にしても、別のセッションで同じキャッシュされたフィルター済みコレクションを取得できます。

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

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

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

この例では、we’ll need to attach some description to every entity in our persistence unit、つまりEmployeePhoneです。 これを行うためだけに、単一の抽象スーパークラスからすべてのエンティティを継承するのは不合理です。

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を参照する列も指定します。 this column will not be a foreign keyは必要な任意のテーブルを参照できるため、注目に値します。

entity_id列も、異なるテーブルが繰り返し識別子を持つ可能性があるため、通常は一意にすることはできません。

ただし、entity_type /entity_idのペアは、参照しているエンティティを一意に表すため、一意である必要があります。

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

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

これを機能させるには、@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.example.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のアノテーションのいくつかについて説明しました。

記事のソースコードはover on GitHubで入手できます。