Spring Data JPA 和 JPA

Spring 中的 JPA 和 Hibernate 有什么区别?Spring Data JPA 能不能脱离 Hibernate 用?

JPA 是 Java 官方定义的 ORM 规范,只定义了接口和注解,不提供实现

Hibernate 是 JPA 规范的一个具体实现,是真正负责把 Java 对象和数据库表之间做映射、执行 SQL。而 Hibernate 的一个依赖就是 JDBC

Spring Data JPA 是 Spring 对 JPA 的再封装,不是 JPA 实现,底层默认用 Hibernate

而且 Spring Data JPA 能脱离 Hibernate 使用,因为 Spring Data JPA 是一个抽象层,支持任何符合 JPA 规范的实现框架

JPA 和 Hibernate 的”全自动 ORM”到底全自动在哪?代价是什么?

JPA 是按字段访问还是按属性访问?它怎么决定的?能混用吗?

Spring Data JPA 有哪几种写查询的方式?分别在什么场景用?方法名派生的关键字都有哪些规则?Criteria API 和 JPQL 有什么区别?各自优缺点?通过 JPA Criteria 和 JPQL 优化复杂查询的好处是什么,为什么不用方法名派生?

一套实体映射你常用的注解有哪些?JPA 中还有哪些常用的注解。详细说说它们。双向 @OneToMany/@ManyToOne 的维护方是哪一边?只设一边会怎样?

四种主键生成策略 AUTO / IDENTITY / SEQUENCE / TABLE 你项目里用哪个?为什么?你都是如何选择主键的?@SequenceGenerator 的 name 和 sequenceName 有什么区别?

实体类为什么必须有或最好有无参构造器?

spring.jpa.open-in-view 默认是 true,你怎么看?关掉之后要注意什么?

save()、persist()、merge()、saveOrUpdate() 有什么区别?为什么我 save() 一条新对象,日志里先出现一条 SELECT?

findById()、getById()、getOne()、getReferenceById() 有什么区别?

JPQL 里执行 update/delete 为什么必须加 @Modifying?你了解 @Modifying的具体行为吗?为什么 SQL 执行了内存里的对象却没变?

CascadeType 都有哪些?orphanRemoval 和 CascadeType.REMOVE 有什么区别?

@Embedded/@Embeddable 用在哪?@Embeddable + @ElementCollection 有什么区别?

JPA 实体为什么要小心重写 equals/hashCode?如何重写他们才能保证不出问题?

代表关联的相关注解了解吗?他们是如何实现关联的?双向关联里谁是维护方?为什么必须在一端配 mappedBy?

Sort 和 Pageable 一起用的时候,是 SQL 排序还是内存排序?

为什么用 JPA 时能不用实体查询就不用?我们都写 DTO 进行查询结果的封装。投影(Projection)和 DTO 查询怎么写?

saveAll() 是批量插入吗?saveAll() 为什么常常还是一条一条 INSERT,甚至每条前面还多一个 SELECT?

用 @Version 做乐观锁,它是怎么实现的?失败时你怎么处理?@Version 支持哪些类型?时间戳可以吗?

@Version 乐观锁有哪些失效场景?什么时候用乐观锁、什么时候用分布式锁?乐观锁冲突了怎么重试才安全?

悲观锁怎么写?@Lock(PESSIMISTIC_WRITE) 背后的 SQL 是什么?@Lock 加了但没生效,可能是什么原因?

JPA 审计 (JPA Auditing) 是怎么做的?@CreatedDate/@LastModifiedDate/@CreatedBy 靠什么生效?Auditing 和你们用 AOP + 自定义注解做的操作审计框架有什么区别?为什么不用同一个方案?

多数据源下 JPA 要配哪些东西?JpaTransactionManager 和 DataSourceTransactionManager 有什么区别?多数据源时 @Transactional 为什么要指定 transactionManager?

Hibernate 支持哪几种多租户隔离方式?Hibernate 6 的 @TenantId 是怎么工作的?有什么盲区?多租户下唯一索引、序列和二级缓存要注意什么?

事务管理

