1. 概述

Hibernate 通过将 Java 的面向对象模型与数据库中的关系模型进行映射,简化了 SQL 和 JDBC 之间的数据处理。虽然 Hibernate 内置支持大多数基本 Java 类型的映射,但对于自定义类型的映射往往较为复杂

在本教程中,我们将探讨 Hibernate 如何扩展基本类型映射以支持自定义 Java 类型。此外,我们还会结合一些常见的自定义类型示例,演示如何通过 Hibernate 的类型映射机制来实现它们。

2. Hibernate 映射类型简介

Hibernate 使用映射类型将 Java 对象转换为 SQL 查询以便存储数据;同时,在数据检索时,也会将 SQL 结果集转换回 Java 对象。

通常,Hibernate 将类型分为两类:

  • 实体类型(Entity Types):用于映射领域特定的 Java 实体,独立于其他类型存在。
  • 值类型(Value Types):用于映射数据对象,通常由实体拥有。

在本教程中,我们将重点讨论值类型的映射,主要包括以下三类:

  • 基本类型(Basic Types):映射基本的 Java 类型
  • 可嵌入类型(Embeddable):映射复合 Java 类型/POJO
  • 集合类型(Collections):映射基本或复合类型的集合

3. Maven 依赖

要创建自定义 Hibernate 类型,我们需要引入 hibernate-core 依赖:

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

4. 自定义类型实现方式

虽然 Hibernate 提供了丰富的基本类型映射,但在某些场景下,我们仍需实现自定义类型。

Hibernate 提供了三种方式来实现自定义类型,下面我们将逐一介绍。

4.1. 实现 BasicType

我们可以通过实现 Hibernate 的 BasicType 接口,或其具体实现类 AbstractSingleColumnStandardBasicType 来创建自定义基本类型。

🎯 典型场景:遗留数据库中的日期存储为 VARCHAR

通常,Hibernate 会将 VARCHAR 映射为 String 类型,这使得日期验证变得困难。为了解决这个问题,我们可以实现一个 LocalDateStringType,将 LocalDate 映射为 VARCHAR:

public class LocalDateStringType extends AbstractSingleColumnStandardBasicType<LocalDate> {

    public static final LocalDateStringType INSTANCE = new LocalDateStringType();

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

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

    public LocalDate stringToObject(String xml) {
        return fromString(xml);
    }

    public String objectToSQLString(LocalDate value, Dialect dialect) {
        return '\'' + LocalDateStringJavaDescriptor.INSTANCE.toString(value) + '\'';
    }
}

其中,构造函数中的两个参数分别代表:

  • SqlTypeDescriptor:SQL 类型描述符(本例中为 VARCHAR)
  • JavaTypeDescriptor:Java 类型描述符(本例中为 LocalDate)

接下来,我们需要实现 LocalDateStringJavaDescriptor,用于处理 LocalDateString 之间的转换:

public class LocalDateStringJavaDescriptor extends AbstractTypeDescriptor<LocalDate> {

    public static final LocalDateStringJavaDescriptor INSTANCE = new LocalDateStringJavaDescriptor();

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

    // other methods
}

然后实现 unwrapwrap 方法:

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

    if (String.class.isAssignableFrom(type))
        return (X) DateTimeFormatter.ISO_LOCAL_DATE.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(DateTimeFormatter.ISO_LOCAL_DATE.parse((CharSequence) value));

    throw unknownWrap(value.getClass());
}

最后,在实体类中使用该自定义类型:

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

    @Column
    @Type(value = UserTypeLegacyBridge.class, 
        parameters = @Parameter(name = UserTypeLegacyBridge.TYPE_NAME_PARAM_KEY, 
        value = "LocalDateString"))
    private LocalDate dateOfJoining;

    // other fields and methods
}

⚠️ 注意:我们后续会介绍如何注册该类型,从而通过注册名(如 "LocalDateString")而非完整类名来引用。

4.2. 实现 UserType

对于复杂的 Java 对象,通常需要映射到多个数据库字段。此时,可以使用 UserType 接口来实现自定义类型。

示例:实现 Salary 类型

public class SalaryType implements UserType<Salary> {
    
    @Override
    public int sqlType() {
        return Types.VARCHAR;
    }

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

    // other methods
}

接下来实现 nullSafeGetnullSafeSet 方法:

@Override
public Salary nullSafeGet(ResultSet rs, int position, 
    SharedSessionContractImplementor session, Object owner) throws SQLException {
    
    Salary salary = new Salary();
    String salaryValue = rs.getString(position);
    salary.setAmount(Long.parseLong(salaryValue.split(" ")[1]));
    salary.setCurrency(salaryValue.split(" ")[0]);
    return salary;
}

@Override
public void nullSafeSet(PreparedStatement st, Salary value, int index, 
    SharedSessionContractImplementor session) throws SQLException {
    
    if (Objects.isNull(value))
        st.setNull(index, Types.VARCHAR);
    else {
        Long salaryValue = SalaryCurrencyConvertor.convert(value.getAmount(), value.getCurrency(), localCurrency);
        st.setString(index, value.getCurrency() + " " + salaryValue);
    }
}

最后在实体中使用:

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

     @Type(value = com.baeldung.hibernate.customtypes.SalaryType.class, 
         parameters = {@Parameter(name = "currency", value = "USD")})
     private Salary salary;
    
    // other fields and methods
}

4.3. 实现 CompositeUserType

当 Java 类型包含集合或级联的复合类型时,UserType 可能不够灵活。这时可以使用 CompositeUserType,用于将多个数据库字段映射到多个 Java 字段。

示例:实现 AddressType

public class AddressType implements CompositeUserType<Address> {

    @Override
    public Object getPropertyValue(Address component, int property) throws HibernateException {
        switch (property) {
            case 0: return component.getAddressLine1();
            case 1: return component.getAddressLine2();
            case 2: return component.getCity();
            case 3: return component.getCountry();
            case 4: return component.getZipCode();
            default: throw new IllegalArgumentException(property + " is invalid");
        }
    }

    @Override
    public Address instantiate(ValueAccess values, SessionFactoryImplementor sessionFactory) {
        return new Address(
            values.getValue(0, String.class),
            values.getValue(1, String.class),
            values.getValue(2, String.class),
            values.getValue(3, String.class),
            values.getValue(4, Integer.class)
        );
    }

    @Override
    public Class<?> embeddable() {
        return Address.class;
    }

    @Override
    public Class<Address> returnedClass() {
        return Address.class;
    }

    // other methods
}

⚠️ 注意:CompositeUserType 通常作为 @Embeddable 的替代方案使用。

4.4. 类型参数化

除了创建自定义类型外,Hibernate 还支持通过参数改变类型的行为。

示例:参数化 SalaryType

public class SalaryType implements UserType<Salary>, DynamicParameterizedType {

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

在实体中传入参数:

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

5. 基本类型注册

Hibernate 使用 BasicTypeRegistry 来管理所有内置基本类型的映射。我们也可以将自定义类型注册进去,从而像内置类型一样使用。

示例:注册 LocalDateStringType

private SessionFactory sessionFactory() {
    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
}

6. 总结

在本教程中,我们介绍了在 Hibernate 中实现自定义类型的多种方式,并结合常见场景演示了如何创建自定义类型并将其注册到 Hibernate 中。

✅ 代码示例可在 GitHub 获取。


原始标题:Custom Types in Hibernate