You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让自定义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函数来封装逻辑:

  1. 实现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);
    }
}
  1. 注册函数到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
  1. 使用自定义函数查询:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 10:24:48