MyBatis 执行流程

首先回顾一下jdbc的执行流程。

MyBatis的执行流程也包含jdbc的执行流程,但是会做一些前置处理。

MyBatis

方法代理(MapperMethod)

使用动态代理调用,可以看到非常熟悉的 invoke 方法,这个invoke方法就是动态代理的逻辑,method.invoke()方法就是实现代理类对原始方法的调用。

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
public class MapperProxy<T> implements InvocationHandler, Serializable {

private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;

public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
}

会话(SqlSession)

SqlSession 是myBatis的门面(采用门面模式设计),核心作用是为用户提供API。API包括增、删、改、查以及提交、关闭等。其自身是没有能力处理这些请求的,所以内部会包含一个唯一的执行器 Executor,所有请求都会交给执行器来处理。

SqlSession 是SqlSessionFactory会话工厂创建出来的一个会话的对象,这个SqlSession对象用于执行具体的SQL语句并返回给用户请求的结果。

执行器(Executor)

Executor是一个大管家,核心功能包括:缓存维护、获取动态SQL、获取连接、以及最终的JDBC调用等。在图中所有蓝色节点全部都是在Executor中完成。

这么多事情无法全部亲力亲为,就需要把任务分派下去。所以Executor内部还会包含若干个组件:

  • 缓存维护:cache
  • 获取连接:Transaction
  • 获取动态sql:SqlSource
  • 调用jdbc:StatementHandler

上述组件中前三个和Executor是1对1关系,只有StatementHandler是1对多。每执行一次SQL 就会构造一个新的StatementHandler。StatementHandler的作用就是专门和JDBC打交道,执行SQL的。

SQL处理器(StatementHandler)

在JDBC中执行一次sql的步骤包括。预编译SQL、设置参数然后执行。StatementHandler就是用来处理这三步。

用于获取预处理器,共有三种类型。通过statementType="STATEMENT|PREPARED|CALLABLE" 可分别进行指定。

  • PreparedStatementHandler:带预处理的执行器
  • CallableStatementHandler:存储过程执行器
  • SimpleStatementHandler:基于Sql执行器

同样它也需要两个助手分别是:

  • 设置参数:ParameterHandler
  • 读取结果:ResultSetHandler,可在SqlSession中查询时自行定义ResultSetHandler

另外的执行是由它自己完成。

主键生成

在平时开发的时候经常会有这样的需求,插入数据返回主键,或者插入数据之前需要获取主键,这样的需求在 mybatis 中也是支持的。只需要在 xml 里配置 useGenerateKey = true 就好了。

其中主要的逻辑部分就在 KeyGenerator 中,其接口方法如下:

processBefore 是在生成 StatementHandler 的时候执行,processAfter 则是在完成插入返回结果之前执行。

1
2
3
4
public interface KeyGenerator {
void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
}

如代码所见 KeyGenerator 非常的简单,主要是通过两个拦截方法实现的:

  • Jdbc3KeyGenerator:主要基于 java.sql.Statement.getGeneratedKeys 的主键返回接口实现的,所以他不需要 processBefore 方法,只需要在获取到结果后使用 processAfter 拦截,然后用反射将主键设置到参数中即可;
  • SelectKeyGenerator:主要是通过 XML 配置或者注解设置 selectKey ,然后单独发出查询语句,在返回拦截方法中使用反射设置主键,其中两个拦截方法只能使用其一,在 selectKey.order 属性中设置 AFTER|BEFORE 来确定;

MyBatis 缓存

myBatis中存在两个缓存,一级缓存和二级缓存。

  • 一级缓存:也叫做会话级缓存,生命周期仅存在于当前会话,不可以直接关关闭。但可以通过flushCache和localCacheScope对其做相应控制。
  • 二级缓存:也叫应用级性缓存,缓存对象存在于整个应用周期,而且可以跨线程使用。

一级缓存

一级缓存的命中场景

关于一级缓存的命中可大致分为两个场景,满足特定命中参数,第二不触发清空方法。

