主要内容
- 多表查询
- 延迟加载
- 缓存
- 四大核心接口及执行流程
- 自定义插件
- 执行器类型
- MyBatis执行原理详解
学习目标
知识点 | 要求 |
---|---|
多表查询 | 掌握 |
延迟加载 | 掌握 |
缓存 | 掌握 |
四大核心接口及执行流程 | 掌握 |
自定义插件 | 掌握 |
执行器类型 | 掌握 |
MyBatis执行原理详解 | 掌握 |
一、多表查询(面试题)
1.介绍
多表查询是在企业中必不可少的,无论多么简单的项目里都会出现多表查询操作。因为只要是关系型数据库,在设计表时都需要按照范式进行设计,为了减少数据冗余,都会拆成多个表。当需要表中数据时,在进行联合查询。
在MySQL学习时,知道表之间关系分为:一对一、一对多、多对多。这三种关系又细分为单向和双向。如果学习的是Hibernate框架,必须要严格区分开表之间的关系,然后才能使用Hibernate框架。但是在MyBatis框架中只有两种情况:当前表对应另外表是一行数据还是多行数据。转换到实体类上:当前实体类包含其他实体类一个对象还是多个对象。再转换到MyBatis的映射文件上:在<resultMap>
标签里面使用<association>
还是<collection>
标签就可以。
所以:在学习MyBatis多表查询时其实就是在学习<association>
标签和<collection>
标签。其中如果一个实体类关联另一个实体类的一个对象使用<association>
。如果一个实体类关联两一个实体类的List集合对象,需要使用<collection>
。所以分析的思路是:先分析需求->分析数据库设计对应关系->创建实体类->根据实体类关联属性类型决定使用哪个标签。
这两个标签根据编写的SQL,分为N+1查询和联合查询两种方式。两种方式优缺点:
- N+1方式:
优点:SQL简单。支持延迟加载。
缺点:多做N次查询。
- 联合查询方式:
优点:一次查询。
缺点:SQL相对复杂。不支持延迟加载。
总体思路:
2 数据库准备
在ssm数据库中创建两张表:分别是Dept和Emp。在表设计时设定一个员工只能有一个部门。一个部门可以包含多个员工。
create table dept(
d_id int (11) primary key auto_increment,
d_name varchar(20)
);
insert into dept values(1,"教学部");
insert into dept values(2,"行政部");
create table emp(
e_id int (11) primary key auto_increment,
e_name varchar(20),
e_d_id int (11),
constraint fk_emp_dept foreign key (e_d_id ) references dept(d_id)
);
insert into emp values(1,'张三',1);
insert into emp values(2,'李四',1);
insert into emp values(3,'王五',1);
insert into emp values(4,'韩梅梅',2);
insert into emp values(5,'小明',2);
数据库模型图
3.项目准备
创建MyBatis项目,并把依赖、配置文件搭建好。搭建时要包含任意一种日志,保证控制台能看到SQL。
重要提示:
1.下面在创建实体类时,直接在实体类中包含了另一个实体类了。但是在项目开发过程中,需要分析需求,在决定是否放置对方实体类的属性。
2.如果整个项目只有在查询部门时需要同时用到员工数据,只需要在Dept中放置Emp属性。
3.如果整个项目只有在查询员工时需要同时用到员工数据,只需要在Emp中放置Dept属性。
4.如果项目在查询员工时会用到部门,同时查询部门时会用到员工,这时两个实体类都需要放置对方的属性。
5.上面的做法是严谨的,也可以分析好表和表之间关系,在实体类直接放上对方属性,不管以后用不用。毕竟除了学习一些语法时要记住必须怎么写。但写项目时需要灵活起来,只要能实现出效果就万事大吉。至于性能优化、小bug,全交给项目2.0版本在处理就好了。
在项目下创建com.bjsxt.pojo.Dept
因为一个部门可以有多个员工,所以在Dept中有个List属性。
public class Dept {
private int id;
private String name;
private List<Emp> list;
// 没有在文档里面粘贴getter/setter和toString(),太占地方
}
在项目下创建com.bjsxt.pojo.Emp实体类。
因为一个员工只能有一个部门,所在在Emp中有一个Dept类型属性。
public class Emp {
private int id;
private String name;
private Dept dept;
// 没有在文档里面粘贴getter/setter和toString(),太占地方
}
4.联合查询方式
联合查询方式中,对SQL编写有一定的能力要求,只要把SQL能编写出来,知道哪些列的值需要放在哪些属性中就可以了。
编写联合查询SQL
select d_id,d_name,e_id,e_name,e_d_id from dept d,emp e where d.d_id=e.e_d_id
根据结果分析,查询结果哪些列放在哪些属性中。
下图中发现了员工信息每行是唯一的,但是部门信息是有重复。MyBatis在填充数据时带去重功能。不需要担心会产生5个Dept对象,去重后只会产生两个不重复数据的Dept对象。
4.1 查询员工信息同时包含部门信息
创建接口com.bjsxt.mapper.EmpMapper
package com.bjsxt.mapper;
import com.bjsxt.pojo.Emp;
import java.util.List;
public interface EmpMapper {
List<Emp> seletAll();
}
在com.bjsxt.mapper下创建映射文件EmpMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bjsxt.mapper.EmpMapper">
<!-- 每行数据最终返回的是Emp对象 -->
<resultMap id="empMap" type="Emp">
<id column="e_id" property="id"/>
<result column="e_name" property="name"/>
<!-- 单个对象类型属性,使用association进行填充 -->
<!-- property:对象名 javaType:对象类型,支持别名-->
<association property="dept" javaType="Dept">
<!-- 对属性对象里面的属性配置映射关系 -->
<id column="d_id" property="id"/>
<result column="d_name" property="name"/>
</association>
</resultMap>
<!-- 使用resultMap配置结果集映射 -->
<select id="seletAll" resultMap="empMap">
select d_id,d_name,e_id,e_name,e_d_id from dept d,emp e where d.d_id=e.e_d_id
</select>
</mapper>
创建测试类com.bjsxt.test.Test,调用EmpMapper接口中selectAll()方法
public class Test {
public static void main(String[] args) throws IOException {
InputStream is = Resources.getResourceAsStream("mybatis.cfg.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = factory.openSession();
EmpMapper empMapper = session.getMapper(EmpMapper.class);
List<Emp> list = empMapper.seletAll();
System.out.println(list);
session.close();
}
}
测试程序,运行结果:
通过结果可以看到Emp中属性被查询到,包含的dept属性也被查询
小提示:
有的同学可能会问,为什么在编写Emp时候没有e_d_id列对应的值?
因为e_d_id列是外键,对应Dept的主键值。查询出Dept的值也就包含了e_d_id的值。
4.2 查询部门信息同时查询包含的员工信息
创建接口com.bjsxt.mapper.DeptMapper
package com.bjsxt.mapper;
import com.bjsxt.pojo.Dept;
import java.util.List;
public interface DeptMapper {
List<Dept> selectAll();
}
在com.bjsxt.mapper下创建映射文件DeptMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bjsxt.mapper.DeptMapper">
<resultMap id="deptMap" type="Dept">
<id column="d_id" property="id"></id>
<result column="d_name" property="name"></result>
<!-- collection标签中使用ofType控制泛型的类型 -->
<collection property="list" ofType="Emp">
<!-- 从数据库查询出来的每行数据对应Emp的哪个属性 -->
<id column="e_id" property="id"/>
<result column="e_name" property="name"/>
</collection>
</resultMap>
<!-- SQL和之前查询Emp的SQL是惊人的相同 -->
<select id="selectAll" resultMap="deptMap">
select d_id,d_name,e_id,e_name,e_d_id from dept d,emp e where d.d_id=e.e_d_id
</select>
</mapper>
创建测试类com.bjsxt.test.Test,调用DeptMapper接口中selectAll()方法
public class Test {
public static void main(String[] args) throws IOException {
InputStream is = Resources.getResourceAsStream("mybatis.cfg.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = factory.openSession();
DeptMapper deptMapper = session.getMapper(DeptMapper.class);
List<Dept> list = deptMapper.selectAll();
System.out.println(list);
session.close();
}
}
测试程序,运行结果:
通过结果可以看到Dept中属性被查询到,包含的list属性也被查询
如果运行过程中出现下面异常信息,说明xml文件中,中文注释编译出现问题了。删除掉中文注释
5.N+1查询方式
N+1查询方式命名由来:当查询Emp表中N调数据时,需要编写1条查询全部的SQL,和N条根据外键列值作为另一张表主键查询条件的N条SQL语句。
N+1查询方式,在进行操作时需要先分析出最终想要的结果需要包含对于两张表的单表查询语句是什么。
5.1 查询员工信息同时包含部门信息
查询员工信息同时包含部门信息,需要编写查询全部员工信息的SQL,然后根据外键列的值去部门表中查询对应的部门信息。
当存在调用和被调用关系时,按照正常编程习惯,都是先编写被调用方。
先在DeptMapper中提供一个根据主键查询的方法。
Dept selectById(int id);
编写映射文件DeptMapper.xml,实现接口方法与SQL绑定。
此处重新定义了一个resultMap,因为当前SQL只查询了Dept表数据。
<resultMap id="deptMap2" type="Dept">
<id column="d_id" property="id"></id>
<result column="d_name" property="name"></result>
</resultMap>
<select id="selectById" resultMap="deptMap2">
select * from dept where d_id=#{id}
</select>
然后编写EmpMapper接口,在里面添加个查询全部的方法。
方法名称没有叫做selectAll(),如果叫做selectAll()和上面联合查询方式的方法名称重名了。
List<Emp> selectAllN1();
编写EmpMapper.xml实现SQL和接口方法绑定。
<resultMap id="empMap2" type="Emp">
<id column="e_id" property="id"/>
<result column="e_name" property="name"/>
<!-- 此处依然使用association填充单个对象属性值.property和javaTye依然需要写 -->
<!-- select:调用的其他查询路径(statement) -->
<!-- column:当前SQL查询结果哪个列值当做参数传递过去。如果是多个参数{"key"=column,"key2"=column2}-->
<association property="dept" javaType="Dept" select="com.bjsxt.mapper.DeptMapper.selectById" column="e_d_id"></association>
</resultMap>
<select id="selectAllN1" resultMap="empMap2">
select e_id,e_name,e_d_id from emp
</select>
在测试类中调用EmpMapper的selectAllN1()方法,查看控制台效果。
发现依然可以成功查询Emp的全部数据,同时也包含上了属性dept的数据。
小提示:
当前方式叫做N+1查询方式,但是发现SQL并不是N+1条,是因为此处有MyBatis的缓存机制。
5.2 查询部门信息同时查询包含的员工信息
查询部门信息时同时包含上员工信息的流程和上面查询员工信息同时包含部门信息的流程是类似的。
还是先编写被调用方的代码。
在EmpMapper接口中添加根据外键查询的方法
List<Emp> selectByEid(int eid);
在映射文件EmpMapper.xml中编写SQL,与接口方法进行绑定。
此处的resultMap里面并没有包含association标签。因为对应当前方法就是个单表查询。
<resultMap id="empMap3" type="Emp">
<id column="e_id" property="id"/>
<result column="e_name" property="name"/>
</resultMap>
<select id="selectByEid" resultMap="empMap3">
select e_id,e_name,e_d_id from emp where e_d_id=#{eid}
</select>
然后在DeptMapper接口中添加查询全部方法。
方法名称没有叫做selectAll()。避免和上面联合查询方式查询全部方法重名。
List<Dept> selectAllN1();
在映射文件DeptMapper.xml编写SQL实现与接口方法绑定。
<resultMap id="deptMap3" type="Dept">
<id column="d_id" property="id"></id>
<result column="d_name" property="name"></result>
<!-- 加载集合类型属性时依然使用collection标签,property和ofType依然存在 -->
<!-- select 调用另一个查询的路径,如果名称唯一,前面namespace可以省略-->
<!-- column 当前查询哪个列的值作为参数传递给另一个参数-->
<collection property="list" ofType="Emp" select="com.bjsxt.mapper.EmpMapper.selectByEid" column="d_id"/>
</resultMap>
<select id="selectAllN1" resultMap="deptMap3">
select * from dept
</select>
修改测试类代码,调用DeptMapper接口的selectAllN1()方法。
6.业务装配
所谓的业务装配是不使用MyBatis进行装配。而是使用Java代码进行装配。具体体现在Web项目中,是在service里面通过Java代码实现结果的填充。
这种方式理解起来简单,写持久层时也简单。但是service的代码写起来更多了。属于N+1方式的另一种写法。需要保证持久中提供上对于两个表的单表查询方法。
无论是查询员工包含部门,还是查询部门包含员工,都需要提供N+1方式的单表查询,resultMap标签不配置association和collection标签。
以查询员工信息包含部门信息举例:
因为对于两个表都是单表操作,所以先写哪个表对应的操作都可以。对于持久层来说没有谁调用谁的说法,把持久层两个表的单表操作希望,在业务层(当前没有业务层,测试类代码相当于业务层)统一调用就行。
在EmpMapper接口中添加方法。
List<Emp> selectAllBusiness();
在映射文件EmpMapper.xml编写SQL实现与接口方法绑定。
发现resultMap中没有配置collection标签,单表操作,查询emp。
<resultMap id="empMap4" type="Emp">
<id column="e_id" property="id"/>
<result column="e_name" property="name"/>
</resultMap>
<select id="selectAllBusiness" resultMap="empMap4">
select e_id,e_name,e_d_id from emp
</select>
在DeptMapper接口已经存在了selectById方法,且是单表操作。不需要再次添加了。
<resultMap id="deptMap2" type="Dept">
<id column="d_id" property="id"></id>
<result column="d_name" property="name"></result>
</resultMap>
<select id="selectById" resultMap="deptMap2">
select * from dept where d_id=#{id}
</select>
在测试类中实现业务装配。
public class Test {
public static void main(String[] args) throws IOException {
InputStream is = Resources.getResourceAsStream("mybatis.cfg.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = factory.openSession();
DeptMapper deptMapper = session.getMapper(DeptMapper.class);
EmpMapper empMapper = session.getMapper(EmpMapper.class);
List<Emp> list = empMapper.selectAllBusiness();
// 循环遍历,每次调用DeptMapper的根据主键查询方法。这就是所谓的业务装配
list.forEach(emp->{
emp.setDept(deptMapper.selectById(emp.getId()));
});
System.out.println(list);
session.close();
}
}
二、延迟加载(面试题)
延迟加载只能出现在多表联合查询的N+1方式中。
表示当执行当前方法时,是否立即执行关联方法的SQL。
1.测试默认情况下效果
以EmpMapper接口的selectAllN1()方法进行举例:当前方法的作用是查询全部Emp信息,并且调用DeptMapper的selectById方法,同时查询Dept的内容。
在测试类中中调用EmpMapper的selectAllN1()方法。
重要提示:
绝对不能输出list,如果输出list对象,表示使用了emp对象。如果输出list对象,关联的SQL一定被执行。
public class Test {
public static void main(String[] args) throws IOException {
InputStream is = Resources.getResourceAsStream("mybatis.cfg.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = factory.openSession();
EmpMapper empMapper = session.getMapper(EmpMapper.class);
List<Emp> list = empMapper.selectAllN1();
// System.out.println(list); 不能输出
session.close();
}
}
观察控制台。发现依然执行查询dept表的SQL。
说明此时MyBatis的延迟加载没有生效。
2.启用延迟加载
配置延迟加载有两种方式:
全局配置。整个项目所有N+1位置都生效。
局部配置。只配置某个N+1位置。
两种方式需要选择其中一种,如果两种方式都使用了,局部配置方式生效。
2.1全局配置方式
官方文档全局设置属性说明
属性名 | 解释说明 | 可取值 | 默认值 |
---|---|---|---|
lazyLoadingEnabled | 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。 | true | false | false |
aggressiveLazyLoading | 开启时,任一方法的调用都会加载该对象的所有延迟加载属性。 否则,每个延迟加载属性会按需加载(参考 lazyLoadTriggerMethods )。 | true | false | false (在 3.4.1 及之前的版本中默认为 true) |
根据上面说明:从3.4.1版本开始需要在MyBatis全局配置文件里面配置lazyLoadingEnabled=true即可在当前项目所有N+1的位置开启延迟加载。
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
再次运行测试类,发现输出的SQL中只有Emp的查询SQL。
2.2 局部配置方式
局部配置方式需要在collection或association标签中配置fetchType属性。fetchType可取值:lazy(延迟加载)和earge(立即加载)。
当配置了fetchType属性后,全局settings的配置被覆盖,对于当前标签以fetchType属性值为准。
<resultMap id="empMap2" type="Emp">
<id column="e_id" property="id"/>
<result column="e_name" property="name"/>
<association property="dept" javaType="Dept"
select="com.bjsxt.mapper.DeptMapper.selectById" column="e_d_id"
fetchType="lazy"></association>
</resultMap>
<select id="selectAllN1" resultMap="empMap2">
select e_id,e_name,e_d_id from emp
</select>
三、缓存(面试题)
1.缓存介绍
缓存是一种临时存储少量数据至内存或者是磁盘的一种技术.减少数据的加载次数,可以降低工作量,提高程序响应速度缓存的重要性是不言而喻的。
MyBatis的缓存将相同查询条件的SQL语句执行一遍后所得到的结果存在内存或者某种缓存介质当中,当下次遇到一模一样的查询SQL时候不在执行SQL与数据库交互,而是直接从缓存中获取结果,不再查询数据库,提升了性能;尤其是在查询越多、缓存命中率越高的情况下,使用缓存对性能的提高更明显。
MyBatis分为一级缓存和二级缓存,同时也可配置关于缓存设置。一级存储是SqlSession上的缓存,二级缓存是在SqlSessionFactory(namespace)上的缓存。默认情况下,MyBatis开启一级缓存,没有开启二级缓存。当数据量大的时候可以借助一些第三方缓存框架或Redis缓存来协助保存Mybatis的二级缓存数据。
2.一级缓存
一级缓存是SqlSession级缓存。只要是同一个SqlSession对象(必须是同一个)调用同一个<select>
标签相同参数值时(不同<select>
完全相同的SQL不会走同一个缓存),将直接使用缓存数据,而不会访问数据库。
重要提示:
一级缓存想要生效,必须同时满足3个条件:
- 同一个SqlSession对象。
- 同一个select标签。本质为底层同一个JDBC的Statemen对象
- 完全相同的SQL,包含SQL的参数值也必须相同
insert、delete、update操作会清空一级缓存数据。
commit也会清空缓存。
2.1 一级缓存流程图
命中缓存:从Map中查询是否存在指定key。如果存在表示命中缓存,如果不存在这个key,需要访问数据库。
更新到缓存:把查询结果put到map中。
2.2 代码演示
以DeptMapper接口的selectById(int id)方法进行演示。目前selectById在映射文件的配置是
<resultMap id="deptMap2" type="Dept">
<id column="d_id" property="id"></id>
<result column="d_name" property="name"></result>
</resultMap>
<select id="selectById" resultMap="deptMap2">
select * from dept where d_id=#{id}
</select>
2.2.1 同一个SqlSession下测试一级缓存
在测试类中测试同一个SqlSession对于相同参数和不同参数是否命中缓存
public class Test {
public static void main(String[] args) throws IOException {
InputStream is = Resources.getResourceAsStream("mybatis.cfg.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = factory.openSession();
DeptMapper deptMapper = session.getMapper(DeptMapper.class);
System.out.println("第一次查询:id=1");
Dept dept = deptMapper.selectById(1);
// 如果有DML操作,缓存会被清空
// deptMapper.insert();
System.out.println("第二次查询:id=1");
Dept dept2 = deptMapper.selectById(1);
System.out.println("第三次查询:id=2");
Dept dept3 = deptMapper.selectById(2);
session.close();
}
}
通过结果可以发现:
(1)第一次查询id=1时,执行了SQL
(2)第二次查询id=1时,没有执行SQL
(3)第三次查询id=2时,执行了SQL
2.2.2 不同SqlSession对象下测试一级缓存
创建两个SqlSession对象,每个对象执行完成相同的操作。
public class Test {
public static void main(String[] args) throws IOException {
InputStream is = Resources.getResourceAsStream("mybatis.cfg.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
// 第一个SqlSession对象
SqlSession session = factory.openSession();
DeptMapper deptMapper = session.getMapper(DeptMapper.class);
System.out.println("第一次查询:id=1");
Dept dept = deptMapper.selectById(1);
System.out.println("第二次查询:id=1");
Dept dept2 = deptMapper.selectById(1);
System.out.println("第三次查询:id=2");
Dept dept3 = deptMapper.selectById(2);
session.close();
// 第二个SqlSession对象
SqlSession session2 = factory.openSession();
DeptMapper deptMapper2 = session2.getMapper(DeptMapper.class);
System.out.println("2:第一次查询:id=1");
Dept dept21 = deptMapper2.selectById(1);
System.out.println("2:第二次查询:id=1");
Dept dept22 = deptMapper2.selectById(1);
System.out.println("2:第三次查询:id=2");
Dept dept23 = deptMapper2.selectById(2);
session2.close();
}
}
观察结果可以发现,第二个SqlSession对象即使里面的SQL完全和第一个SqlSession对象执行的SQL相同,也不会走缓存。
说明了:一级缓存有效范围是同一个SqlSession对象。
3.二级缓存
二级缓存是以namespace为标记的缓存,可能要借助磁盘,磁盘上的缓存,可以是由一个SqlSessionFactory创建的SqlSession之间共享缓存数据。默认并不开启。下面的代码中创建了两个SqlSession,执行相同的SQL语句,尝试让第二个SqlSession使用第一个SqlSession查询后缓存的数据。
二级缓存生效条件:
- 同一个SqlSessionFactory对象。
- 同一个方法()
- SQL完全相同。
重要提示:
二级缓存默认不开启,需要手动开启。
只有当SqlSession执行commit或close时才会把一级缓存数据,刷新到二级缓存中
上面在测试一级缓存时,发现不同SqlSession对象下一级缓存不生效。下面演示配置二级缓存生效的步骤:
-
全局开关:在mybatis.xml文件中的标签配置开启二级缓存
<settings> <setting name="cacheEnabled" value="true"/> </settings>
-
分开关:在要开启二级缓存的mapper文件中开启缓存
使用
<cache/>
配置时,注解的查询无法缓存<mapper namespace="com.bjsxt.mapper.EmpMapper"> <cache/> </mapper>
-
二级缓存未必完全使用内存,有可能占用硬盘存储,缓存中存储的JavaBean对象必须实现序列化接口
public class Dept implements Serializable { }
经过设置后,查询结果如图所示。发现第一个SqlSession会首先去二级缓存中查找,如果不存在,就查询数据库,在commit()或者close()的时候将数据放入到二级缓存。第二个SqlSession执行相同SQL语句查询时就直接从二级缓存中获取了。
注意:
-
MyBatis的二级缓存的缓存介质有多种多样,而并不一定是在内存中,所以需要对JavaBean对象实现序列化接口。
-
二级缓存是以 namespace 为单位的,不同 namespace 下的操作互不影响
-
查询数据顺序 二级–>一级—>数据库—>把数据保存到一级,当sqlsession关闭或者提交的时候,把数据刷入到二级缓存中
-
执行了DML操作,会清空一级缓存,所以数据更不可能到达二级缓存中。
-
cache 有一些可选的属性 type, eviction, flushInterval, size, readOnly, blocking。
<cache type="" readOnly="" eviction=""flushInterval=""size=""blocking=""/>
属性 | 含义 | 默认值 |
---|---|---|
type | 自定义缓存类,要求实现org.apache.ibatis.cache.Cache接口 | null |
readOnly | 是否只读true:给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。false:会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全 | false |
eviction | 缓存策略LRU(默认) – 最近最少使用:移除最长时间不被使用的对象。FIFO – 先进先出:按对象进入缓存的顺序来移除它们。SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。 | LRU |
flushInterval | 刷新间隔,毫秒为单位。默认为null,也就是没有刷新间隔,只有执行update、insert、delete语句才会刷新 | null |
size | 缓存对象个数 | 1024 |
blocking | 是否使用阻塞性缓存BlockingCachetrue:在查询缓存时锁住对应的Key,如果缓存命中了则会释放对应的锁,否则会在查询数据库以后再释放锁,保证只有一个线程到数据库中查找指定key对应的数据 false:不使用阻塞性缓存,性能更好 | false |
- 如果在加入Cache元素的前提下让个别select 元素不使用缓存,可以使用useCache属性,设置为false。useCache控制当前sql语句是否启用缓存 flushCache控制当前sql执行一次后是否刷新缓存
<select id="findByEmpno" resultType="emp" useCache="true" flushCache="false">
四、四大核心接口介绍及执行流程(面试题)
1. 四大核心接口介绍
MyBatis执行过程中涉及到非常重要的四个接口,这个四个接口成为MyBatis的四大核心接口:
-
Executor执行器,执行器负责整个SQL执行过程的总体控制。默认SimpleExecutor执行器。
-
StatementHandler语句处理器,语句处理器负责和JDBC层具体交互,包括prepare语句,执行语句,以及调用ParameterHandler.parameterize()。默认是PreparedStatementHandler。
-
ParameterHandler参数处理器,参数处理器,负责PreparedStatement入参的具体设置。默认使用DefaultParameterHandler。
-
ResultSetHandler结果集处理器,结果处理器负责将JDBC查询结果映射到java对象。默认使用DefaultResultSetHandler。
2.四大核心接口对应的JDBC代码
对应的JDBC代码
3.四大核心接口执行顺序按照下图进行执行
4. 通过断点测试执行流程
可以通过对四大核心接口的实现类中核心方法添加断点。
SimpleExecutor -> doQuery() 方法(必须以查询作为测试,其他类型SQL使用不同方法)
DefaultParameterHandler -> setParameters
PreparedStatementHandler ->query
DefaultResultHandler -> handleResult
里面需要注意的是会在SimpleExecutor先实例化Statement对象,然后调用DefaultParameterHandler 的setParameters,再然后调用PreparedStatementHandler的query。
示例使用DeptMapper的selectById方法进行测试。
方法打断点。通过IDEA的Debug工具调到下个断点查看这几个方法被调用顺序。
最终详细看一遍完整流程。看看四大核心组件是如何调用的。
5.完整执行流程文字说明
(1)使用执行器Executor控制整个执行流程
(2)实例化StatementHandler,进行SQL预处理
(3)使用ParameterHandler设置参数
(4)使用StatementHandler执行SQL
(5)使用ResultSetHandler处理结果集
五、自定义插件-interceptor(面试题)
在上面源码查看过程中会发现下面这样的一段代码.
表示的意思就是让MyBatis插件(MyBatis插件都是基于interceptor实现的)生效。其中InterceptorChain表示拦截器链,本质就是List集合。允许多个拦截器生效。
MyBatis中支持扩展插件,所有插件都必须实现org.apache.ibatis.plugin.Interceptor接口.源码如下:
Interceptor拦截器,可以对四大核心接口进行拦截,拦截的效果和Java EE中学习的Filter有点类似,可以拦截前后做点事情。后面我们会学习别人写好的MyBatis分页插件,今天我们自己手写个MyBatis分页插件。
1.实现流程
以Emp表举例。实现Emp分页查询。
分页插件可以实现的效果:
编写SQL时是查询全部的SQL,通过插件实现类在后面拼接分页关键字。
在下面实现流程中只考虑MySQL分页,没有考虑Oracle等其他数据库的分页。
1.1 在EmpMapper接口中添加查询全部方法
List<Emp> selectAllpage();
1.2 在EmpMapper.xml中编写SQL
<resultMap id="empMap5" type="Emp">
<id column="e_id" property="id"/>
<result column="e_name" property="name"/>
</resultMap>
<select id="selectByEid" resultMap="empMap5">
select e_id,e_name,e_d_id from emp
</select>
1.3 创建分页参数工具类
创建com.bjsxt.interceptor.MyPageHelper。
MyPageHelper类名称自定义的。作用为了设置分页的条件。没有这个类,实现的分页必须写成固定值。
全局变量设置为protected表示同包能方法,此类会和插件类放在一个包下。
startPage提供的静态方法,方便以后设置分页的条件。
public class MyPageHelper {
protected static Integer pageStart;// 分页起始行
protected static Integer pageSize;// 查询条数
public static void startPage(int pageStartArg,int pageSizeArg){
pageStart = pageStartArg;
pageSize = pageSizeArg;
}
}
1.4 创建插件实现类
新建com.bjsxt.interceptor.MyPageHelperInterceptor。
注意下类上面的注解。
@Intercepts 表示当前是一个拦截器。
@Signature 表示签名。
type:拦截器主要拦截的类型.可以是四大核心接口。
method:拦截type中的哪个方法
args:method对应方法的参数。这个很重要,因为Java支持方法重载,不设置参数可能无法精确到具体的方法。
package com.bjsxt.interceptor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import java.sql.Connection;
import java.util.Properties;
// 必须有的注解
@Intercepts(value = {@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class,Integer.class}
)})
public class MyPageHelperInterceptor implements Interceptor {
// 这个方法的作用:实现拦截业务
// 对于自定义分页插件来说,这个方法的作用就是在后面拼接limit x,y
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取拦截的对象
StatementHandler target = (StatementHandler) invocation.getTarget();
// 获取SQL绑定器
BoundSql boundSql = target.getBoundSql();
// 获取SQL语句
String sql = boundSql.getSql();
// 判断是否已经设置了分页条件
if(MyPageHelper.pageStart!=null&&MyPageHelper.pageSize!=null) {
// 注意limit前面空格
sql += " limit " +MyPageHelper.pageStart+","+MyPageHelper.pageSize;
}
// 把修改后的SQL重新放回去
MetaObject metaObject = SystemMetaObject.forObject(target);
// 第一个参数为固定值,表示绑定的SQL
metaObject.setValue("parameterHandler.boundSql.sql",sql);
// 放行继续执行
return invocation.proceed();
}
// 设置拦截器是否生效
@Override
public Object plugin(Object target) {
// System.out.println(target.getClass().getName()); 通过输出可以查询执行此方法时目标对象
// 每次调用四大核心接口都会调用此方法,只需要对StatementHandler进行处理
if(target instanceof StatementHandler){
return Plugin.wrap(target,this);
}
return target;
}
@Override
public void setProperties(Properties properties) {
// 获取到后面配置插件时的属性,设定属性名为dialect(方言),这个属性是自定义的。
System.out.println(properties.getProperty("dialect"));
}
}
1.5 配置插件
在MyBatis全局配置文件中配置插件,如果不配置,插件是无法被识别的。
需要把<plugins>
标签配置在<environment>
标签的上面。这点是DTD的要求。
里面的属性对于当前演示没有实际作用,单纯为了演示如果传递属性。
<plugins>
<plugin interceptor="com.bjsxt.interceptor.MyPageHelperInterceptor">
<property name="dialect" value="mysql"/>
</plugin>
</plugins>
1.6 编写测试类,测试插件
下面一定要注意在执行查询全部方法之前,要设置分页插件条件。否则MyPageHelper中属性没有设置是不会进入到MyPageHelperInterceptor里面if条件的。也就不会拼接limit关键字。
public class Test {
public static void main(String[] args) throws IOException {
InputStream is = Resources.getResourceAsStream("mybatis.cfg.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = factory.openSession();
EmpMapper empMapper = session.getMapper(EmpMapper.class);
// 设置分页条件代码必须放在调用SQL上面
MyPageHelper.startPage(0,2);
List<Emp> list = empMapper.selectAllpage();
System.out.println(list);
session.close();
}
}
1.7 查看控制台结果
通过控制台效果图可以看出来
1. 能够成功获取配置插件时的属性值
2. 在自己定义的SQL后面拼接上了limit关键字
六、执行器类型(面试题)
MyBatis的执行器都实现了Executor接口。作用是控制SQL执行的流程。
在MyBatis中执行器共分为三个类型:SimpleExecutor、ReuseExecutor、BatchExecutor。
- SimpleExecutor是默认的执行器类型。每次执行query和update(DML)都会重新创建Statement对象。
- ReuseExecutor只预编译一次。把Statement放入到Map中,后面复用Statement(JDBC)对象。
- BatchExecutor。用在update(DML)操作中。所有SQL一次性提交。
除了上面的三种执行器还有个Executor接口的实现类CachingExecutor,这个是处理缓存的。无论使用上面三种执行器中的哪个。都是会执行CachingExecutor的
在项目可以通过factory.openSession()方法参数设置执行器类型。通过枚举类型ExecutorType进行设置。
也可以在全局配置文件中通过中defaultExecutorType 进行全局设置(不推荐)。
执行器主要控制的就是Statement对SQL如何进行操作。有效范围:同一个SqlSession对象。
1.SimpleExecutor
SimpleExecutor 是MyBatis默认的执行器类型。在没有明确设置执行器类型时,默认就是这个类型。
下面代码测试的是根据主键查询结果,主键的值分别是1和2.
public class Test {
public static void main(String[] args) throws IOException {
InputStream is = Resources.getResourceAsStream("mybatis.cfg.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = factory.openSession(ExecutorType.SIMPLE);
DeptMapper deptMapper = session.getMapper(DeptMapper.class);
Dept dept = deptMapper.selectById(1);
Dept dept1 = deptMapper.selectById(2);
System.out.println(dept);
System.out.println(dept1);
session.close();
}
}
查看控制台结果,发现每次都是预编译、设置参数、获取结果
2.ReuseExecutor
ReuseExecutor主要用在执行时,重用预编译SQL。在同一个SqlSession对象中下次调用已经预编译的SQL直接设置参数。
public class Test {
public static void main(String[] args) throws IOException {
InputStream is = Resources.getResourceAsStream("mybatis.cfg.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = factory.openSession(ExecutorType.REUSE);// 只有这里类型变了
DeptMapper deptMapper = session.getMapper(DeptMapper.class);
Dept dept = deptMapper.selectById(1);
Dept dept1 = deptMapper.selectById(2);
System.out.println(dept);
System.out.println(dept1);
session.close();
}
}
观察结果,可以清楚的看到,只有预编译一次。第二次直接设置参数。
3.BatchExecutor
BatchExecutor底层使用JDBC的批量操作。每一条SQL都不会立即执行,而是放到了List中,最终统一提交。
由于底层的批量操作只支持DML操作,所以BatchExecutor也主要用在批量新增、批量删除、批量修改中。
在DeptMapper接口中添加一个新增方法。为图简单,直接使用了注解。
@Insert("insert into dept values(default,#{name})")
int insert(String name);
修改测试类代码
public class Test {
public static void main(String[] args) throws IOException {
InputStream is = Resources.getResourceAsStream("mybatis.cfg.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = factory.openSession(ExecutorType.BATCH);// 主要是这里设置了类型
DeptMapper deptMapper = session.getMapper(DeptMapper.class);
int index = deptMapper.insert("行政部");
int index2 = deptMapper.insert("财务部");
session.commit();// 别忘记提交事务
session.close();
}
}
观察结果。可以发现和ReuseExecutor,但是比ReuseExecutor少了执行结果项。因为BatchExecutor是最终统一提交结果。
七、MyBatis执行原理详解(较常见面试题)
对于MyBatis执行原理来说,不同的情况有不同的执行过程,大致可以分下面几种情况:
(1)接口绑定方式、使用SqlSession执行方法
(2)是否有插件
(3)不同的执行器
为了演示一个较为详细的执行流程。整个讲解过程中以SimpleExecutor作为执行器,包含接口和映射文件的接口绑定方案,同时带有自定义插件。其实就是上面自定义插件的代码^
MyBatis项目不能自动运行,测试代码如下,每一行都进入源码进行观察
public class Test {
public static void main(String[] args) throws IOException {
InputStream is = Resources.getResourceAsStream("mybatis.cfg.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = factory.openSession();
EmpMapper empMapper = session.getMapper(EmpMapper.class);
MyPageHelper.startPage(0,2);
List<Emp> list = empMapper.selectAllpage();
System.out.println(list);
//session.commit();
session.close();
}
}
1. 获取配置文件输入流对象
InputStream is = Resources.getResourceAsStream("mybatis.cfg.xml");
这行代码底层比较简单,通过ClassLoader获取配置文件输入流对象。
2.创建SqlSessionFactory
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
其中new SqlSesionFactoryBuilder()只是进行实例化构建器对象,并没有做其他额外操作。
重点需要跟踪build(is);方法。
XMLConfigBuilder 负责使用DOM操作把XML文件流解析为document对象。
parser.parser();解析配置文件具体内容,并存放到Configuration对象存储。
SqlSessionFactory最终使用DefaultSqlSessionFactory进行实例化。
XMLConfigBuilder构造购房使用XPathParser
XPathParser构造方法源码。如果继续进入到createDocument,会发现里面是DOM解析的代码。
3.创建SqlSession对象
SqlSession session = factory.openSession();
进入几次方法,会看到下面代码。
重点记忆:
每个SqlSession对象对应一个事务对象。
SqlSession接口的实现类是DefaultSqlSession。里面存储了从配置文件解析出来的信息(configuration)
4.创建接口代理对象
EmpMapper empMapper = session.getMapper(EmpMapper.class);
多次进入方法,会看到下面代码。代码中使用了JDK动态代理创建接口代理对象。这也是MyBatis可以没有实现类也能创建接口对象的原因。
5.执行接口中的方法
List<Emp> list = empMapper.selectAllpage();
这行代码底层涉及内容比较多。涉及到了四大核心接口和插件的执行。执行过程和四大核心接口执行过程类似。
6.提交事务
提交事务过程会清空本地缓存,清空存储Statement的集合对象,然后提交事务。
7.关闭
关闭的时候会关闭游标,把一些涉及到的对象设置为null
8. MyBatis执行原理文字说明
首先加载全局配置文件为输入流,交给XPathParser解析器解析为Document文档对象,然后使用DOM解析Document文档对象,把解析结果存放在Configuration配置类中。
通过DefaultSqlSessionFactory实例化工厂,实例化时会在全局存储Configuration配置对象。
在通过工厂对象创建DefaultSqlSession对象,在创建过程中,会同时创建Transaction事务对象、Executor执行器对象。如果当前项目有Interceptor拦截器,创建执行器时会执行拦截器。
通过JDK提供的Proxy创建接口的动态代理对象。
可以通过接口的代理对象调用方法。在调用方法时MyBatis会根据方法的类型判断调用SqlSession的哪个方法。例如:selectList、selectOne、update、insert等。
确定好具体调用SqlSession的哪个方法后,会按照执行器类型执行MyBatis四大核心接口,执行时也会触发拦截器Interceptor。最终会返回SQL的执行结果。
执行完方法后需要提交事务,提交时清空缓存、清除存储的Statement对象。
最后关闭SqlSession对象,释放资源。
以上就是MyBatis执行原理。