一、多表查询(面试题,重要)
1. 介绍
在MyBatis框架中只有两种情况:当前表对应另外表是一行数据还是多行数据。转换到实体类上:当前实体类包含其它实体类一个对象还是多个对象。
转换到MyBatis的映射文件上:在<resultMap>
标签里面使用<association>
还是<collection>
标签就可以。
- 如果一个实体类关联另一个实体类的一个对象使用
<association>
。 - 如果一个实体类关联一个实体类的List集合对象,需要使用
<collection>
。
这两个标签根据编写的SQL,分为N+1查询和联合查询两种方式。
两种方式优缺点:
联合查询方式:
优点:一次查询。
缺点:SQL相对复杂。不支持延迟加载。
业务装配
优点:手动实现,灵活度高。
缺点:代码复杂。
N+1方式:
优点:SQL简单。支持延迟加载。
缺点:多做N次查询。
MyBatis多表查询时一定需要使用<resultMap>
标签,因为<association>
标签和<collection>
标签是<resultMap>
的子标签。
2. 准备
在ssm数据库中创建两张表:分别是Dept和Emp。在表设计时设定一个员工只能有一个部门。一个部门可以包含多个员工。
在项目下创建com.bjsxt.pojo.Dept
public class Dept {
private int id;
private String name;
private List<Emp> list;
// 省略无参数,有参数,getter/setter,toString()等方法
}
在项目下创建com.bjsxt.pojo.Emp实体类。
public class Emp {
private int id;
private String name;
private Dept dept;
// 省略无参数,有参数,getter/setter,toString()等方法
}
3. 联合查询(重要)
在mapper.xml文件中配置。
情况一( <association>):
<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>
情况二(<collection>) :
<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>
<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>
4. 业务装配
所谓的业务装配是不使用MyBatis进行装配。而是使用Java代码进行装配。具体体现在Web项目中,是在service里面通过Java代码实现结果的填充。
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();
}
}
5. N+1查询方式(重要)
- N+1查询方式命名由来:当查询Emp表中N条数据时,需要编写1条查询全部的SQL和N条根据外键列值作为另一张表主键查询条件的N条SQL语句。
- N+1查询方式,在进行操作时需要先分析出最终想要的结果需要包含对于两张表的单表查询语句是什么。
注意:条件的字段必须作为查询的字段,否则不生效。
5.1 查询员工信息同时包含部门信息
当存在调用和被调用关系时,按照正常编程习惯,都是先编写被调用方。
先编写映射文件DeptMapper.xml,实现接口方法与SQL绑定。
<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接口,在里面添加个查询全部的方法。
<resultMap id="empMap2" type="Emp">
<id column="e_id" property="id"/>
<result column="e_name" property="name"/>
<!-- 此处依然使用association填充单个对象属性值.property和javaTye依然需要写 -->
<!-- select 调用另一个查询的路径,同一个映射文件中,前面namespace可以省略-->
<!-- 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>
5.2 查询部门信息同时查询包含的员工信息
查询部门信息时同时包含上员工信息的流程和上面查询员工信息同时包含部门信息的流程是类似的。在加载集合类型属性时使用collection标签即可。
<collection property="list" ofType="Emp" select="com.bjsxt.mapper.EmpMapper.selectByEid" column="d_id"/>
二、延迟加载(面试题,重要)
延迟加载只能出现在多表联合查询的N+1方式中。表示当执行当前方法时,是否立即执行关联方法的SQL。
1. 启用延迟加载
延迟加载在默认情况下为不开启。
配置延迟加载有两种方式:
- 全局配置。整个项目所有N+1位置都生效。
- 局部配置。只配置某个N+1位置。
两种方式需要选择其中一种,如果两种方式都使用了,局部配置方式生效。
1.1 全局配置方式
从3.4.1版本开始需要在MyBatis全局配置文件里面配置lazyLoadingEnabled=true即可在当前项目所有N+1的位置开启延迟加载。
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
1.2 局部配置方式
局部配置方式需要在collection或association标签中配置fetchType属性。fetchType可取值:lazy(延迟加载)和earge(立即加载)。
局部配置会覆盖全局配置。
<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>
三、缓存(面试题,重点)
1. 缓存介绍
缓存是一种临时存储少量数据至内存或者是磁盘的一种技术。减少数据的加载次数,可以降低工作量,提高程序响应速度,缓存的重要性是不言而喻的。
MyBatis的缓存将相同查询条件的SQL语句执行一遍后所得到的结果存在内存或者某种缓存介质当中,当下次遇到一模一样的查询SQL时候不在执行SQL与数据库交互,而是直接从缓存中获取结果,不再查询数据库.
MyBatis分为一级缓存和二级缓存,同时也可配置关于缓存设置。
一级存储是SqlSession上的缓存。
二级缓存是在SqlSessionFactory(namespace)上的缓存。
默认情况下,MyBatis开启一级缓存,没有开启二级缓存。当数据量大的时候可以借助一些第三方缓存框架或Redis缓存来协助保存Mybatis的二级缓存数据。
2. 一级缓存
一级缓存是SqlSession级缓存。只要是同一个SqlSession对象(必须是同一个)调用同一个<select>
标签传入相同参数值时(不同<select>
完全相同的SQL不会走同一个缓存),将直接使用缓存数据,而不会访问数据库。
一级缓存想要生效的3个条件:
- 同一个SqlSession对象。
- 同一个select标签。本质为底层同一个JDBC的Statemen对象。
- 3完全相同的SQL,包含SQL的参数值也必须相同。
注意:
insert、delete、update操作会清空一级缓存数据。close(),commit也会清空缓存。
2.1 一级缓存流程图
命中缓存:从Map中查询是否存在指定key。如果存在表示命中缓存,如果不存在这个key,需要访问数据库。
更新到缓存:把查询结果put到map中。
3. 二级缓存
3.1 介绍
二级缓存是以 mapper.xml中的namespace 为标记的缓存。
可能要借助磁盘或磁盘上的缓存,可以由一个SqlSessionFactory创建的SqlSession之间共享缓存数据,默认并不开启。
二级缓存生效条件:
同一个SqlSessionFactory对象的SqlSession。
同一个方法(<select>)。
SQL语句完全相同。
注意:
二级缓存默认不开启,需要手动开启。只有当SqlSession执行commit或close时才会存储到二级缓存中。
3.2 配置二级缓存
1.全局开关:在mybatis.xml文件中的<settings>标签配置开启二级缓存
settings>
<setting name="cacheEnabled" value="true"/>
</settings>
2.分开关:在要开启二级缓存的mapper.xml文件中开启缓存使用<cache/>
配置。
注意:注解的查询无法缓存。
<mapper namespace="com.bjsxt.mapper.EmpMapper">
<cache/>
</mapper>
二级缓存未必完全使用内存,有可能占用硬盘存储,缓存中存储的JavaBean对象必须实现序列化接口
经过设置后,查询过程为:
第一个SqlSession会首先去二级缓存中查找,如果不存在,去一级缓存中查找,没有就查询数据库。若在一级缓存中查找到就保存到二级缓存(临时)。在commit()或者close()的时候将一级缓存中的数据放入到二级缓存(cache的缓存中)。
第二个SqlSession执行相同SQL语句查询时就直接从二级缓存中获取了。
3.3 二级缓存相关事项
1.注意事项
MyBatis的二级缓存的缓存介质有多种多样,而并不一定是在内存中,所以需要对JavaBean对象实现序列化接口。
二级缓存是以 namespace 为单位的,不同 namespace 下的操作互不影响。
查询数据顺序 二级-->一级--->数据库--->把数据保存到一级,当sqlsession关闭或者提交的时候(在commit()或者close()的时候),把一级缓存中数据刷新到二级缓存中。
执行了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 |
size | 缓存对象个数。 | 1024 |
2. 控制是否使用缓存
如果在加入Cache元素的前提下让个别select 元素不使用缓存,可以使用useCache属性,设置为false。
useCache控制当前sql语句是否启用缓存, flushCache控制当前sql执行一次后是否刷新缓存。
<select id="findByEmpno" resultType="emp" useCache="true" flushCache="false">
4.总结(重要)
4.1 一级缓存执行流程(默认开启)
1.根据调用的接口中的方法 + select语句 + ... 建立了缓存的 key。
2.从一级缓存(localCache)中获取key对应的数据。
无:从数据库查询,将查询结果存储到一级缓存中,(key,数据),返回查询到的数据
有:直接返回一级缓存中获取到的数据
3.一级缓存基于SqlSession,使用同一个SqlSession一级缓存生效‘
’ 注意:会清除一级缓存的操作
1.commit(), rollback()
2.insert(), update() ,delete()
3.close()
4.2 二级缓存执行流程(手动开启)
二级缓存在映射文件中通过 <cache/>开启, select标签中设置useCache="false",设置指定select标签不使用二级缓存。每一个映射文件对应着一个映射文件的二级缓存。有效范围为同一个映射文件中的sql 。
1.根据调用的接口中的方法 + select语句 + ... 建立了缓存的 key。
2.从当前映射文件的二级缓存中根据key获取对应数据。(真正存储二级缓存数据的集合:Cache 接口实现PerpetualCache的cache属性[本质为map集合])
无对应数据:
1.从一级缓存(localCache)中获取key对应的数据。
无:从数据库查询,将查询结果存储到一级缓存(key,数据),返回查询到的数据。
有:直接返回一级缓存中获取到的数据。
2.添加到二级缓存,返回数据。(临时存储缓存数据的集合:Cache接口实现类 TransactionalCache的 entriesToAddOnCommit属性[本质为map集合])
3.执行了commit(),close() 将临时缓存map集合(entriesToAddOnCommit)中数据存 储到真正存储缓存map集合(cache)中。
有:直接返回二级缓存中的数据。
注意: 添加,修改,删除 操作会清除二级缓存。
四、四大核心接口介绍及执行流程(面试题)
1.四大核心接口介绍
MyBatis执行过程中涉及到非常重要的四个接口,这四个接口为MyBatis的四大核心接口:
Executor执行器 | 执行器负责整个SQL执行过程的总体控制。默认SimpleExecutor执行器。 |
StatementHandler语句处理器 | 语句处理器负责和JDBC层具体交互,包括prepare语句,执行语句,以及调用ParameterHandler.parameterize()。默认是PreparedStatementHandler。 |
ParameterHandler参数处理器 | ParameterHandler参数处理器,参数处理器,负责PreparedStatement入参的具体设置。默认使用DefaultParameterHandler。 |
ResultSetHandler结果集处理器 | 结果处理器负责将JDBC查询结果映射到java对象。默认使用DefaultResultSetHandler。 |
2. 四大核心接口对应的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处理结果集
五、执行器类型(面试题)
1.介绍
MyBatis的执行器都实现了Executor接口。作用是控制SQL执行的流程。
BaseExecutor:主要是使用了模板设计模式,共性被封装在 BaseExecutor 中,容易变化的内容被分离到了子类中 。
SimpleExecutor:默认的执行器类型。每次执行query和update(DML)都会重新创建Statement对象。
ReuseExecutor:执行器会重用预处理语句。不会每一次调用都去创建一个新的 Statement 对象,而是会重复利用以前创建好的(如果SQL相同的话)。
BatchExecutor:用在update(DML)操作中。所有SQL一次性提交。适用于批量操作。
CachingExecutor:处理缓存的执行器。无论使用上面三种执行器中的哪个。都是会执行CachingExecutor。
在项目可以通过factory.openSession()方法参数设置执行器类型。通过枚举类型ExecutorType进行设置。
也可以在全局配置文件中通过<settings>中defaultExecutorType 进行全局设置(不推荐)。
执行器主要控制的就是Statement对SQL如何进行操作。
执行器有效范围:同一个SqlSession对象。
2. SimpleExecutor(MyBatis默认)
SimpleExecutor 是MyBatis默认的执行器类型。在没有明确设置执行器类型时,默认就是这个类型。
SqlSession session = factory.openSession(ExecutorType.SIMPLE);
查看控制台结果,发现每次都是预编译、设置参数、获取结果。
2. ReuseExecutor(重用预编译SQL)
ReuseExecutor主要用在执行时,重用预编译SQL。在同一个SqlSession对象中下次调用已经预编译的SQL直接设置参数。
SqlSession session = factory.openSession(ExecutorType.REUSE);// 只有这里类型变了
观察结果,可以清楚的看到,只有预编译一次。第二次直接设置参数。
3. BatchExecutor
BatchExecutor底层使用JDBC的批量操作。每一条SQL都不会立即执行,而是放到了List<Statement>中,最终统一提交。
由于底层的批量操作只支持DML操作,所以BatchExecutor也主要用在批量新增、批量删除、批量修改中。
factory.openSession(ExecutorType.BATCH);// 主要是这里设置了类型
观察结果。可以发现和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实例化工厂,实例SqlSession的对象。创建了SqlSession接口的实现类DefaultSqlSession对象,在创建过程中,会同时创建Transaction事务对象、Executor执行器对象。如果当前项目有Interceptor拦截器,创建执行器时会执行拦截器。
- 通过JDK提供的Proxy创建接口的动态代理对象。
- 通过接口的代理对象调用方法。在调用方法时MyBatis会根据方法的类型判断调用SqlSession的哪个方法。例如:selectList、selectOne、update、insert等。
- 确定好具体调用SqlSession的哪个方法后,会按照执行器类型执行MyBatis四大核心接口,执行时也会触发拦截器Interceptor。最终会返回SQL的执行结果。
- 执行完方法后需要提交事务,提交时清空缓存、清除存储的Statement对象。
- 最后关闭SqlSession对象,释放资源。