缓存命中参数:

  1. SQL与参数相同:
  2. 同一个会话:
  3. 相同的MapperStatement ID:
  4. RowBounds行范围相同:

触发清空缓存

  1. 手动调用clearCache
  2. 执行提交回滚
  3. 执行update
  4. 配置flushCache=true
  5. 缓存作用域为Statement

一级缓存源码解析

一级缓存逻辑就存在于 BaseExecutor (基础执行器)里面。当会话接收到查询请求之后,会交给执行器的Query方法,在这里会通过 Sql、参数、分页条件等参数创建一个缓存key,在基于这个key去 PerpetualCache中查找对应的缓存值,如果有命中直接返回。没有就会查询数据库,然后在填充缓存。最终缓存的实现非常简单,就是一个HashMap。

一级缓存的清空

缓存的清空对应BaseExecutor中的 clearLocalCache.方法。只要找到调用该方法地方,就知道哪些场景中会清空缓存了。

  • update: 执行任意增删改
  • select:查询又分为两种情况清空,一前置清空,即配置了flushCache=true。二后置清空,配置了缓存作用域为statement 查询结束合会清空缓存。
  • commit:提交前清空
  • Rolback:回滚前清空

注意:clearLocalCache 不是清空某条具体数据,而清当前会话下所有一级缓存数据。

MyBatis集成Spring后一级缓存失效的问题?

很多人发现,集成一级缓存后会话失效了,以为是spring Bug ,真正原因是Spring 对SqlSession进行了封装,通过SqlSessionTemplae ,使得每次调用Sql,都会重新构建一个SqlSession,具体参见SqlSessionInterceptor。而根据前面所学,一级缓存必须是同一会话才能命中,所以在这些场景当中不能命中。

怎么解决呢?给Spring 添加事物 即可。添加事物之后,SqlSessionInterceptor(会话拦截器)就会去判断两次请求是否在同一事物当中,如果是就会共用同一个SqlSession会话来解决。

二级缓存

二级缓存也称作是应用级缓存,与一级缓存不同的,是它的作用范围是整个应用,而且可以跨线程使用。所以二级缓存有更高的命中率,适合缓存一些修改较少的数据。在流程上是先访问二级缓存,再访问一级缓存。

MyBatis抽象出Cache接口,其只定义了缓存中最基本的功能方法:

  • 设置缓存
  • 获取缓存
  • 清除缓存
  • 获取缓存数量

然后上述中每一个功能都会对应一个组件类,并基于装饰者加责任链的模式,将各个组件进行串联。在执行缓存的基本功能时,其它的缓存逻辑会沿着这个责任链依次往下传递。

这样设计有以下优点:

  1. 职责单一:各个节点只负责自己的逻辑,不需要关心其它节点。
  2. 扩展性强:可根据需要扩展节点、删除节点,还可以调换顺序保证灵活性。
  3. 松耦合:各节点之间不没强制依赖其它节点。而是通过顶层的Cache接口进行间接依赖。

缓存空间声明

二级默认缓存默认是不开启的,需要为其声明缓存空间才可以使用,通过@CacheNamespace 或 为指定的MappedStatement声明。声明之后该缓存为该Mapper所独有,其它Mapper不能访问。如需要多个Mapper共享一个缓存空间可通过@CacheNamespaceRef 或进行引用同一个缓存空间。

二级缓存的命中条件

二级缓存的命中场景与一级缓存类似,不同在于二级可以跨会放使用,还有就是二级缓存的更新,为了保证数据一至性,二级缓存必须是会话提交之才会真正填充,包括对缓存的清空,也必须是会话正常提交之后才生效。

二级缓存结构

为了实现会话提交之后才变更二级缓存,MyBatis为每个会话设立了若干个暂存区,当前会话对指定缓存空间的变更,都存放在对应的暂存区,当会话提交之后才会提交到每个暂存区对应的缓存空间。为了统一管理这些暂存区,每个会话都一个唯一的事物缓存管理器。所以这里暂存区也可叫做事物缓存。