你怎么理解 Spring 事务?@Transactional 的失效场景有哪些?事务同步回调和事务事件怎么用?长事务怎么治理?

这道题一般会连着问,从用法问到失效,再问到事务和业务的边界怎么划,最后问到 NESTED 到底能不能用。

事务是怎么生效的?

@Transactional只是声明。工作的是 Spring AOP 代理 + TransactionInterceptor 拦截器 + PlatformTransactionManager 事务管理器三者的配合:

  1. 容器启动时,AbstractAutoProxyCreator 在 postProcessAfterInitialization 阶段发现 Bean 上有 @Transactional,就为它创建代理(有接口用 JDK 动态代理,否则用 CGLIB),这也是为什么只有走代理的调用才会被拦截;
  2. TransactionAttributeSource 解析注解得出一份事务属性(传播行为、隔离级别、回滚规则、超时、只读),只有真正的写方法才会开启事务,只读方法在 readOnly=true 时是优化而非不开启;
  3. 调用时,TransactionInterceptor 先通过事务管理器拿到或新建事务,把连接绑到当前线程(TransactionSynchronizationManager 的 ThreadLocal);
  4. 目标方法执行完,由拦截器决定提交还是回滚。

失效场景

按照”为什么不生效”的根因分成四类,

第一类:调用没走代理

场景 原因 解决
自调用:同一个类里 A 方法调 B 方法,B 上有 @Transactional this 指向原始对象,不是代理对象,调用没经过拦截器 拆到另一个 Bean;自注入 @Autowired private XxxService self; 再 self.B();AopContext.currentProxy()(需 @EnableAspectJAutoProxy(exposeProxy=true));或者把事务方法下沉到独立的 Service
Bean 没被 Spring 管理 直接 new 出来的对象,或者没加 @Component/@Service 注册 交给容器管理,通过注入获取实例

自调用是所有 AOP 增强注解的通用陷阱:@Async、@Cacheable、@Validated 的自调用一样失效,区别只是”失效”的表现不同——@Transactional 表现为不回滚,@Async 表现为变成同步,@Cacheable 表现为每次都查库。

第二类:代理没法拦截这个方法

场景 原因 解决
private 方法 代理无法拦截私有方法 改成 public
final 方法 / final 类 CGLIB 靠继承生成子类,无法覆写 final 去掉 final
static 方法 静态方法属于类,不属于 Bean 实例 改成实例方法
protected 方法 JDK 代理只能拦接口方法;CGLIB 能拦 public/protected,但 Spring 的规则是只有 public 才生效 改成 public

Spring Boot 2.x 起 spring.aop.proxy-target-class 默认是 true,也就是默认走 CGLIB,所以”类没有实现接口”这件事本身不是失效原因,这一点很多人答错。

第三类:异常没触发回滚

场景 原因 解决
rollbackFor 配错或没配 默认只对 RuntimeException 和 Error 回滚,受检异常不回滚 明确写 @Transactional(rollbackFor = Exception.class),业务异常继承 RuntimeException
异常被 catch 吞掉 提交/回滚发生在方法返回之后,方法正常返回 → 提交 要么 throw 出去,要么在 catch 里 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
异常被包装成别的类型 包装成了受检异常,或者包装后不再匹配 rollbackFor 统一异常体系,或者 rollbackFor 用父类
方法返回前就 return 了 逻辑分支提前返回,写操作在返回之后 检查代码路径,确保事务内的写操作都在同一个方法里完成

第四类:上下文不对

场景 原因 解决
多线程里:事务内 new Thread(...) 或丢线程池 事务是绑定在当前线程的,子线程拿到的是另一个连接,看不到未提交数据,子线程异常也不会让主线程回滚 全程单线程;必须异步的用 @TransactionalEventListener(AFTER_COMMIT) + @Async
@Transactional 与 @Async 同方法 异步执行在新线程,事务上下文不会传递,等于没有事务 拆成两个方法,先提交事务再异步
多数据源用错事务管理器 @Transactional 默认用容器里 @Primary 的那个管理器,操作非主库时事务不生效(自动提交) 显式指定 @Transactional(transactionManager = "xxxTransactionManager")
接口上的 @Transactional JDK 代理下注解在接口上不一定被代理类”继承” 规范做法是写实现类的 public 方法上

