MyBatis 面试题笔记
个人复习笔记,覆盖 MyBatis 核心原理、动态 SQL、缓存、插件机制及 MyBatis-Plus 的高频面试题。
目录
- [MyBatis 基础认知](#MyBatis 基础认知)
- 核心执行流程
- [Mapper 接口原理](#Mapper 接口原理)
- [动态 SQL](#动态 SQL)
- [#{} 与 {}](#{} 与 {})
- 缓存机制
- 延迟加载
- 参数与结果映射
- 插件与拦截器
- MyBatis-Plus
- 实战常见问题
MyBatis 基础认知
MyBatis 是什么?优缺点?
半自动化 ORM 框架:SQL 写在 XML/注解里,MyBatis 负责参数映射、结果集映射、连接与事务管理。
优点:
- SQL 自己写,可精细调优,适合复杂查询和性能敏感场景;
- 学习成本低、与 SQL 一一对应、好排查;
- 动态 SQL 能力,灵活拼接条件。
缺点:
- SQL 量大,简单 CRUD 也要写模板;
- 数据库迁移受 SQL 方言影响(对比 Hibernate);
- 字段变更要同步维护 resultMap。
MyBatis 和 Hibernate / JPA 的区别?
| MyBatis | Hibernate | |
|---|---|---|
| 定位 | 半自动(SQL 自己写) | 全自动(根据对象关系生成 SQL) |
| 学习/使用成本 | 低,掌握 SQL 即可 | 高,HQL/缓存/脏检查体系庞大 |
| 可控性/调优 | 强 | 弱(生成的 SQL 不可控) |
| 跨数据库 | 弱(方言问题) | 强 |
| 适用场景 | 互联网业务、复杂查询、SQL 敏感 | 传统企业应用、领域模型复杂、查询简单 |
国内互联网主流是 MyBatis / MyBatis-Plus。
核心执行流程
MyBatis 的整体执行流程?(高频)
- 解析配置 :
SqlSessionFactoryBuilder解析 mybatis-config.xml 和 Mapper XML,把每条 SQL 封装成MappedStatement,构建出 SqlSessionFactory(全局单例); - 获取会话 :
SqlSessionFactory.openSession()得到 SqlSession(线程不安全,方法级/请求级生命周期,Spring 集成时每次 Mapper 调用一个新 SqlSession); - 获取 Mapper 代理 :
sqlSession.getMapper(UserMapper.class)返回动态代理对象; - 执行 :代理调用
Executor执行器 →StatementHandler创建 JDBC Statement →ParameterHandler设置参数 → 执行 SQL →ResultSetHandler映射结果集; - 关闭 SqlSession,归还连接。
核心四大对象:Executor、StatementHandler、ParameterHandler、ResultSetHandler(插件就是拦截它们)。
Executor 有哪几种?
| 执行器 | 特点 |
|---|---|
SIMPLE(默认) |
每次执行都创建新的 Statement |
REUSE |
复用 Statement 对象,减少预编译开销 |
BATCH |
批量执行,addBatch 累积、统一 executeBatch,适合大批量 insert/update |
批量插入性能问题 :foreach 拼 10 万条 values SQL 太大直接爆栈;正确做法是 ExecutorType.BATCH 分批 flush,或 foreach 分批(每批 500~1000 条)。
SqlSession 是线程安全的吗?
不是。它内部持有连接等状态,多线程共用会出问题。每次操作应该新开一个 SqlSession。Spring 集成后不用担心:SqlSessionTemplate 会为每次 Mapper 调用自动创建/回收 SqlSession。
Mapper 接口原理
Mapper 接口没有实现类,为什么能执行 SQL?(高频)
JDK 动态代理:
- 启动时解析 Mapper XML,把每条 SQL 注册为
MappedStatement(id = 接口全限定名 + 方法名); getMapper()时通过MapperProxyFactory返回 MapperProxy 动态代理对象;- 调用接口方法时,
MapperProxy.invoke()拿到接口全限定名.方法名去匹配 MappedStatement(这就是为什么 XML 的 namespace 必须是接口全限定名、id 必须与方法名一致); - 找到 SQL 后交给 SqlSession 执行,结果按映射规则转换返回。
所以参数是代理转发,接口只是"SQL 的签名"。
接口方法为什么不能重载?
因为 MappedStatement 的唯一标识是 namespace + 方法名,重载方法名相同,映射冲突。
动态 SQL
常用动态 SQL 标签有哪些?(高频)
xml
<!-- if:条件判断 -->
<select id="list" resultType="User">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
<!-- choose/when/otherwise:相当于 switch-case -->
<choose>
<when test="type == 1"> AND status = 1 </when>
<when test="type == 2"> AND status = 2 </when>
<otherwise> AND status = 0 </otherwise>
</choose>
<!-- set:更新时自动去掉末尾多余逗号 -->
<update id="update">
UPDATE user
<set>
<if test="name != null"> name = #{name}, </if>
<if test="age != null"> age = #{age}, </if>
</set>
WHERE id = #{id}
</update>
<!-- foreach:in 查询 / 批量插入 -->
<select id="findByIds">
SELECT * FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<!-- trim:自定义前后缀拼接与去除 -->
<trim prefix="WHERE" prefixOverrides="AND |OR "> ... </trim>
<!-- bind:定义变量,常用于 like -->
<bind name="pattern" value="'%' + name + '%'"/>
WHERE name LIKE #{pattern}
<where> 标签解决了什么问题?
自动处理第一个条件的 AND/OR 前缀:所有 if 都不成立时输出空 WHERE(没有则省略 WHERE 关键字);第一个条件的 AND 会被自动去掉。
foreach 的 collection 属性怎么写?
- 参数是 List:
collection="list"(或 @Param 自定义名,推荐); - 参数是数组:
collection="array"; - 参数是 Map:写 Map 的 key 名;
- 多个参数务必加
@Param,否则只能用 param1/param2,可读性差。
#{} 与 ${}
#{} 和 ${} 的区别?(超高频)
| #{} | ${} | |
|---|---|---|
| 实现 | PreparedStatement 占位符 ? + 预编译 |
字符串直接替换拼接进 SQL |
| SQL 注入 | 防(参数不参与 SQL 解析) | 不防 |
| 类型处理 | 自动类型转换 | 无 |
| 适用场景 | 传值(WHERE 条件、值参数) | 表名、列名、ORDER BY 字段等不能参数化的部分 |
sql
-- ${} 注入演示:入参 "1 OR 1=1" 拼进去变成永真
SELECT * FROM user WHERE id = ${id} -- 危险
SELECT * FROM user WHERE id = #{id} -- 安全
${} 只用于白名单校验后的动态表名/排序列:
xml
ORDER BY ${column} ${order}
<!-- Java 侧先校验 column 是否在允许的列名白名单里,再传进来 -->
缓存机制
MyBatis 的一级缓存和二级缓存?(高频)
| 一级缓存 | 二级缓存 | |
|---|---|---|
| 作用范围 | SqlSession 级 | namespace(Mapper)级,跨 SqlSession |
| 默认开启 | 默认开启,无法关闭(可用 localCacheScope=STATEMENT 降低粒度) | 默认关闭,需手动开启 |
| 存储 | BaseExecutor 的 PerpetualCache(HashMap) | 可配第三方(Redis/Ehcache) |
一级缓存失效条件 (面试常考):执行增删改(清空缓存)、sqlSession.clearCache()、跨 SqlSession、查询不同 MappedStatement、设置了 flushCache=true。
二级缓存的问题与建议:
- 多表查询时,另一张表更新了而本 namespace 缓存没失效,导致脏读;
- 分布式环境下多节点缓存不同步;
- Spring 集成下每次请求新 SqlSession,一级缓存基本用不上;二级缓存要
cache-enabled=true+ 实体可序列化。
实践结论 :生产环境一般不用 MyBatis 缓存,交给 Redis;MyBatis 缓存主要作为面试知识点。
二级缓存为什么提交/关闭 SqlSession 后才可见?
二级缓存放在 TransactionCacheManager 暂存区,事务提交(commit/close)时才把暂存数据刷入真正的二级缓存区域------避免未提交数据被其他 SqlSession 读到。
延迟加载
什么是延迟加载?原理是什么?
关联查询时,不立即加载关联对象,第一次访问关联属性时才发 SQL 去查。
- 开启:
lazyLoadingEnabled=true(aggressiveLazyLoading=false,按需触发); - 原理 :MyBatis 用 CGLIB/Javassist 生成目标对象的代理,拦截 getter;调用关联属性的 getter 时才执行关联查询并填充。
- 触发规则:
lazyLoadTriggerMethods默认包括 equals、clone、hashCode、toString,这些方法也会触发加载(坑点)。
参数与结果映射
resultType 和 resultMap 的区别?
resultType:按列名 = 属性名 自动映射(开了驼峰映射map-underscore-to-camel-case则 user_name → userName);resultMap:手动定义列与属性的映射关系,用于列名与属性名对不上、一对多/多对一关联映射的场景。
怎么实现一对一、一对多映射?
xml
<resultMap id="orderMap" type="Order">
<id column="id" property="id"/>
<!-- 一对一:关联用户 -->
<association property="user" javaType="User">
<id column="user_id" property="id"/>
<result column="user_name" property="name"/>
</association>
<!-- 一对多:订单明细 -->
<collection property="items" ofType="OrderItem">
<id column="item_id" property="id"/>
<result column="product_name" property="productName"/>
</collection>
</resultMap>
两种方式:联表查询 (一次 JOIN 出结果)或分步查询 (先查主表,再按外键查关联,select 属性指向另一条 SQL,可配合延迟加载)。
Mapper 方法传多个参数的方式?
- @Param 注解 (推荐):
User get(@Param("name") String name, @Param("age") Integer age); - Map:传 Map,XML 里用 key 取(可读性差);
- 对象:封装成实体/DTO,XML 里直接用属性名;
- Java 8 保留参数名编译(-parameters)时可不加 @Param,但不通用。
插入后如何拿到自增主键?
xml
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user(name) VALUES(#{name})
</insert>
自增 ID 会回填到入参对象的 id 属性。Oracle 等用序列的数据库用 <selectKey> 先查序列再插入。
插件与拦截器
MyBatis 插件的实现原理?(高频)
责任链 + 动态代理:
- MyBatis 允许拦截四大对象的方法:
Executor(增删改查、事务提交)、StatementHandler(SQL 语法构建)、ParameterHandler(参数设置)、ResultSetHandler(结果处理); - 自定义插件实现
Interceptor接口,@Intercepts注解声明拦截目标(类型 + 方法 + 参数签名); - 创建核心对象时,
InterceptorChain把所有插件包成层层代理(pluginAll → Proxy.newProxyInstance); - 调用被拦截方法时先走插件的
intercept()(可改参数、改返回值、也可invocation.proceed()放行)。
经典应用:分页插件(PageHelper 改写 SQL 加 LIMIT + 发 count 语句)、SQL 性能监控、数据权限、字段加解密、慢 SQL 打印。
PageHelper 分页原理?
ThreadLocal 保存 Page 分页参数 → 拦截 Executor 查询 → 先把原 SQL 改写成 SELECT COUNT(*) 查总数 → 再改写成 LIMIT offset, size(按方言)→ 清除 ThreadLocal → 返回 Page 对象(含 total)。
经典坑 :PageHelper.startPage(pageNum, size) 与查询语句之间不能隔开(如中间抛异常或写了别的查询),否则 ThreadLocal 残留污染下一次查询;用 try-finally 或紧跟查询。
MyBatis-Plus
MyBatis-Plus 是什么?核心能力?
MyBatis 的增强工具(只增强不改变),核心:
- BaseMapper 内置 CRUD:
insert/selectById/updateById/deleteBatchIds等不用写 XML; - 条件构造器 :
LambdaQueryWrapper<User> qw = new LambdaQueryWrapper<>(); qw.eq(User::getName, "张三").ge(User::getAge, 18);,Lambda 引用属性防字段名写错; - IService/ServiceImpl:批处理(saveBatch)、链式查询;
- 分页插件 :
PaginationInnerInterceptor配置后传 Page 对象即分页; - 代码生成器(FastAutoGenerator)一键生成 Entity/Mapper/Service/Controller。
逻辑删除怎么实现?
yaml
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除字段
logic-delete-value: 1
logic-not-delete-value: 0
实体加 @TableLogic 注解后:delete 方法自动变成 UPDATE ... SET deleted = 1,查询自动追加 WHERE deleted = 0。注意唯一索引会和逻辑删除冲突(删除后原值还在,重复插入报唯一键冲突),可用"deleted 存删除时间戳/id"的方案绕开。
自动填充(创建时间/更新时间)怎么做?
实现 MetaObjectHandler,实体字段标 @TableField(fill = FieldFill.INSERT / INSERT_UPDATE),插入/更新时自动填充 createUser、createTime 等,业务代码不用手动 set。
乐观锁插件怎么用?
- 表加
version字段,实体加@Version注解; - 注册
OptimisticLockerInnerInterceptor; - 更新前先查出对象(携带 version),
updateById时自动追加WHERE version = 旧值 AND ...并把 version + 1;影响行数为 0 表示并发修改冲突,业务侧重试。
实战常见问题
like 模糊查询怎么写才安全?
xml
-- 推荐:数据库函数拼接,参数还是占位符
WHERE name LIKE CONCAT('%', #{keyword}, '%')
-- 或 bind 标签
<bind name="kw" value="'%' + keyword + '%'"/>
WHERE name LIKE #{kw}
不要写 LIKE '%${keyword}%'(注入 + 特殊字符问题)。
大数据量导出怎么处理?
- 分页/游标分批查询(按 id > lastId 滚动,避免深分页 LIMIT offset 慢);
ExecutorType.BATCH批量写出;- 流式查询:
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000)+ ResultHandler 回调逐行处理,内存占用恒定。
XML 里 SQL 片段复用?
<sql id="baseColumns">id, name, age</sql> 定义,<include refid="baseColumns"/> 引用,可传属性参数化。
Mapper XML 修改后不生效/找不到 Mapper 排查思路?
- XML 路径是否在
mybatis.mapper-locations扫描范围内(classpath*:mapper/**/*.xml); - namespace 是否 = 接口全限定名、id 是否 = 方法名;
- Spring Boot 打包时 XML 是否被打进 jar(非 resources 目录要配
build的 resources 过滤); - 多个 Mapper XML 的 namespace 是否重复。
数据源与事务:MyBatis 在 Spring 里的事务怎么生效的?
SqlSessionTemplate 使用 SpringManagedTransaction :获取连接走 DataSourceUtils.getConnection(),连接绑定在 ThreadLocal(TransactionSynchronizationManager)上;@Transactional 开启事务时绑定连接,同一个事务内所有 Mapper 共用该连接,由 Spring 统一 commit/rollback。这就是"Mapper 操作能被 @Transactional 管住"的原因。