能否通过Hibernate Criteria API操作变量?复杂动态SQL(含子查询、HAVING子句)的实现疑问
使用Hibernate Criteria API实现复杂动态SQL查询
首先直接给你核心疑问的答案:完全可以! Hibernate Criteria API支持通过Case表达式模拟SQL的IF逻辑,也能轻松操作自定义聚合变量和HAVING子句。下面我会一步步帮你实现需求里的复杂查询,包括你卡住的HAVING逻辑和自定义变量返回。
核心技巧:用CriteriaBuilder.selectCase()模拟SQL的IF函数
SQL里的IF(condition, trueVal, falseVal)在Criteria API中可以通过CriteriaBuilder.selectCase()来实现,结合聚合函数(比如max())就能完成你需要的自定义聚合逻辑。
比如你需要的MAX(IF(ba.term = 'device', 1, 0)) > 0,用Criteria API写出来就是:
// 构建Case表达式:当term等于'device'时返回1,否则返回0 Case<Integer> deviceCase = cb.selectCase() .when(cb.equal(bookAvailabilityJoin.get("term"), "device"), 1) .otherwise(0); // 包裹成MAX聚合,然后判断大于0 Predicate deviceHaving = cb.gt(cb.max(deviceCase), 0);
完整实现:对应第二种SQL的Criteria代码
我会基于你的实体类,实现包含动态条件、关联查询、分组、HAVING子句的完整逻辑,同时兼顾结果转换的需求:
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); // 用Tuple来接收多列结果,比Object[]更清晰 CriteriaQuery<Tuple> query = cb.createTupleQuery(); // 主表Root:BookFilter Root<BookFilter> bookFilterRoot = query.from(BookFilter.class); // 关联BookAvailability(通过bookId关联,内连接) Join<BookFilter, BookAvailability> bookAvailabilityJoin = bookFilterRoot.join("bookId", JoinType.INNER); // 1. 构建WHERE条件集合 List<Predicate> wherePredicates = new ArrayList<>(); // 动态filter_id条件 if (filterIds != null && !filterIds.isEmpty()) { wherePredicates.add(bookFilterRoot.get("filterId").in(filterIds)); } // 动态book_id或filter_id的OR条件(对应SQL里的(ba.book_id IN (...) OR bf.filter_id IN (...))) if (bookIds != null && !bookIds.isEmpty() || filterIdsForOr != null && !filterIdsForOr.isEmpty()) { List<Predicate> orPredicates = new ArrayList<>(); if (bookIds != null && !bookIds.isEmpty()) { orPredicates.add(bookAvailabilityJoin.get("bookId").in(bookIds)); } if (filterIdsForOr != null && !filterIdsForOr.isEmpty()) { orPredicates.add(bookFilterRoot.get("filterId").in(filterIdsForOr)); } wherePredicates.add(cb.or(orPredicates.toArray(new Predicate[0]))); } // 日期范围条件(对应SQL里的date_from和date_to判断) Instant targetDate = Instant.now(); // 替换成你的动态日期 Predicate dateFromPredicate = cb.or( cb.lessThan(bookAvailabilityJoin.get("dateFrom"), targetDate), cb.isNull(bookAvailabilityJoin.get("dateFrom")) ); Predicate dateToPredicate = cb.or( cb.greaterThan(bookAvailabilityJoin.get("dateTo"), targetDate), cb.isNull(bookAvailabilityJoin.get("dateTo")) ); wherePredicates.add(cb.and(dateFromPredicate, dateToPredicate)); // 2. 构建GROUP BY query.groupBy( bookFilterRoot.get("bookId"), bookFilterRoot.get("filterId") ); // 3. 构建HAVING子句(你卡住的部分) // 条件1:MAX(IF(ba.term = 'device', 1, 0)) > 0 Case<Integer> deviceCase = cb.selectCase() .when(cb.equal(bookAvailabilityJoin.get("term"), "device"), 1) .otherwise(0); Predicate deviceHaving = cb.gt(cb.max(deviceCase), 0); // 条件2:MAX(IF(ba.term = 'country', 1, 0)) > 0 Case<Integer> countryCase = cb.selectCase() .when(cb.equal(bookAvailabilityJoin.get("term"), "country"), 1) .otherwise(0); Predicate countryHaving = cb.gt(cb.max(countryCase), 0); // 组合HAVING条件 query.having(cb.and(deviceHaving, countryHaving)); // 4. 定义SELECT投影(返回bookId和filterId) query.select( cb.tuple( bookFilterRoot.get("bookId").alias("bookId"), bookFilterRoot.get("filterId").alias("filterId") ) ); // 5. 执行查询 List<Tuple> results = entityManager.createQuery(query).getResultList();
实现第一种SQL的自定义变量返回
如果你需要像第一种SQL那样返回自定义聚合变量(比如device),只需要在SELECT投影里添加对应的Case聚合即可:
// 自定义device变量:MAX(IF(ba.dimension = 'device', ba.term, null)) Case<String> deviceVarCase = cb.selectCase() .when(cb.equal(bookAvailabilityJoin.get("dimension"), "device"), bookAvailabilityJoin.get("term")) .otherwise(null); Expression<String> deviceVar = cb.max(deviceVarCase); // 在select里添加这个变量 query.select( cb.tuple( bookFilterRoot.get("bookId").alias("bookId"), bookFilterRoot.get("filterId").alias("filterId"), deviceVar.alias("device") ) ); // 后续还可以在WHERE子句(如果是子查询的话)或者结果处理中使用这个变量
结果转换为Map<Integer, HashSet<Integer>>
把查询到的Tuple列表转换成你需要的Map结构很简单:
Map<Integer, HashSet<Integer>> resultMap = new HashMap<>(); for (Tuple tuple : results) { Integer bookId = (Integer) tuple.get("bookId"); Integer filterId = (Integer) tuple.get("filterId"); resultMap.computeIfAbsent(filterId, k -> new HashSet<>()).add(bookId); }
对你现有代码的优化建议
- 避免同时使用两个
Root(你之前的代码里query.from(BookFilter.class)和query.from(BookAvailability.class)),这会产生笛卡尔积,应该用Join来建立关联关系。 - 用
Tuple来接收多列结果,比直接返回实体类更灵活,尤其是当你需要自定义投影的时候。 - 动态条件尽量用集合来管理,避免重复的if判断,代码更整洁。
内容的提问来源于stack exchange,提问作者sicio
相关产品推荐
相关产品推荐