TransactionSynchronization 与 @TransactionalEventListener 是什么关系?

这一块是事务回调

先说 TransactionSynchronization:每个事务都对应一个同步器集合,由 TransactionSynchronizationManager 维护(同样存在 ThreadLocal 里)。实现这个接口,就能在事务的生命周期上挂回调,顺序是:

回调方法 执行时机 典型用途
suspend() / resume() 事务被挂起 / 恢复时(比如内层用了 REQUIRES_NEW) 上下文的保存与还原
beforeCommit(status) 提交之前,此时回滚仍然有效 补充校验、最终一致性检查
beforeCompletion() 提交/回滚前最后一步 资源清理,别在这里写库
afterCommit() 已提交之后 清缓存、发消息、发通知
afterCompletion(status) 无论提交还是回滚都执行 清理 ThreadLocal、释放自定义资源、记录结果

注册方式有两种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 写法一:手动注册(适合在事务方法内部临时挂一个回调)
@Transactional
public void updateProject(ProjectDTO dto) {
projectMapper.update(dto);

TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
// 只有真正提交成功才会走到这里
cacheEvictService.evict(dto.getId());
}

@Override
public void afterCompletion(int status) {
// 提交或回滚都会走到这里,适合做清理
cleanup();
}
});
}
1
2
3
4
5
6
7
8
/**
* 写法二:更优雅的方式——事件驱动
* 事务提交成功后,Spring 会自动把事件投递给监听器
*/
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onProjectUpdated(ProjectUpdatedEvent event) {
cacheEvictService.evict(event.getProjectId());
}

@TransactionalEventListener 底层就是 TransactionSynchronization,Spring 提供了一个 TransactionSynchronizationEventAdapter(本质是一个 TransactionSynchronization 实现),在对应的回调里把事件发布出去。所以说:

  • @TransactionalEventListener 是”事务同步器 + 事件机制”的封装,它让我们不必在每个业务方法里手写 registerSynchronization,只要在事务提交前后发布一个事件就行,业务代码更干净;
  • 手动 TransactionSynchronization 的优势是”就在当前方法里、不需要额外定义事件类”,适合一次性、跟当前方法强绑定的收尾逻辑。

再补充几个参数:

  • phase:BEFORE_COMMIT(默认值是排在后面的 AFTER_COMMIT,注意默认是 AFTER_COMMIT)、AFTER_COMMIT、AFTER_ROLLBACK、AFTER_COMPLETION;
  • fallbackExecution = false(默认):没有活跃事务时监听器不执行。这就是为什么有的同学写了监听器却发现”没有事务时逻辑丢了”——要么给调用的地方加事务,要么显式设成 true;
  • @Async 可以叠加使用,让回调在独立线程里跑,但要注意它拿不到原事务,也不能指望它回滚;
  • AFTER_COMMIT 里不能再写库:此时事务已结束,新写库是自动提交、没有事务保护,如果需要落库,应单独开一个 REQUIRES_NEW 事务,或者落成补偿任务。

为什么值得花力气用它? 因为它解决的是”事务内不能干的事”:发邮件、发 MQ、清缓存、调第三方。这些动作如果放在事务里,一旦后续回滚,副作用已经发生了,收不回来。用 AFTER_COMMIT 把它们挪到”确定提交之后”,才能保证”业务真的发生了,副作用才发生”。

长事务怎么治理?

长事务的危害是连锁的:连接被占住 → 连接池耗尽 → 其他请求全排队 → 服务雪崩;同时锁持有时间长会让别的会话锁等待甚至死锁,还会让 undo log / binlog 膨胀、主从延迟变大。所以说长事务不是慢一点的问题,而是要主动治理的对象。

1. 只读事务 readOnly = true

它有两个作用:一是给数据库一个明确的信号,MySQL 下可以省去 undo log 的写入、优化查询计划;二是给 Hibernate/JPA 一个信号,把 FlushMode 设为 MANUAL,避免脏检查触发无意义的 UPDATE。

