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) 低(每次重新编译)
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 前自动拼接 LIMITCOUNT

注意: 不配置分页拦截器,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/爬虫 实战干货,不让你白来。