ORM 最佳实践(Hibernate N+1、MyBatis 优化、JPA vs JDBC)

共 19 题
📑 题目列表 19 题
#
★★★

1. Hibernate N+1 查询问题的成因,懒加载关联对象导致 1+N 次 SQL?

请说明 Hibernate N+1 查询问题的成因,以及懒加载关联对象为何导致 1+N 次 SQL?

  • N+1 问题的定义与成因
  • 懒加载与关联对象
  • 触发条件

Hibernate 的 N+1 查询问题指:查询出 N 条父记录后,在访问每条父记录的懒加载关联对象时,又各自触发一次 SQL 查询,总共产生 1(查父)+ N(查每条父的关联)次 SQL。成因是:实体关联默认懒加载(lazy),当批量查询父实体后,遍历访问子关联(如 parent.getChildren())时,Hibernate 对每个父实体单独发一条查询子表的 SQL,造成 N+1。典型场景如 session.createQuery("from Parent where ...") 查到 N 个 Parent,再循环访问 getChildren()。N+1 导致大量往返、性能急剧下降,尤其在关联层级深或 N 大时。

N+1 的本质是"懒加载 + 逐条访问"导致的查询放大。解决方向是"一次取齐"(JOIN、批量抓取、子查询)或"避免遍历访问懒加载属性"。识别 N+1 常用开启 SQL 日志打印 count 观察。

#
★★★

2. N+1 问题的解决方案,JOIN FETCH、@EntityGraph、@BatchSize、子查询 FETCH?

请说明 N+1 问题的解决方案,包括 JOIN FETCH、@EntityGraph、@BatchSize 与子查询 FETCH?

  • JOIN FETCH 与 @EntityGraph 的用法
  • @BatchSize 的批量抓取
  • 子查询 FETCH 与适用场景

解决 N+1 的常用方案:JOIN FETCH:在 JPQL/HQL 中用 left join fetch 一次性把关联对象 JOIN 出来,一次 SQL 取齐,适合父表行数较少、关联可 JOIN 的场景;缺点是可能产生结果集重复(笛卡尔膨胀)或分页错误。@EntityGraph:在 JPA 中用 @EntityGraph(attributePaths = {"children"}) 声明抓取路径,底层生成 JOIN 或子查询抓取,语义清晰、可复用。@BatchSize:对实体关联设置 @BatchSize(size = 10),让 Hibernate 一次性批量加载多个父对象的关联(用 IN 子句),减少为 N 次变 N/batch 次。子查询 FETCH(subselect fetch):用一个子查询一次性抓取所有关联(@Fetch(FetchMode.SUBSELECT)),适合整个集合加载。选择依据:JOIN 适合少量行、关联路径确定;subselect 适合一次加载整个集合;@BatchSize 适合无法预知但需批量访问的场景。

各方案本质都是"减少 SQL 次数,把逐条查询变为批量/一次取齐"。JOIN FETCH 最直观但要注意笛卡尔积与分页;@EntityGraph 是 JPA 标准做法;@BatchSize 用 IN 批量加载;subselect 适合一次加载全部集合。需结合数据量、关联深度与分页需求选择。

JOIN FETCH 与 @EntityGraph 示意:

// JPQL:一次取齐 children
TypedQuery<Parent> q = em.createQuery(
  "select p from Parent p left join fetch p.children where p.id = :id", Parent.class);
// JPA 用法:@EntityGraph 声明抓取路径
@EntityGraph(attributePaths = {"children"})
List<Parent> findByStatus(String status);
#
★★★

3. MyBatis 的 ExecutorType.BATCH 与 JDBC BATCH 的性能差异与正确用法?

请说明 MyBatis 的 ExecutorType.BATCH 与 JDBC BATCH 的性能差异与正确用法?

  • ExecutorType.BATCH 的工作方式
  • 与 JDBC addBatch 的关系
  • 正确用法与限制

