MyBatis 面试专题
- 基础概念
- MyBatis中的工作原理
- MyBatis 与 Hibernate 的区别?
- #{} 和 ${} 的区别?
- MyBatis 的核心组件有哪些?
- 映射与配置
- 如何传递多个参数?
- ResultMap 的作用是什么?
- 动态 SQL 常用标签有哪些?
- 如何复用 SQL 片段?
- 当实体中的属性和表中的字段不一致的情况下怎么办?
- 高级特性
- 一级缓存与二级缓存的区别?
- MyBatis中如何实现缓存的扩展
- 如何实现延迟加载(懒加载)?
- MyBatis 插件(Plugin)原理?
- 分页插件的实现原理?
- 关联查询
- 一对一查询如何配置?
- 一对多查询如何配置?
- 嵌套查询 vs 嵌套结果的区别?
- 事务与性能
- MyBatis 如何管理事务?
- 如何执行批量操作?
- 常见问题排查
- 如何防止SQL注入
- Mapper 接口如何绑定到 XML?
- 如何获取自动生成的主键?
- 遇到 BindingException 可能的原因?
- 扩展与集成
- 如何与 Spring 集成?
- MyBatis 如何整合分布式缓存(如 Redis)?
基础概念
MyBatis中的工作原理
- 系统启动的时候会加载解析全局配置文件和对应映射文件。加载解析的相关信息存储在
Configuration
对象@Test public void test1() throws Exception{ // 1.获取配置文件 InputStream in = Resources.getResourceAsStream("mybatis-config.xml"); // 2.加载解析配置文件并获取SqlSessionFactory对象 // SqlSessionFactory 的实例我们没有通过 DefaultSqlSessionFactory直接来获取 // 而是通过一个Builder对象来建造的 // SqlSessionFactory 生产 SqlSession 对象的 SqlSessionFactory 应该是单例 // 全局配置文件和映射文件 也只需要在 系统启动的时候完成加载操作 // 通过建造者模式来 构建复杂的对象 1.完成配置文件的加载解析 2.完成 SqlSessionFactory的创建 SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in); // 3.根据SqlSessionFactory对象获取SqlSession对象 SqlSession sqlSession = factory.openSession(); // 4.通过SqlSession中提供的 API方法来操作数据库 List<User> list = sqlSession.selectList("com.boge.mapper.UserMapper.selectUserList"); // 获取接口的代码对象 得到的其实是 通过JDBC代理模式获取的一个代理对象 // UserMapper mapper = sqlSession.getMapper(UserMapper.class); //List<User> list = mapper.selectUserList(); System.out.println("list.size() = " + list.size()); // 5.关闭会话 sqlSession.close(); // 关闭session 清空一级缓存 }
- SqlSessionFactory: new DefaultSqlSessionFactory 全局配置文件的加载解析【Configuration】,映射文件的加载解析【Configuration,MappedStatement】
- SqlSession:new DefaultSqlSession,创建相关的事务工厂,完成Executor的创建,已经二级缓存CachingExecutor的装饰,同时完成了插件逻辑的植入。
- selectOne(); 二级缓存 -> 一级缓存 --> 数据库插入。
- SqlSession.getMapper()。
源码结构
MyBatis 与 Hibernate 的区别?
- MyBatis:半自动 ORM,需手动编写 SQL,灵活性高,适合复杂查询或对 SQL 优化有要求的场景。
- Hibernate:全自动 ORM,通过 HQL 或 Criteria API 操作对象,适合快速开发简单 CRUD,但对复杂 SQL 支持较弱。
#{} 和 ${} 的区别?
- #{}:预编译处理(占位符 ?),防止 SQL 注入,自动处理类型转换。
- ${}:字符串替换(直接拼接 SQL),需手动防注入,常用于动态表名、列名等非参数场景。
MyBatis 的核心组件有哪些?
SqlSessionFactory(会话工厂)、SqlSession(会话)、Mapper 接口、Executor(执行器)、MappedStatement(SQL 映射)。
映射与配置
如何传递多个参数?
使用 @Param
注解标注参数名,或通过 POJO 对象、Map 封装参数。
ResultMap 的作用是什么?
解决数据库列名与 Java 对象属性名不一致的问题,支持复杂关联映射(一对一、一对多)。
动态 SQL 常用标签有哪些?
<if>、<choose>/<when>/<otherwise>、<foreach>、<where>、<set>、<trim>
<select id="queryPageAllEmployee" resultType="com.todaytech.salary.basicInfo.model.vo.EmployeeVO">
SELECT * FROM (
SELECT un.agency_name AS unit_name,un.mof_dep_code, d.dept_name AS dept_name, eunf.zj_tied_date, eunf.xs_retire_expert, ef.* ,eunf.stopsign
FROM zz_employeefix ef
LEFT JOIN zz_employeeunfix eunf ON ef.emp_id = eunf.emp_id
INNER JOIN sys_agency un ON ef.unit_id = un.id
INNER JOIN zz_department d ON (ef.unit_id = d.unit_id AND ef.dept_id = d.dept_id)
<choose>
<when test="salaryAgencyType != null and salaryAgencyType != ''">
WHERE (ef.salary_agency_type = #{salaryAgencyType}
<if test="childrenAgencyIds != null and childrenAgencyIds.size() > 0 ">
OR ef.unit_id IN <foreach collection="childrenAgencyIds" item="item" open="(" separator="," close=")"> #{item} </foreach>
</if>)
</when>
<otherwise>
<if test="childrenAgencyIds != null and childrenAgencyIds.size() > 0 ">
WHERE ef.unit_id IN <foreach collection="childrenAgencyIds" item="item" open="(" separator="," close=")"> #{item} </foreach>
</if>
</otherwise>
</choose> ) em
${ew.customSqlSegment}
ORDER BY emp_id DESC,emp_modified DESC
</select>
如何复用 SQL 片段?
使用 <sql>
定义片段,通过 <include>
引用。
<sql id="getChildNotDeptSql">
select orgclosure_id as id from sys_agency_relation where orgclosure_pid=#{agencyId}
</sql>
<select id="queryUnitIds" resultType="java.lang.Integer">
select * from (
<include refid="com.todaytech.vue.mapper.SysAgencyMapper.getChildNotDeptSql">
<property name="agencyId" value="#{agencyId}"/>
</include> ) ug
</select>
当实体中的属性和表中的字段不一致的情况下怎么办?
- 我们可以在对应的SQL语句中通过
别名
的方式来解决这个问题。 - 我们通过自定义
resultMap
标签来设置属性和字段的映射关系
高级特性
一级缓存与二级缓存的区别?
- 缓存的作用:减低数据源的访问频率。从而提高数据源的处理能力。或者提高服务器的响应速度。
- 一级缓存:基于 SqlSession 级别,默认开启,会话结束或执行更新操作(增删改)时失效。
- 二级缓存:基于 namespace(Mapper 级别),需手动开启,跨会话共享,通过序列化机制实现。
- 为什么会先走二级缓存再走一级缓存?
- 二级缓存的作用域是SqlSessionFactory级别-90%找到。
- 一级缓存是SqlSession级别的-5%找到。
MyBatis中如何实现缓存的扩展
- 创建
Cache
接口的实现。重写getObject
和putObject
方法。 - 怎么让我们自定义的实现:在
cache标签
中通过type属性
关联我们自定义的Cache接口的实现。
如何实现延迟加载(懒加载)?
在 <association>
或 <collection>
中配置 fetchType="lazy"
,需全局启用 lazyLoadingEnabled=true
- 全局懒加载
<settings> <setting name="lazyLoadingEnabled" value="true"/> <setting name="aggressiveLazyLoading" value="false"/> </settings>
- 局部懒加载
<resultMap id="userResultMap" type="User"> <id property="id" column="id"/> <result property="username" column="username"/> <collection property="orders" javaType="ArrayList" ofType="Order" select="selectOrdersByUserId" column="id" fetchType="lazy"/> </resultMap> <select id="selectUserById" resultMap="userResultMap"> SELECT * FROM user WHERE id = #{id} </select> <select id="selectOrdersByUserId" resultType="Order"> SELECT * FROM orders WHERE user_id = #{userId} </select>
MyBatis 插件(Plugin)原理?
基于动态代理,拦截 Executor
、StatementHandler
、ParameterHandler
、ResultSetHandler
的核心方法,实现自定义逻辑(如分页、性能监控)。
分页插件的实现原理?
拦截待执行 SQL,重写为分页查询(如 MySQL 的 LIMIT
),并查询总数。
关联查询
一对一查询如何配置?
使用 <association>
标签,通过 property
指定对象属性,resultMap
或 column
映射关联结果。
<resultMap id="UserResultMap" type="User">
<id property="id" column="id" />
<result property="name" column="name" />
<association property="address" javaType="Address">
<id property="id" column="address_id" />
<result property="city" column="city" />
<result property="state" column="state" />
</association>
</resultMap>
<select id="selectUserWithAddress" resultMap="UserResultMap">
SELECT u.id, u.name, a.id as address_id, a.city, a.state
FROM User u
LEFT JOIN Address a ON u.address_id = a.id
</select>
一对多查询如何配置?
使用 <collection>
标签,类似一对一,但映射对象为集合类型。
<resultMap id="UserResultMap" type="User">
<id property="id" column="id" />
<result property="name" column="name" />
<collection property="orders" ofType="Order">
<id property="id" column="order_id" />
<result property="orderDate" column="order_date" />
</collection>
</resultMap>
<select id="selectUserWithOrders" resultMap="UserResultMap">
SELECT u.id, u.name, o.id as order_id, o.order_date
FROM User u
LEFT JOIN Order o ON u.id = o.user_id
</select>
嵌套查询 vs 嵌套结果的区别?
- 嵌套查询:执行多次 SQL(可能引发 N+1 问题)。
- 嵌套结果:单次复杂查询,通过 ResultMap 映射结果。
事务与性能
MyBatis 如何管理事务?
默认使用 JDBC 事务(通过 Connection
的提交/回滚),集成 Spring 后由 Spring 统一管理。
如何执行批量操作?
使用 SqlSession
的 BatchExecutor
,或在动态 SQL 中使用 <foreach>
拼接批量语句。
常见问题排查
如何防止SQL注入
-
优先使用 #{} 占位符
SELECT * FROM users WHERE username = #{username}
-
避免直接使用 ${} 拼接字符串
SELECT * FROM users WHERE username = '${username}'
-
安全使用动态 SQL 标签
<select id="findUsers" parameterType="map"> SELECT * FROM users <where> <if test="username != null"> AND username = #{username} </if> </where> </select>
-
强制校验 ${} 的使用场景
- 必须使用时(如动态表名、排序字段):
- 白名单过滤:仅允许预定义的合法值(如 orderBy 参数只接受 id, name)。
- 避免用户输入直接控制:如从下拉菜单选择排序字段,而非自由输入。
- 必须使用时(如动态表名、排序字段):
Mapper 接口如何绑定到 XML?
通过 namespace 指定接口全限定名,方法名与 XML 中的 id 一致。
即:Java(包名+类名)=xml(namespace+id)
如何获取自动生成的主键?
配置 useGeneratedKeys="true"
和 keyProperty
(如返回自增 ID 到对象的 id
属性)。
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (name, email)
VALUES (#{name}, #{email})
</insert>
遇到 BindingException 可能的原因?
Mapper 接口未扫描到、XML 中 id
与接口方法名不匹配、namespace
配置错误。
扩展与集成
如何与 Spring 集成?
使用 mybatis-spring
库,配置 SqlSessionFactoryBean
和 MapperScannerConfigurer
扫描 Mapper 接口。
MyBatis 如何整合分布式缓存(如 Redis)?
自定义 Cache
接口实现类,替换默认的 PerpetualCache。