注意两个坑:一是设了 readOnly = true 之后执行增删改可能直接报错(MySQL 会拒绝,或者乐观锁失效),所以纯查询方法才加;二是 readOnly 不会传播给内层,内层如果自己是写事务,要按自己的语义声明。

2. 事务内不要做 RPC、不要循环里逐个写

这是长事务最常见的成因。@Transactional 方法里调第三方接口(微信、短信、外部 API),网络 I/O 可能几秒钟,这几秒里数据库连接一直是占着的。治理方式是”事务边界和工作边界对齐”:

  • 先把本地状态落库(比如记为 pending)并提交事务;
  • 提交后再去同步或异步调外部接口;
  • 回调/重试里再用新事务更新最终状态。

循环里逐个 insert 同理,应该改成批量操作(foreach 批量插入)或者分批提交;如果确实数据量大,就分批 + 幂等 + 断点续跑,别指望一个事务吞下十万条。

3. 给事务设超时

别让慢操作无限期拖住连接,@Transactional(timeout = 3) 让超时直接抛出 TransactionTimedOutException,把问题暴露在代码层而不是连接池层。

4. REQUIRES_NEW 与连接池大小的关系

关键在于:REQUIRES_NEW 是”挂起外层 + 开一个全新的独立事务”,它需要从连接池里再拿一条连接,而外层那条连接并不会释放。

1
2
3
4
@Transactional                          // 占用连接 A
public void outer() {
innerService.doSomething(); // REQUIRES_NEW,再占用连接 B
}

后果是:一次请求就可能占用 2 条甚至更多连接。如果并发 100、每条链路嵌套两层,就要 200 条连接;而 maximum-pool-size 假设只有 20,就会出现”外层全部占用连接、等着内层拿连接,内层又拿不到”的连接池死锁,典型表现是 connection-timeout 超时异常。

所以结论是:REQUIRES_NEW 要当”稀缺资源”用——常见于”审计日志/操作记录必须独立提交”、“失败重试要开新事务不受外层回滚影响”这类场景。用的时候要同步评估连接池大小:线程池并发数 + 嵌套层数,配合 HikariCP 的 maximumPoolSize 和 connectionTimeout 一起算,并监控活跃连接数。

5. 分清 REQUIRES_NEW 和 NESTED

REQUIRES_NEW NESTED
底层机制 挂起外层事务,新建一个独立事务 外层事务内的 savepoint(保存点)
连接 需要另拿一条连接 复用同一条连接
内层回滚的影响 只影响自己,外层不受影响但双方提交时机独立 只回滚到 savepoint,外层可以继续提交
外层回滚的影响 内层已提交则无法回滚,数据会残留 外层回滚时,内层的修改一起回滚
是否原子 不原子 整体仍然是一个事务,可保证原子性

一句话选择标准:“内层的失败不能拖累外层,且内层要真的提交”→ REQUIRES_NEW;“内层失败只回滚自己这一小段,整体还要保持一致”→ NESTED。

NESTED 的实现依赖什么?

NESTED 的本质是数据库的 savepoint,而 savepoint 是连接级别的能力,所以要求两个条件同时成立:

  • 事务管理器要支持 savepoint:DataSourceTransactionManager / JdbcTransactionManager 是支持的(isNestedTransactionAllowed() 为 true);
  • 底层 JDBC 驱动和数据库要支持:MySQL、PostgreSQL 都支持 savepoint。

所以各个组合的结论是:

组合 NESTED 支持情况
MyBatis + DataSourceTransactionManager ✅ 支持最直接,就是 connection.setSavepoint() 的原生能力
JDBC JdbcTemplate + JdbcTransactionManager ✅ 支持
JPA/Hibernate + JpaTransactionManager 需要确认:它委托给底层的 JdbcTransactionManager,理论上有 savepoint 能力,但要和 Hibernate 的 FlushMode、Session 生命周期配合。坑在于”回滚到 savepoint 之后,Hibernate 一级缓存里可能还留着已回滚的实体状态”,容易出现”数据库回滚了但内存里的对象还以为是脏的”这种不一致。所以我一般不在 JPA 场景用 NESTED,用 REQUIRES_NEW 或者干脆拆事务更稳

