1. 概要

Hibernateは、Javaのオブジェクト指向モデルをデータベースのリレーショナルモデルにマッピングすることにより、SQLとJDBC間のデータ処理を簡素化します。 基本的なJavaクラスのマッピングはHibernateに組み込まれていますが、カスタムタイプのマッピングは多くの場合複雑です。

このチュートリアルでは、Hibernateを使用して基本的な型マッピングをカスタムJavaクラスに拡張する方法を説明します。 それに加えて、カスタムタイプの一般的な例をいくつか見て、Hibernateのタイプマッピングメカニズムを使用してそれらを実装します。

2. Hibernateマッピングタイプ

Hibernateは、マッピングタイプを使用してJavaオブジェクトをSQLクエリに変換し、データを格納します。 同様に、データの取得中にSQLResultSetをJavaオブジェクトに変換するためにマッピングタイプを使用します。

一般に、Hibernateはタイプをエンティティタイプと値タイプに分類します具体的には、エンティティタイプは、ドメイン固有のJavaエンティティをマップするために使用されるため、アプリケーション内の他のタイプとは独立して存在します。 対照的に、値型は代わりにデータオブジェクトをマップするために使用され、ほとんどの場合、エンティティによって所有されます。

このチュートリアルでは、さらに次のように分類される値型のマッピングに焦点を当てます。

  • 基本タイプ–基本的なJavaタイプのマッピング
  • 埋め込み可能–複合javaタイプ/POJOのマッピング
  • コレクション–基本および複合Javaタイプのコレクションのマッピング

3. Mavenの依存関係

カスタムHibernateタイプを作成するには、hibernate-core依存関係が必要です。

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>5.6.7.Final</version>
</dependency>

4. Hibernateのカスタムタイプ

ほとんどのユーザードメインでHibernateの基本的なマッピングタイプを使用できます。 ただし、カスタムタイプを実装する必要がある多くのユースケースがあります。

Hibernateを使用すると、カスタムタイプの実装が比較的簡単になります。 Hibernateでカスタムタイプを実装するには3つのアプローチがあります。 それぞれについて詳しく説明しましょう。

4.1. BasicTypeの実装

HibernateのBasicTypeまたはその特定の実装の1つであるAbstractSingleColumnStandardBasicType。を実装することにより、カスタム基本型を作成できます。

最初のカスタムタイプを実装する前に、基本タイプを実装するための一般的なユースケースを見てみましょう。 日付をVARCHARとして格納するレガシーデータベースを使用する必要があるとします。 通常は、 HibernateはこれをStringJava型にマップします。 これにより、アプリケーション開発者にとって日付の検証が困難になります。 

それでは、 LocalDateString typeを実装してみましょう。これは、 LocalDateJavaタイプをVARCHARとして格納します。

public class LocalDateStringType 
  extends AbstractSingleColumnStandardBasicType<LocalDate> {

    public static final LocalDateStringType INSTANCE = new LocalDateStringType();

    public LocalDateStringType() {
        super(VarcharTypeDescriptor.INSTANCE, LocalDateStringJavaDescriptor.INSTANCE);
    }

    @Override
    public String getName() {
        return "LocalDateString";
    }
}

このコードで最も重要なことは、コンストラクターパラメーターです。 まず、 SqlTypeDescriptor のインスタンスです。これは、HibernateのSQLタイプ表現であり、この例ではVARCHARです。 また、2番目の引数は、Javaタイプを表すJavaTypeDescriptorのインスタンスです。

これで、LocalDateをVARCHARとして保存および取得するためのLocalDateStringJavaDescriptorを実装できます:

public class LocalDateStringJavaDescriptor extends AbstractTypeDescriptor<LocalDate> {

    public static final LocalDateStringJavaDescriptor INSTANCE = 
      new LocalDateStringJavaDescriptor();

    public LocalDateStringJavaDescriptor() {
        super(LocalDate.class, ImmutableMutabilityPlan.INSTANCE);
    }
	
    // other methods
}

次に、JavaタイプをSQLに変換するためのwrapおよびunwrapメソッドをオーバーライドする必要があります。 unwrap:から始めましょう

@Override
public <X> X unwrap(LocalDate value, Class<X> type, WrapperOptions options) {

    if (value == null)
        return null;

    if (String.class.isAssignableFrom(type))
        return (X) LocalDateType.FORMATTER.format(value);

    throw unknownUnwrap(type);
}

次に、ラップメソッド:

@Override
public <X> LocalDate wrap(X value, WrapperOptions options) {
    if (value == null)
        return null;

    if(String.class.isInstance(value))
        return LocalDate.from(LocalDateType.FORMATTER.parse((CharSequence) value));

    throw unknownWrap(value.getClass());
}

