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) | 低(每次重新编译) |
xml
<!-- #{} 安全写法 -->
<select id="getUser" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 实际执行:SELECT * FROM user WHERE id = ? -->
<!-- ${} 危险写法 -->
<select id="getUser" resultType="User">
SELECT * FROM user WHERE id = ${id}
</select>
<!-- 传入 1 OR 1=1 → 全表数据被查出 -->
结论: 能用 #{} 的地方绝不用 ${}。只有动态表名、动态列名这种不得不用的场景才用 ${},并且要做好参数校验。
3. MyBatis 的一级缓存和二级缓存
一级缓存(默认开启):
同一个 SqlSession 中,两次相同的查询会走缓存,不会重复查数据库。
SqlSession 关闭或执行了 insert/update/delete 后缓存失效。
二级缓存(需手动开启):
同一个 SqlSessionFactory 下,多个 SqlSession 共享缓存。
适合:查询多、修改少、并发要求不高的场景。
不适合:对数据实时性要求高的场景。
xml
<!-- 开启二级缓存 -->
<mapper namespace="com.zhang.mapper.UserMapper">
<cache eviction="LRU" flushInterval="60000" size="512"/>
</mapper>
面试常问: MyBatis 的缓存机制了解吗?一级缓存和二级缓存的区别?
二、MyBatis-Plus 面试题
1. MyBatis-Plus 和 MyBatis 的区别
| MyBatis | MyBatis-Plus | |
|---|---|---|
| 基础 CRUD | 手写 SQL | 自动提供,不用写 |
| 分页 | 手写 Limit | 分页插件,一行代码搞定 |
| 条件查询 | 手写动态 SQL | LambdaQueryWrapper 链式调用 |
| 代码量 | 多 | 减少 50%+ |
| 灵活度 | 高,完全控制 SQL | 复杂 SQL 还是要手写 |
一句话总结: MyBatis-Plus 是 MyBatis 的增强工具,不为零改动------你在 MyBatis 里写复杂 SQL 的地方,MP 一样支持。
2. MyBatis-Plus 的分页原理
java
// 使用
Page<User> page = new Page<>(1, 10);
userMapper.selectPage(page, null);
// 自动生成 SELECT * FROM user LIMIT 0, 10
// 还会自动执行 SELECT COUNT(*) FROM user 查总条数
原理: 通过 PaginationInnerInterceptor 拦截器,在执行 SQL 前自动拼接 LIMIT 和 COUNT。
注意: 不配置分页拦截器,Page 对象传进去也不会生效------这是面试常挖的坑。
3. 乐观锁插件
java
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
java
@Version
private Integer version;
原理: 更新时 SET version = version + 1 WHERE version = 旧值。影响行数为 0 说明数据被修改过,需要重试。
4. 逻辑删除
java
@TableLogic
private Integer isDeleted;
原理: 调用 deleteById 时实际执行 UPDATE SET is_deleted = 1 WHERE id = ?,查询时自动拼接 is_deleted = 0。
5. 自动填充
java
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
配合 MetaObjectHandler 实现 createTime 和 updateTime 自动填充,不用手动 set。
三、XML 映射文件高频问题
1. resultType 和 resultMap 的区别
xml
<!-- resultType:列名和属性名一致时用,简单 -->
<select id="getUser" resultType="com.zhang.User">
SELECT id, name, email FROM user WHERE id = #{id}
</select>
<!-- resultMap:列名和属性名不一致、有复杂关联时用 -->
<resultMap id="UserMap" type="User">
<id column="id" property="id"/>
<result column="user_name" property="name"/>
<association property="dept" javaType="Dept">
<id column="dept_id" property="id"/>
<result column="dept_name" property="name"/>
</association>
</resultMap>
2. 批量插入怎么优化
xml
<!-- 最慢:逐条插入 -->
INSERT INTO user (name, email) VALUES (#{name}, #{email})
<!-- 最快:批量插入 -->
INSERT INTO user (name, email) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.name}, #{item.email})
</foreach>
但要注意: MySQL 对单条 INSERT 的 VALUES 数量有限制(默认 2000 条以内),数据量大时要分批。
xml
<!-- Service 层分批 -->
userService.saveBatch(userList, 1000); // MyBatis-Plus 自带,每批 1000 条
3. 用 distinct 还是 group by 去重
xml
<!-- 单字段去重 -->
SELECT DISTINCT name FROM user
<!-- 多字段分组统计 -->
SELECT name, COUNT(*) AS cnt FROM user GROUP BY name HAVING cnt > 1
DISTINCT 适合简单的去重,GROUP BY 适合需要统计的场景。
四、实战场景题
场景 1:分页查询用户列表,支持姓名模糊搜索和按创建时间排序
xml
<select id="queryUserPage" resultType="User">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
name LIKE CONCAT('%', #{name}, '%')
</if>
</where>
ORDER BY create_time DESC
</select>
java
// Service 层
Page<User> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(name), User::getName, name);
wrapper.orderByDesc(User::getCreateTime);
return userMapper.selectPage(page, wrapper);
场景 2:涉及多表的关联查询
xml
<select id="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:插入后需要返回主键
xml
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (name, email) VALUES (#{name}, #{email})
</insert>
java
User user = new User();
user.setName("张三");
userMapper.insertUser(user);
System.out.println("自增主键: " + user.getId()); // 插入后自动回填
MP 的 save 方法默认返回主键,不需要额外配置。
五、MyBatis 与 JPA 对比(面试拓展)
| 对比 | MyBatis | JPA/Hibernate |
|---|---|---|
| 上手难度 | 中等,需要写 SQL | 简单,不用写 SQL |
| 复杂 SQL | ✅ 完全控制 | ❌ 复杂关联难搞 |
| 自动建表 | ❌ | ✅ 自动建 |
| 性能优化 | ✅ 亲手写 SQL,好优化 | ❌ 自动生成 SQL 可能不好 |
| 国内主流 | ✅ 绝大多数企业在用 | 较少 |
选型建议: 国内企业主流是 MyBatis/MyBatis-Plus,面试也主要问 MyBatis。JPA 在外企和部分新项目中有用,但不是重点。
💡 觉得有用的话,点赞 + 关注【张老师技术栈】吧!每周更新 Java/Python/爬虫 实战干货,不让你白来。