MyBatis
MyBatis 是一款优秀的半自动 ORM 持久层框架,前身是 Apache 的 iBatis。它封装了 JDBC 的繁琐操作,让开发者只需要关注 SQL 本身,通过 XML 或注解将 Java 对象与 SQL 语句映射,同时保留了对 SQL 的完全控制权。
一、MyBatis 核心架构与原理
1.1 整体架构
┌──────────────────────────────────────────────────────────────┐
│ 接口层 │
│ SqlSession (API 入口,增删改查操作) │
├──────────────────────────────────────────────────────────────┤
│ 数据处理层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Parameter │ │ SQL 执行 │ │ Result │ │
│ │ Handler │ │ (Executor) │ │ SetHandler │ │
│ │ 参数映射 │ │ │ │ 结果映射 │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
├──────────────────────────────────────────────────────────────┤
│ 框架支撑层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ SQL 解析 │ │ 缓存机制 │ │ 事务管理 │ │ 插件机制 │ │
│ │ (XML/注解)│ │ (L1/L2) │ │ │ │ (Interceptor)│ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │
├──────────────────────────────────────────────────────────────┤
│ 基础支持层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ 数据源 │ │ 反射模块 │ │ 日志模块 │ │ 类型转换 │ │
│ │ (DS Pool) │ │ │ │ │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────────┘
1.2 核心组件
|
组件 |
作用 |
生命周期 |
|---|---|---|
|
SqlSessionFactory |
创建 SqlSession 的工厂,全局唯一 |
应用级别(单例) |
|
SqlSession |
执行 SQL、管理事务的会话对象 |
每次请求/方法级别(用完关闭) |
|
Executor |
底层执行器,负责 SQL 执行和缓存管理 |
与 SqlSession 相同 |
|
StatementHandler |
封装 JDBC Statement,处理参数和结果集 |
单次 SQL 调用 |
|
ParameterHandler |
将 Java 参数映射到 JDBC 参数 |
单次 SQL 调用 |
|
ResultSetHandler |
将 JDBC ResultSet 映射为 Java 对象 |
单次 SQL 调用 |
|
TypeHandler |
负责 Java 类型与 JDBC 类型的互相转换 |
全局 |
|
MappedStatement |
封装一条 SQL 映射的完整信息(XML 中一个 |
全局 |
|
Configuration |
全局配置对象,封装所有解析后的配置 |
应用级别 |
1.3 执行流程(完整链路)
1. 获取 SqlSession
SqlSession session = sqlSessionFactory.openSession();
2. 获取 Mapper 代理(JDK 动态代理)
UserMapper mapper = session.getMapper(UserMapper.class);
3. 调用 Mapper 方法
User user = mapper.selectById(1);
4. 内部执行链路:
MapperProxy (动态代理)
→ SqlSession.selectOne(statementId, parameter)
→ Configuration → MappedStatement(找到 SQL 配置)
→ Executor(处理缓存 → 执行)
→ StatementHandler(构建 JDBC Statement)
→ ParameterHandler(设置参数)
→ JDBC Statement.execute()
→ ResultSetHandler(映射结果集 → Java 对象)
→ 返回结果
5. 关闭 Session
session.close();
1.4 MyBatis 核心原理总结
|
原理 |
说明 |
|---|---|
|
XML/注解解析 |
启动时解析 XxxMapper.xml 或注解,生成 MappedStatement 存入 Configuration |
|
动态代理 |
|
|
反射 + 类型转换 |
参数设置和结果映射大量使用反射(读取/设置字段值),TypeHandler 负责类型转换 |
|
工厂模式 |
SqlSessionFactory、DataSourceFactory 等 |
|
建造者模式 |
SqlSessionFactoryBuilder 构建 SqlSessionFactory |
二、MyBatis 各种标签详解
2.1 全局配置标签(mybatis-config.xml)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 1. properties:引入外部属性文件 -->
<properties resource="db.properties">
<property name="key" value="value"/>
</properties>
<!-- 2. settings:全局行为设置 -->
<settings>
<setting name="cacheEnabled" value="true"/> <!-- 二级缓存开关 -->
<setting name="lazyLoadingEnabled" value="true"/> <!-- 延迟加载 -->
<setting name="aggressiveLazyLoading" value="false"/> <!-- 按需加载(3.4.1+) -->
<setting name="mapUnderscoreToCamelCase" value="true"/> <!-- 驼峰映射 ⭐ -->
<setting name="logImpl" value="STDOUT_LOGGING"/> <!-- 日志实现 -->
<setting name="useGeneratedKeys" value="true"/> <!-- 自动获取自增主键 -->
<setting name="defaultExecutorType" value="SIMPLE"/> <!-- 执行器类型 -->
<setting name="localCacheScope" value="SESSION"/> <!-- 一级缓存作用域 -->
</settings>
<!-- 3. typeAliases:类型别名 -->
<typeAliases>
<package name="com.example.entity"/> <!-- 包扫描,别名 = 类名首字母小写 -->
<typeAlias type="com.example.User" alias="User"/>
</typeAliases>
<!-- 4. typeHandlers:类型处理器 -->
<!-- 5. objectFactory:对象工厂 -->
<!-- 6. plugins:插件 -->
<!-- 7. environments:环境配置 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<!-- 8. mappers:映射器 -->
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
<package name="com.example.mapper"/> <!-- 包扫描 -->
</mappers>
</configuration>
2.2 SQL 映射文件标签(XxxMapper.xml)
SELECT 查询
<select id="selectById" parameterType="int" resultType="User">
SELECT * FROM t_user WHERE id = #{id}
</select>
<select id="selectByCondition" parameterType="map" resultMap="userResultMap">
SELECT * FROM t_user
WHERE 1=1
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
ORDER BY create_time DESC
</select>
<select> 属性说明:
|
属性 |
说明 |
|---|---|
|
|
唯一标识,对应 Mapper 接口方法名 |
|
|
参数类型(可省略,框架自动推断) |
|
|
返回结果类型(实体类全限定名或别名) |
|
|
结果映射引用(与 |
|
|
是否使用二级缓存(默认 true) |
|
|
查询超时秒数 |
|
|
批量获取数量 |
|
|
STATEMENT / PREPARED(默认,预编译)/ CALLABLE |
|
|
执行前是否清空一二级缓存 |
INSERT / UPDATE / DELETE
<!-- 插入(自动获取主键) -->
<insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO t_user(name, age, email) VALUES(#{name}, #{age}, #{email})
</insert>
<!-- 插入(selectKey 方式获取主键,适用于不支持自增的数据库) -->
<insert id="insert2" parameterType="User">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
SELECT seq_user.nextval FROM dual <!-- Oracle 序列 -->
</selectKey>
INSERT INTO t_user(id, name) VALUES(#{id}, #{name})
</insert>
<!-- 更新 -->
<update id="update" parameterType="User">
UPDATE t_user SET name=#{name}, age=#{age} WHERE id=#{id}
</update>
<!-- 删除 -->
<delete id="deleteById" parameterType="int">
DELETE FROM t_user WHERE id=#{id}
</delete>
2.3 动态 SQL 标签(核心!面试高频)
<if> — 条件判断
<select id="selectByQuery" resultType="User">
SELECT * FROM t_user WHERE 1=1
<if test="name != null and name != ''">
AND name = #{name}
</if>
<if test="age != null">
AND age >= #{age}
</if>
</select>
<choose> / <when> / <otherwise> — 多选一(相当于 switch-case)
<select id="selectByAny" resultType="User">
SELECT * FROM t_user WHERE 1=1
<choose>
<when test="name != null">
AND name = #{name}
</when>
<when test="email != null">
AND email = #{email}
</when>
<otherwise>
AND status = 1
</otherwise>
</choose>
</select>
<where> — 自动处理 WHERE 和 AND 前缀(代替 WHERE 1=1)
<select id="selectByQuery2" resultType="User">
SELECT * FROM t_user
<where>
<if test="name != null">AND name = #{name}</if>
<if test="age != null">AND age >= #{age}</if>
</where>
</select>
<!-- 自动处理:
- 没有条件时,不生成 WHERE
- 有条件时,自动去掉第一个 AND / OR
-->
<set> — 自动处理 UPDATE SET 中的逗号
<update id="updateSelective">
UPDATE t_user
<set>
<if test="name != null">name = #{name},</if>
<if test="age != null">age = #{age},</if>
<if test="email != null">email = #{email},</if>
</set>
WHERE id = #{id}
</update>
<!-- 自动去掉最后一个多余的逗号 -->
<trim> — 万能裁剪(<where> 和 <set> 的底层实现)
<!-- 等价于 <where> -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="name != null">AND name = #{name}</if>
<if test="age != null">AND age = #{age}</if>
</trim>
<!-- 等价于 <set> -->
<trim prefix="SET" suffixOverrides=",">
<if test="name != null">name = #{name},</if>
<if test="age != null">age = #{age},</if>
</trim>
<trim> 四属性:
|
属性 |
说明 |
|---|---|
|
|
在前面拼接的字符串 |
|
|
在后面拼接的字符串 |
|
|
去掉最前面的指定字符串( |
|
|
去掉最后面的指定字符串 |
<foreach> — 集合遍历
<!-- IN 查询 -->
<select id="selectByIds" resultType="User">
SELECT * FROM t_user WHERE id IN
<foreach collection="list" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
<!-- 批量插入 -->
<insert id="batchInsert">
INSERT INTO t_user(name, age) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.age})
</foreach>
</insert>
<foreach> 属性:
|
属性 |
说明 |
|---|---|
|
|
集合参数名: |
|
|
集合中每个元素的别名 |
|
|
索引名(遍历 List 时是下标,Map 时是 Key) |
|
|
循环开始拼接的字符串 |
|
|
循环结束拼接的字符串 |
|
|
元素间分隔符 |
<sql> / <include> — SQL 片段复用
<!-- 定义 SQL 片段 -->
<sql id="baseColumns">
id, name, age, email, create_time, update_time
</sql>
<sql id="baseConditions">
<where>
<if test="status != null">AND status = #{status}</if>
<if test="deleted != null">AND is_deleted = #{deleted}</if>
</where>
</sql>
<!-- 引用 -->
<select id="selectAll" resultType="User">
SELECT <include refid="baseColumns"/> FROM t_user
<include refid="baseConditions"/>
ORDER BY create_time DESC
</select>
<!-- 跨 XML 引用 -->
<select id="selectAll" resultType="User">
SELECT <include refid="com.example.mapper.UserMapper.baseColumns"/>
FROM t_user
</select>
<resultMap> — 高级结果映射
<!-- 基础映射 -->
<resultMap id="userMap" type="User">
<id property="id" column="id"/> <!-- 主键-->
<result property="name" column="user_name"/> <!-- 普通字段 -->
<result property="createTime" column="create_time"/> <!-- 驼峰 - 下划线映射 -->
</resultMap>
<!-- 一对一关联 (association) -->
<resultMap id="orderMap" type="Order">
<id property="id" column="id"/>
<result property="orderNo" column="order_no"/>
<!-- 嵌套查询(分两次 SQL) -->
<association property="user" column="user_id"
javaType="User" select="selectUserById"/>
<!-- 嵌套结果(一条 SQL join) -->
<association property="user" javaType="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
</association>
</resultMap>
<!-- 一对多关联 (collection) -->
<resultMap id="userOrdersMap" type="User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!-- 一对多 -->
<collection property="orders" ofType="Order"
column="id" select="selectOrdersByUserId"/>
<!-- 或嵌套结果 -->
<collection property="orders" ofType="Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
</collection>
</resultMap>
<!-- 鉴别器 (discriminator) — 根据某字段值映射不同子类 -->
<resultMap id="vehicleMap" type="Vehicle">
<discriminator javaType="int" column="vehicle_type">
<case value="1" resultType="Car"/>
<case value="2" resultType="Truck"/>
</discriminator>
</resultMap>
<bind> — 创建变量
<select id="selectByName" resultType="User">
<bind name="pattern" value="'%' + name + '%'"/>
SELECT * FROM t_user WHERE name LIKE #{pattern}
</select>
<!-- 解决不同数据库 CONCAT 语法差异 -->
2.4 注解标签
@Select("SELECT * FROM t_user WHERE id = #{id}")
User selectById(@Param("id") int id);
@Insert("INSERT INTO t_user(name, age) VALUES(#{name}, #{age})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
@Update("UPDATE t_user SET name=#{name} WHERE id=#{id}")
int update(User user);
@Delete("DELETE FROM t_user WHERE id=#{id}")
int deleteById(int id);
// 动态 SQL 注解(复杂 SQL 不推荐,建议用 XML)
@SelectProvider(type = UserSqlProvider.class, method = "selectByCondition")
List<User> selectByCondition(Map<String, Object> params);
2.5 标签速查总表
|
标签 |
位置 |
用途 |
频率 |
|---|---|---|---|
|
|
Mapper.xml |
查询语句 |
⭐⭐⭐⭐⭐ |
|
|
Mapper.xml |
插入语句 |
⭐⭐⭐⭐⭐ |
|
|
Mapper.xml |
更新语句 |
⭐⭐⭐⭐⭐ |
|
|
Mapper.xml |
删除语句 |
⭐⭐⭐⭐ |
|
|
Mapper.xml |
定义可复用的 SQL 片段 |
⭐⭐⭐ |
|
|
Mapper.xml |
引用 SQL 片段 |
⭐⭐⭐ |
|
|
Mapper.xml |
自定义结果映射 |
⭐⭐⭐⭐⭐ |
|
|
|
主键映射 |
⭐⭐⭐⭐ |
|
|
|
普通字段映射 |
⭐⭐⭐⭐ |
|
|
|
一对一关联映射 |
⭐⭐⭐ |
|
|
|
一对多关联映射 |
⭐⭐⭐ |
|
|
|
鉴别器(很少用) |
⭐ |
|
|
动态 SQL |
条件判断 |
⭐⭐⭐⭐⭐ |
|
|
动态 SQL |
自动处理 WHERE |
⭐⭐⭐⭐⭐ |
|
|
动态 SQL |
自动处理 SET |
⭐⭐⭐⭐ |
|
|
动态 SQL |
多选一 |
⭐⭐⭐ |
|
|
动态 SQL |
集合遍历(IN/批量) |
⭐⭐⭐⭐⭐ |
|
|
动态 SQL |
万能裁剪 |
⭐⭐⭐ |
|
|
动态 SQL |
创建变量 |
⭐ |
|
|
|
获取主键 |
⭐⭐ |
|
|
mybatis-config |
全局行为配置 |
⭐⭐⭐⭐ |
|
|
mybatis-config |
类型别名 |
⭐⭐⭐ |
|
|
mybatis-config |
插件(分页拦截器) |
⭐⭐⭐ |
三、SQL 注入与 MyBatis 防御
3.1 什么是 SQL 注入?
攻击者通过构造恶意输入,拼接并执行非预期的 SQL 语句。
// 用户输入:' OR '1'='1' --
String username = request.getParameter("username");
String sql = "SELECT * FROM users WHERE name = '" + username + "'";
// 拼接后:SELECT * FROM users WHERE name = '' OR '1'='1' --'
// 结果:返回全部用户!
3.2 MyBatis 中的 #{} vs ${}
这是面试最高频考点!
|
对比维度 |
|
|
|---|---|---|
|
处理方式 |
预编译(PreparedStatement), |
直接拼接,字符串替换 |
|
防 SQL 注入 |
✅ 安全 |
❌ 不安全 |
|
类型处理 |
自动设置 JDBC 类型参数 |
原样输出,无类型处理 |
|
适用场景 |
WHERE 条件中的值 |
动态表名、动态列名、ORDER BY、GROUP BY、LIKE 拼接 |
|
语法示例 |
|
|
原理对比:
-- #{} 生成的预编译 SQL
SELECT * FROM t_user WHERE name = ? -- 参数 'Tom' 作为值安全传入
-- ${} 生成的 SQL(直接拼接!)
SELECT * FROM t_user WHERE name = Tom -- 如果 Tom 是 ' OR '1'='1' 就注入了
3.3 ${} 的安全使用场景
<!-- 1. 动态表名/列名(无法用 #{}) -->
<select id="selectByTable">
SELECT * FROM ${tableName} WHERE id = #{id} <!-- ⚠️ tableName 必须服务端白名单校验 -->
</select>
<!-- 2. ORDER BY 排序字段 -->
<select id="selectByOrder">
SELECT * FROM t_user ORDER BY ${orderColumn} ${orderDirection}
<!-- 解决方法:服务端对 orderColumn 做白名单校验 -->
</select>
<!-- 3. LIKE 模糊查询 -->
<!-- ❌ 不安全 -->
<select id="selectLike">SELECT * FROM t_user WHERE name LIKE '%${name}%'</select>
<!-- ✅ 安全——用 #{} + bind 或 CONCAT -->
<select id="selectLike">SELECT * FROM t_user WHERE name LIKE CONCAT('%', #{name}, '%')</select>
3.4 防御 SQL 注入的完整策略
- 默认使用
#{}:所有参数值绑定都用#{} - 必须用
${}时加白名单校验:
private static final Set<String> ALLOWED_COLUMNS = Set.of("id", "name", "age", "create_time");
private static final Set<String> ALLOWED_DIRECTIONS = Set.of("ASC", "DESC");
public List<User> selectByOrder(String orderColumn, String orderDirection) {
if (orderColumn == null || !ALLOWED_COLUMNS.contains(orderColumn)) {
throw new IllegalArgumentException("非法列名: " + orderColumn);
}
if (orderDirection == null || !ALLOWED_DIRECTIONS.contains(orderDirection.toUpperCase())) {
throw new IllegalArgumentException("非法排序: " + orderDirection);
}
return mapper.selectByOrder(orderColumn, orderDirection.toUpperCase());
}
- 不要拼接 SQL 字符串
- 使用 ORM 框架(MyBatis 本身提供基础防护)
- 最小权限原则:数据库账户只给必要权限
四、MyBatis 缓存机制
4.1 一级缓存(SqlSession 级别)
默认开启,无需配置。
|
特性 |
说明 |
|---|---|
|
作用域 |
同一个 SqlSession 内 |
|
生命周期 |
SqlSession 创建 → 关闭(或执行 insert/update/delete) |
|
清空条件 |
执行增删改操作、手动 |
|
命中条件 |
相同的 statementId + 参数 + RowBounds + 分页 |
SqlSession session = factory.openSession();
User u1 = session.selectOne("selectById", 1); // 查数据库
User u2 = session.selectOne("selectById", 1); // 命中缓存,不查数据库
// u1 == u2 为 true(同一个对象,引用相同)
session.close();
4.2 二级缓存(Mapper 级别)
默认关闭,需手动开启。
<!-- 1. mybatis-config.xml -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- 2. XxxMapper.xml 添加 <cache/> -->
<mapper namespace="com.example.mapper.UserMapper">
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="false"/>
</mapper>
<cache> 属性:
|
属性 |
说明 |
默认值 |
|---|---|---|
|
|
回收策略:LRU / FIFO / SOFT / WEAK |
LRU |
|
|
刷新间隔(毫秒) |
不设置(仅增删改时清空) |
|
|
缓存对象数量 |
1024 |
|
|
只读(true 时返回同一对象快但不安全) |
false |
二级缓存注意事项:
- 查出的对象需序列化(实现
Serializable) - 跨 SqlSession 共享
- 不建议在频繁修改的表上开启
- 生产环境通常用 Redis 等外部缓存替代 MyBatis 二级缓存
4.3 缓存执行顺序
查询请求
→ 二级缓存(Mapper 级别,跨 SqlSession)
命中?→ 返回
未命中?↓
→ 一级缓存(SqlSession 级别)
命中?→ 返回
未命中?↓
→ 查询数据库 → 存入一级缓存 → 一级缓存提交后会刷入二级缓存
五、MyBatis 面试常问问题
Q1:MyBatis 中 #{} 和 ${} 的区别?(最高频)
见上文第三章,核心:预编译 vs 直接拼接;防 SQL 注入的角度回答。
Q2:MyBatis 是如何实现 Mapper 接口与 SQL 绑定的?
答案:JDK 动态代理 + XML 解析。
// 1. 启动时:解析 XML/注解 → 生成 MappedStatement → 存入 Configuration
// MappedStatement 包含:id(namespace.statementId)、SQL、参数映射、结果映射
// 2. 运行时:sqlSession.getMapper(UserMapper.class)
// → Configuration.getMapper() → MapperRegistry.getMapper()
// → MapperProxyFactory.newInstance() ← JDK 动态代理
// 3. 调用 mapper.selectById(1) 时:
// → MapperProxy.invoke() 拦截
// → 根据「接口名.方法名」拼接 statementId = "com.xxx.mapper.UserMapper.selectById"
// → 从 Configuration 中获取 MappedStatement
// → 调用 SqlSession.selectOne(statementId, args)
// → Executor → StatementHandler → JDBC
关键代码概念:
// MapperProxy 是 JDK 的 InvocationHandler
public class MapperProxy<T> implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
// 如果方法是 Object 的方法(toString/hashCode),直接执行
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 获取 MapperMethod,调用 execute
MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
}
Q3:MyBatis 和 Hibernate 的区别?如何选型?
|
对比维度 |
MyBatis |
Hibernate |
|---|---|---|
|
类型 |
半自动 ORM |
全自动 ORM |
|
SQL 控制 |
手动编写 SQL,灵活 |
自动生成(HQL),封装度高 |
|
性能优化 |
直接写 SQL 优化 |
掌握成本较高 |
|
学习曲线 |
低(会 SQL 即可) |
高(需理解 Session/Lazy/Cascade 等) |
|
数据库移植 |
差(SQL 可能绑定特定数据库) |
好(方言自动适配) |
|
适用场景 |
复杂查询多、性能要求高、SQL 优化频繁 |
CRUD 简单、数据库迁移需求、快速原型 |
|
动态 SQL |
强大( |
较弱(需 Criteria API) |
Q4:MyBatis 延迟加载(懒加载)的原理?
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
<resultMap id="userMap" type="User">
<id property="id" column="id"/>
<!-- 分两次查询:查询用户 → 需要时才查订单 -->
<collection property="orders" ofType="Order"
column="id" select="selectOrdersByUserId"
fetchType="lazy"/> <!-- lazy(懒加载) / eager(立即加载) -->
</resultMap>
原理:CGLIB/Javassist 动态代理。
1. 查询 User 时,只查 User 表,不查关联的 Orders
2. MyBatis 为 User 生成代理对象(CGLIB)
3. 当第一次调用 user.getOrders() 时:
→ 代理对象拦截方法调用
→ 执行 selectOrdersByUserId(user.id)
→ 将查询结果设置到 orders 属性
→ 后续调用直接返回缓存值
注意事项:
- 需引入 CGLIB 或 Javassist
- 延迟加载期间 SqlSession 必须存活(Spring 中使用需配置事务保证)
- 可能导致 N+1 问题,在循环中访问关联对象会产生大量 SQL
Q5:MyBatis 插件原理(分页插件怎么实现)?
答案:责任链模式 + JDK 动态代理 + 拦截器。
// 自定义拦截器示例
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare",
args = {Connection.class, Integer.class})
})
public class MyInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 前置处理
System.out.println("SQL执行前...");
Object result = invocation.proceed(); // 执行原方法
// 后置处理
System.out.println("SQL执行后...");
return result;
}
}
可拦截的四大对象:
|
拦截对象 |
可拦截方法 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
PageHelper 分页插件原理:
1. PageHelper.startPage(1, 10)
→ 将分页参数存入 ThreadLocal
2. 执行查询 SQL 时,Executor 的 query 方法被拦截
→ 读取 ThreadLocal 中的分页参数
→ 先执行 SELECT COUNT(*) 查总数
→ 修改原 SQL,追加 LIMIT 子句
→ 执行分页查询
→ 封装 PageInfo 返回
3. ThreadLocal 用完清除,避免内存泄漏
Q6:MyBatis 一级缓存和二级缓存的区别?
|
对比维度 |
一级缓存 |
二级缓存 |
|---|---|---|
|
作用域 |
SqlSession |
Mapper (namespace) |
|
默认开启 |
✅ 是 |
❌ 否 |
|
跨 SqlSession |
不共享 |
共享 |
|
存储方式 |
HashMap |
可自定义(默认 HashMap) |
|
清空时机 |
SqlSession close / 增删改操作 / clearCache() |
增删改 / flush-interval 到期 |
|
序列化 |
不需要 |
需要(实现 Serializable) |
|
适用场景 |
同一会话内重复查询 |
不经常变的基础数据 |
|
生产建议 |
默认使用 |
谨慎使用(推荐用 Redis 替代) |
Q7:MyBatis 批量插入怎么做?有哪些方式?
<!-- 方式 1:foreach 拼接 SQL(推荐,简单高效) -->
<insert id="batchInsert">
INSERT INTO t_user(name, age) VALUES
<foreach collection="list" item="u" separator=",">
(#{u.name}, #{u.age})
</foreach>
</insert>
<!-- 注意:SQL 有长度限制,单次不建议超过 5000 条 -->
<!-- 方式 2:SqlSession Batch 模式 -->
<insert id="insert">INSERT INTO t_user(name,age) VALUES(#{name},#{age})</insert>
// 方式 2 的调用方式(JDBC 批量)
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
UserMapper mapper = session.getMapper(UserMapper.class);
for (User user : list) {
mapper.insert(user); // 只提交到批量队列,不立即执行
}
session.commit(); // 一次性批量执行
session.close();
// 缺点:无法获取每条记录的自增主键
// 方式 3:ExecutorType.BATCH + flushStatements(推荐)
// 每 1000 条 flush 一次,兼顾内存和性能
Q8:MyBatis 中 DAO 和 Mapper 的区别?如何传多个参数?
DAO vs Mapper:
|
|
DAO 模式 |
Mapper 代理模式 |
|---|---|---|
|
实现 |
手动写实现类,调用 SqlSession API |
MyBatis 动态代理自动生成实现 |
|
开发量 |
多(接口 + 实现类) |
少(只需接口 + XML/注解) |
|
推荐 |
❌ 老式写法 |
✅ 现在主流 |
多参数传递方式:
// 方式 1:@Param(推荐,明确语义)
User selectByAgeAndName(@Param("age") int age, @Param("name") String name);
// XML: WHERE age = #{age} AND name = #{name}
// 方式 2:Map(不推荐,key 是字符串无类型安全)
Map<String, Object> params = Map.of("age", 18, "name", "Tom");
// 方式 3:对象(多个筛选条件建议封装为 Query 对象)
UserQuery query = new UserQuery();
query.setAge(18); query.setName("Tom");
// 方式 4:arg0/arg1 或 param1/param2(默认索引名,不推荐)
// XML: WHERE age = #{arg0} AND name = #{arg1}
// XML: WHERE age = #{param1} AND name = #{param2}
Q9:当实体类字段名和数据库列名不一致时,如何处理?
<!-- 方法 1:SQL 别名 -->
<select id="selectAll" resultType="User">
SELECT id, user_name AS userName, create_time AS createTime FROM t_user
</select>
<!-- 方法 2:开启驼峰自动映射(最简单) -->
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<!-- 方法 3:resultMap 显式映射(最灵活) -->
<resultMap id="userMap" type="User">
<result property="userName" column="user_name"/>
<result property="createTime" column="create_time"/>
</resultMap>
Q10:MyBatis-Plus 和 MyBatis 的关系?
|
对比 |
MyBatis |
MyBatis-Plus |
|---|---|---|
|
定位 |
基础持久层框架 |
MyBatis 的增强工具 |
|
CRUD |
手写 SQL |
内置通用 Mapper(BaseMapper) |
|
分页 |
手动实现/PageHelper |
内置分页插件 |
|
代码生成器 |
无 |
有(AutoGenerator) |
|
乐观锁 |
手动实现 |
注解 + 拦截器自动处理 |
|
逻辑删除 |
手动实现 |
|
|
主键策略 |
手动处理 |
内置雪花算法等多种策略 |
|
兼容性 |
— |
完全兼容 MyBatis,无侵入 |
附录:快速启动配置示例
Spring Boot 集成(application.yml)
# 数据源配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/mybatis_demo?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# MyBatis 配置
mybatis:
mapper-locations: classpath:mapper/*.xml # Mapper XML 位置
type-aliases-package: com.example.entity # 实体类包(别名)
configuration:
map-underscore-to-camel-case: true # 驼峰映射
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # SQL 日志
cache-enabled: true # 二级缓存总开关
Maven 依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>