MyBatis 与 MyBatis-Plus 面试题汇总——从原理到实战

MyBatis 是国内 Java 项目中最主流的 ORM 框架,MyBatis-Plus 是它的增强工具。面试中围绕它们的底层原理、#{} 和 ${} 区别、分页原理、缓存机制等问得非常多。这篇一次说清楚。

一、MyBatis 核心原理

1. MyBatis 的工作流程

配置文件(mybatis-config.xml) ↓ SqlSessionFactoryBuilder.build() ↓ SqlSessionFactory(解析配置,构建会话工厂) ↓ SqlSession.openSession() ↓ 通过动态代理生成 Mapper 接口的实现类 ↓ 执行 Mapper 中的 SQL 语句 ↓ 返回结果

关键:Mapper 接口为什么不用写实现类?

MyBatis 用 JDK 动态代理为每个 Mapper 接口生成代理对象,代理对象根据namespace + 方法名找到对应的 SQL 并执行。

2. #{} 和 ${} 的区别

这是 MyBatis 最高频的面试题,没有之一。

对比#{}${}
处理方式预编译,替换为?直接字符串拼接
SQL 注入✅ 安全,参数值不走 SQL 编译有注入风险
场景传参(insert、update、where 条件)表名、列名动态传入(少用)
性能高(可复用预编译 SQL)低(每次重新编译)
<!-- #{} 安全写法 --><selectid="getUser"resultType="User">SELECT * FROM user WHERE id = #{id}</select><!-- 实际执行:SELECT * FROM user WHERE id = ? --><!-- ${} 危险写法 --><selectid="getUser"resultType="User">SELECT * FROM user WHERE id = ${id}</select><!-- 传入 1 OR 1=1 → 全表数据被查出 -->

结论:能用#{}的地方绝不用${}。只有动态表名、动态列名这种不得不用的场景才用${},并且要做好参数校验。

3. MyBatis 的一级缓存和二级缓存

一级缓存(默认开启):

同一个 SqlSession 中,两次相同的查询会走缓存,不会重复查数据库。 SqlSession 关闭或执行了 insert/update/delete 后缓存失效。

二级缓存(需手动开启):

同一个 SqlSessionFactory 下,多个 SqlSession 共享缓存。 适合:查询多、修改少、并发要求不高的场景。 不适合:对数据实时性要求高的场景。
<!-- 开启二级缓存 --><mappernamespace="com.zhang.mapper.UserMapper"><cacheeviction="LRU"flushInterval="60000"size="512"/></mapper>

面试常问:MyBatis 的缓存机制了解吗?一级缓存和二级缓存的区别?

二、MyBatis-Plus 面试题

1. MyBatis-Plus 和 MyBatis 的区别

MyBatisMyBatis-Plus
基础 CRUD手写 SQL自动提供,不用写
分页手写 Limit分页插件,一行代码搞定
条件查询手写动态 SQLLambdaQueryWrapper 链式调用
代码量减少 50%+
灵活度高,完全控制 SQL复杂 SQL 还是要手写

一句话总结:MyBatis-Plus 是 MyBatis 的增强工具,不为零改动——你在 MyBatis 里写复杂 SQL 的地方,MP 一样支持。

2. MyBatis-Plus 的分页原理

// 使用Page<User>page=newPage<>(1,10);userMapper.selectPage(page,null);// 自动生成 SELECT * FROM user LIMIT 0, 10// 还会自动执行 SELECT COUNT(*) FROM user 查总条数

原理:通过PaginationInnerInterceptor拦截器,在执行 SQL 前自动拼接LIMITCOUNT

注意:不配置分页拦截器,Page 对象传进去也不会生效——这是面试常挖的坑。

3. 乐观锁插件

@BeanpublicMybatisPlusInterceptormybatisPlusInterceptor(){MybatisPlusInterceptorinterceptor=newMybatisPlusInterceptor();interceptor.addInnerInterceptor(newOptimisticLockerInnerInterceptor());returninterceptor;}
@VersionprivateIntegerversion;

原理:更新时SET version = version + 1 WHERE version = 旧值。影响行数为 0 说明数据被修改过,需要重试。

4. 逻辑删除

@TableLogicprivateIntegerisDeleted;

