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

Hibernate 6 CriteriaQuery聚合函数max()在绑定实现Serializable的自定义UserType时执行失败

Hibernate 6 CriteriaQuery聚合函数max()在绑定实现Serializable的自定义UserType时执行失败

问题重现

我这边也复现了你遇到的问题:当自定义类型Salary同时实现Serializable和Comparable<Salary>,并通过@Type绑定自定义UserType时,使用CriteriaQuery的greatest()(对应SQL的max())会抛出如下错误:

org.hibernate.QueryException: Parameter 1 of function max() has type COMPARABLE, but argument is of type local.jd.example.office.Salary

你的简化复现代码逻辑很清晰,核心场景包括:

  • 数据库表t_jd_office_employee用varchar字段存储薪资
  • 自定义Salary类型实现Serializable和Comparable,通过SalaryType完成与数据库字段的转换
  • 使用CriteriaQuery的greatest()方法查询最大薪资时触发类型不匹配异常

问题原因

Hibernate 6的Criteria API类型检查机制在处理同时实现Serializable和Comparable的自定义类型时,会优先识别Serializable接口,导致类型推断出错——它认为传入的参数是Serializable类型,而max()/greatest()要求的是COMPARABLE类型,因此抛出类型不匹配的异常。当你去掉Serializable后,Hibernate能正确识别Comparable接口,查询自然恢复正常。

解决方案

这里有几个可行的解决办法,你可以根据项目情况选择:

1. 显式转换类型(最简单的临时修复)

在使用greatest()时,通过as()方法显式将属性转换为Comparable类型,强制Hibernate识别正确的接口:

private Optional<Salary> queryMaxSalary() {
    try (var em = emf.createEntityManager()) {
        final var criteriaBuilder = em.getCriteriaBuilder();
        final var query = criteriaBuilder.createQuery(Salary.class);
        final var root = query.from(OfficeEmployee.class);
        // 显式转换为Comparable类型,触发Hibernate识别正确的接口
        query.select(criteriaBuilder.greatest(root.get(OfficeEmployee_.salary).as(Comparable.class)));
        try {
            final var result = em.createQuery(query).getSingleResult();
            return Optional.of(result);
        } catch (NoResultException ex) {
            logger.warn("No max salary found");
            return Optional.empty();
        }
    }
}

2. 增强自定义UserType的类型描述(更彻底的修复)

修改SalaryType,显式告诉Hibernate这个类型是Comparable的。通过重写getJavaType()方法,明确标记类型的可比较特性:

public class SalaryType implements UserType<Salary> {

    @Override
    public int getSqlType() {
        return Types.VARCHAR;
    }

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

    // 新增:显式定义类型描述符,标记为可比较类型
    @Override
    public JavaType<Salary> getJavaType() {
        return new AbstractJavaType<Salary>(Salary.class) {
            @Override
            public <X> X unwrap(Salary value, Class<X> type, WrapperOptions options) {
                if (value == null) {
                    return null;
                }
                if (String.class.isAssignableFrom(type)) {
                    return (X) value.asStringValue();
                }
                if (Comparable.class.isAssignableFrom(type) && type.isInstance(value)) {
                    return (X) value;
                }
                throw new IllegalArgumentException("Cannot unwrap Salary to " + type);
            }

            @Override
            public <X> Salary wrap(X value, WrapperOptions options) {
                if (value == null) {
                    return null;
                }
                if (String.class.isInstance(value)) {
                    // 复用nullSafeGet中的解析逻辑
                    final var salaryValue = (String) value;
                    final var matcher = Pattern.compile("^(\\d+)?( )*([A-Z]{3})?$").matcher(salaryValue);
                    if (matcher.matches()) {
                        final var amount = matcher.group(1) == null ? null : Long.parseLong(matcher.group(1));
                        final var salary = new Salary();
                        salary.setAmount(amount);
                        salary.setCurrency(matcher.group(3));
                        return salary;
                    } else {
                        return null;
                    }
                }
                if (Salary.class.isInstance(value)) {
                    return (Salary) value;
                }
                throw new IllegalArgumentException("Cannot wrap " + value.getClass() + " to Salary");
            }

            @Override
            public boolean isComparable() {
                // 关键:告诉Hibernate该类型支持比较操作
                return true;
            }
        };
    }

    // 以下为原有方法,保持不变
    @Override
    public boolean equals(Salary x, Salary y) {
        return x != null && x.equals(y);
    }

    @Override
    public int hashCode(Salary x) {
        return x == null ? Objects.hashCode(null) : x.hashCode();
    }

    @Override
    public Salary nullSafeGet(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) throws SQLException {
        final var salaryValue = rs.getString(position);
        if (salaryValue == null) {
            return null;
        }
        final var matcher = Pattern.compile("^(\\d+)?( )*([A-Z]{3})?$").matcher(salaryValue);
        if (matcher.matches()) {
            final var amount = matcher.group(1) == null ? null : Long.parseLong(matcher.group(1));
            final var salary = new Salary();
            salary.setAmount(amount);
            salary.setCurrency(matcher.group(3));
            return salary;
        } else {
            return null;
        }
    }

    @Override
    public void nullSafeSet(PreparedStatement st, Salary value, int index, SharedSessionContractImplementor session) throws SQLException {
        if (Objects.isNull(value))
            st.setNull(index, Types.VARCHAR);
        else {
            st.setString(index, value.asStringValue());
        }
    }

    @Override
    public Salary deepCopy(Salary value) {
        if (value == null) {
            return null;
        }
        final var salary = new Salary();
        salary.setAmount(value.getAmount());
        salary.setCurrency(value.getCurrency());
        return salary;
    }

    @Override
    public boolean isMutable() {
        return false;
    }

    @Override
    public Serializable disassemble(Salary value) {
        return value;
    }

    @Override
    public Salary assemble(Serializable cached, Object owner) {
        return (Salary) cached;
    }
}

3. 改用JPQL查询(快速替代方案)

如果Criteria的调整暂时有困难,可以直接改用JPQL查询,JPQL的类型推断机制能正确识别Salary的Comparable接口:

private Optional<Salary> queryMaxSalary() {
    try (var em = emf.createEntityManager()) {
        try {
            final var result = em.createQuery(
                "SELECT MAX(e.salary) FROM OfficeEmployee e", 
                Salary.class
            ).getSingleResult();
            return Optional.of(result);
        } catch (NoResultException ex) {
            logger.warn("No max salary found");
            return Optional.empty();
        }
    }
}

验证建议

你可以先尝试第一个方案(显式类型转换),因为它改动最小,能快速验证是否解决问题。如果需要更彻底的修复,再考虑第二个方案增强自定义UserType的类型描述。

备注:内容来源于stack exchange,提问作者Juan Diego

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.15 03:20:37