Contents

Hibernate 自定义类型简介

1. 概述

Hibernate 通过将 Java 中的面向对象模型与数据库中的关系模型映射来简化 SQL 和 JDBC 之间的数据处理。尽管Hibernate 内置了基本 Java 类的映射,但自定义类型的映射通常很复杂。

在本教程中,我们将看到 Hibernate 如何允许我们将基本类型映射扩展到自定义 Java 类。除此之外,我们还将看到一些自定义类型的常见示例,并使用 Hibernate 的类型映射机制来实现它们。

2. Hibernate 映射类型

Hibernate 使用映射类型将 Java 对象转换为用于存储数据的 SQL 查询。同样,它使用映射类型在检索数据时将 SQL ResultSet 转换为 Java 对象。

通常,Hibernate 将类型分类为 Entity Types 和 Value Types。具体来说,实体类型用于映射特定于域的 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 中实现自定义类型有三种方法。让我们详细讨论它们中的每一个。

4.1. 实现BasicType

我们可以通过实现 Hibernate 的BasicType或其特定实现之一 AbstractSingleColumnStandardBasicType来创建自定义基本类型。

在我们实现我们的第一个自定义类型之前,让我们看一个实现基本类型的常见用例。假设我们必须使用一个遗留数据库,它将日期存储为 VARCHAR。通常,Hibernate 会将其映射到 String Java 类型。因此,使应用程序开发人员更难进行日期验证。

所以让我们实现我们的LocalDateString类型,它将LocalDate Java 类型存储为 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。并且,第二个参数是代表 Java 类型的JavaTypeDescriptor实例。

现在,我们可以实现一个LocalDateStringJavaDescriptor来将LocalDate存储和检索 为 VARCHAR:

public class LocalDateStringJavaDescriptor extends AbstractTypeDescriptor<LocalDate> {
    public static final LocalDateStringJavaDescriptor INSTANCE = 
      new LocalDateStringJavaDescriptor();
    public LocalDateStringJavaDescriptor() {
        super(LocalDate.class, ImmutableMutabilityPlan.INSTANCE);
    }

    // other methods
}

接下来,我们需要重写 用于将 Java 类型转换为 SQL 的wrapunwrap方法。让我们从 展开开始:

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

接下来是 wrap方法:

@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());
}

PreparedStatement绑定 期间调用 unwrap() 将LocalDate转换为 String 类型,该类型映射到 VARCHAR。同样, 在ResultSet检索 期间调用 wrap() 以将String转换为 Java LocalDate

最后,我们可以在 Entity 类中使用我们的自定义类型:

@Entity
@Table(name = "OfficeEmployee")
public class OfficeEmployee {
    @Column
    @Type(type = "com.blogdemo.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 方法返回字段的 SQL 类型,其顺序与我们在PhoneNumber类中声明的顺序相同。同样,returnedClass方法返回我们的PhoneNumber Java 类型。

剩下要做的就是实现在 Java 类型和 SQL 类型之间转换的方法,就像我们对BasicType所做的那样。

首先、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.blogdemo.hibernate.customtypes.PhoneNumberType")
    private PhoneNumber employeeNumber;

    // other fields and methods
}

4.3. 实现 CompositeUserType

实现 UserType适用于简单类型。但是,映射复杂的 Java 类型(使用 Collections 和 Cascaded 复合类型)需要更加复杂。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 映射我们的Address类的属性名称 。更重要的是,getPropertyType方法返回每个属性的映射类型。

此外,我们还需要实现 getPropertyValue和 setPropertyValue方法来将PreparedStatement和 ResultSet索引映射到类型属性。例如,考虑 我们的 AddressTypegetPropertyValue

@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());
}

最后,我们需要实现 nullSafeGet和 nullSafeSet方法以在 Java 和 SQL 类型之间进行转换。这类似于我们之前在 PhoneNumberType 中所做的。

请注意 CompositeType通常被实现为可嵌入类型的替代映射机制。

4.4. 类型参数化

除了创建自定义类型,Hibernate 还允许我们根据参数改变类型的行为。

例如,假设我们需要存储 *OfficeEmployee *的薪水。更重要的是,应用程序必须将工资金额转换为地理当地货币金额。

所以,让我们实现我们的参数化 SalaryType,它接受 currency作为参数:

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()*方法。现在,  SalaryType接受一个 *currency *参数,并将在存储之前转换任何金额。

在声明薪水时,我们将 currency 作为参数 传递:

@Entity
@Table(name = "OfficeEmployee")
public class OfficeEmployee {
    @Type(type = "com.blogdemo.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
}