unwrap()は、 PreparedStatement バインディング中に呼び出され、LocalDateを文字列タイプに変換します。これはVARCHARにマップされます。 同様に、 wrap()は、 ResultSet の取得中に呼び出され、StringをJavaLocalDateに変換します。

最後に、Entityクラスでカスタムタイプを使用できます。

@Entity
@Table(name = "OfficeEmployee")
public class OfficeEmployee {

    @Column
    @Type(type = "com.baeldung.hibernate.customtypes.LocalDateStringType")
    private LocalDate dateOfJoining;

    // other fields and methods
}

後で、このタイプをHibernateに登録する方法を説明します。 その結果、完全修飾クラス名の代わりに登録キーを使用してこのタイプを参照します。

4.2. UserTypeの実装

Hibernateにはさまざまな基本タイプがあるため、カスタム基本タイプを実装する必要があることは非常にまれです。 対照的に、より一般的なユースケースは、複雑なJavaドメインオブジェクトをデータベースにマップすることです。 このようなドメインオブジェクトは通常、複数のデータベース列に格納されます。

それでは、 UserType:を実装して、複雑なPhoneNumberオブジェクトを実装しましょう。

public class PhoneNumberType implements UserType {
    @Override
    public int[] sqlTypes() {
        return new int[]{Types.INTEGER, Types.INTEGER, Types.INTEGER};
    }

    @Override
    public Class returnedClass() {
        return PhoneNumber.class;
    }

    // other methods
}	

ここで、オーバーライドされた sqlTypes メソッドは、PhoneNumberクラスで宣言されているのと同じ順序でフィールドのSQLタイプを返します。 同様に、 returnedClass メソッドは、 PhoneNumberJavaタイプを返します。

BasicType で行ったように、JavaタイプとSQLタイプの間で変換するメソッドを実装するだけです。

まず、 nullSafeGetメソッド:

@Override
public Object nullSafeGet(ResultSet rs, String[] names, 
  SharedSessionContractImplementor session, Object owner) 
  throws HibernateException, SQLException {
    int countryCode = rs.getInt(names[0]);

    if (rs.wasNull())
        return null;

    int cityCode = rs.getInt(names[1]);
    int number = rs.getInt(names[2]);
    PhoneNumber employeeNumber = new PhoneNumber(countryCode, cityCode, number);

    return employeeNumber;
}

次に、 nullSafeSet メソッド:

@Override
public void nullSafeSet(PreparedStatement st, Object value, 
  int index, SharedSessionContractImplementor session) 
  throws HibernateException, SQLException {

    if (Objects.isNull(value)) {
        st.setNull(index, Types.INTEGER);
        st.setNull(index + 1, Types.INTEGER);
        st.setNull(index + 2, Types.INTEGER);
    } else {
        PhoneNumber employeeNumber = (PhoneNumber) value;
        st.setInt(index,employeeNumber.getCountryCode());
        st.setInt(index+1,employeeNumber.getCityCode());
        st.setInt(index+2,employeeNumber.getNumber());
    }
}

最後に、OfficeEmployeeエンティティクラスでカスタムPhoneNumberTypeを宣言できます。

@Entity
@Table(name = "OfficeEmployee")
public class OfficeEmployee {

    @Columns(columns = { @Column(name = "country_code"), 
      @Column(name = "city_code"), @Column(name = "number") })
    @Type(type = "com.baeldung.hibernate.customtypes.PhoneNumberType")
    private PhoneNumber employeeNumber;
	
    // other fields and methods
}

4.3. CompositeUserTypeの実装

UserType の実装は、単純なタイプに適しています。 ただし、複雑なJavaタイプ(コレクションおよびカスケード複合タイプを使用)のマッピングには、より高度な機能が必要です。 Hibernateを使用すると、CompositeUserTypeインターフェイスを実装することでこのようなタイプをマップできます。

それでは、前に使用したOfficeEmployeeエンティティにAddressTypeを実装して、これが実際に動作することを確認しましょう。

public class AddressType implements CompositeUserType {

    @Override
    public String[] getPropertyNames() {
        return new String[] { "addressLine1", "addressLine2", 
          "city", "country", "zipcode" };
    }

    @Override
    public Type[] getPropertyTypes() {
        return new Type[] { StringType.INSTANCE, 
          StringType.INSTANCE, 
          StringType.INSTANCE, 
          StringType.INSTANCE, 
          IntegerType.INSTANCE };
    }

    // other methods
}

タイププロパティのインデックスをマップするUserTypesとは異なり、CompositeTypeアドレスクラスのプロパティ名をマップします。 さらに重要なことに、 getPropertyType メソッドは、各プロパティのマッピングタイプを返します。

さらに、getPropertyValueおよびsetPropertyValueメソッドを実装して、PreparedStatementおよびResultSetインデックスをタイププロパティにマッピングする必要もあります。 例として、 AddressType:getPropertyValueについて考えてみます。

