简介
什么是 MyBatis?
- MyBatis 是一款优秀的持久层框架
- 它支持定制化 SQL、存储过程以及高级映射。
- MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集(ResultSet)。
- MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中 的记录。
如何获得 MyBatis?
- github 下载地址:https://github.com/mybatis/mybatis-3/releases
- 中文文档:https://github.com/tuguangquan/mybatis
- Maven 仓库:
1 | <!‐‐ https://mvnrepository.com/artifact/org.mybatis/mybatis ‐‐> |
持久化
数据持久化
持久化就是将程序的数据在持久状态和瞬时状态转化的过程
内存特性:断电即失
因此有了:数据库(JDBC),io 文件持久化 进行存储生活:冷藏、罐头。
为什么需要持久化? 因为有一些对象,不能让他丢掉。
持久层
层次分为:Dao 层、Service 层、Controller 层…..
因此 持久层 = 完成持久化工作的代码块
为什么需要 Mybatis?
- 帮助程序员将数据存入到数据库中。 方便
- 传统的 JDBC 代码太复杂了。简化、框架、自动化。
- 不用 Mybatis 也可以。更容易上手。但是是技术没有高低之分
- 优点:
- 简单易学
- 灵活
- sql 和代码的分离,提高了可维护性。
- 提供映射标签,支持对象与数据库的orm 字段关系映射
- 提供对象关系映射标签,支持对象关系组建维护
- 提供 xml 标签,支持编写动态 sql。
- 最重要的一点:使用的人多!
- 和 Spring SpringMVC SpringBoot 一起搭配使用
第一个 Mybatis 程序
首先需要注意 ,如果用的是 IDEA,把配置文件写在 resource 包下的话,需要引用以下代码到 pom.xml 以解决资源路径问题:
1 | <build> |
思路:搭建环境‐‐>导入 Mybatis‐‐>编写代码‐‐>测试!
目录结构:
搭建数据库:
1 | CREATE TABLE tbl_employee( |
新建项目:
依赖:
1 | <dependencies> |
创建实体类:
1 | package com.hpg.mybatis.pojo; |
编写全局配置文件:
1 |
|
编写映射文件(Mapper 文件)
1 |
|
编写测试类:
1 | import com.hpg.mybatis.pojo.Employee; |
对于 selectOne 函数:
第一个参数:sql 唯一标识符;
第二个参数:执行 sql 要用的参数
打印结果:
总的来说可以分成这几步:
- 根据 xml 配置文件(全局配置文件)创建一个 SqlSessionFactory 对象 有数据源一些运行环境信息
- sql 映射文件;配置了每一个 sql,以及 sql 的封装规则等。
- 将 sql 映射文件注册在全局配置文件中
- 写代码
- 根据全局配置文件得到 SqlSessionFactory;
- 使用 sqlSession 工厂,获取到 sqlSession 对象使用他来执行增删改查,一个 sqlSession 就是代表和数据库的一次会话,用完关闭
- 使用 sql 的唯一标志来告诉 MyBatis 执行哪个 sql。sql 都是保存在 sql 映射文件中的。
接口式编程
目录结构:
编写接口:
1 | package com.hpg.mybatis.dao; |
将 EmployeeMapper 中的命名空间替换掉:
1 | <!--原来--> |
1 |
|
1 | public SqlSessionFactory getSqlSessionFactory() throws IOException { |
打印结果:
明明我们没写实现类,那究竟结果是怎么实现的呢?
是由 Mybatis 为接口自动创建的一个代理对象,sqlSession.getMapper(xxxx.class)
由这个对象去实现 CRUD 的,当然了,使用之前需要将接口和 xml 进行绑定操作
接口式编程
- 原生:Dao → DaoImpl、
- Mybatis:Mapper → XXXMapper.xml
SqlSession 代表和数据库的一次会话,因此用完必须关闭
SqlSession 和 connection 一样是非线程安全的,因此不能够设置 private …每次使用的时候应该去主动获取
有两个重要的配置文件
- mybatis 全局配置文件(可以不专门写一个出来,可以 new 出来):包含了数据库连接池信息,事务管理器信息,系统运行环境信息等
- sql 映射文件:保存了每一个 sql 语句的映射信息;这个文件将 sql 抽取了出来
全局配置文件
configuration 下有以下几个标签:
properties 标签
resource:resource 属性是按照类路径的写法来写的,因此必须存在于类路径下
url:URL: Uniform Resource Locator 统一资源定位符
比如这个就是一个 URL
http://localhost:8080/ABC/DEF
其中
协议是:http 主机是:localhost 端口是 8080
其他的都是 URI(在这里就是/ABC/DEF)
编写一个配置文件 properties
1 | com.mysql.jdbc.Driver = |
修改一下全局配置文件:
1 |
|
测试
1 | public void InterfaceTest() throws IOException { |
这一部分之后与 Spring 整合的时候会进一步优化,由 Spring 进行托管
Settings 标签
以最后一个为例:数据库一个属性叫 A_xxx,但你 sql 语句写了一个 select xxx,如果不设置驼峰参数的话,就读不了,设置了后就能自动进行映射,用于读取属性;
TypeAliases 标签
- 别名处理器,能为 java 类型起别名
1 | <typeAliases> |
但一般还是写全路径名,方便后期排查;0
typeHandlers 标签
类型处理器:使 java 对象的属性映射成数据库对应的变量属性(比如:java String → 数据库 varchar)
自定义类型处理器:
plugins 标签
MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed):执行器,作用是在 sql 执行增删改之前改变一些 mybatis 行为,达到自定义效果
- ParameterHandler (getParameterObject, setParameters):参数处理器,预编译的时候设置以及获取参数
- ResultSetHandler (handleResultSets, handleOutputParameters):结果集处理器,作用就是把得到的结果形成一个结果集并且封装成javabean 对象
- StatementHandler (prepare, parameterize, batch, update, query):SQL 语句处理器
environments 标签
一个 environment 标签内需要有 transactionManager 和 dataSource 标签
环境标签的 id:代表当前环境的唯一标识
比如一个环境用来测试,而另一个环境专门用来开发;
事务管理器 transactionManager
type:表示事务管理器的事务类型(JDBC/MANAGE)
JDBC(JdbcTransactionFactory) <!--code16-->
自定义事务管理器:实现 TransactionFactory 接口.type 指定为全类名
数据源 dataSource
type:数据源类型
UNPOOLED(UnpooledDataSourceFactory) <!--code17-->
JNDI(JndiDataSourceFactory) <!--code18-->
例子:
在全局配置文件中加入
1 | <databaseIdProvider type="DB_VENDOR"> |
对应的 sql 标签中添加属性 databaseId:
1 | <update id="updateByMap" databaseId="mysql"> |
这样就可以做到 从不同数据库中查了;
mappers 标签
每一个 mappers 标签的作用:将 sql 映射注册到了全局配置中
resource:引用类路径下的 sql 映射文件
url:引用网络路径 或者 磁盘路径下的 sql 映射文件(file://xxxxx/xxx/xx.xml)
class:引用接口
如果要使用这个进行映射的话,要满足
若有 sql 映射文件,则要和接口同名,且同一目录(包)下
![image-20210306164452142](
https://er11.oss-cn-shenzhen.aliyuncs.com/img/image-20210306164452142.png)
1
2<mapper resource = "xxx/xxx/abc.xml"/>
<mapper class = "xxx.xxx.abc"/>没有 sql 映射文件,根据注解进行配置
在全局配置文件中配置:1
2
3
4
5
6
7
8
9
10
11package com.hpg.mybatis.dao;
import com.hpg.mybatis.pojo.Employee;
import org.apache.ibatis.annotations.Select;
public interface EmployeeMapperAnnotation {
public Employee getEmpById(Integer id);
}编写测试类:1
<mapper class="com.hpg.mybatis.dao.EmployeeMapperAnnotation"/>
结果: ![image-20210306164353447](1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void AnnotationTest() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//获取sqlSession实例 能够执行已经映射的sql语句
SqlSession openSession = sqlSessionFactory.openSession();
try{
EmployeeMapperAnnotation mapper = openSession.getMapper(EmployeeMapperAnnotation.class);
Employee empById = mapper.getEmpById(1);
System.out.println(empById);
}finally{
openSession.close();
}
}https://er11.oss-cn-shenzhen.aliyuncs.com/img/image-20210306164353447.png)
Mybatis 映射文件
CRUD
目录结构
别忘了需要提交openSession.commit()
当然了,如果设置了sqlSessionFactory.openSession(true)
就可以进行自动提交啦
insert
1 | <!--添加方法--> |
1 |
|
测试结果:
- 获取自增主键的值:
mysql 支持自增主键,自增主键值的获取,mybatis 也是利用 statement.getGenreatedKeys();
useGeneratedKeys=”true”;使用自增主键获取主键值策略
keyProperty;指定对应的主键属性,也就是 mybatis 获取到主键值以后,将这个值封装给 javaBean 的哪个属性
1 | <insert id="addEmp" parameterType="com.atguigu.mybatis.bean.Employee" |
update
把一号员工也改成二号员工数据:
1 | <!-- public void updateEmp(Employee employee); --> |
1 | mapper.updateEmp(new Employee(1, "jerry", "123@qq.com", "1")); |
测试结果:
delete
把二号员工删掉
1 | <!-- public void deleteEmpById(Integer id); --> |
1 | mapper.deleteEmpById(2); |
select
Select 获取 List
1 | <!-- public List<Employee> getEmpsByLastNameLike(String lastName); --> |
1 |
|
(为啥 lastName 是 null 呢?好像是因为 Mybatis 找不到的原因 这个问题可以在下一节解决)
Select 获取 Map
1 | //多条记录封装一个map:Map<String,Employee>:键是这条记录的主键,值是记录封装后的javaBean |
1 | <!--public Map<Integer, Employee> getEmpByLastNameLikeReturnMap(String lastName); --> |
1 |
|
自定义 resultMap(重点)
还记得不记得上面有个问题,取到的值为 null 吗?这个问题可以用ResultMap进行解决
- 自定义某个 javaBean 的封装规则:
- type:自定义规则的 Java 类型
- id:取的名字,方便引用
- 指定主键类的封装规则
- column:指定哪一列
- property:指定对应的 javaBean 属性
要使用的时候,只需要把原来的resultType
替换成resultMap
并且 id 填上我们自己取得 ResultMap 的 id 即可;
1 | <!--自定义某个javaBean的封装规则 |
1 |
|
联表查询
现在员工有一个属性叫部门 Department,现在希望查询员工的时候把这个信息也显示出来
(其实就是结合 Mysql 的外键约束进行联表查询)
在 Employee 类中添加
1 | private Department dept; |
设置一个 Department 类
1 | package com.hpg.pojo; |
进行联合查询
1 | <!-- |
测试:
1 |
|
association 标签方法进行联合查询
1 | <!-- public Employee getEmpAndDept(Integer id);--> |
还是之前的测试代码,结果:
association 实现分步查询
- association 定义关联对象的封装语法
- property:关联的对象,填入属性名(id)
- select:表明使用哪个方法,填入对应的方法
- column:指定将数据库中哪一列的值传入这个方法
运行的流程:使用select指定的方法 传入 column 指定的这列参数的值,查出来对象,并封装给 property 指定的属性
测试:
创建一个 department 接口 带有方法:
1 | package com.hpg.dao; |
配置好 Department 的 mapper 文件
1 |
|
在 EmployeeMapperPlus.xml 中进行分步查询:
1 | <!-- 使用association进行分步查询: |
测试:
1 |
|
懒加载/延迟查询
懒加载针对级联使用的,懒加载的目的是减少内存的浪费和减轻系统负担。
懒加载是一种按需加载,也就是只用调用到关联的数据的时候,才会与数据库进行交互(执行相应 sql)
以上述代码为例,我们知道 Employee 中有一个 Department 属性;
但我有时候不需要特定的部门的员工,只需要干净的员工信息即可,这就可以使用懒加载来只加载 Employee 表的信息,而不加载 Department 表的信息了
使用方法:
在全局配置文件中:
这是用于打印 sql 的设置,添加在全局配置中
1 | <setting name="logImpl" value="STDOUT_LOGGING" /> |
同时需要引入依赖:
1 | <dependency> |
开启方式:
1 | <settings> |
测试:
1 |
|
在关闭配置的时候,我们只需要进行查询员工的 lastName,但依旧执行了多余的 sql:
开启之后:
collection 标签实现关联集合查询
首先需要明白 collection 和 association 标签区别:
他们都能进行多表的查询,但是 association 是【关联】,collection 是【集合】
什么意思呢?
- association 用于一对一和多对一 一个人对应一个部门,但部门可以有多人,希望查一个人顺带部门的时候,就用association
- collection 是用于一对多,一个部门可容纳多人,希望查一个部门的时候,顺带把部门中的人查出来就用 collection
修改一下数据库:
DepartmentMapper 接口中新增方法
1 | public Department getDeptByIdPlus(Integer id); |
1 | <!-- |
测试
1 |
|
结果
collection 实现分步查询
1 | public Department getDeptByIdStep(Integer id); |
1 | public Employee getEmpByIdStep(Integer id); |
EmployeeMapperPlus.xml
首先查询出来特定部门的员工数据
1 | <!-- public List<Employee> getEmpsByDeptId(Integer deptId); --> |
1 | <!-- collection:分段查询 --> |
1 |
|
这里的 Employee 查询查询部分 没有配置 resultMap 因此 lastName 是 null;
collection 标签 extra
多列的值传递过去:
将多列的值封装 map 传递;使用:column=”{key1=column1,key2=column2}”比如在上一个例子中:
1
2
3<collection property="emps"
select="com.hpg.dao.EmployeeMapperPlus.getEmpsByDeptId"
column="id"></collection>处的 id 也可以写成:
{deptId = id}
其中 deptId 的由来是 EmployeeMaaperPlus 中的 sql 语句:1
2
3
4<!-- public List<Employee> getEmpsByDeptId(Integer deptId); -->
<select id="getEmpsByDeptId" resultType="com.hpg.pojo.Employee">
select * from tbl_employee where d_id=#{deptId}
</select>延迟加载
fetchType=”lazy”:表示使用延迟加载;
- lazy:延迟
- er:立即
discriminator 鉴别器
Mybatis可以使用discriminator判断某一列的值,然后根据某一列的值改变封装行为
现在模拟一个场景:
- 如果查出的是女生:就把部门信息查询出来,否则不查询;
- 如果是男生,把 last_name 这一列的值赋值给 email;
修改数据库:gender 0 代表女生, 1 代表男生
1 | <discriminator javaType="" column=""></discriminator> |
javaType 为鉴别的列值对应的属性 , column 为列名,case 代表不同的情况
1 | <resultMap type="com.hpg.pojo.Employee" id="MyEmpDis"> |
1 |
|
测试:
男生情况(gender=1):email=lastName,且不显示部门信息
女生情况(gender=0):email 不变,显示部门信息
Mybatis 参数处理
- 单个参数:mybatis 不会做特殊处理,参数名可以随便写(当作占位符)
#{参数名/任意名}:取出参数值。 - 多个参数:mybatis 会做特殊处理,多个参数会被封装成 一个map
key:param1…paramN,或者参数的索引也可以
value:表示传入的参数值
#{}就是从 map 中获取指定的 key 的值;
例子:
假如接口有一个方法:
1 | public Employee getEmpByIdAndLastName(Integer id,String lastName); |
并编写对应的 sql 映射:
1 | <!-- public Employee getEmpByIdAndLastName(Integer id,String lastName);--> |
测试
1 | Employee emp = mapper.getEmpByIdAndLastName(1, "jerry"); |
执行:
1 | 异常: |
修改:
1 | <!-- public Employee getEmpByIdAndLastName(Integer id,String lastName);--> |
结果:
然而如果参数过多的话,占位符都写 param1,param2 会显得很臃肿,因此有了*【命名参数】 *
【命名参数】
形如:
1
public Employee getEmpByIdAndLastName( Integer id, String lastName);
作用:明确指定封装参数时 map 的 key;
形式:@Param(“id”)
多个参数会被封装成 一个 map,
key:使用@Param 注解指定的值
value:参数值
#{指定的 key}取出对应的参数值如果用这种形式的话,我们的 xml 文件就可以写成:
1
2
3
4<!-- public Employee getEmpByIdAndLastName(Integer id,String lastName);-->
<select id="getEmpByIdAndLastName" resultType="com.hpg.pojo.Employee">
select * from tbl_employee where id = #{id} and last_name=#{lastName}
</select>
再测试:
POJO:
如果多个参数正好是我们业务逻辑的数据模型,我们就可以直接传入 pojo;
#{属性名}:取出传入的 pojo 的属性值Map:
如果多个参数不是业务模型中的数据,没有对应的 pojo,不经常使用,为了方便,我们也可以传入 map
#{key}:取出 map 中对应的值1
2
3
4<!--public Employee getEmpByMap(Map<String, Object> map);-->
<select id="getEmpByMap" resultType="com.hpg.pojo.Employee">
select * from tbl_employee where id=#{id} and last_name=#{lastName}
</select>1
2
3
4
5
6Map<String, Object> map = new HashMap<>();
map.put("id", 1);
map.put("lastName", "jerry");
Employee emp = mapper.getEmpByMap(map);
sqlSession.commit();
System.out.println(emp);
参数处理总结
public Employee getEmp(@Param("id")Integer id,String lastName);
id → #{id/param1}
lastName → #{param2}
public Employee getEmp(Integer id,@Param("e")Employee emp);
id → #{param1}
lastName → #{param2.lastName / e.lastName}
因为 param2 才代表 employee
##特别注意:
如果是 Collection(List、Set)类型或者是数组,也会特殊处理,是把传入的 list 或者数组封装在 map 中。
- 此时的 key:
- Collection(collection),
- 如果是 List 还 key(list)
- 数组(array)
- 以
public Employee getEmpById(List<Integer> ids);
为例
现在取值:取出第一个 id 的值: #{list[0]},就不能用#{param[0]}或者是#{ids[0]}
参数传递源码分析
我们知道 mapper 是以动态代理为原理执行 sql 的,下面看看整个运行流程
测试:
1 | SqlSession openSession = sqlSessionFactory.openSession(); |
第三行处进行断点调试,先来到一个名字叫 mapperProxy.class 动态代理的 InvocationHandler
1 | public class MapperProxy<T> implements InvocationHandler, Serializable {} |
看看这个类中的 invoke 方法:
1 |
|
我们发现这里的mapperMethod.execute(sqlSession, args);
方法中的参数传入的是 1 和 Tom
进入 execute 方法
1 | public Object execute(SqlSession sqlSession, Object[] args) { |
在 convertArgsToSqlCommandParam(args)方法中 参数确实是传过来了
在这个方法中:
1 | public Object convertArgsToSqlCommandParam(Object[] args) { |
它调用的又是
1 | paramNameResolver.getNamedParams(args); |
再 step in 进入这个方法,下面这个方法将我们传入的参数封装成了 param1,和 param2
1 | /** |
这行代码的第一段执行了一个final int ParamCount = names.size();
也就是先把参数的数量确认为了 names 的大小。
那么这个names又是什么呢?是ParamNameResolver
类下的一个属性:private final SortedMap<Integer, String> names;
也就是说,本质是一个 map;
names值的确定:
1 | public ParamNameResolver(Configuration config, Method method) { |
总的来说 names 的确定流程:
- 获取每个标注了@Param 注解的参数的值,并且把这些值赋值给 name(一个字符串)
- 接着每次解析一个参数,形成一个{key(参数索引),value(name 的值}放到 map 中
- 如果加了@Param 注解,name 的值就是注解值
- 没有标注@Param 注解的话
- 如果配置了
useActualParamName(jdk1.8)
那么 name=参数名 - 否则,name=map.size() 相当于元素索引
- 如果配置了
举个例子 来理解:对于public Employee getEmpByIdAndLastName(@Param("id")Integer id, @Param("lastName")String lastName);
来说
由于 id,lastName 这两个属性前加了注解@Param,因此这俩的 name 值分别为:id 和 lastName,因此确定完 names 应该是:{0 : id, 1 : lastName}
假如再有另一个属性没加@Param 注解,那最终的 names 应该是{0 : id, 1 : lastName, 2 : 2} (key=2,value=2)
下面回到getNamedParams
方法继续:
1 | public Object getNamedParams(Object[] args) { |
关于 names 集合遍历理解:
- 首先我们有了一个 names 集合:{0:id, 1:lastName}和一个 args(之前传入的参数列表):【1,“Tom”】
- 接着新建了一个 map 我们称为 Param,这个新 map 的 key 我们使用 names 集合中的 value,而 value 我们得参数列表的值;举个例子就是,现在 map 应该为:{id:args[0] = 1, lastName : args[1] = “Tom”}、
- 同时啊,我们还贴心的将 Param1,Param2….ParamN 也做为了 key 与参数列表中的各个参数进行了映射封装进了 map 中,这样子以后不仅可以通过#{key}去获取值了,#{Param}也能取到值
参数值的获取(#{}与${})
主要探究#{}和${}的区别:
例子:
1 | select * from tbl_employee where id=${id} and last_name=#{lastName} |
控制台:Preparing: select * from tbl_employee where id=2 and last_name=?
我们发现使用了$的 id 能打印出来,而使用了#的属性就不能获取到;
#{}:是以预编译的形式,将参数设置到 sql 语句中;PreparedStatement;防止 sql 注入
${}:取出的值直接拼装在 sql 语句中;会有安全问题;
但我们仍有一些场景是能够去使用的:比如原生 jdbc 不支持占位符的地方我们就可以使用${}进行取值
按照年份分表拆分:select _ from ${year}
排序:select _ from tbl_employee order by ${f_name} ${order}
#{}取参数的一些规则
参数位置支持的属性:
javaType、 jdbcType、 mode(存储过程)、 numericScale、 resultMap、 typeHandler、 jdbcTypeName、 expression(未来准备支持的功能);
jdbcType 通常需要在某种特定的条件下被设置:
在我们数据为 null 的时候,有些数据库可能不能识别 mybatis 对 null 的默认处理,比如 Oracle(报错):JdbcType OTHER:无效的类型;
因为 mybatis 对所有的 null 都映射的是原生 Jdbc 的 OTHER 类型,oracle 不能正确处理;
由于全局配置中:jdbcTypeForNull=OTHER;oracle 不支持;
两种办法进行解决:
- #{email,jdbcType=OTHER};
- jdbcTypeForNull=NULL
动态 SQL
if
1 | //携带了哪个字段查询条件就带上这个字段的值 |
1 | <!-- |
测试
1 |
|
where
由于查询时候如果某些条件没带会出现SQL 拼接问题
因此有以下解决方案
在 where 后添加1=1,以后的 sql 条件语句使用and xxx
另一种方法是使用where 标签去将所有的查询条件包括在内
也就是通过where 标签与if 标签配合使用来取代 where 条件
1 | <select id="getEmpsByConditionIf" resultType="com.hpg.pojo.Employee"> |
trim
- trim 标签是一个格式化的标签,可以完成 set 或者是 where 标记的功能
trim 标签的四个属性如上:
- prefix:前缀,该属性的值将会在拼接后的整个字符串作为前缀出现
- prefixOverrides:前缀覆盖,会去掉字符串前面多余的字符
- suffix:后缀,该属性的值将会在拼接后的整个字符串作为后缀出现
- suffixOverrides:后缀覆盖,会去掉字符串后面多余的字符(可以用来去除多余的【逗号,】)
1 | <select id="getEmpsByConditionIf" resultType="com.hpg.pojo.Employee"> |
choose
类似于 java 中的 switch case 进行分支选择
choose 标签中 的 case 呢 则用了when 标签,而 default 情况呢 则使用了otherwise 标签
下面模拟一个场景:
假如我们想搜索的 id 不为空,那么执行的 sql 语句为:
select * from tbl_employee WHERE id=?
假如我们想搜索的 lastname 不为空,执行
select * from tbl_employee WHERE last_name like ?
假如我们想搜索的 email 不为空,则执行
select * from tbl_employee WHERE email = ?
都为空,则执行
select * from tbl_employee WHERE gender = 0
1 |
|
set
关于 set 这个标签,我们是在更新状态中使用,也就是实现动态更新的效果(满足某种条件去对应更新)
1 | <!--set 动态更新--> |
foreach
属性 | 描述 |
---|---|
collection | 表示迭代集合的名称,可以使用@Param 注解指定,如下图所示 该参数为必选![image-20210309204419205]( |
https://er11.oss-cn-shenzhen.aliyuncs.com/img/image-20210309204419205.png) | |
item | 表示本次迭代获取的元素,若 collection 为 List、Set 或者数组,则表示其中的元素;若 collection 为 map,则代表 key-value 的 value,该参数为必选 |
open | 表示该语句以什么开始,最常用的是左括弧’(’,注意:mybatis 会将该字符拼接到整体的 sql 语句之前,并且只拼接一次,该参数为可选项 |
close | 表示该语句以什么结束,最常用的是右括弧’)’,注意:mybatis 会将该字符拼接到整体的 sql 语句之后,该参数为可选项 |
separator | mybatis 会在每次迭代后给 sql 语句 append 上 separator 属性指定的字符,该参数为可选项 |
index | 在 list、Set 和数组中,index 表示当前迭代的位置,在 map 中,index 代指是元素的 key,该参数是可选项。 |
foreach 查询
1 | //查询员工id'在给定集合中的 |
1 | <!--public List<Employee> getEmpsByConditionForeach(List<Integer> ids); --> |
1 |
|
数据库表中 id:1 2 5,测试结果为 2 条数据 正确
foreach 批量保存
1 | <!-- 批量保存 --> |
1 |
|
两个内置参数
- _parameter:表示真个参数
- _databaseId:表示当前数据库的别名
1 | <!-- 两个内置参数: |
bind
- bind 标签中,value 对应传入实体类的某个字段,name 属性既给对应字段取的变量名。在 value 属性中可以使用字符串拼接等特殊处理。
以上述场景为例,如果要想使用 like 进行模糊查询:where last_name lile '%#{lastNmae}%'
这样写 是不可以的,需要将 lastName 绑定成一个变量传进去:<bind name="xxx" value="'%'+ id + '%'"/>
其中,不仅仅可以写成 % + id + % ,也可以写成_ + id + % ,看需求变化;
1 | <!--public List<Employee> getEmpsTestInnerParameter(Employee employee); --> |
sql 标签 - 抽取/重用
抽取可重用的 sql 片段。方便后面引用
- sql 抽取:经常将要查询的列名,或者插入用的列名抽取出来方便引用
- include 来引用已经抽取的 sql:
- include 还可以自定义一些 property,sql 标签内部就能使用自定义的属性
- nclude-property:取值的正确方式${prop},
- #{不能使用这种方式}
1 | <sql id="insertColumn"> |
缓存(重点)
- Mybatis 包含了一个时分强大的查询缓存特性,可以非常方便的配置和定制;缓存的作用呢,是可以极大的提升查询效率
- Mybatis 系统默认定义了两级缓存(一级缓存和二级缓存)
- 默认情况下,只有一级缓存(SqlSession 级别的缓存,AKA 本地缓存)开启
- 二级缓存需要手动的去开启和配置,是基于namespace级别的缓存
- 为了提高扩展石呢,MYbatis 定义了缓存接口 Cache,因此我们可以通过实现这个接口去自定义我们的二级缓存
两级缓存
缓存分为:一级缓存和二级缓存
一级缓存
- 一级缓存(本地缓存):与数据库同一次会话期间查询到的数据会放到本地缓存中,之后若需要获取相同数据,则可以直接从缓存去获取,没必要去查询数据库了
下面进行一个测试去体会一下:
对于同一个请求,我们执行两次,却发现,只执行了第一次的 sql;
第二次的数据是直接获取到的。是从哪里获取的呢?答案就是一级缓存中
1 |
|
- 那么我们不由得就会产生一个疑问了,什么场景下我们的一级缓存不再适用了呢?
sqlSession 不同。
1
2
3SqlSession openSession2 = sqlSessionFactory.openSession();
EmployeeMapper mapper2 = openSession2.getMapper(EmployeeMapper.class);
Employee empTest = mapper.getEmpById(1);使用的 sqlSession 相同
但查询条件不同
1
Employee emp01 = mapper.getEmpById(2);
但两次查询之间执行了一次 CRU(增删改)操作(因为有可能会导致数据受到影响)
1
mapper.addEmp(new Employee(null, "testCache", "cache", "1"));
但手动清除过了一级缓存(缓存清空)
1
openSession.clearCache();
- 一个一级缓存的生命周期有多长呢?
- MyBatis 在开启一个数据库会话时,会 创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象。Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
- 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。
- 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。
- SqlSession中执行了任何一个update 操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用
二级缓存
为什么要有二级缓存呢?肯定是一级缓存有一定的不足,需要一个补丁或者是进化版缓存去弥补啦;
二级缓存(全局缓存):基于 namespace 级别的缓存,即一个 namespace 对应一个二级缓存
工作机制
- 一个会话,去查询一条数据,这个数据会被放在当前会话的一级缓存中
- 会话关闭了,一级缓存的数据就会被保存到二级缓存中,这时候有一个新的会话去查询信息,就可以参照二级缓存去查了;
- 一个 sqlSession 去开启两个 Mapper,两个 Mapper 又各自对应不同的对象,此时这两个对象是放在不同的二级缓存中的(因为一个 namespace 对应一个二级缓存)
二级缓存的使用
开启方式:在全局配置文件中
1
2<!--显式的指定每个我们需要更改的配置的值,即使他是默认的。防止版本更新带来的问题 -->
<setting name="cacheEnabled" value="true"/>使用方式:哪个 mapper 文件要使用,就在哪个 mapper 文件中添加
1
<cache></cache>
关于
cache
标签的几个属性eviction:缓存的回收策略
- LRU - 最近最少使用的:移除最长时间不被使用的对象。
- FIFO - 先进先出:按对象进入缓存的顺序来移除它们。
- SOFT - 软引用:移除基于垃圾回收器状态和软引用规则的对象。
- WEAK - 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
flushIntercal:缓存刷新间隔。可以被设置为任意的正整数,而且它们代表一个合理的毫秒 形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。
readOnly:是否只读。属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓 存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存 会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全,因此默认是 false。
true:只读;mybatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。
mybatis 为了加快获取速度,直接就会将数据在缓存中的引用交给用户。不安全,速度快
false:非只读:mybatis 觉得获取的数据可能会被修改。
mybatis 会利用序列化&反序列的技术克隆一份新的数据给你。安全,速度慢
size:引用数目,可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的 可用内存资源数目。默认值是 1024。
type:指定自定义的全类名,实现 Cache 接口即可;
要想实现缓存,我们的 POJO 类(不是 Mapeer 类)就需要去实现序列化接口(Serializable)
如果不实现的话:
下面进行下测试:
当不开启缓存的时候:发送了两次 sql
当开启缓存的时候:只发送了一次 sql 同时可以发现一句话:Cache Hit Ratio
也就是缓存命中了 后面的 0.5 是什么意思呢?
代表着第二次查缓存命中了,即 1/2 = 0.5
1 | <cache eviction="FIFO" flushInterval="60000" readOnly="false" size="1024"></cache> |
ps:需要注意的是,查出来的数据都会被默认先放在一级缓存中,只有当会话提交或者关闭之后,一级缓存中的数据才会转移到二级缓存
也就是说,假如上述的例子。第一个 sqlSession 是在建立第二个 sqlSession 之后才关闭的,就算开启了二级缓存,那么照样会发送 2 条 sql
缓存相关设置
cacheEnabled=true:false:关闭缓存(二级缓存关闭)(一级缓存一直可用的)
在 mapper 配置中,每个select 标签都有一个
useCache = "true"
.那么假如设置为 false 是设置一级缓存还是二级缓存为 false 呢?答案是:一级缓存还能正常使用,二级缓存被禁止了
每个增删改标签中有
flushCache="true"
(刷新缓存):表示每次增删改执行之后 就会清除缓存(一级二级缓存都会被清除)每个查询标签中
flushCache="false"
假如设置为 true,则每次查询之前都会清除缓存sqlSession.clearCache()
只清除当前 session 的一级缓存localCacheScope:本地缓存作用域
- 一级缓存 SESSION:当前会话的所有数据保存在会话缓存中;
- STATEMENT:可以禁用一级缓存;
Mybatis 与 Spring 进行整合
MyBatis-Spring 会帮助你将 MyBatis 代码无缝地整合到 Spring 中。它将允许 MyBatis 参与到 Spring 的事务管理之中,创建映射器 mapper 和 SqlSession
并注入到 bean 中,以及将 Mybatis 的异常转换为 Spring 的 DataAccessException
。 最终,可以做到应用代码不依赖于 MyBatis,Spring 或 MyBatis-Spring。
(之后再补)
Mybatis 运行原理
- MyBatis 层次图
首先我们回顾一下运用 Mybatis 来执行 CRUD 的几个步骤:
获取 SqlSessionFacroty 对象
1
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
获取 SqlSession 对象
1
SqlSession openSession = sqlSessionFactory.openSession();
获取接口的实现对象(接口的代理对象 MapperProxy)
1
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
执行 CRUD 方法
1
Employee employee = mapper.getEmpById(1);
下面就根据这 4 步,来仔细看看源码,探究下每一步都发生了什么事情
SqlSessionFacroty 初始化
首先把一个配置文件转成流的形式,把这个流作为参数传到下列方法即可获取到一个 SqlSessionFactory
1 | return new SqlSessionFactoryBuilder().build(inputStream); |
那么其本质是怎么运作的呢?我们进入biuld
方法
1 | public SqlSessionFactory build(InputStream inputStream) { |
发现其实呢 他调用的是自己的biuld
方法
1 | public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { |
看起来代码很多,但是真正执行的关键部分是
1 | XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); |
- 通过调用 XMLConfigBuilder(对 mybatsi 的配置文件进行解析的类)的 parse 方法
- 将上一步获取到的元素作为参数传入 biuld 方法,返回值
那么下面探究一下这个XMLConfigBuilder参数解析器类的parse()方法,我们 step in:
1 | public Configuration parse() { |
它的核心代码(如下)又做了什么呢?
1 | //这里的/configuration 对应的是全局配置文件中的 /configuration标签 |
其中parser.evalNode("/configuration")
是获取了一个根结点
我们进入这个parseConfiguration
类看看
1 | private void parseConfiguration(XNode root) { |
我们在这里发现了很多我们很眼熟的标签:properties,typeAliases,environments,mappers
这不由得让我们思考:这段代码与 xml 中的标签是否有关呢?答案是肯定的
- 这段代码的作用是:解析每一个标签,把其中详细的信息保存在Configuration中
那么又是怎么解析的呢?我们点进mapperElement(root.evalNode("mappers"));
看看
1 | private void mapperElement(XNode parent) throws Exception { |
OK,上面我们说到了使用mapperParser.parse();
方法进行解析,我们继续 Step in 再看看
1 | public void parse() { |
它的第一行:configurationElement(parser.evalNode("/mapper"));
又是干嘛的呢?我们继续 Step in’
1 | private void configurationElement(XNode context) { |
这个方法的作用就是解析各种标签,我们以 buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
为例,探究一下是如何解析增删改查标签的
我们首先看context.evalNodes("select|insert|update|delete")
部分:
1 | public List<XNode> evalNodes(String expression) { |
这段代码的返回值是一个 List,也就是得到了一个增删改查所有标签的 LIst
接着通过buildStatementFromContext()
方法
1 | private void buildStatementFromContext(List<XNode> list) { |
不管 databaseId 是否为空,我们都需要调用buildStatementFromContext()
这个方法 我们 Step in
1 | private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { |
首先呢,创建了一个 XMLStatementBuilder 对象,这个对象的用途是将用户的CRUD配置解析成对应的MappedStatement
对象.
关于MappedStatement
对象:
接着,通过这个对象调用statementParser.parseStatementNode();
方法
将 mapper.xml 中的每一个元素信息解析出来并且保存在configuration类(全局配置)中
1 | public void parseStatementNode() { |
我们进入builderAssistant.addMappedStatement
方法
1 | public MappedStatement addMappedStatement( |
我们进入这个方法,看到前面的所有代码都是为了下面这三句代码服务的
1 | MappedStatement statement = statementBuilder.build(); |
我们首先看看第一句,鼠标放在 satement 对象上,看看里面都有哪些信息
1 | MappedStatement statement = statementBuilder.build(); |
从下图,我们可以发现,一个 MappedStatement 保存了一个增删改查标签的详细信息
第二句:
1 | configuration.addMappedStatement(statement); |
将我们辛辛苦苦创建出来的 statement 对象,存到了 configuration 对象中;
还记得我们一开始从哪个方法进来的吗?
1 | mapperElement(root.evalNode("mappers")); |
对的,这个代码又是在哪里被执行的呢?parseConfiguration(parser.evalNode("/configuration"));
mapper 解析完了意味着这个方法也跑完了,最后我们就获取了一个 configuration
1 | public Configuration parse() { |
我们也可以来看看这个 configuration 到底保存了什么
我们仅仅是拿了一个解析 CRUD 标签为例,但肯定是不止解析了 CRUD 标签的,由此我们可以推断:
- 这个 configuration 类保存了所有配置文件的详细信息
在这个类中有两个重要属性:
当然了,还没完,我们获取到了 configuration 后,按照流程走的话,调用的是:
1 | XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); |
或者换句话说,其实调用的是build(configuration);
因为本质上传的参数就是我们的 configuration 嘛
其中这个 biuld 方法的返回值,是一个 DefaultSqlSessionFactory
1 | public SqlSessionFactory build(Configuration config) { |
这个 DefaultSqlSessionFactory 也就是我们最后的返回值了;
1 | public DefaultSqlSessionFactory(Configuration configuration) { |
至此,大功告成,我们明白了,原来 SqlSessionFactory 初始化这一步的最终结果就是获取了一个 DefaultSqlSessionFactory
- 我们用一张图来总结一下流程
获取 SqlSession 对象
我们知道,获取 SqlSession 的方法是调用了 SqlSession 类中的 openSession()方法
1 |
|
关于
1 | openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); |
第一个参数的获取configuration.getDefaultExecutorType()
如下 默认是 SIMPLE
我们进入这个方法看看:
1 | private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { |
Step in newExecutor(tx, execType);
看看
1 | public Executor newExecutor(Transaction transaction, ExecutorType executorType) { |
原来啊,这个方法是通过 Executor 在全局配置中的类型,去创建以下三种类型的执行器 Executor
- BatchExecutor:每执行一次 update 或 select 就开启一个 Statement 对象,用完立刻关闭 Statement 对象;
- ReuseExecutor:执行 update 或 select,以 SQL 作为 key 查找 Statement 对象,存在就使用,不存在就创建,用完后不关闭 Statement 对象,而是放置于 Map 内供下一次使用。简言之,就是重复使用 Statement 对象;
- SimpleExecutor:执行 update(没有 select,jdbc 批处理不支持 select),将所有 SQL 都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个 Statement 对象,每个 Statement 对象都是 addBatch()完毕后,等待逐一执行 executeBatch()批处理,与 jdbc 批处理相同。
- 这些执行器的特点,严格限制在 SqlSession 的生命周期范围内
同时,根据有没有配置二级缓存,去决定是否包装成一个CachingExecutor
接着执行很重要的一步:
1 | executor = (Executor) interceptorChain.pluginAll(executor); |
进入这个方法,我们发现其作用是使用每一个拦截器重新包装 executor 并且返回
1 | public Object pluginAll(Object target) { |
上述全部执行完后,执行完
1 | final Executor executor = configuration.newExecutor(tx, execType); |
后,执行的就应该是下列代码,new 了一个 DefaultSqlSession (包含了 configuration 和 execut);
1 | return new DefaultSqlSession(configuration, executor, autoCommit); |
并且返回这个 SqlSession,至此,我们成功获取到了 SqlSession 对象
接口的代理对象 MapperProxy 生成
我们知道,Mapper 代理对象是由 SqlSession 生成,那么我们进入 DefaultSqlSession 查看其中的 getMapper 方法
1 |
|
我们进入configuration.getMapper(type, this);
方法
1 | public <T> T getMapper(Class<T> type, SqlSession sqlSession) { |
他是通过 mapperRegistry(上文其实有提及过)的 getMapper 方法获取代理对象的,我们继续 Step in
1 | public <T> T getMapper(Class<T> type, SqlSession sqlSession) { |
- 首先生成一个代理对象的工厂对象
- 通过这个工厂的方法去进行下一步
1 | public T newInstance(SqlSession sqlSession) { |
我们 step in 这重载的 newInstance 方法看看
1 | protected T newInstance(MapperProxy<T> mapperProxy) { |
终于,我们守得云开见月明,发现其本质就是调用了 jdk 中的 newProxyInstance 动态代理方法,为我们生成了一个MapperProxy的代理对象
1 |
|
至此,返回一个接口的代理对象,结束;
CRUD 的执行(以查询为例)
以下列代码为例,我们继续 Step in
1 | Employee employee = mapper.getEmpById(1); |
首先,我们进入的是 MapperProxy 的 invoke 方法,其中将传入的方法包装成了一个 MapperMthod,通过这个类的 execute 方法去执行我们调用的方法
1 |
|
下面我们继续 step in 这个 execute 方法:
1 | public Object execute(SqlSession sqlSession, Object[] args) { |
首先进行了方法的选择,之后将我们的参数解析包装成了 sql 中的参数,之后,调用 sqlSession 的 selectOne 方法(因为我们这里是查询单个嘛,如果是查询多个就是executeForMany
了,视情况而定;
1 | result = sqlSession.selectOne(command.getName(), param); |
我们 step in 这个selectOne
方法
1 |
|
t 首先通过:List<T> list = this.selectList(statement, parameter);
创建了一个 list,然后根据 list 的大小进相应的操作;那这个 selectList 又进行了什么操作呢?
1 |
|
通过全局配置中的 getMappedStatement 获得了 关于增删改查的全部详细信息
接着调用执行器 executor 的方法,其中传入的参数 ms 我们知道是一个 MappedStatement 对象,那后面的
wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER
又是什么呢?我们不妨看看private Object wrapCollection(final Object object) { if (object instanceof Collection) { StrictMap<Object> map = new StrictMap<>(); map.put("collection", object); if (object instanceof List) { map.put("list", object); } return map; } else if (object != null && object.getClass().isArray()) { StrictMap<Object> map = new StrictMap<>(); map.put("array", object); return map; } return object; } <!--code154-->
首先通过 mapperstatement 获取了 boundSql,这个类含有 sql 语句、参数映射等信息 总的来说 这是一个包含了 sql 详细信息的类
1 | //sql语句 |
接着第二步CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
得到了一个缓存
获取完boundSql和CacheKey后,执行query
方法
1 |
|
最终调用了Executor的query方法;
1 |
|
首先从本地缓存中通过 key 拿到 list;第一次调用的话,key 应该是 null,因此无法从缓存中获取
若缓存中没有 list,则调用真正的 query 方法:
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}在这个方法中通过
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
创建 list,且通过localCache.putObject(key, list);
将 list 放入本地缓存中
下面进入 doQery 方法:
1 |
|
第一步:创建 StatementHandler
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
1
2
3
4
5
6public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
//statementHandler在此处被创建出来
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}先走第一步:
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
//根据mapperstatement中存储的statement的类型进行切换(type的设置在mapper文件中写sql语句可选择配)
switch (ms.getStatementType()) {
//type = STATEMENT 非预编译形式
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
//type = PREPARED 预编译形式 这是默认的
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
//type = CALLABLE
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}会走预编译形式 的执行器的构造方法
1
2
3public PreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
super(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql);
}
然后走第二步:
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
包装拦截器 之前也有讲过
1
2
3
4
5
6public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}第二步:
stmt = prepareStatement(handler, ms.getStatementLog());
通过这个方法,创建出了原生的Statement 对象;继续 step in:1
2
3
4
5
6
7
8
9
10private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
//获取连接
Connection connection = getConnection(statementLog);
//通过handler进行参数预编译 设置到statemtnt对象中
stmt = handler.prepare(connection, transaction.getTimeout());
//调用ParameterHandler去设置参数
handler.parameterize(stmt);
return stmt;
}- 进入 parameterize 方法
1
2
3
4
5// private final StatementHandler delegate;
public void parameterize(Statement statement) throws SQLException {
delegate.parameterize(statement);
}本质上调用了StatementHandler的parameterize方法
即:
1
2
3
4
5
6
7//PreparedStatementHandler extends BaseStatementHandler
//BaseStatementHandler implements StatementHandler
public void parameterize(Statement statement) throws SQLException {
// protected final ParameterHandler parameterHandler; 调用的是ParameterHandler的方法
parameterHandler.setParameters((PreparedStatement) statement);
}其中有人会好奇,这个 ParameterHandler 是什么时候创建的呢?答案是在 BaseStatementHandler 被创建时就创建了
1
2
3
4
5
6
7
8protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
.....
this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
}我们来看看这个 ParameterHandler 他是一个接口,有着我们想要的 setParameters 即 设置参数方法
1
2
3
4
5
6
7
8public interface ParameterHandler {
Object getParameterObject();
void setParameters(PreparedStatement ps)
throws SQLException;
}这个方法是由
public class DefaultParameterHandler implements ParameterHandler
类 去实现的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
//获得类型处理器
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
//调用类型处理器的setParameter方法 把预编译语句 下标 值 类型 传进去
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}第三步:
return handler.query(stmt, resultHandler);
1
2
3
4
5
6
7
8
9
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
//将语句转成预编译语句
PreparedStatement ps = (PreparedStatement) statement;
//执行
ps.execute();
//使用resultSetHandler去封装处理结果
return resultSetHandler.handleResultSets(ps);
}execute()方法:(看不看得懂是一回事,先放上来…)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97public boolean execute() throws SQLException {
synchronized (checkClosed().getConnectionMutex()) {
MySQLConnection locallyScopedConn = this.connection;
if (!this.doPingInstead && !checkReadOnlySafeStatement()) {
throw SQLError.createSQLException(Messages.getString("PreparedStatement.20") + Messages.getString("PreparedStatement.21"),
SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
}
ResultSetInternalMethods rs = null;
this.lastQueryIsOnDupKeyUpdate = false;
if (this.retrieveGeneratedKeys) {
this.lastQueryIsOnDupKeyUpdate = containsOnDuplicateKeyUpdateInSQL();
}
this.batchedGeneratedKeys = null;
resetCancelledState();
implicitlyCloseAllOpenResults();
clearWarnings();
if (this.doPingInstead) {
doPingInstead();
return true;
}
setupStreamingTimeout(locallyScopedConn);
Buffer sendPacket = fillSendPacket();
String oldCatalog = null;
if (!locallyScopedConn.getCatalog().equals(this.currentCatalog)) {
oldCatalog = locallyScopedConn.getCatalog();
locallyScopedConn.setCatalog(this.currentCatalog);
}
//
// Check if we have cached metadata for this query...
//
CachedResultSetMetaData cachedMetadata = null;
if (locallyScopedConn.getCacheResultSetMetadata()) {
cachedMetadata = locallyScopedConn.getCachedMetaData(this.originalSql);
}
Field[] metadataFromCache = null;
if (cachedMetadata != null) {
metadataFromCache = cachedMetadata.fields;
}
boolean oldInfoMsgState = false;
if (this.retrieveGeneratedKeys) {
oldInfoMsgState = locallyScopedConn.isReadInfoMsgEnabled();
locallyScopedConn.setReadInfoMsgEnabled(true);
}
//
// Only apply max_rows to selects
//
locallyScopedConn.setSessionMaxRows(this.firstCharOfStmt == 'S' ? this.maxRows : -1);
rs = executeInternal(this.maxRows, sendPacket, createStreamingResultSet(), (this.firstCharOfStmt == 'S'), metadataFromCache, false);
if (cachedMetadata != null) {
locallyScopedConn.initializeResultsMetadataFromCache(this.originalSql, cachedMetadata, rs);
} else {
if (rs.reallyResult() && locallyScopedConn.getCacheResultSetMetadata()) {
locallyScopedConn.initializeResultsMetadataFromCache(this.originalSql, null /* will be created */, rs);
}
}
if (this.retrieveGeneratedKeys) {
locallyScopedConn.setReadInfoMsgEnabled(oldInfoMsgState);
rs.setFirstCharOfQuery(this.firstCharOfStmt);
}
if (oldCatalog != null) {
locallyScopedConn.setCatalog(oldCatalog);
}
if (rs != null) {
this.lastInsertId = rs.getUpdateID();
this.results = rs;
}
return ((rs != null) && rs.reallyResult());
}
}resultSetHandler.handleResultSets(ps);
方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
ResultSetWrapper rsw = getFirstResultSet(stmt);
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
return collapseSingleResultList(multipleResults);
}总的来说就是查出数据,使用 ResultSetHandler 去处理结果;然后通过 TypeHandler 去获取结果值
总的来说:
- 根据具体传入的参数,动态地生成需要执行的 SQL 语句,用 BoundSql 对象表示
- 为当前的查询创建一个缓存 Key
- 缓存中没有值,直接从数据库中读取数据
- 执行查询,返回 List 结果,然后 将查询的结果放入缓存之中
- 根据既有的参数,创建 StatementHandler 对象来执行查询操作
- 将创建 Statement 传递给 StatementHandler 对象,调用 parameterize()方法赋值
- 调用 StatementHandler.query()方法,返回 List 结果集
查询流程总结
- 查询流程
我们的对象调用过程:代理对象 → DefaultSqlSession → Executor → StatementHandler
关于StatementHandler
的类结构:(短暂过一下,不做细究)
- RoutingStatementHandler:路由处理器,这个相当于一个静态代理,根据 MappedStatement.statementType 创建对应的对处理器;
- SimpleStatementHandler:不需要预编译的简单处理器;
- PreparedStatementHandler:预编译的 SQL 处理器;
- CallableStatementHandler:主要用于存储过程的调度;
以及ResultSetHandler
的类继承结构:
在我们的查询流程中,我们主要使用的是PreparedStatementHandler和ResultSetHandler
- 通过前者进行设置参数预编译
- 后者进行结果集的一定处理
这两个类在处理过程中,都使用了typeHandler
类分别进行参数和结果集的处理:
DefaultParameterHandler typeHandler.setParameter(ps, i + 1, value, jdbcType) <!--code173-->
因此,我们就知道了TypeHandler的作用了:进行数据库类型和javaBean 类型的映射
当然了,所有的类的方法,追根溯源都依靠的是 JDBC 去执行,换句话说,都依靠的是Statement和PreparedStatement去执行的;
全局总结
思维导图:
Mybatis 插件
插件原理
要自己编写插件之前肯定得明白原理,即插件是什么,是怎么运作的,我们往下看;
首先,抛开插件,我们要明白,在四大对象(Executor、ParameterHandler、ResultSetHandler、StatementHandler)被创建的的时候,都调用了一个方法:interceptorChain.pluginAll();
1 | public Object pluginAll(Object target) { |
这段代码的作用,就是遍历/获取到所有的Interceptor(拦截器),之后通过interceptor.plugin(target)
返回 target 包装后的对象
因此!到这里,我们可以思考一下,假如我们也能实现一个拦截器,那我们在创建四大对象的时候,相当于外面套了一层我们做的衣服/功能,这样的话,岂不是就可以让 Mybatis 做一些我们 想让他做的事情了吗?这就是插件的原理:利用动态代理去给目标对象创建一个代理对象,用以实现特殊功能;
插件编写测试
如我们上述所言,若我们能实现一个拦截器,就达到了插件的效果了,接下来就动手来做一做
首先,插件编写需要以下几步
- 编写 Interceptor 的实现类
- 使用@Intercept 注解去完成插件的签名
- 将写好的插件注册到全局配置文件中
需要实现拦截器,就需要实现Interceptor
接口
1 | public interface Interceptor { |
测试代码
- 自定义插件:
1 | package com.hpg.dao; |
全局配置:
1 | <!--注意:插件的注册不能写在settings的前面--> |
执行结果:
关键信息
尽管我们的标签里面设置了只对
StatementHandler
对象进行包装,但是我们也会生成四个包装对象:即分别对 ParameterHandler、ResultSetHandler、StatementHandler、Executor 进行了包装。这是我们需要注意的;
1
2
3
4
5
6
7
8MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.executor.CachingExecutor@6973b51b
MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.executor.CachingExecutor@6973b51b
MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.scripting.defaults.DefaultParameterHandler@399f45b1
MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.scripting.defaults.DefaultParameterHandler@399f45b1
MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.executor.resultset.DefaultResultSetHandler@35cabb2a
MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.executor.resultset.DefaultResultSetHandler@35cabb2a
MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.executor.statement.RoutingStatementHandler@7e07db1f
MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.executor.statement.RoutingStatementHandler@7e07db1f
在执行完 sql 之后:
我们的插件也对设置参数方法进行了影响/拦截,此时就只会对我们想要包装的对象的方法进行拦截了,就不会像上面生成代理对象这一步时一样,对四大对象都生成代理对象。
1 | MyFirstPlugin 影响的方法public abstract void org.apache.ibatis.executor.statement.StatementHandler.parameterize(java.sql.Statement) throws java.sql.SQLException |
关于插件一些细节
假如我们在写一个插件,与第一个插件共同运作时,会怎么样呢?
1 |
|
我们发现 在产生代理对象时候是先进行 first 插件,再进行 second 插件
但是在拦截方法的时候,是先进行 second 插件,再进行 first 插件;
因此我们可以得出结论
插件会产生目标对象的代理对象,多个插件就会产生多层代理对象
创建动态代理的时候,是按照插件配置顺序层层去创建代理对象。然而执行目标方法时,是按照逆向顺序执行
开发插件
有了上述例子,我们可以试着去推断开发插件的思路了:
从 invocation 参数中获取执行方法的对象/方法/方法参数 → 获取该对象的元数据 → 获取想要修改的参数 → 修改该参数
下面我们改造一下第一个插件的intercept
方法 我们输入的员工 id 为 1,我们修改成 11
1 |
|
结果:
插件实战 - PageHelper 分页插件
PageHelper 失效问题在于版本没有对应:https://blog.csdn.net/sinat_34104446/article/details/92679046
解决方案:
1 | <plugins> |
数据库信息:
外部依赖:
pom.xml:
1 | <dependency> |
全局配置:
1 | <plugins> |
1 |
|