MyBatis 的 ExecutorType.BATCH 使用 JDBC 的批量执行(PreparedStatement.addBatch + executeBatch)把多条 SQL 合并为一次网络往返执行,显著提升批量写入性能。MyBatis 中的批量执行器(BatchExecutor)会缓存并复用 PreparedStatement,把同一 SQL 的多次执行批量化,直到 flush 才真正发送。与直接用 JDBC BATCH 的差异:JDBC BATCH 是底层机制,需开发者手动 addBatch 并控制批大小;MyBatis 的 BATCH 执行器封装了批处理,更易用,但需注意开启 rewriteBatchedStatements=true(MySQL)才能真正合并以减少往返。正确用法:在 Mapper 上通过 sessionFactory.openSession(ExecutorType.BATCH) 或 SqlSessionTemplate 配置 BATCH,批量插入时复用同一 SqlSession,并及时 flush 与清理,避免批缓存过大。

批量性能的关键是"减少往返次数"。MyBatis BATCH 通过 JDBC 批处理实现,配合 rewriteBatchedStatements 在 MySQL 下才能把多条 INSERT 合并为一条多值 INSERT。使用时要控制批大小、避免把缓存无限累积,并注意 BATCH 下查询结果延迟(需 flush 才能看到)。

#
★★★

4. JPA vs JDBC vs MyBatis,不同场景下的选型依据与性能权衡?

请说明 JPA、JDBC 与 MyBatis 在不同场景下选型依据与性能权衡?

  • 三种方式的抽象程度与特点
  • 开发效率与性能权衡
  • 场景选型

JPA(Hibernate)是高度抽象的 ORM:自动管理实体映射、缓存、脏检查、懒加载,开发效率高、代码量少,但隐藏 SQL、性能优化(如 N+1、批量)需深入理解,适合复杂领域模型、CRUD 密集、需要快速开发的业务。MyBatis 是半自动 ORM:SQL 由开发者编写,映射灵活,适合复杂 SQL、动态 SQL、需要精细控制 SQL 的场景,性能可控、DBA 友好,但需手写大量 SQL。JDBC 是最底层:直接操作 SQL 与结果集,性能最高、控制最细,但开发量大、易出错,适合对性能极度敏感或简单的 SQL 场景。选型权衡:开发效率(JPA > MyBatis > JDBC)与性能/控制力(JDBC > MyBatis > JPA)呈反比。业务复杂、SQL 复杂选 MyBatis;实体模型复杂、CRUD 多选 JPA;性能极致或特殊情况选 JDBC。

选型核心是"在开发效率、SQL 可控性、性能之间权衡"。JPA 抽象高但付出"性能理解成本";MyBatis 平衡了 SQL 控制与映射;JDBC 最底层但效率低。实际项目常组合使用(JPA 做简单 CRUD,MyBatis 做复杂 SQL,JDBC 做批量)。

#
★★★

5. ORM 批量写入的批大小与 flush 策略(JDBC batch 与 Hibernate 的 flush 模式)

请说明 ORM 批量写入的批大小与 flush 策略,以及 JDBC batch 与 Hibernate 的 flush 模式?

  • JDBC batch 的批大小控制
  • Hibernate 的 flush 模式(AUTO/COMMIT/MANUAL)
  • 批大小与事务长度权衡

ORM 批量写入要控制批大小与 flush 时机。JDBC batch 通过 addBatch 累积 N 条后 executeBatch 发送,批大小需权衡:过大占用内存、导致长事务与长锁;过小则往返多、性能差,通常取几百到几千。Hibernate 的 flush 模式决定何时把持久化上下文中的变更同步到数据库:AUTO(默认,查询前自动 flush,保证一致性)、COMMIT(事务提交时 flush)、MANUAL(手动 flush,需调用 flush())。批量写入时用 MANUAL 或 AUTO 配合手动分批 flush,避免在持久化上下文里堆积过多实体导致内存膨胀。正确做法:每 N 条 flush 一次并 clear() 清理上下文,控制事务长度与批大小,避免长事务持锁。

批量写入性能与内存、事务长度的平衡是核心。批大小决定 SQL 往返次数与内存占用,flush 时机决定与数据库的同步节奏。Hibernate 中若上下文过大,flush 时一次性发送大量 SQL 会突发压力,因此要"分批 flush + clear"。事务长度要控制,避免长事务持锁拖垮并发。

#
★★★

6. 只读事务(readOnly=true)为什么能提升性能?跳过脏检查与 flush、连接只读路由与数据库端只读优化的机制是什么?

请说明只读事务(readOnly=true)为什么能提升性能,以及跳过脏检查与 flush、连接只读路由与数据库端只读优化的机制?

  • readOnly 事务跳过脏检查与 flush
  • 连接只读路由
  • 数据库端只读优化

