前言

MyBatis 面试题通常会从“它和 Hibernate/JPA 有什么区别”开始,继续追问 Mapper 接口为什么不用实现类,SQL 是怎么执行的,#{}${} 有什么区别,一级缓存和二级缓存怎么生效,动态 SQL 如何拼接,插件拦截哪些对象,分页插件原理是什么,懒加载为什么可能产生 N+1 问题。

如果只把 MyBatis 理解成“写 SQL 的 ORM 框架”,回答很容易停留在使用层。更好的方式是把它理解成一条完整链路:

1
调用 Mapper 接口 -> 动态代理生成调用 -> 找到 MappedStatement -> Executor 执行 -> StatementHandler 创建 SQL -> JDBC 查询 -> ResultSetHandler 映射结果

本文按面试中最常见的模块梳理 MyBatis 要点,适合作为面试前的复习清单。

MyBatis 整体定位

MyBatis 是一个半自动 ORM 框架。它不像 Hibernate 那样尽量屏蔽 SQL,而是让开发者显式编写 SQL,同时提供参数绑定、结果映射、动态 SQL、缓存和插件扩展能力。

面试中可以这样概括:

MyBatis 的核心价值是把 Java 方法调用和 SQL 执行建立映射关系。开发者控制 SQL,MyBatis 负责 Mapper 代理、参数处理、JDBC 执行、结果集映射和扩展机制。

核心组件

MyBatis 常见核心组件包括:

  • SqlSessionFactory:创建 SqlSession 的工厂。
  • SqlSession:执行 SQL 和获取 Mapper 的入口。
  • MapperProxy:Mapper 接口动态代理。
  • MappedStatement:一条 SQL 映射语句的元数据。
  • Executor:执行器,负责查询、更新、缓存和事务协调。
  • StatementHandler:创建和执行 JDBC Statement。
  • ParameterHandler:处理 SQL 参数。
  • ResultSetHandler:处理结果集映射。
  • TypeHandler:Java 类型和 JDBC 类型转换。
  • Configuration:全局配置对象。
flowchart TD
    A[Mapper Interface] --> B[MapperProxy]
    B --> C[MappedStatement]
    C --> D[Executor]
    D --> E[StatementHandler]
    E --> F[ParameterHandler]
    E --> G[JDBC Statement]
    G --> H[ResultSetHandler]
    H --> I[Java Object]

Mapper 代理原理

Mapper 接口没有实现类也能调用,是因为 MyBatis 为 Mapper 接口创建了 JDK 动态代理。

常见调用方式:

1
2
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectById(1L);

大致流程:

  1. getMapper 返回 Mapper 接口代理对象。
  2. 调用接口方法时进入 MapperProxy
  3. 根据接口全限定名和方法名找到对应 MappedStatement
  4. 判断方法类型是 select、insert、update 还是 delete。
  5. 调用 SqlSession 执行 SQL。
  6. 把结果转换为方法返回类型。

SQL id 通常是:

1
namespace + "." + methodName

所以 XML 的 namespace 要和 Mapper 接口全限定名对应,SQL 标签 id 要和方法名对应。

SQL 执行流程

MyBatis 查询执行流程可以概括为:

sequenceDiagram
    participant App
    participant MapperProxy
    participant SqlSession
    participant Executor
    participant StatementHandler
    participant DB
    participant ResultSetHandler

    App->>MapperProxy: selectById(id)
    MapperProxy->>SqlSession: selectOne(statement, param)
    SqlSession->>Executor: query()
    Executor->>StatementHandler: prepare / parameterize
    StatementHandler->>DB: execute SQL
    DB-->>StatementHandler: ResultSet
    StatementHandler->>ResultSetHandler: handleResultSets
    ResultSetHandler-->>App: User

关键点:

  • MapperProxy 负责把方法调用转换为 statement 调用。
  • Executor 负责执行和缓存。
  • StatementHandler 负责 JDBC Statement 创建和执行。
  • ParameterHandler 负责参数绑定。
  • ResultSetHandler 负责结果映射。

#{} 和 ${}

#{}${} 是 MyBatis 高频面试题。

#{} 使用 PreparedStatement 参数占位:

1
select * from user where id = #{id}

最终类似:

1
select * from user where id = ?

优点是防 SQL 注入,并能进行类型处理。

${} 是字符串直接拼接:

1
select * from user order by ${column}

它不会做预编译参数绑定,存在 SQL 注入风险。

常见原则:

  • 普通参数值使用 #{}
  • 表名、列名、排序字段这类 SQL 结构片段如果必须动态拼接,才使用 ${}
  • 使用 ${} 时必须做白名单校验。

动态 SQL

MyBatis 动态 SQL 让 SQL 可以根据条件拼接。

