Hibernate 6报错:已注册副本SqmBasicValuedSimplePath
问题场景
基于现有Criteria复制构建用于统计计数的Criteria时,Hibernate 6抛出如下错误,相同代码在Hibernate 5中可正常运行:
Caused by: java.lang.IllegalArgumentException: Already registered a copy:
SqmBasicValuedSimplePath(com.example.domain.Test(175781908930100).name)
问题原因
Hibernate 6的SQM(语义查询模型)对查询元素的上下文管理机制比Hibernate 5更严格。原代码直接将原Criteria中的Predicate(包含绑定到原Root/Join的路径表达式)复用给新的计数Criteria,但新Criteria已经创建了独立的Root实例,原路径属于旧的查询上下文,无法在新上下文中重复注册,因此触发重复注册的错误。
解决方案
需要在复制查询条件时,将原Predicate、GroupBy、Having中的路径表达式替换为新Criteria对应的Root/Join实例,确保所有表达式都属于新的查询上下文。具体改造如下:
1. 新增表达式替换工具方法
用于遍历表达式树,将原From对象(Root/Join)替换为新的对应实例:
private static Expression<?> replaceExpression(Expression<?> original, Map<From<?,?>, From<?,?>> fromMap) { if (original instanceof From<?,?>) { return fromMap.get(original); } if (original instanceof Predicate) { Predicate predicate = (Predicate) original; Predicate.BooleanOperator operator = predicate.getOperator(); List<Expression<?>> replacedExpressions = new ArrayList<>(); for (Expression<?> expr : predicate.getExpressions()) { replacedExpressions.add(replaceExpression(expr, fromMap)); } if (operator == Predicate.BooleanOperator.AND) { return predicate.getBuilder().and(replacedExpressions.toArray(new Expression[0])); } else if (operator == Predicate.BooleanOperator.OR) { return predicate.getBuilder().or(replacedExpressions.toArray(new Expression[0])); } else { return predicate.getBuilder().not((Predicate) replacedExpressions.get(0)); } } if (original instanceof Path<?>) { Path<?> path = (Path<?>) original; Expression<?> parent = replaceExpression(path.getParentPath(), fromMap); if (parent instanceof Path<?>) { return ((Path<?>) parent).get(path.getJavaType()); } } return original; } private static List<Expression<?>> replaceExpressions(List<Expression<?>> expressions, Map<From<?,?>, From<?,?>> fromMap) { List<Expression<?>> replaced = new ArrayList<>(); for (Expression<?> expr : expressions) { replaced.add(replaceExpression(expr, fromMap)); } return replaced; }
2. 改造复制Criteria的方法
维护原From到新From的映射,替换所有表达式后再设置到新Criteria中:
private static void copyCriteriaWithoutSelectionAndOrder( CriteriaQuery<?> from, CriteriaQuery<?> to) { Map<From<?,?>, From<?,?>> fromMap = new HashMap<>(); // 复制Root并建立映射 for (Root<?> root : from.getRoots()) { Root<?> dest = to.from(root.getJavaType()); dest.alias(getOrCreateAlias(root)); fromMap.put(root, dest); copyJoins(root, dest, fromMap); } // 替换GroupBy表达式 to.groupBy(replaceExpressions(from.getGroupList(), fromMap)); to.distinct(from.isDistinct()); // 替换Having表达式 if (from.getGroupRestriction() != null) { Predicate replacedHaving = (Predicate) replaceExpression(from.getGroupRestriction(), fromMap); to.having(replacedHaving); } // 替换Where条件表达式 Predicate predicate = from.getRestriction(); if (predicate != null) { Predicate replacedPredicate = (Predicate) replaceExpression(predicate, fromMap); to.where(replacedPredicate); } } private static void copyJoins(From<?,?> from, From<?,?> to, Map<From<?,?>, From<?,?>> fromMap) { for (Join<?,?> join : from.getJoins()) { Join<?,?> destJoin = to.join(join.getAttribute().getName(), join.getJoinType()); destJoin.alias(getOrCreateAlias(join)); fromMap.put(join, destJoin); copyJoins(join, destJoin, fromMap); } } // 保留原有的别名获取方法 private static String getOrCreateAlias(Root<?> root) { String alias = root.getAlias(); return alias != null ? alias : "root_" + System.identityHashCode(root); }
3. 其他代码保持不变
原有的getResultsPage、getResultList、count、countCriteria方法无需修改。
效果说明
改造后的代码通过上下文映射替换所有路径表达式,确保新的计数Criteria使用自身的Root/Join实例,避免了Hibernate 6中严格的SQM注册机制触发的重复注册错误,同时兼容Hibernate 5的运行逻辑。
内容的提问来源于stack exchange,提问作者Jana