只读事务(readOnly=true)提升性能的机制:Hibernate 在只读事务中不会进行脏检查(dirty checking)与不必要的 flush,避免缓存快照比较与变更同步的开销,也避免产生写操作;JPA/Hibernate 会设置 FlushMode 为 MANUAL/NEVER,减少无谓的 SQL 发送。连接只读路由:持久化框架(配合读写分离)会把只读事务路由到只读副本(从库),分担主库压力、提升读吞吐。数据库端只读优化:数据库可对只读事务做只读设置(如 MySQL 的 SET TRANSACTION READ ONLY),优化器可据此选择更优计划、减少锁开销,某些场景还允许快照读(MVCC)减少锁等待。综合这些机制,只读事务减少了不必要的工作并优化了资源利用。

readOnly=true 的本质是"向框架与数据库声明:此事务不会写",从而省去脏检查/flush、路由到只读节点、使用数据库只读优化。注意:readOnly 不是强制只读,若实际写入可能报错或绕过,因此要配合事务边界与数据库只读设置才能真正生效。

#
★★★

7. ORM 分页生成的 SQL 与方言差异(LIMIT/OFFSET、ROW_NUMBER)如何影响性能?深分页为什么慢、keyset 分页如何改写?

请说明 ORM 分页生成的 SQL 与方言差异(LIMIT/OFFSET、ROW_NUMBER)如何影响性能,为什么深分页慢,以及 keyset 分页如何改写?

  • 分页的方言差异(LIMIT/OFFSET vs ROW_NUMBER)
  • 深分页慢的原因
  • keyset 分页(sargable)改写

ORM 分页会按数据库方言生成 SQL:MySQL/PostgreSQL 用 LIMIT ? OFFSET ?,SQL Server/Oracle 用 ROW_NUMBER() OVER(...)FETCH NEXT。深分页(OFFSET 很大)慢的原因:数据库仍需扫描并丢弃前 OFFSET 行,越往后越慢,因为要读取并跳过大量数据,且无法利用索引直接定位。keyset 分页(sargable/seek 分页)改写:不用 OFFSET,而是用上一页最后一条的排序键作为条件,如 WHERE id > lastId ORDER BY id LIMIT 20,利用索引直接定位到起点,只取需要的行,性能稳定且与页码无关。ORM 中可用 where id > :lastId(需传入游标)实现,或借助 Pageable 的 Interceptor 改写。keyset 分页适合"下一页"场景,但不适合任意跳页(跳页仍需 OFFSET)。

深分页的瓶颈是"OFFSET 跳过 + 排序",keyset 通过"条件定位 + 索引"消除跳扫,把耗时从 O(offset) 降为 O(page)。但它有"上次位置"状态,不适用于随机跳页。选型:数据量大时优先 keyset;小数据量或需要跳页时用 OFFSET。

keyset 分页示意:

-- 传统深分页(第 100000 页,慢)
SELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 1999980;
-- keyset 分页:用上一页最后一条 id 定位,走索引,快
SELECT * FROM orders WHERE id > 1999980 ORDER BY id LIMIT 20;
#
★★

8. Open Session in View(OSIV)模式的风险,长 Session 导致连接池耗尽与 LazyInitializationException?

请说明 Open Session in View(OSIV)模式的风险,包括长 Session 导致连接池耗尽与 LazyInitializationException?

  • OSIV 的工作方式
  • 长 Session 占连接的风险
  • LazyInitializationException 的成因

OSIV(Open Session in View)模式让 Hibernate Session(数据库连接)在 HTTP 请求的整个生命周期(包括视图渲染阶段)保持打开,以便在视图/序列化时懒加载关联对象。其风险:长 Session 长时间占用数据库连接,在高并发下连接池连接被占满导致连接耗尽、应用阻塞;同时事务边界被模糊,事务外的读写与懒加载可能引发一致性问题。LazyInitializationException 成因:Session 关闭后访问懒加载属性会抛异常;OSIV 通过延长 Session 生命周期避免该异常,但代价是长连接。此外 OSIV 使数据库连接在响应渲染期间一直占用,放大资源损耗。