@Override
public Object getPropertyValue(Object component, int property) throws HibernateException {

    Address empAdd = (Address) component;

    switch (property) {
    case 0:
        return empAdd.getAddressLine1();
    case 1:
        return empAdd.getAddressLine2();
    case 2:
        return empAdd.getCity();
    case 3:
        return empAdd.getCountry();
    case 4:
        return Integer.valueOf(empAdd.getZipCode());
    }

    throw new IllegalArgumentException(property + " is an invalid property index for class type "
      + component.getClass().getName());
}

最後に、JavaタイプとSQLタイプを変換するために、nullSafeGetメソッドとnullSafeSetメソッドを実装する必要があります。 これは、以前にPhoneNumberType。で行ったことと似ています。

CompositeTypeは通常、埋め込み可能タイプの代替マッピングメカニズムとして実装されていることに注意してください。

4.4. タイプのパラメータ化

カスタムタイプの作成に加えて、 Hibernateでは、パラメーターに基づいてタイプの動作を変更することもできます。

たとえば、 給料私たちの OfficeEmployee。 さらに重要なことに、アプリケーションは給与額を変換する必要があります地理的な現地通貨の金額に。

それでは、通貨をパラメーターとして受け入れるパラメーター化されたSalaryTypeを実装しましょう。

public class SalaryType implements CompositeUserType, DynamicParameterizedType {

    private String localCurrency;
	
    @Override
    public void setParameterValues(Properties parameters) {
        this.localCurrency = parameters.getProperty("currency");
    }
	
    // other method implementations from CompositeUserType
}

パラメータ化に焦点を当てるために、この例ではCompositeUserTypeメソッドをスキップしていることに注意してください。 ここでは、Hibernateの DynamicParameterizedType を実装し、 setParameterValues()メソッドをオーバーライドしただけです。 これで、 SalaryTypecurrencyパラメーターを受け入れ、保存する前に任意の金額を変換します。

Salary:を宣言する際に、currencyをパラメーターとして渡します。

@Entity
@Table(name = "OfficeEmployee")
public class OfficeEmployee {

    @Type(type = "com.baeldung.hibernate.customtypes.SalaryType", 
      parameters = { @Parameter(name = "currency", value = "USD") })
    @Columns(columns = { @Column(name = "amount"), @Column(name = "currency") })
    private Salary salary;

    // other fields and methods
}

5. 基本タイプレジストリ

Hibernateは、BasicTypeRegistryに組み込まれているすべての基本型のマッピングを維持します。 したがって、そのようなタイプのマッピング情報に注釈を付ける必要がなくなります。

さらに、Hibernateを使用すると、基本タイプと同様に、カスタムタイプをBasicTypeRegistryに登録できます。 通常、アプリケーションはブートストラップ中にカスタムタイプを登録します SessionFactory。 登録してこれを理解しましょう LocalDateString 以前に実装したタイプ:

private static SessionFactory makeSessionFactory() {
    ServiceRegistry serviceRegistry = StandardServiceRegistryBuilder()
      .applySettings(getProperties()).build();
														  
    MetadataSources metadataSources = new MetadataSources(serviceRegistry);
    Metadata metadata = metadataSources
      .addAnnotatedClass(OfficeEmployee.class)
      .getMetadataBuilder()
      .applyBasicType(LocalDateStringType.INSTANCE)
      .build();
														  
    return metadata.buildSessionFactory()
}

private static Properties getProperties() {
    // return hibernate properties
}

したがって、タイプマッピングで完全修飾クラス名を使用する際の制限がなくなります。

@Entity
@Table(name = "OfficeEmployee")
public class OfficeEmployee {

    @Column
    @Type(type = "LocalDateString")
    private LocalDate dateOfJoining;
	
    // other methods
}

ここで、 LocalDateString は、LocalDateStringTypeがマップされるキーです。

または、 TypeDefs:を定義して、タイプ登録をスキップすることもできます。

@TypeDef(name = "PhoneNumber", typeClass = PhoneNumberType.class, 
  defaultForType = PhoneNumber.class)
@Entity
@Table(name = "OfficeEmployee")
public class OfficeEmployee {

    @Columns(columns = {@Column(name = "country_code"),
    @Column(name = "city_code"),
    @Column(name = "number")})
    private PhoneNumber employeeNumber;
	
    // other methods
}

6. 結論

このチュートリアルでは、Hibernateでカスタムタイプを定義するための複数のアプローチについて説明しました。 さらに、新しいカスタムタイプが役立ついくつかの一般的なユースケースに基づいて、エンティティクラスにいくつかのカスタムタイプを実装しました。

いつものように、コードサンプルはGitHubから入手できます。