MyBatis(二)MyBatis体系结构与工作原理

MyBatis体系结构与工作原理

工作流程分析

MyBatis的主要工作流程

  1. 解析配置文件
    首先在MyBatis启动的时候我们要去解析配置文件,包括全局配置文件和映射器配置文件,这里包含了我们怎样控制MyBatis的行为,和我们要对数据库下达的指令,也就是SQL信息。我们会把它们解析成一个configuration对象。

  2. 提供操作接口
    接下来就是我们操作数据库的接口,它在应用程序和数据库中间,代表我们跟数据库之间的一次连接,这个就是SqlSession对象。我们获得一个会话(SqlSession),必须有一个会话工厂SqlSessionFactory。SqlSessionFactory里面又必须包含我们的所有的配置信息,所以我们会通过一个Build(SqlSessionFactoryBuild)来创建工厂类。
    MyBatis是对JDBC的封装,也就意味着底层一定会出现JDBC的一些核心对象,例如执行SQL的Statement,结果集ResultSet。在MyBatis里面,SqlSession只是提供给应用的一个接口,还不是SQL的真正执行对象。

  3. 执行SQL操作
    SqlSession持有一个Executor对象,用来封装对数据库的操作。
    在执行器Executor执行Query或update操作的时候我们创建一系列的对象,来处理参数、执行SQL、处理结果集,这里我们把它简化为一个对象StatementHandler,可以把它理解为对Statement的封装,
    在这里插入图片描述

架构层次与模块划分

在MyBatis的主要工作流程里面,不同的功能是由很多不同的类协作完成的,他们分布在MyBatis jar包的不同的package里面。
在这里插入图片描述

接口层

首先是接口层,核心对象是SqlSession,它是上层应用和MyBatis打交道的桥梁,SqlSession上定义了非常多的对数据库的操作方法。接口层在收到调用的请求的时候,会调用核心处理层的相关模型来完成具体的数据库操作。

核心处理层

核心处理层主要做了几件事:
1. 把接口中传入的参数解析并且映射成JDBC类型;
2. 解析xml文件中的SQL语句,包括插入参数,和动态SQL生成;
3. 执行SQL语句
4. 处理结果集,并且映射成Java对象

基础支持层

抽取出来的通用功能,用来支持核心处理层的功能

缓存详解

缓存是一般的ORM框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力

缓存体系结构

MyBatis跟缓存相关的类都在cache包里面,其中有一个Cache接口,只有个一个默认的实现类PerpetualCache,它是用HashMap实现的。
PerpetualCache这个对象一定会创建,所以这个叫做基础缓存。但是缓存又可以有很多额外的功能,比如回收策略、日志记录、定时刷新等。如果有需要的话,皆可以给基础缓存加上这些功能过呢,如果不需要,就不加。

除了基本缓存之外,MyBatis也定义了很多的装饰器,同样实现了Cache接口,通过这些装饰器可以额外实现很多功能。

所有的缓存实现类总体可以分为三类:基本缓存、淘汰算法缓存、装饰器缓存。
在这里插入图片描述

一级缓存

一级缓存也叫本地缓存(local Cache),MyBatis的一级缓存实在会话(SqlSession)层面进行缓存的。MyBatis的一级缓存是默认开启的,不需要进行任何的配置(localCacheScope设置为STATEMENT关闭一级缓存)

BaseExecutor的query()

if(configuration.getLocalCacheScope() == LocalCacheScope.STATEMET) 	{
	clearLocalCache();
}

首先必须弄清楚一个问题,在MyBatis执行的流程里面,涉及到这么多的对象,那么缓存PerpetualCache应该放在那一个对象中去维护。
如果在同一个会话里面共享一级缓存,最好的办法实在sqlSession里面创建的,作为SqlSession的一个属性,跟SqlSession共存亡,这样就不需要为SqlSession编号,再根据SqlSession的编号去查找对应的缓存了。
DefaultSqlSession里面只有两个对象属性:Configuration和Executor。
Configuration是全局的不属于SqlSession,所有缓存只能放在Executor里面维护。实际它实在基本执行器的构造函数中持有了PerpetualCache。
在同一个会话里面,多次执行相同的SQL语句,会直接从内存中取到缓存的结果,不会再发送SQL到数据库。但是不同的会话里面,及时执行的SQL相同,也不能使用到一级缓存。
在这里插入图片描述

一级缓存在BaseExecutor的query()——queryFromDatabase()中存入,在queryFromDatabase之前会get()。

一级缓存什么时候put, 什么时候get, 什么时候clear
put:

BaseExecutor的queryFromDataBase()
localCache.putObject(key, list);

get:

BaseExecutor的query()
list = resultHandler == null ? (List<E>)localCache.getObject(key) : null;

