MyBatis进阶详解
MyBatis 进阶详解
本章位置:第二阶段 Java 核心框架
前置知识:MyBatis 基础、JDBC、反射、JavaBean、集合、MySQL、Web CRUD
下一篇:SpringBoot 请求响应
学习目标:掌握动态 SQL、复杂结果映射、一对一、一对多、缓存、批处理、TypeHandler、插件机制以及 MyBatis 的核心执行流程。
一、为什么要学 MyBatis 进阶
MyBatis 基础阶段我们已经会:
配置 MyBatis
创建 Mapper
编写 Mapper XML
select
insert
update
delete
#{}
@Param
resultType
resultMap
commit / rollback
但真实项目会马上遇到:
查询条件不固定怎么办?
IN 查询参数数量不固定怎么办?
修改时只更新非空字段怎么办?
多表查询怎么封装对象?
一对多怎么映射?
缓存到底什么时候生效?
一次插入很多数据怎么做?
枚举和数据库字段怎么转换?
Mapper 接口为什么没有实现类?
MyBatis 插件怎么拦截 SQL?
这些就是 MyBatis 进阶内容。
二、MyBatis 进阶知识结构
MyBatis 进阶
│
├─ 动态 SQL
│ ├─ if
│ ├─ where
│ ├─ set
│ ├─ trim
│ ├─ choose
│ ├─ foreach
│ ├─ bind
│ └─ sql/include
│
├─ 复杂映射
│ ├─ resultMap
│ ├─ association
│ ├─ collection
│ ├─ 一对一
│ └─ 一对多
│
├─ 缓存
│ ├─ 一级缓存
│ └─ 二级缓存
│
├─ 批处理
│ ├─ foreach 批量 SQL
│ └─ Batch Executor
│
├─ 扩展
│ ├─ TypeHandler
│ ├─ Plugin
│ └─ Executor
│
└─ 执行原理
├─ MapperProxy
├─ MappedStatement
├─ BoundSql
├─ StatementHandler
├─ ParameterHandler
└─ ResultSetHandler
三、动态 SQL 是什么
动态 SQL:
根据参数不同,动态生成不同 SQL。
例如搜索学生:
可能传姓名
可能传专业
可能传最小年龄
可能全部都不传
如果不用动态 SQL:
要写大量不同 SQL
MyBatis 提供:
if
where
choose
trim
set
foreach
来解决。
四、if 标签
例如:
<select
id="findByCondition"
resultType="Student">
SELECT
id,
student_no,
name,
age,
major
FROM student
WHERE 1 = 1
<if test="name != null and name != ''">
AND name = #{name}
</if>
<if test="major != null and major != ''">
AND major = #{major}
</if>
</select>
五、为什么很多人写 WHERE 1=1
因为动态条件可能:
有
也可能没有
写:
WHERE 1 = 1
后面每个条件统一:
AND ...
不容易拼错。
但 MyBatis 有更好的:
<where>
六、where 标签
推荐:
<select
id="findByCondition"
resultType="Student">
SELECT
id,
student_no,
name,
age,
major
FROM student
<where>
<if test="name != null and name != ''">
name = #{name}
</if>
<if test="major != null and major != ''">
AND major = #{major}
</if>
</where>
</select>
七、where 标签解决什么
<where> 会:
有条件时自动添加 WHERE
没有条件时不添加 WHERE
自动处理开头多余 AND / OR
所以非常适合:
动态查询
八、动态模糊查询
<if test="keyword != null and keyword != ''">
AND
(
name LIKE CONCAT(
'%',
#{keyword},
'%'
)
OR
student_no LIKE CONCAT(
'%',
#{keyword},
'%'
)
)
</if>
九、多个条件查询对象
建议:
public class StudentQuery {
private String keyword;
private String major;
private Integer minAge;
private Integer maxAge;
}
Mapper:
List<Student> findByCondition(
StudentQuery query
);
XML:
<where>
<if test="keyword != null and keyword != ''">
AND name LIKE CONCAT(
'%',
#{keyword},
'%'
)
</if>
<if test="major != null and major != ''">
AND major = #{major}
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
<if test="maxAge != null">
AND age <= #{maxAge}
</if>
</where>
十、OGNL 简单理解
MyBatis:
test="name != null and name != ''"
使用的是:
OGNL 风格表达式
可以访问:
属性
集合
Map
简单逻辑
十一、choose / when / otherwise
类似 Java:
if
else if
else
例如:
<choose>
<when test="studentNo != null and studentNo != ''">
student_no = #{studentNo}
</when>
<when test="name != null and name != ''">
name = #{name}
</when>
<otherwise>
id = -1
</otherwise>
</choose>
十二、choose 适合什么
当业务要求:
多个条件只选一个
非常适合。
例如:
优先按学号查
没学号再按姓名查
都没有则不给结果
十三、set 标签
动态修改:
<update
id="updateSelective">
UPDATE student
<set>
<if test="name != null">
name = #{name},
</if>
<if test="age != null">
age = #{age},
</if>
<if test="major != null">
major = #{major},
</if>
</set>
WHERE id = #{id}
</update>
十四、set 标签解决什么
<set> 会:
自动添加 SET
自动去掉最后多余逗号
所以比手工拼接更安全。
十五、什么是选择性更新
例如:
前端只提交 name
如果 SQL:
SET
name = ?,
age = ?,
major = ?
没有传的字段可能被更新成:
NULL
选择性更新:
只更新非空字段
十六、选择性更新的风险
如果业务真的希望:
把某字段修改成 NULL
那么:
null = 没传
和:
null = 要清空
就无法区分。
真实项目通常需要:
DTO
明确更新语义
不要机械使用 selective update。
十七、trim 标签
trim 是更通用的动态 SQL 标签。
例如:
<trim
prefix="WHERE"
prefixOverrides="AND |OR ">
<if test="name != null">
AND name = #{name}
</if>
</trim>
十八、trim 可以替代 where / set
where 和 set 本质上可以理解为:
特殊封装的 trim
十九、foreach
foreach 是 MyBatis 进阶必须掌握的标签。
常见场景:
IN 查询
批量新增
批量删除
二十、IN 查询
Mapper:
List<Student> findByIds(
@Param("ids")
List<Long> ids
);
XML:
<select
id="findByIds"
resultType="Student">
SELECT
id,
student_no,
name,
age,
major
FROM student
WHERE id IN
<foreach
collection="ids"
item="id"
open="("
separator=","
close=")"
>
#{id}
</foreach>
</select>
二十一、foreach 参数说明
collection
要遍历的集合
item
当前元素
index
当前下标或 Map key
open
开始字符
separator
元素分隔符
close
结束字符
二十二、List 参数 collection
如果使用:
@Param("ids")
List<Long> ids
则:
collection="ids"
最清晰。
二十三、批量删除
<delete
id="deleteByIds">
DELETE
FROM student
WHERE id IN
<foreach
collection="ids"
item="id"
open="("
separator=","
close=")"
>
#{id}
</foreach>
</delete>
二十四、批量新增
<insert
id="batchInsert">
INSERT INTO student
(
student_no,
name,
age,
major
)
VALUES
<foreach
collection="students"
item="student"
separator=","
>
(
#{student.studentNo},
#{student.name},
#{student.age},
#{student.major}
)
</foreach>
</insert>
二十五、foreach 空集合问题
如果:
ids = []
可能生成:
WHERE id IN ()
导致:
SQL 语法错误
所以 Service 层应该:
if (
ids == null
|| ids.isEmpty()
) {
return List.of();
}
二十六、sql 标签
重复 SQL 片段可以:
<sql
id="studentColumns">
id,
student_no,
name,
age,
major,
score
</sql>
二十七、include
使用:
SELECT
<include
refid="studentColumns"/>
FROM student
减少:
重复列清单
二十八、为什么不推荐 SELECT *
使用 <sql> 可以方便维护:
统一列清单
同时避免:
SELECT *
二十九、bind 标签
可以创建 OGNL 变量。
例如:
<bind
name="pattern"
value="'%' + keyword + '%'"/>
然后:
WHERE name LIKE #{pattern}
三十、#{ } 和 ${ } 再复习
#{}
PreparedStatement 参数
${}
直接字符串拼接
默认:
优先 #{}
三十一、动态排序为什么可能需要 ${ }
例如:
ORDER BY create_time DESC
列名不能通过:
?
参数化。
所以有时:
ORDER BY ${sortColumn}
三十二、${ } 必须白名单
不要直接:
前端传什么列名
就拼什么
应该在 Java 中:
switch (
sort
) {
case "name" ->
sortColumn =
"name";
case "score" ->
sortColumn =
"score";
default ->
sortColumn =
"id";
}
再传给:
${sortColumn}
三十三、resultMap 进阶
基础 resultMap:
<resultMap
id="studentResultMap"
type="Student">
<id
property="id"
column="id"/>
<result
property="studentNo"
column="student_no"/>
</resultMap>
进阶主要解决:
对象嵌套
一对一
一对多
三十四、一对一关系
例如:
Student
属于
ClassInfo
Student:
public class Student {
private Long id;
private String name;
private ClassInfo classInfo;
}
三十五、ClassInfo
public class ClassInfo {
private Long id;
private String className;
}
三十六、JOIN 查询一对一
SQL:
SELECT
s.id AS student_id,
s.name AS student_name,
c.id AS class_id,
c.class_name
FROM student s
LEFT JOIN class_info c
ON s.class_id = c.id;
三十七、association
<resultMap
id="studentWithClassMap"
type="Student">
<id
property="id"
column="student_id"/>
<result
property="name"
column="student_name"/>
<association
property="classInfo"
javaType="ClassInfo">
<id
property="id"
column="class_id"/>
<result
property="className"
column="class_name"/>
</association>
</resultMap>
三十八、association 是什么
用于:
一对一
多对一
对象嵌套映射。
例如:
Student
→
ClassInfo
三十九、一对多关系
例如:
一个 ClassInfo
有多个 Student
Java:
public class ClassInfo {
private Long id;
private String className;
private List<Student> students;
}
四十、collection
<resultMap
id="classWithStudentsMap"
type="ClassInfo">
<id
property="id"
column="class_id"/>
<result
property="className"
column="class_name"/>
<collection
property="students"
ofType="Student">
<id
property="id"
column="student_id"/>
<result
property="name"
column="student_name"/>
</collection>
</resultMap>
四十一、collection 是什么
用于:
一对多
例如:
ClassInfo
→
List<Student>
四十二、association 和 collection 区别
association
属性是一个对象
collection
属性是一个集合
四十三、复杂 resultMap 为什么要写 id
一对多查询会产生:
重复父表行
MyBatis 需要根据:
<id>
识别:
哪些行属于同一个父对象
所以主键映射非常重要。
四十四、JOIN 一对多示例
SQL:
SELECT
c.id AS class_id,
c.class_name,
s.id AS student_id,
s.name AS student_name
FROM class_info c
LEFT JOIN student s
ON c.id = s.class_id
WHERE c.id = #{id};
四十五、嵌套查询方式
除了 JOIN,
也可以:
先查班级
再根据 class_id 查学生
例如:
<collection
property="students"
column="id"
select="findStudentsByClassId"/>
四十六、嵌套 select
父查询:
<select
id="findClassById"
resultMap="classNestedMap">
SELECT
id,
class_name
FROM class_info
WHERE id = #{id}
</select>
resultMap:
<resultMap
id="classNestedMap"
type="ClassInfo">
<id
property="id"
column="id"/>
<result
property="className"
column="class_name"/>
<collection
property="students"
column="id"
select="findStudentsByClassId"/>
</resultMap>
四十七、N+1 查询问题
如果先查:
100 个班级
再为每个班级查询学生:
1 次主查询
+
100 次子查询
总共:
101 次 SQL
这叫:
N+1 问题
四十八、什么时候 JOIN 更合适
如果数据规模可控:
一次 JOIN
常能减少:
SQL 次数
四十九、什么时候嵌套查询有意义
例如:
需要延迟加载
结构更清晰
子查询可以单独复用
但一定要关注:
N+1
五十、延迟加载
MyBatis 支持:
Lazy Loading
简单理解:
关联对象真正被访问时,才执行对应查询。
五十一、开启延迟加载
配置示例:
<settings>
<setting
name="lazyLoadingEnabled"
value="true"/>
</settings>
五十二、延迟加载不是万能优化
优点:
不用的数据不查
风险:
无意间触发大量 SQL
N+1 更隐蔽
所以真实项目:
需要明确控制
五十三、resultMap 继承
可以:
<resultMap
id="studentDetailMap"
type="Student"
extends="studentBaseMap">
减少:
重复映射
五十四、discriminator 简单了解
discriminator 类似:
根据某个字段
选择不同映射
例如:
不同类型用户
映射到不同对象结构
使用频率没有:
association
collection
高,了解即可。
五十五、一级缓存
MyBatis 默认开启:
一级缓存
作用域:
SqlSession
五十六、一级缓存例子
Student a =
mapper.findById(
1L
);
Student b =
mapper.findById(
1L
);
同一个 SqlSession:
第二次可能不再执行 SQL
五十七、一级缓存什么时候失效
常见:
SqlSession 关闭
执行 insert/update/delete
clearCache()
查询条件变化
五十八、为什么修改会清缓存
因为:
原来的查询结果可能已经过期
为了避免:
脏缓存
需要失效。
五十九、localCacheScope
可配置:
SESSION
STATEMENT
默认常见:
SESSION
如果配置:
STATEMENT
一级缓存范围更小。
六十、二级缓存
二级缓存作用域:
Mapper namespace
可以跨:
多个 SqlSession
六十一、开启二级缓存
Mapper XML:
<cache/>
然后相关对象通常需要:
可序列化
具体依缓存实现而定。
六十二、二级缓存为什么真实项目要谨慎
问题:
跨事务数据一致性
多节点应用缓存不共享
复杂联表缓存难管理
外部更新数据库时无法感知
因此很多真实项目更倾向:
Redis
Caffeine
等明确缓存方案。
六十三、不要为了“快”就打开所有缓存
缓存最大难题不是:
怎么存
而是:
什么时候失效
六十四、flushCache
查询和更新语句可以配置缓存行为。
例如:
update
默认会刷新相关缓存
六十五、useCache
select 可以控制:
是否使用二级缓存
例如:
<select
id="findById"
useCache="false">
六十六、批量操作有两种思路
常见:
1. 一条 SQL 批量插入
2. ExecutorType.BATCH
六十七、foreach 批量插入
INSERT INTO student (...)
VALUES
(...),
(...),
(...);
优点:
SQL 次数少
实现简单
六十八、Batch Executor
SqlSession sqlSession =
factory.openSession(
ExecutorType.BATCH
);
然后:
for (
Student student :
students
) {
mapper.insert(
student
);
}
最后:
sqlSession.flushStatements();
sqlSession.commit();
六十九、Batch Executor 的特点
它会:
积累一批 JDBC 操作
再统一发送
适合:
大量相同类型写操作
七十、批处理不能无限大
如果一次:
几十万条
全部堆内存:
可能占用过大
常见做法:
分批
例如:
每 500
每 1000
提交一次。
七十一、批处理和事务
批处理通常仍要:
明确 commit / rollback
发生异常:
回滚
七十二、ExecutorType
常见:
SIMPLE
REUSE
BATCH
七十三、SIMPLE
每次执行:
创建新的 Statement
默认常见。
七十四、REUSE
尝试:
复用 Statement
七十五、BATCH
用于:
批处理
七十六、TypeHandler 是什么
TypeHandler:
负责 Java 类型和 JDBC 类型之间的转换。
例如:
Java LocalDateTime
↔
数据库 DATETIME
MyBatis 已经内置很多 TypeHandler。
七十七、自定义 TypeHandler 什么时候用
例如数据库保存:
0
1
Java 希望使用:
enum Gender {
MALE,
FEMALE
}
可以自定义:
TypeHandler
完成转换。
七十八、自定义枚举示例
public enum Gender {
MALE(1),
FEMALE(2);
private final int code;
Gender(
int code
) {
this.code = code;
}
public int getCode() {
return code;
}
}
七十九、自定义 TypeHandler 骨架
public class GenderTypeHandler
extends BaseTypeHandler<Gender> {
@Override
public void setNonNullParameter(
PreparedStatement ps,
int i,
Gender parameter,
JdbcType jdbcType
)
throws SQLException {
ps.setInt(
i,
parameter.getCode()
);
}
@Override
public Gender getNullableResult(
ResultSet rs,
String columnName
)
throws SQLException {
int code =
rs.getInt(
columnName
);
return fromCode(
code
);
}
// 其他方法略
}
八十、TypeHandler 的价值
把转换逻辑集中:
一处处理
避免每个 Mapper / Service 都:
手工转换
八十一、插件 Plugin 是什么
MyBatis 允许插件拦截核心组件的方法。
常见拦截目标:
Executor
StatementHandler
ParameterHandler
ResultSetHandler
八十二、插件能做什么
例如:
分页
SQL 日志
性能统计
多租户
数据权限
SQL 重写
八十三、插件机制本质
可以理解为:
代理 + 拦截器
这和前面学习的:
动态代理
AOP 思想
有联系。
八十四、Interceptor
自定义插件通常实现:
Interceptor
八十五、@Intercepts
用于声明:
拦截哪个类的哪个方法
例如:
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {
Connection.class,
Integer.class
}
)
})
八十六、插件使用要谨慎
插件属于:
全局底层扩展
写错可能:
影响所有 SQL
造成性能下降
破坏 SQL
引入线程安全问题
所以不要为了学习:
随便写复杂插件
先理解原理。
八十七、MyBatis Mapper 代理执行流程
调用:
studentMapper.findById(
1L
);
大致流程:
MapperProxy
↓
MapperMethod
↓
SqlSession
↓
Executor
↓
MappedStatement
↓
StatementHandler
↓
ParameterHandler
↓
JDBC
↓
ResultSetHandler
↓
返回对象
八十八、MapperProxy
Mapper 接口:
没有实现类
MyBatis 通过:
JDK 动态代理
生成:
MapperProxy
八十九、MapperMethod
负责分析:
Mapper 方法签名
参数
返回值
SQL command 类型
然后调用:
SqlSession
九十、MappedStatement
MyBatis 启动时解析:
<select>
<insert>
<update>
<delete>
会生成:
MappedStatement
其中保存:
SQL 配置
参数映射
结果映射
缓存配置
Statement 类型
九十一、BoundSql
动态 SQL 最终执行前会变成:
BoundSql
它包含:
最终 SQL 字符串
参数映射信息
附加参数
九十二、Executor
Executor 负责:
执行数据库操作
一级缓存
事务相关协调
常见:
SimpleExecutor
ReuseExecutor
BatchExecutor
九十三、StatementHandler
负责:
创建 JDBC Statement
准备 SQL
执行 Statement
九十四、ParameterHandler
负责:
把 Java 参数
设置到 PreparedStatement
例如:
#{name}
最终会在这里:
设置 JDBC 参数
九十五、ResultSetHandler
负责:
处理 ResultSet
并按照:
resultType
resultMap
映射成:
Java 对象
九十六、TypeHandler 在哪里参与
参数设置:
Java
→ JDBC
结果读取:
JDBC
→ Java
TypeHandler 都会参与。
九十七、MyBatis 完整执行链
Mapper 方法
↓
MapperProxy
↓
MapperMethod
↓
SqlSession
↓
Executor
↓
MappedStatement
↓
BoundSql
↓
StatementHandler
↓
ParameterHandler
↓
PreparedStatement
↓
Database
↓
ResultSet
↓
ResultSetHandler
↓
TypeHandler
↓
Java Object
九十八、为什么理解执行链很重要
遇到问题时能判断:
Mapper 找不到
是映射问题
SQL 错
是 BoundSql / Mapper XML 问题
参数错
是 ParameterHandler / TypeHandler 问题
结果错
是 ResultSetHandler / resultMap 问题
九十九、Mapper XML namespace 再强调
<mapper
namespace="com.example.mapper.StudentMapper">
必须对应:
Mapper 接口全限定名
一百、id 再强调
<select
id="findById">
对应:
findById(...)
最终标识类似:
com.example.mapper.StudentMapper.findById
一百零一、SQL 片段复用不要过度
<sql> 很方便,
但如果把 SQL 拆得太碎:
看一个查询要跳十几个片段
反而:
可读性下降
一百零二、动态 SQL 不要太复杂
如果 Mapper XML 出现:
几十层 if
大量 choose
大量嵌套 foreach
说明:
查询设计可能过度复杂
可以考虑:
拆查询
重新设计 DTO
使用查询构造器
数据库视图
一百零三、分页不要用 RowBounds 做大数据物理分页
MyBatis 有:
RowBounds
但不要简单理解为:
数据库 LIMIT
很多情况下它并不等同于真正数据库分页。
大数据分页更推荐:
SQL LIMIT
或使用成熟分页插件。
一百零四、Cursor 简单了解
MyBatis 支持:
Cursor
用于:
流式处理大量查询结果
避免一次把所有结果:
全部加载进内存
一百零五、Cursor 使用场景
例如:
导出百万行
批量数据处理
但使用时要注意:
连接生命周期
事务
资源关闭
一百零六、ResultHandler
MyBatis 还可以通过:
ResultHandler
逐行处理结果。
适合:
特殊大数据处理
普通 CRUD:
不需要复杂化
一百零七、注解 Mapper
除了 XML,
MyBatis 也支持:
@Select
@Insert
@Update
@Delete
例如:
@Select(
"""
SELECT
id,
name
FROM student
WHERE id = #{id}
"""
)
Student findById(
Long id
);
一百零八、XML 和注解怎么选
简单 SQL:
注解
可以很方便
复杂 SQL:
XML
通常更清晰
尤其:
动态 SQL
复杂 resultMap
多表映射
更适合 XML。
一百零九、不要同一个方法同时定义两份 SQL
例如:
XML 有 findById
接口又 @Select findById
可能造成:
映射冲突
保持:
一个方法一个明确 SQL 来源
一百一十、selectKey 简单了解
如果数据库生成主键方式不是:
JDBC generated keys
可以用:
<selectKey>
在插入前或插入后获取主键。
一百一十一、MySQL 自增主键通常优先
useGeneratedKeys="true"
keyProperty="id"
简单直接。
一百一十二、MyBatis 与事务
原生 MyBatis:
SqlSession sqlSession =
factory.openSession();
增删改后:
sqlSession.commit();
异常:
sqlSession.rollback();
一百一十三、事务边界仍然适合 Service
因为业务可能:
Mapper A 操作
Mapper B 操作
Mapper C 操作
必须:
一个事务
所以要保证:
同一个 SqlSession / Connection
一百一十四、不要每个 Mapper 方法自己开 SqlSession
否则:
一个 Service 中多个 Mapper 调用
可能变成:
多个事务
失去原子性。
一百一十五、Spring 集成后会发生什么
Spring + MyBatis 后:
SqlSessionFactory
由 Spring 管理
Mapper
由 Spring 扫描
SqlSession
由框架管理
事务
由 Spring 管理
开发者通常不用:
手工 openSession
commit
rollback
一百一十六、为什么仍然要学原生 MyBatis
因为 SpringBoot 中:
@Autowired Mapper
看起来很简单。
但底层仍然:
MapperProxy
SqlSession
Executor
MappedStatement
JDBC
一百一十七、常见错误:动态 SQL 多余 AND
错误:
WHERE
<if test="name != null">
AND name = #{name}
</if>
如果条件为空:
可能只剩 WHERE
推荐:
<where>
一百一十八、常见错误:动态 update 多余逗号
错误:
SET
name = ?,
age = ?,
WHERE id = ?
推荐:
<set>
一百一十九、常见错误:foreach collection 名错误
Mapper:
findByIds(
@Param("ids")
List<Long> ids
)
XML:
collection="list"
会找不到。
应该:
collection="ids"
一百二十、常见错误:IN 空集合
ids = []
生成:
IN ()
所以:
Service 提前处理
一百二十一、常见错误:association property 写错
Java:
private ClassInfo classInfo;
XML:
property="clazz"
自然映射失败。
一百二十二、常见错误:collection 没写 id
复杂 JOIN:
父对象重复
子对象重复
优先检查:
父级 <id>
子级 <id>
是否正确。
一百二十三、常见错误:一对多结果数量异常
SQL JOIN 本身会产生:
父表重复行
要判断:
是 SQL 结果重复
还是 MyBatis 映射错误
不要一看到重复:
就乱加 DISTINCT
一百二十四、常见错误:N+1
发现日志:
查一次列表
然后连续执行几十次相同结构 SQL
很可能:
嵌套 select
+
N+1
一百二十五、常见错误:二级缓存拿到旧数据
缓存和数据库更新路径不统一:
MyBatis 外部直接改数据库
缓存无法自动知道。
所以:
二级缓存要谨慎
一百二十六、常见错误:Batch 忘记 flush / commit
使用 Batch Executor:
数据可能还在批处理中
需要:
flushStatements();
commit();
一百二十七、常见错误:SqlSession 被多个线程共享
SqlSession:
不要作为 Servlet static 成员
不要作为全局单例
否则:
事务
缓存
连接
都会混乱。
一百二十八、常见错误:Mapper 跨 SqlSession 保存
Mapper 代理依赖:
当前 SqlSession
不要:
openSession 后拿 Mapper
关闭 Session
然后长期保存 Mapper
一百二十九、常见错误:直接 ${keyword}
WHERE name = '${keyword}'
存在:
SQL 注入
应该:
WHERE name = #{keyword}
一百三十、常见错误:动态 ORDER BY 不做白名单
即使确实需要:
${sortColumn}
也必须:
后端白名单
一百三十一、常见错误:缓存掩盖 SQL 问题
调试时如果发现:
第二次查询不打印 SQL
可能:
一级缓存命中
可以:
换 SqlSession
clearCache()
验证。
一百三十二、常见错误:resultMap 字段命名冲突
JOIN:
SELECT
s.id,
c.id
两个列都叫:
id
映射容易混乱。
推荐:
s.id AS student_id,
c.id AS class_id
一百三十三、复杂 JOIN 一定要起别名
例如:
student_id
student_name
class_id
class_name
然后 resultMap:
明确映射
一百三十四、性能优化 1:只查询需要字段
不要:
SELECT *
复杂映射尤其要:
字段明确
一百三十五、性能优化 2:减少 N+1
可以考虑:
JOIN
批量 IN
分两次批量查询后 Java 组装
一百三十六、性能优化 3:分页交给数据库
不要:
查询全部
Java 截取
推荐:
LIMIT
一百三十七、性能优化 4:批量写入
大量 insert:
不要一条一条独立提交
可以:
多 VALUES
Batch Executor
合理分批事务
一百三十八、性能优化 5:SQL 先在数据库验证
MyBatis SQL 慢:
先把最终 SQL 和参数
复制到 MySQL
然后:
EXPLAIN
不要先怀疑:
MyBatis 本身慢
一百三十九、性能优化 6:避免过大事务
MyBatis 一个 SqlSession:
持续太久
可能:
持锁太久
一级缓存太大
连接占用太久
一百四十、练习题 1:动态查询
完成:
keyword
major
minAge
maxAge
任意组合查询。
要求:
使用 <where> + <if>
一百四十一、练习题 2:动态更新
实现:
只更新非空字段
使用:
<set>
一百四十二、练习题 3:IN 查询
Mapper:
findByIds(
List<Long> ids
)
使用:
foreach
一百四十三、练习题 4:批量新增
一次插入:
5 个 Student
使用:
foreach
生成多 VALUES。
一百四十四、练习题 5:sql/include
抽取:
学生公共列
所有查询复用。
一百四十五、练习题 6:一对一
建立:
student
class_info
Student 中:
ClassInfo classInfo
使用:
association
映射。
一百四十六、练习题 7:一对多
ClassInfo 中:
List<Student>
使用:
collection
映射。
一百四十七、练习题 8:N+1
分别实现:
嵌套 select
JOIN
观察 SQL 日志次数。
一百四十八、练习题 9:一级缓存
同一个 SqlSession:
连续 findById(1)
观察 SQL 日志。
再:
执行 update
重新查询,观察变化。
一百四十九、练习题 10:二级缓存
给 Mapper:
<cache/>
创建两个 SqlSession:
测试跨 Session 缓存
理解:
提交
关闭
缓存生效
等行为。
一百五十、练习题 11:Batch Executor
批量插入:
1000 条 Student
每:
200 条
flush / commit。
一百五十一、练习题 12:TypeHandler
数据库:
gender INT
Java:
Gender enum
自定义 TypeHandler。
一百五十二、练习题 13:执行流程
自己画出:
Mapper
↓
MapperProxy
↓
SqlSession
↓
Executor
↓
StatementHandler
↓
JDBC
↓
ResultSetHandler
一百五十三、必须掌握动态 SQL
if
where
set
trim
choose
when
otherwise
foreach
bind
sql
include
一百五十四、必须掌握复杂映射
resultMap
id
result
association
collection
一百五十五、必须掌握缓存
一级缓存
SqlSession 级别
二级缓存
Mapper namespace 级别
一百五十六、必须掌握执行组件
MapperProxy
MappedStatement
BoundSql
Executor
StatementHandler
ParameterHandler
ResultSetHandler
TypeHandler
一百五十七、必须回答的问题
学完后应该能够回答:
1. 什么是动态 SQL?
2. if 和 where 分别解决什么?
3. set 为什么适合动态更新?
4. choose 和 Java 的什么结构类似?
5. foreach 可以解决哪些场景?
6. foreach 的 collection/item/open/separator/close 是什么?
7. sql/include 有什么用?
8. #{} 和 ${} 有什么区别?
9. 动态 ORDER BY 为什么可能需要 ${}?
10. 为什么 ${} 必须白名单?
11. association 和 collection 有什么区别?
12. 一对多 resultMap 为什么要正确配置 id?
13. 什么是 N+1 查询?
14. JOIN 和嵌套 select 各有什么优缺点?
15. 什么是一级缓存?
16. 一级缓存什么时候失效?
17. 什么是二级缓存?
18. 为什么真实项目要谨慎使用 MyBatis 二级缓存?
19. ExecutorType 有哪些?
20. Batch Executor 有什么作用?
21. TypeHandler 是什么?
22. MyBatis Plugin 可以拦截哪些核心对象?
23. Mapper 接口为什么没有实现类也能执行?
24. MappedStatement 是什么?
25. BoundSql 是什么?
26. Executor 是什么?
27. StatementHandler 是什么?
28. ParameterHandler 是什么?
29. ResultSetHandler 是什么?
30. SqlSession 为什么不能全局多线程共享?
一百五十八、MyBatis 执行流程总图
Mapper Interface
↓
MapperProxy
↓
MapperMethod
↓
SqlSession
↓
Executor
↓
MappedStatement
↓
Dynamic SQL
↓
BoundSql
↓
StatementHandler
↓
ParameterHandler
↓
TypeHandler
↓
PreparedStatement
↓
MySQL
↓
ResultSet
↓
ResultSetHandler
↓
TypeHandler
↓
resultMap
↓
Java Object
一百五十九、从 MyBatis 进阶到 SpringBoot
学到这里已经能看到:
MyBatis 本身负责数据库映射
但传统项目仍然要自己:
创建 SqlSession
管理事务
创建 Service
创建 Servlet
读取 request
输出 response
下一阶段 SpringBoot / Spring MVC 会逐渐帮我们:
管理对象
自动绑定请求参数
管理 Mapper
管理事务
统一处理 HTTP 请求响应
一百六十、本章总结
MyBatis 进阶最核心的四块:
动态 SQL
复杂映射
缓存与批处理
执行原理
动态 SQL:
if
where
set
choose
foreach
复杂映射:
association
一对一 / 多对一
collection
一对多
性能重点:
避免 N+1
批量写入
数据库分页
只查需要字段
合理使用缓存
底层执行链:
MapperProxy
↓
SqlSession
↓
Executor
↓
MappedStatement
↓
BoundSql
↓
StatementHandler
↓
ParameterHandler
↓
JDBC
↓
ResultSetHandler
一定牢记:
SqlSessionFactory
适合应用级复用
SqlSession
适合事务/业务级使用
Mapper
依赖 SqlSession
不要跨线程长期共享 SqlSession
到这里:
MyBatis 基础
+
MyBatis 进阶
已经形成一套完整知识体系。
按照课程表,下一篇正式进入:
SpringBoot 请求响应
会开始学习:
SpringBoot Web 项目
Controller
@RequestMapping
@GetMapping
@PostMapping
请求参数绑定
JSON 请求
JSON 响应
统一响应结果