二级缓存的执行流程

原本会话是通过Executor实现SQL调用,这里基于装饰器模式使用CachingExecutor对SQL调用逻辑进行拦截。以嵌入二级缓存相关逻辑。

查询操作query

当会话调用query() 时,会基于查询语句、参数等数据组成缓存Key,然后尝试从二级缓存中读取数据。读到就直接返回,没有就调用被装饰的Executor去查询数据库,然后在填充至对应的暂存区。

请注意,这里的查询是实时从缓存空间读取的,而变更,只会记录在暂存区

更新操作update

当执行update操作时,同样会基于查询的语句和参数组成缓存KEY,然后在执行update之前清空缓存。这里清空只针对暂存区,同时记录清空的标记,以便当会话提交之时,依据该标记去清空二级缓存空间。

如果在查询操作中配置了flushCache=true ,也会执行相同的操作。

提交操作commit

当会话执行commit操作后,会将该会话下所有暂存区的变更,更新到对应二级缓存空间去。

Hibernate和MyBatis的区别

相同点

Hibernate与MyBatis都可以是通过SessionFactoryBuider由XML配置文件生成SessionFactory,然后由SessionFactory 生成Session,最后由Session来开启执行事务和SQL语句。

其中SessionFactoryBuider,SessionFactory,Session的生命周期都是差不多的。Hibernate和MyBatis都支持 JDBC 和 JTA 事务处理。

不同点

hibernate是全自动,而mybatis是半自动

hibernate完全可以通过对象关系模型实现对数据库的操作,拥有完整的JavaBean对象与数据库的映射结构来自动生成sql。而mybatis仅有基本的字段映射,对象数据以及对象实际关系仍然需要通过手写sql来实现和管理。

hibernate数据库移植性远大于mybatis

hibernate通过它强大的映射结构和hql语言,大大降低了对象与数据库(Oracle、MySQL等)的耦合性,而mybatis由于需要手写sql,因此与数据库的耦合性直接取决于程序员写sql的方法,如果sql不具通用性而用了很多某数据库特性的sql语句的话,移植性也会随之降低很多,成本很高。

hibernate拥有完整的日志系统

hibernate日志系统非常健全,涉及广泛,包括:sql记录、关系异常、优化警告、缓存提示、脏数据警告等;而mybatis则除了基本记录功能外,功能薄弱很多。

mybatis相比hibernate需要关心很多细节

hibernate配置要比mybatis复杂的多,学习成本也比mybatis高。但也正因为mybatis使用简单,才导致它要比hibernate关心很多技术细节。mybatis由于不用考虑很多细节,开发模式上与传统jdbc区别很小,因此很容易上手并开发项目,但忽略细节会导致项目前期bug较多,因而开发出相对稳定的软件很慢,而开发出软件却很快。hibernate则正好与之相反。但是如果使用hibernate很熟练的话,实际上开发效率丝毫不差于甚至超越mybatis。

sql直接优化上,mybatis要比hibernate方便很多

由于mybatis的sql都是写在xml里,因此优化sql比hibernate方便很多。而hibernate的sql很多都是自动生成的,无法直接维护sql;虽有hql,但功能还是不及sql强大,见到报表等变态需求时,hql也歇菜,也就是说hql是有局限的;hibernate虽然也支持原生sql,但开发模式上却与orm不同,需要转换思维,因此使用上不是非常方便。总之写sql的灵活度上hibernate不及mybatis。

缓存机制上,hibernate要比mybatis更好一些

MyBatis的二级缓存配置都是在每个具体的表-对象映射中进行详细配置,这样针对不同的表可以自定义不同的缓存机制。并且Mybatis可以在命名空间中共享相同的缓存配置和实例,通过Cache-ref来实现。

而Hibernate对查询对象有着良好的管理机制,用户无需关心SQL。所以在使用二级缓存时如果出现脏数据,系统会报出错误并提示。