clear:

BaseExecutor的update或delete
clearLocalCache();

在query的sql语句中设置(flushCache=true)时候,query也会清空缓存。

一级缓存的不足:使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话或者分布式环境中,会存在查到过时数据的问题。

二级缓存

二级缓存是用来解决一级缓存不能跨会话共享的问题,范围是namespace级别的,可以被多个SqlSession共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。

思考:如果开启了二级缓存,二级缓存应该是工作在一级缓存之前还是在一级缓存之后?二级缓存在哪里维护?

作为一个作用范围更广的缓存,它肯定是在SqlSession的外层,否则不可能被多个SqlSession共享。
而一级缓存实在SQLSession内部的,所有第一个问题:工作在一级缓存之前,也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。
第二个问题:二级缓存放在哪个对象中去维护?要跨会话共享的话,SqlSession本身和它里面的BaseExecutor已经满足不了需求了,那我们应该在BaseExecutor之外创建一个对象。
但是二级缓存不一定开启,也就是说开启了二级缓存,就启动这个对象,如果没有就不用这个对象(典型的装饰器模式)。
实际上MyBatis用了一个装饰器的类来维护,就是CachingExecutor。如果启用了二级缓存,MyBatis在创建Executor对象的时候会对Executor进行装饰。

CachingExecutor对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有就委派交给真正的查询器Executor实现类,比如SimpleExecute来执行查询,在走到一级缓存的流程。最后把结果缓存起来,并返回给用户。
在这里插入图片描述
开启二级缓存的方法

第一步:在mybatis-config.xml中配置了(可以不配置,默认为true)

<setting name="cacheEnabled" value="true" />

只要没有显示地设置cacheEnabled=false,都会用CachingExecutor装饰基本的执行器(SIMPLE,REUSE,BATCH)。

二级缓存的总开关是默认打开的,但是每一个Mapper的二级缓存开关是默认关闭的。一个Mapper要用到二级缓存,还要单独打开开关。

第二步:在Mappe.xml中配置cache 标签:

<cache type="org.apache.ibatis.cache.impl.PerpetualCache" 
size="1024" <!-- 最多缓存对象个数,默认1024 -->
evication="LRU" <!-- 回收策略 -->
flushInterval="120000" <!-- 自动刷新时间 ms, 未配置时只有调用时刷新 -->
readOnly="false" /> <!--默认是false(安全),改为true可读写,对象必须支持序列化 -->

在这里插入图片描述
Mapper.xml配置了cache标签后,select() 会被缓存。update()、delete()、insert()会刷新缓存。

如果二级缓存拿到了结果,就直接返回,否则再到一级缓存中,最后到数据库。

如果cacheEnabled=true,Mapper.xml没有配置<cache标签,还会有二级缓存吗?还会出现CachingExecutor包装对象吗?
只要cacheEnabled=true基本执行器就会被装饰。没有配置<cache标签,决定了在启动的时候不会创建这个mapper的Cache对象,最终会影响到CachingExecutor query方法里面的判断:

if(cache != null){

也就是说,此时会有装饰,但是没有cache对象,依然不会走二级流程。

如果一个Mapper需要开启二级缓存,但是这个里面的某些查询方法对数据的实时性要求较高,不需要二级缓存:
此时我们可以在单个StatementID上显示关闭二级缓存(默认是true)

<select id = "selectById" resutlMap="BaseResultMap" userCache = "false">

CachingExecutor query方法有对这个属性的判断

if(ms.isUseCache() && resultHandler == null) {

事务不提交,二级缓存不存在。

因为二级缓存使用TransactionalCacheManager(TCM)来管理,最后又调用了TransactionalCache的getObject()、putObject()和commit()方法,TransactionalCache里面又持有真正的Cache对象。
在putObject的时候,只是添加到了entriesToAddOnCommit里面,只有它的commit()方法被调用的时候才会调用flushPendingEntity()真正写入缓存。它就是在DefaultSqlSession调用commit()的时候被调用的。

为什么增删改操作会清空缓存?
在CachingExecutor的update()方法里面会调用flushCacheIfRequired(ms),isFlushCacheRequired就是从标签里面的flushCache的值。而增删改操作的flushCache属性默认为true

什么时候开启二级缓存?

  1. 因为都有的增删改都会刷新二级缓存,导致二级缓存失效,所以适合在以查询为主的应用中使用,例如历史交易、订单查询等。否则缓存就失去了意义。
  2. 如果多个namespace中有针对同一个表的操作,比如Blog表,如果在一个namespace中刷新了缓存,另一个namespace中没有刷寻,就会出现读到脏数据的情况,所以推荐在一个Mapper里面只操作单表的情况下使用。

如果要让多个namespace共享一个二级缓存,应该怎么做?
跨namespace的缓存共享的问题,可以使用cache-ref解决:

<cache-ref namespace="" />

cache-ref代表引用别的命名空间的cache配置,两个命名空间的操作使用的是同一个Cache。在关联的表比较小,或者按照业务可以对表进行分组的时候可以使用。
注意,在这种情况下,多个Mapper的操作都会引起缓存刷新,缓存的意义已经不大了。

第三方缓存做二级缓存

MyBatis管官方提供了一些第三方缓存集合方式,比如ehcache和redis:

<dependency>
	<groupId>org.mybatis.caches</groupId>
	<artifactId>mybatis-redis</artifactId>
	<version>1.0.0-beta2</version>
</dependency>

Mapper.xml配置,type使用RedisCache

<cache type="org.mybatis.caches.redis.RedisCache" 
	eviction="FIFO", flushInterval="60000" size="1024" readOnly = "true" />

redis.properties配置

host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
database=0

MyBatis源码解读

看源码注意事项

1. 带着问题看源码,猜想验证。
2. 不要只记忆流程,学会变成风格、设计思路(代码为什么这样写?如果不这么写呢?包括接口的定义,类的职责,涉及模式的应用,高级语法等)。
3. 先抓重点。
4. 记录核心流程和对象,总接层次、结构、关系和输出。
5. 培养看源码的信心和感觉。
6. debug源码

关于MyBatis源码,分为五步:

第一步:创建一个工厂类,配置文件的解析就是在这一步完成的,包括mybatis-config.xml和Mapper.xml
问题:解析的时候做了什么,产生了什么对象,解析的结果存在在哪里。解析的结果决定着我们后面有什么对象可以使用,和到哪里去取。

第二步:通过SqlSessionFactory创建一个SqlSession
问题:SqlSession上面定义了各种增删改查的API,是给客户端调用的。返回了什么实现类?除了SqlSession,还创建了什么对象,创建了什么环境?

第三步:获得一个Mapper对象
问题:Mapper是一个接口,没有实现类,是不能被实例化的,那获取到的这个Mapper对象是什么对象?为什么要从SqlSession里面去获取?为什么传进去一个接口,然后还要用接口类型来接收?

第四步:调用接口方法
问题:我们的接口没有创建实现类,为什么可以调用它的方法?那它调用的是什么方法?
这一步实际做的事情是执行SQL,那它有事根据什么找到XML映射器里面的SQL的?此外我们的方法参数是怎么转换成SQL参数的?获取到的结果集是怎么转换成对象的。

第五步:关闭session,这一步是必须要做的。

public void test() throw Exception {
	
	//1. 获取配置文件
	InputStream in = Resources.getResourceAsStream("mybatis-config.xml");

	//2. 创建SqlSessionFactory对象
	SqlSessionFactory factory = new SqlSessionFactoryBuilder().builder(in);

	//3. 创建SqlSession对象
	SqlSession sqlSession = factory.openSession();

	//4. 创建代理对象Mapper
	UserMapper mapper = sqlSession.getMapper(User.class);
	List<User> list = mapper.selectUserList();

	//5. 关闭session
	sqlSession.close();
}

MyBatis核心对象

  1. Configuration:
    相关对象:MapperRegistry,TypeAliasRegistry,TypaHandlerRegistry
    作用:包含了MyBatis的所有的配置信息

  2. SQLSession
    相关对象:SqlSessionFactory,DefaultSqlSession
    作用:对操作数据库的增删改查的API进行封装,提供给应用层使用

  3. Executor:
    相关对象:BaseExecutor,SimpleExecutor,BatchExecutor,ReuseExecutor
    作用:MyBatis执行器,是MyBatis调度的核心,负责SQL语句的生成和查询缓存的维护

  4. StatementHandler:
    相关对象:BaseStatementHandler,SimpleStatementHandler,PreparedStatementHandler,CallableStatementHandler
    作用:封装了JDBC Statement操作,负责对JDBC Statement的操作,如果设置参数、将Statement结果集转换成List集合

  5. ParameterHandler:
    相关对象:DefaultParameterHandler
    作用:把用户传递的参数转换成JDBC Statement所需要的参数

  6. ResultSetHandler:
    相关对象:DefaultResultSetHandler
    作用:把JDBC返回的ResultSet结果集对象换成List类型的集合

  7. MapperProxy:
    相关对象:MapperProxyFactory
    作用:触发管理类,用于代理Mapper接口方法

  8. MappedStatement:
    相关对象:SQLSource、BoundSql
    作用:MappedStatement维护了一条<select|update|delete|insert>节点的封装,标示一条SQL,包括了SQL信息、入参信息、出参信息

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值