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.4.12.Final</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.194</version>
</dependency>

hibernate-core ライブラリの現在のバージョンについては、 MavenCentralにアクセスしてください。

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

}

明らかな欠点は、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であるため、マッピングがデータベースに依存する可能性があることです。

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

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 アノテーションの場合と同様に、生の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 側から、削除可能なphoneのコレクションを次のようにマップできます。

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つ( Type UserType 、または CompositeUserType )の名前であり、この場合は int[ X157X]。

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

高負荷のアプリケーションがある場合は、Hibernateの第2レベルのキャッシュを有効にする必要があります。これは、パフォーマンスを大幅に向上させる可能性があります。 @Filterアノテーションはキャッシングではうまく機能しないことに注意してください。

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

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

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

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

この例では、永続性ユニット内のすべてのエンティティ、つまり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を参照する列も指定します。 この列は外部キーではないことに注意してください。これは、必要な任意のテーブルを参照できるためです。

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

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

6.2. @Anyマッピングを@AnyMetaDefで定義する

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

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

package-info。javaファイルに@AnyMetaDefアノテーションが付いていると次のようになります。

@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 列の許容値(「従業員」および「電話」)および対応するエンティティタイプ。

ここで、次のように記述された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のアノテーションのいくつかについて説明しました。

この記事のソースコードは、GitHubから入手できます。