ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Hibernate 查询体系详解

Hibernate 查询体系详解 Hibernate 查询体系详解定位Hibernate JPQL/HQL、JOIN 体系、批量变更、Criteria、投影与分页全解适用版本Hibernate ORM 6.xJakarta Persistence 3.1目录JPQL / HQL 基础JOIN 与 fetch join批量变更语句Criteria API 与类型安全原生查询与投影分页与排序Hibernate 6 新查询能力总结常见高频面试题一、JPQL / HQL 基础1.1 语言定位JPQLJakarta Persistence Query Language是面向实体与属性的查询语言selectofromOrderowhereo.statusPAID它不关心表名与列名——Order是实体、status是属性翻译为方言 SQL 的工作由 Hibernate 完成。这带来数据库无关性代价是查询与物理结构隔了一层调试时要习惯看生成的 SQL。HQL 与 JPQL 的关系JPQL 是标准HQL 是 Hibernate 的超集。Hibernate 6 把两者统一为「Hibernate Query Language」日常无需区分只需知道个别语法如elements()是扩展。书写惯例关键字大写、实体名与属性名原样属性区分大小写o.customer对、o.Customer错。1.2 基本语法族路径导航隐式 JOINselectofromOrderowhereo.customer.city.name杭州每个.关联导航在 SQL 中翻译为一次 JOIN。三层导航 三次 JOIN路径过深是性能信号——要么预加载设计有问题要么该用原生查询。参数绑定ListOrderordersem.createQuery(select o from Order o where o.status :status and o.amount :min,Order.class).setParameter(status,OrderStatus.PAID).setParameter(min,newBigDecimal(100)).getResultList();命名参数:name可读性优于位置参数?1且参数顺序无关生产统一用命名参数。参数绑定走 PreparedStatement天然防注入。分组与聚合selecto.customer,count(o),sum(o.amount)fromOrderowhereo.createTime:fromgroupbyo.customerhavingsum(o.amount):thresholdorderbysum(o.amount)desc聚合查询的结果不再是实体而是Object[]元组或构造表达式对象见第五章投影。1.3 表达式与函数类别示例比较/逻辑,,,and,or,not范围/集合between x and y,in (:ids),like %x%空值is null/is not null聚合count,sum,avg,min,max条件case when ... then ... else ... end、coalesce(a, b)、nullif(a, b)字符串concat,substring,trim,lower,length,locate日期current_date,current_timestamp,extract(year from ...)6.xHibernate 6 重构了函数库以 SQL Standard 函数为基准方言负责翻译未收录的数据库函数可用function(db_func, args)透传。模糊查询注意参数化.setParameter(kw,%keyword%)// 通配符在参数里拼不在 HQL 里拼字符串1.4 多态查询继承映射02 篇下查询父类会覆盖所有子类selectpfromPayment p-- 返回 WechatPay/Alipay/BankCardPay 全部selectpfromPayment pwheretype(p)WechatPayselectpfromPayment pwherep instanceofWechatPay访问子类独有属性用TREATselectpfromPayment pwheretreat(pasWechatPay).openId:openId多态查询在 SINGLE_TABLE 策略下最高效单表加判别列过滤TABLE_PER_CLASS 会退化为 UNION——这是 02 篇不推荐该策略的原因之一。二、JOIN 与 fetch join2.1 普通 JOIN用于条件与聚合selectofromOrderojoino.items iwherei.sku:skuandi.quantity1普通 join 的目的是过滤结果仍是订单不重复Hibernate 6 自动去重。注意隐式路径导航o.items.sku也会产生 JOIN两者等价显式 join 在需要引用关联别名时更清晰。2.2 fetch join预加载消灭 N1问题场景展示订单列表并显示每个订单的订单项。// ❌ N11 条查订单 N 条逐个查子项ListOrderordersem.createQuery(select o from Order o,Order.class).getResultList();orders.forEach(o-o.getItems().size());// 每次触发一条 SELECT// ✅ 一条 SQL 带回订单与子项ListOrderordersem.createQuery(select distinct o from Order o join fetch o.items,Order.class).getResultList();join fetch生成的 SQL 是一条 JOIN把子项一并载入持久化上下文访问o.getItems()不再发 SQL。这是 Hibernate 处理 N1 的首选手段。使用规则违反即事故不能分页setFirstResult/setMaxResults与集合 fetch join 同用时Hibernate 先全量加载再内存截取HHH000104 警告大表直接打爆内存一条查询最多 fetch 一个集合fetch 两个集合 笛卡尔乘积爆炸distinct去重JOIN 会让父实体重复出现select distinct 保证列表不重复6.2 对 fetch join 自动去重显式写更稳。2.3 关联外 JOINHibernate 6JPA 2.x 时代没有映射关系的实体无法直接 JOINHibernate 6 支持selecto,cfromOrderojoinCustomer cono.customerIdc.id以及关联上的附加条件selectofromOrderoleftjoino.items ioni.quantity5前者替代了旧式的where o.customerId c.id隐式交叉写法后者替代了旧with子句。三、批量变更语句3.1 语法与语义introwsem.createQuery(update Order set status :closed where createTime :deadline).setParameter(closed,OrderStatus.CLOSED).setParameter(deadline,LocalDateTime.now().minusDays(30)).executeUpdate();em.createQuery(delete from OrderItem where order.id :oid).setParameter(oid,orderId).executeUpdate();批量变更直接翻译为一条 UPDATE/DELETE 执行绕过持久化上下文带来三条语义特性上下文不同步执行前已加载到上下文中的实体不会被更新之后读到的可能是旧值不级联无视cascade/orphanRemoval配置不触发实体级监听/校验。3.2 使用纪律批量变更后的安全动作 方案 A执行后 em.clear()强制后续重新加载 方案 B把批量变更隔离在独立事务/独立方法中适用场景大批量状态迁移关单、归档、数据清理——这些场景本来就不需要逐实体操作。日常业务修改仍应走实体享受级联、校验、审计。3.3 Hibernate 6 的 MutationQueryMutationQuerymqsession.createMutationQuery(update Order set status :s where id :id);mq.setParameter(s,OrderStatus.CLOSED).setParameter(id,1L);introwsmq.executeUpdate();类型化 API 是 6.x 的方向但 JPQL 字符串本体未变迁移成本低。四、Criteria API 与类型安全4.1 动态条件构建运行期按参数有无拼装条件替代 MyBatis 动态 SQL 的角色CriteriaBuildercbem.getCriteriaBuilder();CriteriaQueryOrdercqcb.createQuery(Order.class);RootOrderrootcq.from(Order.class);ListPredicatepredicatesnewArrayList();if(status!null){predicates.add(cb.equal(root.get(status),status));}if(minAmount!null){predicates.add(cb.ge(root.get(amount),minAmount));}if(StringUtils.hasText(customerName)){predicates.add(cb.like(root.join(customer).get(name),%customerName%));}cq.where(predicates.toArray(newPredicate[0])).orderBy(cb.desc(root.get(createTime)));ListOrderordersem.createQuery(cq).getResultList();Criteria 的价值在运行期组合管理后台的多条件筛选器、开放查询接口。静态查询一律用 JPQL——Criteria 的可读性差是公认代价。4.2 静态元模型字符串属性名root.get(status)拼错要到运行期才报错。引入hibernate-jpamodelgen注解处理器后编译期生成元模型类// 自动生成类target/generated-sourcesStaticMetamodel(Order.class)publicabstractclassOrder_{publicstaticvolatileSingularAttributeOrder,OrderStatusstatus;publicstaticvolatileSingularAttributeOrder,BigDecimalamount;publicstaticvolatileSetAttributeOrder,OrderItemitems;}// 使用root.get(Order_.status)// 编译期校验属性存在与类型在 Spring Data 的 Specification 场景中是标配07 篇展开。4.3 子查询SubqueryLongsubcq.subquery(Long.class);RootOrderItemitemRootsub.from(OrderItem.class);sub.select(itemRoot.get(order).get(id)).where(cb.gt(itemRoot.get(quantity),10));cq.where(root.get(id).in(sub));JPQL 子查询只允许在 WHERE/HAVING 中FROM 子查询不支持这与 SQL 习惯不同复杂派生表场景请改用原生查询。五、原生查询与投影5.1 原生查询映射实体/DTO// 映射实体ListOrderordersem.createNativeQuery(select * from t_order where status ?,Order.class).setParameter(1,PAID).getResultList();// 映射 DTOSqlResultSetMappingSqlResultSetMapping(nameOrderSummaryMapping,classesConstructorResult(targetClassOrderSummary.class,columns{ColumnResult(nameid,typeLong.class),ColumnResult(nametotal,typeBigDecimal.class)}))原生查询是「逃生通道」数据库特有函数、复杂窗口分析、存量视图。代价是失去数据库无关性并绕开二级缓存与部分优化。5.2 构造表达式推荐读路径ListOrderSummarysummariesem.createQuery(select new com.example.dto.OrderSummary(o.id, o.amount, o.customer.name) from Order o where o.status :status,OrderSummary.class).setParameter(status,OrderStatus.PAID).getResultList();DTO 需要与 select 列表匹配的构造器。投影查询的优势只取需要的列减少内存与网络返回非实体不进入持久化上下文、不做脏检查只读场景性能最优避开懒加载陷阱没有关联代理可言。原则读路径能投影就投影写路径才加载完整实体。5.3 结果形态与异常查询形态返回类型单实体ListOrder多列无构造表达式ListObject[]或ListTuple标量聚合ListLong等getSingleResult()无结果抛NoResultException多结果抛NonUniqueResultExceptiongetSingleResult的异常语义要显式处理列表查询空结果返回空集合不抛异常。5.4 命名查询EntityNamedQuery(nameOrder.findPaidAfter,queryselect o from Order o where o.status PAID and o.createTime :from)publicclassOrder{...}ListOrderordersem.createNamedQuery(Order.findPaidAfter).setParameter(from,yesterday).getResultList();价值启动期即校验语法与属性存在性拼错名直接启动失败而不是线上爆雷查询集中在实体声明处。Spring Data 的Query也有同样的启动校验效果07 篇。六、分页与排序6.1 基本分页ListOrderpageem.createQuery(select o from Order o order by o.createTime desc,Order.class).setFirstResult((pageNum-1)*pageSize)// offset.setMaxResults(pageSize)// limit.getResultList();Hibernate 按方言改写MySQLLIMIT ? OFFSET ?、Oracle 12cOFFSET ? ROWS FETCH NEXT ? ROWS ONLY。必须配 order by否则分页结果顺序未定义、翻页重复/丢行。6.2 深分页问题OFFSET 100000意味着数据库先扫描丢弃十万行成本随行数线性上涨。治理手段与数据库知识库交叉键集分页where id :lastId order by id desc limit :size用上一页末尾 ID 作锚点走索引业务上限制最大页数搜索类需求交给搜索引擎中间件知识库。6.3 fetch join 与分页的冲突select distinct o from Order o join fetch o.items setMaxResults(20)SQL 层 JOIN 后行数 订单×子项数据库无法在 JOIN 前「截取 20 个订单」。Hibernate 的处理是先全量执行再内存截取并输出警告HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!标准解法是两步查询// 第一步分页取订单 ID无集合 JOIN可安全分页ListLongidsem.createQuery(select o.id from Order o order by o.createTime desc,Long.class).setFirstResult(offset).setMaxResults(size).getResultList();// 第二步按 ID 批量 fetch 子项ListOrderordersem.createQuery(select o from Order o join fetch o.items where o.id in :ids,Order.class).setParameter(ids,ids).getResultList();或用 EntityGraph 动态指定抓取路径05 篇。七、Hibernate 6 新查询能力7.1 SelectionQuery / MutationQueryListOrderorderssession.createSelectionQuery(from Order where status :s,Order.class).setParameter(s,OrderStatus.PAID).setMaxResults(20).getResultList();泛型化结果类型、链式调用取代旧org.hibernate.query.Query的部分职责。与 JPA 的TypedQuery并存面向移植用 JPA API用原生 Session 能力时用新 API。7.2 SQM统一的语义模型Hibernate 6 把 HQL、JPQL、Criteria 统一解析为 **SQMSemantic Query Model**语义树再翻译为 SqlAst、最终输出方言 SQLHQL / Criteria │ 解析 ▼ SQM语义树实体/属性/谓词类型已校验 │ 翻译 ▼ SqlAstSQL 抽象树 │ 渲染 ▼ 方言 SQL收益属性存在性与类型在语义期就校验报错更早更准、Criteria 与 HQL 行为一致、优化空间统一。用户感知层面是报错信息质量明显提升。7.3 其他增强窗口函数透传select ..., row_number() over (partition by o.customer order by o.amount desc) ...可直接写在 HQL 中VALUES 子句构造内存行集参与 JOIN替代临时表场景JSON 函数配合JdbcTypeCode(SqlTypes.JSON)的字段做路径访问。八、总结JPQL 操作实体与属性由实现翻译为方言 SQL路径导航即隐式 JOIN深路径是性能信号参数一律绑定命名参数优先。fetch join 是消灭 N1 的首选但三条铁律不能与分页同用、单查询最多 fetch 一个集合、配 distinct。批量变更绕过持久化上下文不同步实体、不级联执行后应 clear 或隔离事务适合大规模状态迁移。Criteria 解决运行期动态条件配静态元模型获得编译期校验静态查询用 JPQL。读路径优先投影构造表达式/DTO不进上下文、不做脏检查写路径才加载实体。分页必须配 order by深分页改键集fetch join 分页用两步查询。Hibernate 6 的 SQM 统一了查询解析SelectionQuery/MutationQuery 是类型化新入口窗口函数等能力增强。九、常见高频面试题1. JPQL 和 SQL 的区别为什么要用 JPQL要点JPQL 面向实体与属性SQL 面向表与列JPQL 由实现翻译为方言 SQL获得数据库无关性且能利用缓存与状态管理集成。代价是与物理结构隔一层、个别复杂查询能力受限此时用原生查询逃生。2. fetch join 的作用和使用限制要点fetch join 在查询时把关联对象一并初始化一次 SQL 消灭 N1。限制不能与 setFirstResult/setMaxResults 同用会内存分页HHH000104 警告一条查询只应 fetch 一个集合多集合笛卡尔爆炸通常配 distinct 对父去重。分页场景用两步查询先查 ID 再 fetch。3. HQL/JPQL 的 update/delete 语句有什么特殊性要点批量变更直接执行 SQL绕过持久化上下文已加载实体不会被同步可能读到旧状态、不触发级联与实体监听。执行后应 clear 上下文或隔离在独立事务。适合大批量状态迁移日常业务修改仍走实体。4. 什么是 N1 问题Hibernate 中怎么解决要点先查父列表 1 条再逐个触发关联查询 N 条。解法按优先级fetch join一次 JOIN 带回、EntityGraph 动态指定抓取路径、BatchSize 批量 IN、DTO 投影绕开关联。检测靠 SQL 日志统计条数或慢查询监控。5. Criteria API 适合什么场景有什么缺点要点适合运行期动态拼装条件管理后台筛选器、开放查询配合静态元模型jpamodelgen获得编译期属性校验。缺点代码冗长、可读性差复杂查询维护成本高静态查询推荐 JPQL。6. 为什么只读查询推荐用 DTO 投影而不是加载实体要点构造表达式select new DTO(…)只取需要的列减少内存与网络开销结果不是实体不进入持久化上下文、不做脏检查、无懒加载陷阱。读路径投影、写路径实体是标准分工。7. Hibernate 分页为什么要配 order by深分页怎么优化要点无 order by 时结果顺序未定义翻页会出现重复或丢行。深分页大 offset数据库要先扫描丢弃前面所有行成本线性上涨改用键集分页where id :lastId order by id limit n走索引或业务限制页数、搜索场景交给搜索引擎。8. fetch join 加内存分页HHH000104是怎么产生的如何规避要点集合 fetch 后结果集是父×子行级笛卡尔数据库无法在 JOIN 前截取「N 个父」Hibernate 只能全量加载后在内存截取并告警。规避两步查询第一步纯父表分页取 ID第二步 in ids fetch join 加载或用 EntityGraph 分页父表分页后再按图抓取。9. 多态查询怎么写不同继承策略下的性能差异要点from Parent 自动覆盖所有子类type(e)/instance of 过滤子类treat(e as Sub) 访问子类独有属性。SINGLE_TABLE 单表加判别列最高效JOINED 需要 JOINTABLE_PER_CLASS 多态查询退化为 UNION性能最差。10. Hibernate 6 查询体系有哪些变化要点HQL 与 JPQL 统一解析为 SQM 语义树再翻译为方言 SQL语义期校验更早、报错更准新增 SelectionQuery/MutationQuery 类型化 API支持关联外 JOIN ON、窗口函数、VALUES 子句旧 Query API 仍兼容但推荐新入口。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进