还有三个容易踩的细节:

  1. 没有外层事务时,NESTED 会退化成 REQUIRED,也就是自己开一个新事务,此时就没有 savepoint 可言了;

  2. 内层要正确声明回滚:内层方法上也要写 rollbackFor,否则受检异常一样不回滚,savepoint 也就不会触发;

  3. 外层必须”消化”内层的异常:因为内层回滚后异常还会向上抛,如果不 catch,外层也会连带整体回滚——那就失去了”内层失败不影响外层”的意义。

实战里我是怎么划事务边界的

  1. 事务方法必须是 public、必须有明确的 rollbackFor,这是底线规范;
  2. 查询用 readOnly = true,写操作才开事务,别让只读接口占用写事务的资源;
  3. 事务内不调第三方、不发消息、不写缓存,这些统统通过 @TransactionalEventListener(AFTER_COMMIT) 挪到提交之后;
  4. 事务粒度尽量小:能在一个方法里几步写完就写完,别跨方法拼装一个巨大的逻辑事务;
  5. REQUIRES_NEW 和 NESTED 按需用,并且评估连接池,别在循环里开新事务;
  6. 跨库/跨服务的一致性不要靠 @Transactional 硬扛,用本地消息表、补偿任务或者分布式事务框架。

REQUIRES_NEW 在 JPA 场景有什么额外代价?为什么 NESTED 在 JPA 里要慎用?

@Transactional(readOnly = true) 对 Hibernate 做了什么?能随便加吗?

@Transactional 和持久层的关系:为什么 JPA 的懒加载依赖事务?MyBatis 的 SqlSession 又依赖什么?

同一个事务里 find 两次同一个 ID,是同一个对象吗?同一事务里 save 之后再查同一实体,查到的是新值吗?

Hibernate

为什么 GenerationType.IDENTITY 会让 Hibernate 批量插入彻底失效?你怎么解决?Hibernate 6 里 @GeneratedValue(strategy = AUTO) 的行为变了,你了解吗?

说说实体的四种状态,以及它们之间怎么转换?

什么是持久化上下文(Persistence Context)?EntityManager 是线程安全的吗?

Hibernate 的脏检查(dirty checking)是怎么实现的?

Hibernate 什么时候会把 SQL 发到数据库(flush 的时机)?

延迟加载(懒加载)在 JPA 里是怎么实现的?LazyInitializationException 什么情况下出现?你们项目怎么解决?

为什么 JOIN FETCH 一对多集合再分页会报警告/结果不对? 一次 JOIN FETCH 两个 List 集合为什么会抛 MultipleBagFetchException?怎么破?

FetchType.LAZY 和 @Fetch(FetchMode.SUBSELECT) 有什么区别?

JPA 的一级缓存和二级缓存分别是什么?二级缓存默认开吗?JPA 的二级缓存和 Redis 缓存的定位区别?你项目里两者怎么分工?

结合 @QueryHints 优化 fetch size 与二级缓存”,@QueryHints 到底能设什么?

hibernate.jdbc.batch_size、order_inserts、order_updates 分别解决什么?

@DynamicUpdate / @DynamicInsert 是干什么的?

MyBatis

为什么说 MyBatis 是半自动 ORM 映射工具?它与全自动的区别在哪里?

Hibernate 属于全自动 ORM 映射工具,使用 Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。

而 MyBatis 在查询关联对象或关联集合对象时,需要手动编写 sql 来完成,所以,称之为半自动 ORM 映射工具。

Mybatis里的 # 和 $ 的区别?为什么有了 #{} 还要 ${}?

  • Mybatis 在处理 #{} 时,会创建预编译的 SQL 语句,将 SQL 中的 #{} 替换为 ? 号,在执行 SQL 时会为预编译 SQL 中的占位符 ? 赋值,调用 PreparedStatement 的 set 方法来赋值,预编译的 SQL 语句执行效率高,并且可以防止 SQL 注入,提供更高的安全性,适合传递参数值。
  • Mybatis 在处理 ${} 时,只是创建普通的 SQL 语句,然后在执行 SQL 语句时 MyBatis 将参数直接拼入到 SQL 里,不能防止 SQL 注入,因为参数直接拼接到 SQL 语句中,如果参数未经过验证、过滤,可能会导致安全问题。

