如何让自定义Hibernate CompositeUserType支持JPA-QL/HQL查询比较?
解决Hibernate中OffsetDateTime偏移量丢失及支持HQL查询的方案
我完全理解你的痛点——Hibernate默认对OffsetDateTime的映射会直接转成Timestamp存储,彻底丢失了时区偏移信息,这对需要精确追踪时间时区的场景来说根本没法用。下面我会给出完整的自定义CompositeUserType实现,以及如何让它支持HQL/JPA-QL中的比较操作。
第一步:完整实现OffsetDateTimeHibernateType
CompositeUserType是Hibernate专门用来将Java类型映射到多数据库列的接口,我们可以把OffsetDateTime拆成UTC时间戳和时区偏移字符串两个字段存储,这样既能保留偏移信息,又能利用时间戳的索引优势。
import org.hibernate.HibernateException; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.type.CompositeUserType; import org.hibernate.type.StringType; import org.hibernate.type.TimestampType; import org.hibernate.type.Type; import java.io.Serializable; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.time.Instant; import java.time.OffsetDateTime; import java.time.ZoneOffset; public class OffsetDateTimeHibernateType implements CompositeUserType { @Override public Class returnedClass() { return OffsetDateTime.class; } @Override public String[] getPropertyNames() { // 对应拆分出的两个属性:UTC时间戳、时区偏移标识 return new String[]{"utcDateTime", "zoneOffset"}; } @Override public Type[] getPropertyTypes() { // 对应数据库列的类型:Timestamp用于存时间,Varchar存偏移字符串 return new Type[]{TimestampType.INSTANCE, StringType.INSTANCE}; } @Override public Object getPropertyValue(Object component, int property) throws HibernateException { OffsetDateTime odt = (OffsetDateTime) component; if (property == 0) { // 把OffsetDateTime转成UTC时间戳 return Timestamp.from(odt.toInstant()); } else if (property == 1) { // 获取时区偏移的字符串形式(比如"+08:00") return odt.getOffset().getId(); } throw new IllegalArgumentException("无效的属性索引: " + property); } @Override public void setPropertyValue(Object component, int property, Object value) throws HibernateException { // OffsetDateTime是不可变类,不需要实现属性修改逻辑 } @Override public boolean isMutable() { return false; // 不可变类型返回false } @Override public boolean equals(Object x, Object y) throws HibernateException { if (x == y) return true; if (x == null || y == null) return false; return x.equals(y); } @Override public int hashCode(Object x) throws HibernateException { return x.hashCode(); } @Override public Object deepCopy(Object value) throws HibernateException { return value; // 不可变对象无需复制,直接返回原实例 } @Override public Serializable disassemble(Object value, SharedSessionContractImplementor session) throws HibernateException { return (Serializable) value; // OffsetDateTime本身支持序列化 } @Override public Object assemble(Serializable cached, SharedSessionContractImplementor session, Object owner) throws HibernateException { return cached; // 直接返回缓存的序列化对象 } @Override public Object replace(Object original, Object target, SharedSessionContractImplementor session, Object owner) throws HibernateException { return original; // 不可变对象直接返回原实例 } @Override public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException { Timestamp utcTimestamp = rs.getTimestamp(names[0]); String offsetId = rs.getString(names[1]); if (rs.wasNull()) { return null; } // 从UTC时间戳和偏移字符串重建OffsetDateTime Instant instant = utcTimestamp.toInstant(); ZoneOffset offset = ZoneOffset.of(offsetId); return OffsetDateTime.ofInstant(instant, offset); } @Override public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) throws HibernateException, SQLException { if (value == null) { st.setNull(index, java.sql.Types.TIMESTAMP); st.setNull(index + 1, java.sql.Types.VARCHAR); return; } OffsetDateTime odt = (OffsetDateTime) value; // 分别设置UTC时间戳和偏移字符串 st.setTimestamp(index, Timestamp.from(odt.toInstant())); st.setString(index + 1, odt.getOffset().getId()); } }
第二步:实体类中的映射配置
在你的实体类里,用@Type指定自定义类型,再用@Columns映射到数据库的两个对应列:
import javax.persistence.Column; import javax.persistence.Columns; import javax.persistence.Entity; import javax.persistence.Id; import org.hibernate.annotations.Type; import java.time.OffsetDateTime; @Entity public class Event { @Id private Long id; @Type(type = "com.yourpackage.OffsetDateTimeHibernateType") @Columns(columns = { @Column(name = "event_utc_datetime"), @Column(name = "event_zone_offset") }) private OffsetDateTime eventTime; // 构造器、getter、setter省略 }
第三步:支持HQL/JPA-QL中的比较操作
因为我们用的是复合类型,Hibernate允许直接访问它拆分出的子属性,所以可以通过组合子属性的条件来实现比较,这和Java中OffsetDateTime的原生比较逻辑完全一致:
示例1:直接通过子属性查询
OffsetDateTime targetTime = OffsetDateTime.parse("2024-05-20T14:30:00+02:00"); Timestamp utcTimestamp = Timestamp.from(targetTime.toInstant()); String offsetId = targetTime.getOffset().getId(); // HQL逻辑:先比UTC时间戳,相等时再比偏移量,和OffsetDateTime.compareTo逻辑一致 String hql = "SELECT e FROM Event e WHERE e.eventTime.utcDateTime > :utcTime " + "OR (e.eventTime.utcDateTime = :utcTime AND e.eventTime.zoneOffset >= :offset)"; Query<Event> query = entityManager.createQuery(hql, Event.class); query.setParameter("utcTime", utcTimestamp); query.setParameter("offset", offsetId); List<Event> results = query.getResultList();
示例2:自定义SQL函数简化查询(可选)
如果觉得每次写子属性比较太繁琐,可以注册一个自定义SQL函数来封装逻辑:
- 实现SQL函数:
import org.hibernate.QueryException; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.type.BooleanType; import org.hibernate.type.CompositeType; import org.hibernate.type.Type; import org.hibernate.usertype.UserType; import org.hibernate.dialect.function.SQLFunction; import java.util.List; public class OffsetDateTimeGreaterThanFunction implements SQLFunction { @Override public boolean hasArguments() { return true; } @Override public boolean hasParenthesesIfNoArguments() { return true; } @Override public Type getReturnType(Type firstArgumentType, org.hibernate.engine.spi.Mapping mapping) throws QueryException { return BooleanType.INSTANCE; } @Override public String render(Type firstArgumentType, List args, SessionFactoryImplementor factory) throws QueryException { CompositeType compositeType = (CompositeType) firstArgumentType; // 获取复合类型子属性对应的SQL列名 String leftDateTime = compositeType.getPropertyTypes()[0].toSqlString(null, args.get(0) + ".utcDateTime", factory); String leftOffset = compositeType.getPropertyTypes()[1].toSqlString(null, args.get(0) + ".zoneOffset", factory); // 生成比较逻辑的SQL return String.format("(%s > ?) OR (%s = ? AND %s >= ?)", leftDateTime, leftDateTime, leftOffset); } }
- 注册函数到Hibernate:
创建类型贡献者类:
import org.hibernate.boot.model.TypeContributions; import org.hibernate.boot.model.TypeContributor; import org.hibernate.service.ServiceRegistry; public class CustomTypeContributor implements TypeContributor { @Override public void contribute(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { // 注册自定义OffsetDateTime类型 typeContributions.contributeType(new OffsetDateTimeHibernateType()); // 注册自定义比较函数 typeContributions.contributeSqlFunction("odt_gt", new OffsetDateTimeGreaterThanFunction()); } }
然后在hibernate.properties中配置:
hibernate.type_contributors=com.yourpackage.CustomTypeContributor
- 使用自定义函数查询:
OffsetDateTime targetTime = OffsetDateTime.parse("2024-05-20T14:30:00+02:00"); String hql = "SELECT e FROM Event e WHERE odt_gt(e.eventTime, :targetTime)"; Query<Event> query = entityManager.createQuery(hql, Event.class); query.setParameter("targetTime", targetTime); List<Event> results = query.getResultList();
这样就能用更简洁的HQL语法实现OffsetDateTime的比较了。
内容的提问来源于stack exchange,提问作者Geoffrey De Smet
相关产品推荐
相关产品推荐