OSIV 是"用长连接换懒加载便利"的妥协,适合低并发/简单场景,但在高并发下危险。更优做法是:在事务/服务层预先加载所需数据(JOIN FETCH、DTO、批量抓取),关闭 OSIV,把连接生命周期缩短到事务边界。LazyInitializationException 应通过"正确规划抓取"解决,而非靠 OSIV 硬扛。

#
★★

9. Hibernate 的 StatelessSession 与无状态批量操作优化?

请说明 Hibernate 的 StatelessSession 与无状态批量操作优化?

  • StatelessSession 的特点
  • 无状态批量操作的性能优势
  • 适用场景与限制

Hibernate 的 StatelessSession 跳过持久化上下文(一级缓存)与脏检查,不维护实体快照、不级联、不懒加载,直接以无状态方式执行 SQL。它非常适合批量写入:由于不做实体缓存与脏检查,批量插入/更新时不会在内存中堆积实体,也无需 flush 清理,性能显著优于普通 Session 的逐条持久化。用法:sessionFactory.openStatelessSession(),用 insert()/update()/delete() 直接操作。其优势是内存占用低、速度快;局限是失去 JPA 的缓存、级联、懒加载等高级特性,需要手动管理关联与顺序,且不适用需要实体状态管理的复杂场景。

无状态批量操作的优化本质是"去掉 ORM 的缓存与脏检查开销"。批量导入/ETL 等场景用 StatelessSession 最合适,配合 JDBC batch 效果更佳。但它牺牲了对象生命周期管理,适合"纯数据搬运"而非"业务对象交互"。

#
★★

10. MyBatis 的 foreach 批量插入与 rewriteBatchedStatements 的协同优化?

请说明 MyBatis 的 foreach 批量插入与 rewriteBatchedStatements 的协同优化?

  • foreach 批量插入的 SQL 形式
  • rewriteBatchedStatements 的作用
  • 协同优化的原理

MyBatis 的 foreach 批量插入会生成一条多值 INSERT(INSERT INTO t (cols) VALUES (...),(...),...),一条 SQL 插入多行,减少往返。但要真正生效,需配合 MySQL 的 rewriteBatchedStatements=true 连接参数。该参数的作用是:让 JDBC 驱动把一批 addBatch 的单条 INSERT 重写为一条多值 INSERT(合并 VALUES 子句),从而把多次往返合并为一次;配合 MyBatis 的 ExecutorType.BATCH,即使不显式写 foreach,也能获得合并效果。协同优化后,批量插入的 SQL 往返次数大幅减少,网络与执行开销显著下降。注意:rewriteBatchedStatements 对 UPSERT 等场景有特殊处理,且受 max_allowed_packet 限制,批大小需控制。

批量插入优化核心是"减少往返、合并 SQL"。foreach 生成多值 INSERT 是"应用层合并",rewriteBatchedStatements 是"驱动层合并",两者都依赖批大小适配 max_allowed_packet,避免单条 SQL 过大。

MyBatis foreach 批量插入示意:

<insert id="batchInsert">
  INSERT INTO orders (id, user_id, amount)
  VALUES
  <foreach collection="list" item="o" separator=",">
    (#{o.id}, #{o.userId}, #{o.amount})
  </foreach>
</insert>
// JDBC URL 打开 rewriteBatchedStatements 以合并批量 SQL
jdbc:mysql://host:3306/db?rewriteBatchedStatements=true
#
★★

11. Hibernate 的 @Version 乐观锁实现与 OptimisticLockException 处理?

请说明 Hibernate 的 @Version 乐观锁实现与 OptimisticLockException 处理?

  • @Version 乐观锁原理
  • 更新冲突检测
  • OptimisticLockException 处理与重试

Hibernate 的 @Version 乐观锁通过版本号字段实现:@Version private long version;@Version private Date updateTime;。更新时 Hibernate 会带上版本条件(WHERE id=? AND version=?),若版本不匹配(其他事务已更新),则更新影响行数为 0,Hibernate 抛出 OptimisticLockException(或 StaleObjectStateException),表示并发冲突。乐观锁不持锁,适合读多写少、并发冲突少的场景。处理 OptimisticLockException:捕获异常后,重新加载最新数据、合并用户修改后重试,或直接提示用户冲突;对批量操作可设置重试次数。注意:乐观锁依赖版本列,需保证版本字段正确映射,且乐观锁在跨节点/跨库时需配合分布式事务。

乐观锁的核心是"先比较后更新",用版本号做比较条件,冲突时更新失败。它比悲观锁并发生高(无锁),但需要处理冲突。正确做法是捕获冲突、重载数据、合并重试,避免静默覆盖数据。

@Version 实体与冲突处理示意:

@Entity
public class Order {
    @Id private Long id;
    @Version private long version; // 乐观锁版本
    private BigDecimal amount;
}
try {
    orderRepo.save(order);
} catch (OptimisticLockException ex) {
    // 冲突:重新加载最新版本,合并修改后重试
    Order latest = orderRepo.findById(order.getId()).orElseThrow();
    latest.setAmount(order.getAmount());
    orderRepo.save(latest);
}
#
★★

12. MyBatis 的 ${} 与 #{} 的安全差异,SQL 注入风险?

请说明 MyBatis 的 ${} 与 #{} 的安全差异与 SQL 注入风险?

  • #{} 的参数绑定(PreparedStatement)
  • ${} 的字符串拼接与注入风险
  • 正确使用方式

MyBatis 中 #{} 使用预处理语句(PreparedStatement)参数绑定,把值作为参数传给数据库,数据库编译 SQL 时把参数当数据,天然防注入。${} 则做字符串拼接,直接把值拼进 SQL 文本,若值来自用户输入则存在 SQL 注入风险(如 name=${name} 被注入 ' OR 1=1 --)。因此 #{} 是安全首选,用于所有普通参数绑定。${} 只应用于无法用参数占位符的场景(如动态表名、列名、ORDER BY 字段、LIMIT 等),且这些值必须来自可信来源(白名单、枚举、内部配置),不能直接使用用户输入。使用 ${} 时务必做白名单校验。

区别的本质是"参数化" vs "拼接"。#{} 走参数化杜绝注入;${} 走拼接,若拼接不可信数据则注入。原则:能用 #{} 一律用 #{},只有表达式需要 LIKE 部分拼接、动态表名列名等才用 ${},并做严格白名单。

#{} 与 ${} 的对比示意:

<!-- 安全:参数化绑定 -->
<select id="findByName" resultType="User">
  SELECT * FROM user WHERE name = #{name}
</select>
<!-- 危险:字符串拼接,若 name 来自用户输入则可能注入 -->
<select id="findByNameUnsafe" resultType="User">
  SELECT * FROM user WHERE name = '${name}'
</select>
#
★★

13. MyBatis 的批处理,ExecutorType.BATCH 与 foreach insert?

请说明 MyBatis 的批处理,ExecutorType.BATCH 与 foreach insert 的区别与用法?

  • ExecutorType.BATCH 的机制
  • foreach insert 的机制
  • 两者的区别与选择

MyBatis 批处理有两种方式。ExecutorType.BATCH:通过 SqlSessionFactory.openSession(ExecutorType.BATCH) 使用批量执行器,重用 PreparedStatement 并调用 addBatch 累积,flush 时一次性执行,多次调用同一语句会合并为一条批处理;适合"循环调用 Mapper 单条插入"的场景。foreach insert:在 XML 中用 <foreach> 生成一条多值 INSERT,一次性插入多行;适合一次性传入 List 批量插入。两者区别:BATCH 是"多条语句合并为一条批处理",foreach 是"一条 SQL 包含多行";BATCH 对 SQL 拼接更灵活(可复用不同语句),foreach 需显式生成多值 SQL。性能上,BATCH 配合 rewriteBatchedStatements 也很高效;foreach 生成大 SQL 受 max_allowed_packet 限制。选择:数据量大且需复用语句用 BATCH,一次性插入一个集合用 foreach。

两者都是批量手段,核心差异是"合并方式":BATCH 在驱动层合并,foreach 在 SQL 层合并。BATCH 不改变 SQL 单条结构,foreach 生成大 SQL。实际使用中要控制批大小,避免内存与 SQL 过大。

#
★★

14. Hibernate/JPA 的 DTO 投影(Constructor Expression/Interface Projection)与实体查询的性能差异

请说明 Hibernate/JPA 的 DTO 投影(Constructor Expression/Interface Projection)与实体查询的性能差异?

  • DTO 投影的两种形式(Constructor Expression、Interface Projection)
  • 投影与实体查询的性能差异
  • 适用场景

JPA 的 DTO 投影只查询需要的列,不加载整个实体。Constructor Expression:在 JPQL 中用 new com.example.OrderDTO(id, amount) 形式,把查询结果构造成 DTO,只 SELECT 指定列。Interface Projection:定义接口,用 Spring Data 的接口投影(closed projection)只取接口声明的字段。性能差异:实体查询会加载所有列并纳入持久化上下文(缓存、脏检查、懒加载管理),而 DTO 投影只返回所需列、不进入持久化上下文,减少列传输、内存占用与对象管理开销,且通常生成的 SQL 更精简(只 SELECT 需要的列)。对只读列表、报表等场景,DTO 投影性能更好、更轻量。但 DTO 投影失去实体状态管理,不能 lazy 级联(需显式加载)。

DTO 投影的性能收益来自"只取所需列 + 不进入持久化上下文"。适合只读展示、报表。实体查询适合需要状态管理、级联、缓存更新等场景。应按需选择,避免"查全表实体只为展示几个字段"。

Constructor Expression 投影示意:

// JPQL 构造 DTO 投影,只取所需列
TypedQuery<OrderDTO> q = em.createQuery(
  "select new com.example.OrderDTO(o.id, o.amount) from Order o", OrderDTO.class);
#
★★

15. 批量写为什么需要分批提交?max_allowed_packet 与批大小、事务长度如何权衡,失败时整批回滚的粒度如何设计?

请说明批量写为什么需要分批提交,max_allowed_packet 与批大小、事务长度如何权衡,以及失败时整批回滚的粒度如何设计?

  • 分批提交的原因
  • max_allowed_packet 与批大小、事务长度权衡
  • 回滚粒度设计

批量写需要分批提交的原因:一次提交过多会占用大量内存、产生长事务(持锁时间长、阻塞并发、回滚日志膨胀),且单条 SQL 若拼接过多行可能超过数据库限制(如 max_allowed_packet)。因此把大批量拆成多个批次,每批提交一次。权衡:批大小受 max_allowed_packet 限制(一批生成的 SQL 不能超过该值),同时批大小与事务长度权衡——批越大、事务越长、锁越久、回滚成本越高;批越小、往返越多、性能越差,需取平衡。回滚粒度设计:失败时整批回滚的粒度取决于业务——若要求"错误批次整体回滚、其他批次成功",则每批一个事务,失败回滚整批;若不要求部分成功,可接受"批内部分成功",则不做批内事务控制。一般采用"每批一个事务",失败时重试该批或记录失败批次,避免大批量整体回滚导致的工作量浪费。

分批提交的核心是"在性能、内存、事务长度、回滚代价之间平衡"。批大小要适配 max_allowed_packet 与事务设计,回滚粒度按"是否允许部分成功"设计。批量导入常见策略是"多批事务 + 失败重试 + 失败记录",保证可恢复。

#

16. Hibernate 的 @NaturalId 缓存与二级缓存的关系?自然键查找如何减少 SQL 往返?

请说明 Hibernate 的 @NaturalId 缓存与二级缓存的关系,以及自然键查找如何减少 SQL 往返?

  • @NaturalId 与二级缓存的关系
  • 自然键缓存查找
  • 减少 SQL 往返的原理

Hibernate 的 @NaturalId 标记实体的自然键(业务唯一键,如用户名、身份证号),基于自然键的查找可用 byNaturalId() 进行。@NaturalId 缓存与二级缓存的关系:自然键缓存(NaturalIdCache)通常挂在二级缓存(Second Level Cache)之上,通过自然键先查缓存拿到主键,再经主键查二级缓存。启用 @NaturalId(mutable = true/false) 且配置二级缓存后,byNaturalId().load() 先查自然键缓存(命中缓存则无需查库),从而减少 SQL 往返。若自然键缓存未命中,才回落到数据库查询。合理地启用自然键缓存能让"按业务键查找"变为缓存命中,显著减少数据库往返,尤其适合频繁按自然键查询的场景。

自然键缓存的价值是"把业务键查找变成缓存查找"。它依赖二级缓存,需同时配置二级缓存。注意:自然键不可变(mutable=false)时缓存更安全;缓存失效与一致性需由缓存策略管理。它是"减少往返"的缓存手段,非唯一解。

自然键查找示意:

// 通过自然键查找,先查自然键缓存再查实体缓存
User u = session.byNaturalId(User.class)
    .using("username", "zhangsan")
    .load();
#

17. JPA(Java Persistence API)的 EntityGraph 与 Criteria API 动态查询?

请说明 JPA 的 EntityGraph 与 Criteria API 动态查询?

  • EntityGraph 的抓取图
  • Criteria API 的类型安全动态查询
  • 使用场景

JPA 的 EntityGraph 定义"抓取图"(fetch graph),声明查询时要加载哪些关联属性,避免 N+1,通过 @EntityGraph(attributePaths = {...}) 或编程式 em.createEntityGraph() 创建,可作用于 JPQL/Criteria 查询,控制 JOIN 抓取。Criteria API 是类型安全的动态查询 API,用 CriteriaBuilder、CriteriaQuery、Root 等对象构建查询,避免了字符串拼接的 SQL 注入风险与编译期错误,适合运行时动态拼接查询条件(如按多个可选条件过滤)。两者可以结合:用 Criteria 构建动态条件,用 EntityGraph 控制关联抓取。EntityGraph 解决"抓取粒度",Criteria 解决"动态条件",配合使用能兼顾性能与灵活性。局限是 Criteria 代码较繁琐、可读性差。

EntityGraph 是 JPA 对"抓取计划"的标准化表达,用于控制懒加载范围;Criteria 是类型安全动态查询。两者常结合用于复杂查询。EntityGraph 通过声明抓取路径减少往返,Criteria 通过对象化构建避免动态 SQL 注入。

#

18. ORM 生成 SQL 的审查(show-sql、格式化、慢 SQL 定位)

请说明 ORM 生成 SQL 的审查,包括 show-sql、格式化与慢 SQL 定位?

  • show-sql 与 SQL 格式化
  • SQL 日志与执行计划
  • 慢 SQL 定位

ORM 生成 SQL 的审查用于发现生成的 SQL 是否高效。show-sql:开启 Hibernate 的 hibernate.show_sql=true 或 MyBatis 的日志,打印生成的 SQL,便于检查(如是否多表 join、是否缺索引、是否产生 N+1)。格式化:设置 hibernate.format_sql=true 让 SQL 易读,配合 hibernate.use_sql_comments=true 标注执行的 mapper/方法定位来源。慢 SQL 定位:把生成的 SQL 拿到数据库执行 GET 执行计划(EXPLAIN),分析是否走索引、扫描行数、连接方式;结合数据库慢查询日志与 ORM 日志的时间戳,定位耗时 SQL。还可通过 p6spy 等代理工具捕获并审查 ORM 实际执行的 SQL。审查的产出是优化建议(加索引、改写抓取、调整关联)。

ORM 审查的意义是"把 ORM 生成的 SQL 纳入性能监控"。show-sql 看生成内容,EXPLAIN 看执行计划,慢日志看耗时。关键是把"ORM 操作"与"底层 SQL"关联起来,定位问题(如 N+1、轮询、无索引)。

#

19. ORM 会话何时获取数据库连接(延迟获取)?与连接池超时、事务边界如何配合避免长事务占连接?

请说明 ORM 会话何时获取数据库连接(延迟获取),以及与连接池超时、事务边界如何配合避免长事务占连接?

  • 连接的延迟获取(lazy connection acquisition)
  • 事务边界与连接生命周期
  • 避免长事务占连接

很多 ORM/持久化框架会"延迟获取连接"(lazy connection acquisition):只有真正执行 SQL(或显式开启事务)时才从连接池获取连接,而不是在打开 Session 时立即获取。这样可避免"创建 Session 但长时间不执行 SQL"时白白占用连接。为避免长事务占连接,需配合事务边界:连接在事务开始(或首次 SQL)时获取,在事务提交/回滚时释放;要确保事务尽快提交,避免在事务内做耗时操作(调用外部服务、等待、大循环)。与连接池超时配合:连接池有获取超时(getConnectionTimeout)与空闲回收(maxIdleTime)等,若事务超时或连接占用过久,最终会池耗尽。最佳实践:事务保持短小、快速提交;Session 生命周期与事务对齐;读操作用只读事务并尽快释放。

延迟获取连接减少无谓占用,事务边界控制连接生命周期。核心是"让连接跟事务走,事务短小快提交"。避免在事务内做慢操作,且 Session 用完即关(try-with-resources),配合连接池超时与回收策略,防止连接耗尽。