原理:调用deleteById时实际执行UPDATE SET is_deleted = 1 WHERE id = ?,查询时自动拼接is_deleted = 0

5. 自动填充

@TableField(fill=FieldFill.INSERT)privateLocalDateTimecreateTime;@TableField(fill=FieldFill.INSERT_UPDATE)privateLocalDateTimeupdateTime;

配合MetaObjectHandler实现 createTime 和 updateTime 自动填充,不用手动 set。

三、XML 映射文件高频问题

1. resultType 和 resultMap 的区别

<!-- resultType:列名和属性名一致时用,简单 --><selectid="getUser"resultType="com.zhang.User">SELECT id, name, email FROM user WHERE id = #{id}</select><!-- resultMap:列名和属性名不一致、有复杂关联时用 --><resultMapid="UserMap"type="User"><idcolumn="id"property="id"/><resultcolumn="user_name"property="name"/><associationproperty="dept"javaType="Dept"><idcolumn="dept_id"property="id"/><resultcolumn="dept_name"property="name"/></association></resultMap>

2. 批量插入怎么优化

<!-- 最慢:逐条插入 -->INSERT INTO user (name, email) VALUES (#{name}, #{email})<!-- 最快:批量插入 -->INSERT INTO user (name, email) VALUES<foreachcollection="list"item="item"separator=",">(#{item.name}, #{item.email})</foreach>

但要注意:MySQL 对单条 INSERT 的 VALUES 数量有限制(默认 2000 条以内),数据量大时要分批。

<!-- Service 层分批 -->userService.saveBatch(userList, 1000); // MyBatis-Plus 自带,每批 1000 条

3. 用 distinct 还是 group by 去重

<!-- 单字段去重 -->SELECT DISTINCT name FROM user<!-- 多字段分组统计 -->SELECT name, COUNT(*) AS cnt FROM user GROUP BY name HAVING cnt > 1

DISTINCT 适合简单的去重,GROUP BY 适合需要统计的场景。

四、实战场景题

场景 1:分页查询用户列表,支持姓名模糊搜索和按创建时间排序

<selectid="queryUserPage"resultType="User">SELECT * FROM user<where><iftest="name != null and name != ''">name LIKE CONCAT('%', #{name}, '%')</if></where>ORDER BY create_time DESC</select>
// Service 层Page<User>page=newPage<>(pageNum,pageSize);LambdaQueryWrapper<User>wrapper=newLambdaQueryWrapper<>();wrapper.like(StringUtils.isNotBlank(name),User::getName,name);wrapper.orderByDesc(User::getCreateTime);returnuserMapper.selectPage(page,wrapper);

场景 2:涉及多表的关联查询

<selectid="getOrderDetail"resultMap="OrderDetailMap">SELECT o.id AS order_id, o.order_no, p.id AS product_id, p.product_name, p.price FROM seckill_order o LEFT JOIN seckill_product p ON o.product_id = p.id WHERE o.id = #{id}</select>

MyBatis-Plus 不擅长多表关联查询,复杂关联还是写 XML 更清晰。

场景 3:插入后需要返回主键

<insertid="insertUser"useGeneratedKeys="true"keyProperty="id">INSERT INTO user (name, email) VALUES (#{name}, #{email})</insert>
Useruser=newUser();user.setName("张三");userMapper.insertUser(user);System.out.println("自增主键: "+user.getId());// 插入后自动回填

MP 的save方法默认返回主键,不需要额外配置。

五、MyBatis 与 JPA 对比(面试拓展)

对比MyBatisJPA/Hibernate
上手难度中等,需要写 SQL简单,不用写 SQL
复杂 SQL✅ 完全控制❌ 复杂关联难搞
自动建表✅ 自动建
性能优化✅ 亲手写 SQL,好优化❌ 自动生成 SQL 可能不好
国内主流✅ 绝大多数企业在用较少

选型建议:国内企业主流是 MyBatis/MyBatis-Plus,面试也主要问 MyBatis。JPA 在外企和部分新项目中有用,但不是重点。


💡 觉得有用的话,点赞 + 关注【张老师技术栈】吧!每周更新 Java/Python/爬虫 实战干货,不让你白来。