常见标签:

  • if
  • choose / when / otherwise
  • where
  • set
  • foreach
  • trim

示例:

1
2
3
4
5
6
7
8
9
10
11
<select id="listUsers" resultType="User">
select * from user
<where>
<if test="name != null and name != ''">
and name like concat('%', #{name}, '%')
</if>
<if test="status != null">
and status = #{status}
</if>
</where>
</select>

where 标签会自动处理多余的 andor,避免拼接出非法 SQL。

foreach 常用于 in 查询:

1
2
3
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>

ResultMap

ResultMap 用于处理结果集到对象的映射。

常见场景:

  • 字段名和属性名不一致。
  • 一对一关联。
  • 一对多集合。
  • 嵌套查询。
  • 嵌套结果映射。

示例:

1
2
3
4
<resultMap id="userMap" type="User">
<id property="id" column="id"/>
<result property="userName" column="user_name"/>
</resultMap>

如果只是下划线转驼峰,可以开启:

1
2
3
mybatis:
configuration:
map-underscore-to-camel-case: true

复杂关联查询要警惕结果行膨胀和 N+1 查询问题。

一级缓存

MyBatis 一级缓存默认开启,作用域是 SqlSession。

同一个 SqlSession 中,执行相同 SQL 和相同参数时,可能直接命中一级缓存。

特点:

  • 默认开启。
  • SqlSession 级别。
  • update、insert、delete 会清空缓存。
  • commit、rollback、close 后缓存失效。

常见问题:为什么一级缓存可能读到旧数据?

如果同一个 SqlSession 中先查询,再由其他事务修改了数据库,本 SqlSession 再次查询相同语句可能命中缓存,读到旧结果。通常 Web 应用中 SqlSession 生命周期较短,风险相对可控。

二级缓存

二级缓存作用域是 Mapper namespace。

特点:

  • 默认不开启。
  • 需要全局开启,并在 Mapper 中配置 cache。
  • 多个 SqlSession 可以共享。
  • 事务提交后才会写入二级缓存。
  • 更新操作会清空对应 namespace 缓存。

启用示例:

1
<cache/>

二级缓存面试中要谨慎回答:它不是所有场景都适合。对于频繁更新、多表关联、强一致要求高的业务,二级缓存容易带来一致性问题。实际项目中更常用 Redis 等外部缓存,并显式设计缓存更新策略。

Executor

MyBatis 常见 Executor 类型:

  • SimpleExecutor:每次执行都创建新的 Statement。
  • ReuseExecutor:复用 Statement。
  • BatchExecutor:批处理执行更新。

Executor 负责:

  • 执行查询和更新。
  • 管理一级缓存。
  • 协调事务提交回滚。
  • 创建 StatementHandler。

批量插入、批量更新场景可以使用批处理能力,但要注意批量大小、事务提交和数据库压力。

插件机制

MyBatis 插件基于拦截器实现,可以拦截四类核心对象:

  • Executor
  • StatementHandler
  • ParameterHandler
  • ResultSetHandler

常见用途:

  • 分页。
  • SQL 日志。
  • 慢 SQL 监控。
  • 数据权限。
  • 多租户。
  • 字段加解密。

插件本质上通过动态代理包装目标对象,在目标方法执行前后插入逻辑。

分页插件常拦截 StatementHandler 或 Executor,在 SQL 执行前改写 SQL,追加 limit,并执行 count 查询。

分页

MyBatis 本身不强制提供物理分页,常见方式:

  • 手写 limit offset, size
  • 使用 PageHelper 等分页插件。
  • 业务层基于游标或主键分页。

深分页问题:

1
select * from orders order by id limit 1000000, 20

数据库需要扫描并丢弃大量数据,性能较差。

优化方式:

  • 使用主键游标分页。
  • 使用覆盖索引。
  • 限制最大页数。
  • 搜索类场景使用搜索引擎。

例如:

1
2
3
4
select * from orders
where id > #{lastId}
order by id
limit #{size}

懒加载和 N+1

MyBatis 支持关联对象懒加载。懒加载可以减少不必要查询,但也可能导致 N+1 问题。

例如查询 100 个用户,再懒加载每个用户的订单:

1
1 次查询用户列表 + 100 次查询订单 = 101 次 SQL

优化方式:

  • 使用 join 一次查出。
  • 分两次批量查询,再在内存中组装。
  • 控制懒加载使用范围。
  • 通过 SQL 日志发现异常查询次数。

面试中要强调:懒加载不是万能优化,使用不当会把问题从单条 SQL 变成大量小 SQL。

事务

MyBatis 可以独立管理事务,也可以交给 Spring 管理。

在 Spring 项目中,常见方式是通过 @Transactional 管理事务,MyBatis 的 SqlSession 和数据库连接会绑定到当前事务上下文。

常见事务问题:

  • 方法没有被 Spring 代理。
  • 同类内部调用导致事务失效。
  • 异常被捕获未抛出。
  • 默认 checked exception 不回滚。
  • 多数据源事务未正确配置。
  • 批处理时未控制提交边界。

MyBatis 本身执行 SQL,事务边界通常由 Spring 事务管理器控制。

MyBatis 和 JPA 的区别

对比项 MyBatis JPA/Hibernate
SQL 控制 手写 SQL,控制力强 自动生成 SQL 为主
学习成本 相对直接 对 ORM 状态管理要求高
复杂查询 适合手写优化 复杂查询可能难调优
对象关系映射 半自动 更完整的 ORM
性能可控性 SQL 可控,便于优化 依赖 ORM 生成和调优
适合场景 复杂 SQL、报表、业务查询 领域模型清晰、CRUD 较多

面试中可以总结:MyBatis 更适合 SQL 可控性要求高的项目,JPA 更适合领域模型驱动和标准化 CRUD 较多的项目。

常见性能优化

SQL 层:

  • 使用合适索引。
  • 避免 select *
  • 避免大范围深分页。
  • 避免循环中查询数据库。
  • 慢 SQL 使用执行计划分析。

MyBatis 层:

  • 合理使用 ResultMap。
  • 谨慎使用懒加载。
  • 批量操作控制批量大小。
  • 避免不必要的二级缓存。
  • 插件不要做过重逻辑。

应用层:

  • 控制事务范围。
  • 避免长事务。
  • 数据库连接池设置合理。
  • 热点数据使用外部缓存。
  • 关键 SQL 打印耗时和参数。

常见排查思路

如果面试官问“MyBatis 查询慢怎么排查”,可以按以下路径回答:

  1. 打开 SQL 日志,确认真实执行 SQL 和参数。
  2. 看是否存在 N+1 查询或循环查库。
  3. 用数据库执行计划检查索引命中情况。
  4. 看是否使用了深分页、模糊前缀、函数包裹索引列。
  5. 看返回字段和结果集是否过大。
  6. 看 ResultMap 是否过于复杂。
  7. 看分页插件是否额外执行了慢 count。
  8. 看连接池是否耗尽、事务是否过长。

如果是 Mapper 找不到 SQL,可以重点检查 namespace、方法名、XML 是否被扫描、参数名是否匹配、mapper-locations 配置是否正确。

高频面试题

Mapper 接口为什么不用实现类?

因为 MyBatis 会为 Mapper 接口创建 JDK 动态代理。调用接口方法时,代理根据接口名和方法名找到对应 MappedStatement,再通过 SqlSession 执行 SQL。

#{} 和 ${} 有什么区别?

#{} 使用预编译参数占位,能防 SQL 注入;${} 是字符串拼接,适合表名、列名等 SQL 结构动态拼接,但必须做白名单校验。

一级缓存和二级缓存有什么区别?

一级缓存是 SqlSession 级别,默认开启;二级缓存是 namespace 级别,默认不开启,可以跨 SqlSession 共享,但要注意数据一致性问题。

MyBatis 插件能拦截什么?

能拦截 Executor、StatementHandler、ParameterHandler、ResultSetHandler 四类对象。分页插件、SQL 日志、数据权限、多租户常用插件机制实现。

分页插件原理是什么?

分页插件通常拦截 SQL 执行过程,在真正执行前改写 SQL,追加数据库方言的分页语句,并可能额外执行 count SQL 统计总数。

MyBatis 如何防止 SQL 注入?

普通参数使用 #{},不要用 ${} 拼接用户输入。必须动态拼接表名、字段名、排序字段时,要做白名单校验。

为什么会出现 N+1 查询?

通常是关联对象懒加载或循环中查询导致的。查询一个列表后,又为列表中每条记录单独查询关联数据,最终产生大量 SQL。可以用 join、批量查询或手动组装优化。

总结

MyBatis 面试的主线可以围绕四个问题展开:

  1. 方法怎么变成 SQL:Mapper 代理、MappedStatement、SqlSession。
  2. SQL 怎么执行:Executor、StatementHandler、ParameterHandler、ResultSetHandler。
  3. 性能怎么治理:动态 SQL、分页、缓存、懒加载、N+1。
  4. 线上怎么排查:真实 SQL、执行计划、连接池、事务、插件和日志。

把这条链路讲清楚,再结合 #{}/${}、缓存、插件、分页和慢 SQL 排查,MyBatis 相关问题就能从“会写 Mapper”升级成“理解 SQL 映射框架运行机制”。