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

1.はじめに

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

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

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

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

 org.hibernate hibernate-core 5.4.12.Final   com.h2database h2 1.4.194 

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

明らかな欠点は、ゲッターがこの仮想フィールドにアクセスするたびに再計算を行う必要があることです

すでに計算された値をデータベースから取得する方がはるかに簡単です。これは、@ 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を使用したエンティティのフィルタリング

エンティティを要求するたびに、クエリに追加の条件を提供するとします。

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

アプリケーション内の既存および将来のすべてのクエリには細心の注意を払う必要があります。この追加の条件をすべてのクエリに提供する必要があります。幸い、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; }

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

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アノテーションの問題は、パラメーターなしで静的クエリのみを指定できることであり、要求によって無効または有効にすることはできません。

The @Filter annotation works the same way as @Where, but it also can be enabled or disabled on session level, and also parameterized.

5.1. Defining the @Filter

To demonstrate how @Filter works, let's first add the following filter definition to the Employee entity:

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

The @FilterDef annotation defines the filter name and a set of its parameters that will participate in the query. The type of the parameter is the name of one of the Hibernate types (Type, UserType or CompositeUserType), in our case, an int.

The @FilterDef annotation may be placed either on the type or on package level. Note that it does not specify the filter condition itself (although we could specify the defaultCondition parameter).

This means that we can define the filter (its name and set of parameters) in one place and then define the conditions for the filter in multiple other places differently.

This can be done with the @Filter annotation. In our case, we put it in the same class for simplicity. The syntax of the condition is a raw SQL with parameter names preceded by colons.

5.2. Accessing Filtered Entities

Another difference of @Filter from @Where is that @Filter is not enabled by default. We have to enable it on the session level manually, and provide the parameter values for it:

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

Now suppose we have the following three employees in the database:

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

Then with the filter enabled, as shown above, only two of them will be visible by querying:

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

Note that both the enabled filter and its parameter values are applied only inside the current session. In a new session without filter enabled, we'll see all three employees:

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

Also, when directly fetching the entity by id, the filter is not applied:

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

5.3. @Filter and Second-Level Caching

If we have a high-load application, then we'd definitely want to enable Hibernate second-level cache, which can be a huge performance benefit. We should keep in mind that the @Filter annotation does not play nicely with caching.

The second-level cache only keeps full unfiltered collections. If it wasn't the case, then we could read a collection in one session with filter enabled, and then get the same cached filtered collection in another session even with filter disabled.

This is why the @Filter annotation basically disables caching for the entity.

6. Mapping Any Entity Reference With @Any

Sometimes we want to map a reference to any of multiple entity types, even if they are not based on a single @MappedSuperclass. They could even be mapped to different unrelated tables. We can achieve this with the @Any annotation.

In our example, we'll need to attach some description to every entity in our persistence unit, namely, Employee and Phone. It'd be unreasonable to inherit all entities from a single abstract superclass just to do this.

6.1. Mapping Relation With @Any

Here's how we can define a reference to any entity that implements Serializable (i.e., to any entity at all):

@Entity public class EntityDescription implements Serializable { private String description; @Any( metaDef = "EntityDescriptionMetaDef", metaColumn = @Column(name = "entity_type")) @JoinColumn(name = "entity_id") private Serializable entity; }

The metaDef property is the name of the definition, and metaColumn is the name of the column that will be used to distinguish the entity type (not unlike the discriminator column in the single table hierarchy mapping).

We also specify the column that will reference the id of the entity. It's worth noting that this column will not be a foreign key because it can reference any table that we want.

The entity_id column also can't generally be unique because different tables could have repeated identifiers.

The entity_type/entity_id pair, however, should be unique, as it uniquely describes the entity that we're referring to.

6.2. Defining the @Any Mapping With @AnyMetaDef

Right now, Hibernate does not know how to distinguish different entity types, because we did not specify what the entity_type column could contain.

To make this work, we need to add the meta-definition of the mapping with the @AnyMetaDef annotation. The best place to put it would be the package level, so we could reuse it in other mappings.

Here's how the package-info.java file with the @AnyMetaDef annotation would look like:

@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の列(文字列)、のタイプ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で入手できます。