XML Mapper 映射文件中,除了常见的 select、insert、update、delete 标签之外,还有哪些标签?resultType 和 resultMap 怎么选?<id> 标签是干嘛的?

结果映射类:用于处理Java对象与数据库字段之间的复杂映射关系

  • <resultMap>:自定义结果集映射规则,解决字段名与属性名不一致、或处理复杂的关联对象
    • <id>:映射主键字段。
    • <result>:映射普通字段。
    • <association>:处理一对一的关联关系。
    • <collection>:处理一对多的关联关系。

动态SQL类:在XML中编写灵活的、条件化的SQL语句,避免在Java代码中拼接SQL。

  • <if>:条件判断,如果test条件为真,则拼接其内部的SQL片段。
  • <choose>, <when>, <otherwise>:实现多条件分支选择
  • <where>:智能处理WHERE子句。它会自动去除第一个条件前多余的AND或OR,并处理是否要插入WHERE关键字。
  • <set>:智能处理UPDATE语句的SET子句。它会动态地添加SET关键字,并自动去除最后一个多余的逗号。
  • foreach>:遍历集合,常用于构建IN条件或批量插入语句。

其他功能类

  • <selectKey>:用于在插入(<insert>)操作前后,从数据库获取新生成的主键值

MyBatis 能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别?association 和 collection 分别映射什么?一对多 join 出 100 行怎么变成 5 个主对象?

MyBatis 完全支持一对一和一对多的关联查询。它通过 <resultMap> 标签下不同的子标签来实现

  • 嵌套查询

    这种方式会执行多条SQL。先查主表,再根据主表的结果,去关联表执行额外的查询。

    • 一对一(<association>):通过单独的 SQL 查询关联对象。
    • 一对多(<collection>):通过单独的 SQL 查询关联的集合。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!-- 一对一:查询订单及其关联的用户 -->
    <resultMap id="OrderWithUserResultMap" type="Order">
    <id property="id" column="order_id"/>
    <result property="orderNo" column="order_no"/>
    <!-- select属性指向另一个namespace下的查询方法,column是传递给它的参数 -->
    <association property="user" column="user_id"
    select="com.example.mapper.UserMapper.selectUserById"
    fetchType="lazy"/>
    </resultMap>

    <!-- 一对多:查询用户及其所有订单 -->
    <resultMap id="UserWithOrdersResultMap" type="User">
    <id property="id" column="user_id"/>
    <result property="username" column="username"/>
    <!-- column="user_id" 作为参数传给订单查询 -->
    <collection property="orders" column="user_id"
    select="com.example.mapper.OrderMapper.selectOrdersByUserId"
    fetchType="lazy"/>
    </resultMap>
    • select:指定要执行的额外SQL的ID。
    • column:作为参数传递给该SQL的字段。
    • fetchType:可选 lazy(懒加载)或 eager(立即加载),用于控制性能。
  • 嵌套结果

    只执行一条多表关联查询(JOIN) 的SQL,然后在 <resultMap> 中通过映射关系,将结果集填充到对象中。

    • 一对一(<association>):使用 javaType 指定关联对象的类型。
    • 一对多(<collection>):使用 ofType 指定集合中元素的类型。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <!-- 一对一:一条JOIN查询完成 -->
    <resultMap id="OrderWithUserResultMap" type="Order">
    <id property="id" column="order_id"/>
    <result property="orderNo" column="order_no"/>
    <!-- 直接映射关联对象的属性 -->
    <association property="user" javaType="User">
    <id property="id" column="user_id"/>
    <result property="username" column="username"/>
    <result property="email" column="email"/>
    </association>
    </resultMap>

    <!-- 一对多:一条JOIN查询,注意order表字段的别名 -->
    <resultMap id="UserWithOrdersResultMap" type="User">
    <id property="id" column="user_id"/>
    <result property="username" column="username"/>
    <!-- ofType指定集合元素类型 -->
    <collection property="orders" ofType="Order">
    <id property="id" column="order_id"/>
    <result property="orderNo" column="order_no"/>
    <result property="amount" column="amount"/>
    </collection>
    </resultMap>

