MyBatis 面试题笔记

MyBatis 面试题笔记

个人复习笔记,覆盖 MyBatis 核心原理、动态 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 的整体执行流程?(高频)

  1. 解析配置 :SqlSessionFactoryBuilder 解析 mybatis-config.xml 和 Mapper XML,把每条 SQL 封装成 MappedStatement,构建出 SqlSessionFactory(全局单例);
  2. 获取会话 :SqlSessionFactory.openSession() 得到 SqlSession(线程不安全,方法级/请求级生命周期,Spring 集成时每次 Mapper 调用一个新 SqlSession);
  3. 获取 Mapper 代理 :sqlSession.getMapper(UserMapper.class) 返回动态代理对象;
  4. 执行 :代理调用 Executor 执行器 → StatementHandler 创建 JDBC Statement → ParameterHandler 设置参数 → 执行 SQL → ResultSetHandler 映射结果集;
  5. 关闭 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 动态代理:

  1. 启动时解析 Mapper XML,把每条 SQL 注册为 MappedStatement(id = 接口全限定名 + 方法名);
  2. getMapper() 时通过 MapperProxyFactory 返回 MapperProxy 动态代理对象;
  3. 调用接口方法时,MapperProxy.invoke() 拿到接口全限定名.方法名去匹配 MappedStatement(这就是为什么 XML 的 namespace 必须是接口全限定名、id 必须与方法名一致);
  4. 找到 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。

二级缓存的问题与建议:

  1. 多表查询时,另一张表更新了而本 namespace 缓存没失效,导致脏读;
  2. 分布式环境下多节点缓存不同步;
  3. 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 方法传多个参数的方式?

  1. @Param 注解 (推荐):User get(@Param("name") String name, @Param("age") Integer age);
  2. Map:传 Map,XML 里用 key 取(可读性差);
  3. 对象:封装成实体/DTO,XML 里直接用属性名;
  4. Java 8 保留参数名编译(-parameters)时可不加 @Param,但不通用。

插入后如何拿到自增主键?

xml 复制代码
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO user(name) VALUES(#{name})
</insert>

自增 ID 会回填到入参对象的 id 属性。Oracle 等用序列的数据库用 <selectKey> 先查序列再插入。


插件与拦截器

MyBatis 插件的实现原理?(高频)

责任链 + 动态代理:

  1. MyBatis 允许拦截四大对象的方法:Executor(增删改查、事务提交)、StatementHandler(SQL 语法构建)、ParameterHandler(参数设置)、ResultSetHandler(结果处理);
  2. 自定义插件实现 Interceptor 接口,@Intercepts 注解声明拦截目标(类型 + 方法 + 参数签名);
  3. 创建核心对象时,InterceptorChain 把所有插件包成层层代理(pluginAll → Proxy.newProxyInstance);
  4. 调用被拦截方法时先走插件的 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 的增强工具(只增强不改变),核心:

  1. BaseMapper 内置 CRUD:insert/selectById/updateById/deleteBatchIds 等不用写 XML;
  2. 条件构造器 :LambdaQueryWrapper<User> qw = new LambdaQueryWrapper<>(); qw.eq(User::getName, "张三").ge(User::getAge, 18);,Lambda 引用属性防字段名写错;
  3. IService/ServiceImpl:批处理(saveBatch)、链式查询;
  4. 分页插件 :PaginationInnerInterceptor 配置后传 Page 对象即分页;
  5. 代码生成器(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。

乐观锁插件怎么用?

  1. 表加 version 字段,实体加 @Version 注解;
  2. 注册 OptimisticLockerInnerInterceptor;
  3. 更新前先查出对象(携带 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}%'(注入 + 特殊字符问题)。

大数据量导出怎么处理?

  1. 分页/游标分批查询(按 id > lastId 滚动,避免深分页 LIMIT offset 慢);
  2. ExecutorType.BATCH 批量写出;
  3. 流式查询:@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000) + ResultHandler 回调逐行处理,内存占用恒定。

XML 里 SQL 片段复用?

<sql id="baseColumns">id, name, age</sql> 定义,<include refid="baseColumns"/> 引用,可传属性参数化。

Mapper XML 修改后不生效/找不到 Mapper 排查思路?

  1. XML 路径是否在 mybatis.mapper-locations 扫描范围内(classpath*:mapper/**/*.xml);
  2. namespace 是否 = 接口全限定名、id 是否 = 方法名;
  3. Spring Boot 打包时 XML 是否被打进 jar(非 resources 目录要配 build 的 resources 过滤);
  4. 多个 Mapper XML 的 namespace 是否重复。

数据源与事务:MyBatis 在 Spring 里的事务怎么生效的?

SqlSessionTemplate 使用 SpringManagedTransaction :获取连接走 DataSourceUtils.getConnection(),连接绑定在 ThreadLocal(TransactionSynchronizationManager)上;@Transactional 开启事务时绑定连接,同一个事务内所有 Mapper 共用该连接,由 Spring 统一 commit/rollback。这就是"Mapper 操作能被 @Transactional 管住"的原因。

相关推荐
Azure++6 小时前
JVM 面试题笔记
个人笔记
我的golang之路果然有问题9 个月前
linux 个人笔记导出之网络,防火墙,定时,权限,后台
linux·运维·服务器·网络·笔记·个人笔记
我的golang之路果然有问题9 个月前
mysql 个人笔记导出之-数据库时间戳问题以及增删改查
数据库·笔记·学习·mysql·分享·个人笔记