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