嵌套查询(select 属性)和嵌套结果(join)怎么选?

Mapper 就是个接口没有实现类,为什么能调通?MapperProxy 和 MapperMethod 各干什么? Mapper 方法能重载吗?为什么?

从调用 Mapper 到拿到结果,完整链路讲一遍。

MyBatis 中一级缓存是什么级别?它的 key 由什么组成?什么时候失效?我在 Service 里连续调两次同一个 Mapper 方法,为什么一级缓存没生效?

MyBatis 中二级缓存怎么开?为什么大厂规范普遍禁用它?二级缓存和 Spring 事务一起用会有什么坑?

MyBatis 延迟加载的原理是什么?有哪些失效场景?

PageHelper 的原理是什么?为什么 startPage 之后必须紧跟查询?

MyBatis 中如何处理类型转换?TypeHandler 是干什么的?在哪个环节被调用?

枚举怎么入库?EnumTypeHandler 和 EnumOrdinalTypeHandler 有什么区别?

使用 MyBatis,你们 MySQL 里的 JSON 或者 PG 里的 JSONB 怎么映射?

MyBatis 怎么做多数据源?@DS 或 AbstractRoutingDataSource 的原理是什么?多数据源下 MyBatis 有哪些坑?

MyBatis Plus

Mybatis Plus 和 Mybatis 的区别?能替代吗?

Mybatis Plus是一个基于MyBatis的增强工具库,旨在简化开发并提高效率。

selectById 和 selectOne 有什么区别?selectOne 查到多条会怎样?

Wrapper 有哪几种?讲解一下他们。然后为什么推荐 LambdaWrapper?

MP 的乐观锁插件原理是什么?有哪些限制?

MP 的逻辑删除是怎么实现的?和唯一索引为什么会冲突?

MP 主键默认策略是什么?雪花算法的结构和缺点?MP 的自动填充(MetaObjectHandler)怎么用?不生效的常见原因?

持久层相关实践

你简历里既写 MyBatis Plus 又写 Hibernate,实际项目里怎么选型?给我一个你做决策的标准和思路。

N+1 在 JPA 和 MyBatis 里分别怎么产生、怎么解决?

你说你用预加载解决了 N+1,具体讲讲 N+1 是怎么产生的、你怎么定位的?JPA 里解决 N+1 的所有手段你了解吗?@BatchSize / default_batch_fetch_size 为什么能把 N+1 变成 1+N 批次?

解释一下你在 AIP 平台写的 PageableAdapter,为什么不能直接把 Page 传给 Dubbo?为什么 Pageable 不适合直接跨服务(比如 Dubbo)传?你写的 PageableAdapter 解决了什么?

你怎么发现并优化一条慢的 JPA 查询?如果用 Arthas 定位过 N+1 和慢 SQL,具体敲了哪些命令、看到了什么?

JPA 怎么做逻辑删除?和 MyBatis-Plus 的 @TableLogic 比呢?

DDD 里的聚合根、值对象怎么落到 JPA 上?有什么阻抗失配?CQRS 的读模型你会用 JPA 还是 MyBatis/原生 SQL?

手写一个插件的步骤是什么?MyBatis 中,你们用插件做过什么?插件顺序有讲究吗?

百万级数据导出/处理,你怎么避免 OOM?Cursor 和 ResultHandler 有什么区别?fetchSize 设了为什么有时不生效?MySQL 有什么特殊要求?流式查询期间有什么风险?

批量插入你有几种写法?性能差多少?你如何进行选择?批量插入怎么回填自增主键?

rewriteBatchedStatements=true 不加则 BATCH 模式退化为逐条往返

大批量写入 JPA 和 MyBatis 各怎么做?为什么 JPA 容易踩坑?

分页在 JPA 和 MyBatis 里分别怎么实现?深分页怎么办?

UUID 主键有什么利弊?