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
,用于处理 LocalDate
和 String
之间的转换:
public class LocalDateStringJavaDescriptor extends AbstractTypeDescriptor<LocalDate> {
public static final LocalDateStringJavaDescriptor INSTANCE = new LocalDateStringJavaDescriptor();
public LocalDateStringJavaDescriptor() {
super(LocalDate.class, ImmutableMutabilityPlan.INSTANCE);
}
// other methods
}
然后实现 unwrap
和 wrap
方法:
@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
}
接下来实现 nullSafeGet
和 nullSafeSet
方法:
@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 获取。