Mybatis 源码解析 (一)
一、 ORM框架的作用
实际开发系统时,我们可通过JDBC完成多种数据库操作。这里以传统JDBC编程过程中的查询操作为例进行说明,其主要步骤如下:
(1)注册数据库驱动类,明确指定数据库 URL地址、数据库用户名、密码等连接信息。
(2)通过 DriverManager 打开数据库连接。
(3)通过数据库连接创建 Statement 对象。
(4)通过 Statement 对象执行SQL 语句,得到 ResultSet 对象。
(5)通过 ResultSet 读取数据,并将数据转换成 JavaBean 对象。
(6)关闭 ResultSet 、Statement 对象以及数据库连接,释放相关资源。
按以上JDBC的方式,会出现
问题1:上述步骤(1)到步骤(4) 以及步骤(6) 在每次查询操作中都会出现,在保存、更新、删除等其他数据库操作中也有类似的重复性代码。
问题2:没有统一管理sql的地方
问题3:封装管理所有ResultSet 和 Statement 查询结果的关系映射也比较复杂
问题4:实际生产环境中对系统的性能是有一定要求的,数据库作为系统中比较珍贵的资源,极易成为整个系统的性能瓶颈。应用程序一般需要通过集成缓存、数据源、数据库连接池等组件进行优化
为了解决上面的问题, ORM ( Object Relational Mapping ,对象-关系映射)框架应运而生。如图所示, ORM 框架的主要功能就是根据映射配置文件,完成数据在对象模型与关系模型之间的映射,同时也屏蔽了上述重复的代码,只暴露简单的API 供开发人员使用。
二、mybatis整体架构
MyBatis 的整体架构分为三层, 分别是基础支持层、核心处理层和接口层:
核心处理层
配置解析 | 在MyBatis 初始化过程中,会加载mybatis-config.xml 配置文件、映射配置文件以及Mapper 接口中的注解信息,解析后的配置信息会形成相应的对象并保存到Configuration 对象中。 之后,利用该Configuration 对象创建SqlSessionFactory 对象。 待MyBatis 初始化之后,开发人员可以通过初始化得到Sq!SessionFactory 创建SqlSession 对象并完成数据库操作。 |
---|---|
SOL 解析与scripting 模块 | MyBatis 实现动态SQL 语句的功能,提供了多种动态SQL 语句对应的节点,例如,< where>节点、< if>节点、< foreach>节点等。通过这些节点的组合使用, 开发人员可以写出几乎满足所有需求的动态SQL 语句。 My Batis 中的scripting 模块会根据用户传入的实参,解析映射文件中定义的动态SQL节点,并形成数据库可执行的SQL 语句。之后会处理SQL 语句中的占位符,绑定用户传入的实参。 |
SOL 执行 | SQL 语句的执行涉及多个组件,其中比较重要的是Executor 、StatementHandler 、ParameterHandler 和ResultSetHandler。 Executor 主要负责维护一级缓存和二级缓存,并提供事务管理的相关操作,它会将数据库相关操作委托给StatementHandler 完成。StatementHandler 首先通过ParameterHandler 完成SQL 语句的实参绑定; 然后通过java.sql.Statement 对象执行SQL 语句并得到结果集; 最后通过ResultSetHandler 完成结果集的映射,得到结果对象并返回。 |
插件 | 通过添加用户自定义插件的方式对MyBatis 进行扩展。 |
接口层
核心是SqlSession 接口,该接口中定义了MyBatis 暴露给应用程序调用的API ,也就是上层应用与MyBatis 交互的桥梁。接口层在接收到调用请求时,会调用核心处理层的相应模块来完成具体的数据库操作。
MyBatis 执行一条SQL 语句的大致过程:
MyBatis的主要成员
-
Configuration MyBatis所有的配置信息都保存在Configuration对象之中,配置文件中的大部分配置都会存储到该类中
-
SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互时的会话,完成必要数据库增删改查功能
-
Executor MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
-
StatementHandler 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数等
-
ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所对应的数据类型
-
ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合
-
TypeHandler 负责java数据类型和jdbc数据类型(也可以说是数据表列类型)之间的映射和转换
-
MappedStatement MappedStatement维护一条<select|update|delete|insert>节点的封装
-
SqlSource 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
-
BoundSql 表示动态生成的SQL语句以及相应的参数信息
三、初始化配置
mybatis扩展性非常好,可以加入各种插件,许多开源组件也都是基于它进行拓展的,从其初始化配置起,工厂模式就用起来了,同时在解析各个标签时,功能解耦也做得非常好。
1.xml文件结构
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="local/application.properties"></properties>
<settings>
<!-- 打印查询语句 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
<!-- 控制全局缓存(二级缓存)-->
<setting name="cacheEnabled" value="true"/>
<!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
<!--<setting name="proxyFactory" value="CGLIB" />-->
<!-- STATEMENT级别的缓存,使一级缓存,只针对当前执行的这一statement有效 -->
<!--
<setting name="localCacheScope" value="STATEMENT"/>
-->
<setting name="localCacheScope" value="SESSION"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/><!-- 单独使用时配置成MANAGED没有事务 -->
<dataSource type="POOLED">
<property name="driver" value="${spring.datasource.driver-class-name}"/>
<property name="url" value="${spring.datasource.jdbc-url}"/>
<property name="username" value="${spring.datasource.username}"/>
<property name="password" value="${spring.datasource.password}"/>
</dataSource>
</environment>
</environments>
//根据数据源的不同执行不同的代码
<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql" />
<property name="Oracle" value="oracle" />
</databaseIdProvider>
<mappers>
<mapper resource="xml/InterfaceBaseInfoExtMapper.xml"/>
<mapper resource="xml/InterfaceBaseInfoMapper.xml"/>
</mappers>
</configuration>
2.解析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.elements.user.dao.dbMapper" >
<resultMap id="BaseResultMap" type="com.sf.zjw.dao.domain.InterfaceBaseInfo">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
<result column="del_flag" jdbcType="BIGINT" property="delFlag" />
<result column="interface_name" jdbcType="VARCHAR" property="interfaceName" />
<result column="emp_num" jdbcType="VARCHAR" property="empNum" />
<result column="emp_name" jdbcType="VARCHAR" property="empName" />
<result column="api_path" jdbcType="VARCHAR" property="apiPath" />
<result column="app_api_path" jdbcType="VARCHAR" property="appApiPath" />
</resultMap>
<select id="SelectTime" resultType="String" databaseId="mysql">
SELECT NOW() FROM dual
</select>
<select id="SelectTime" resultType="String" databaseId="oracle">
SELECT 'oralce'||to_char(sysdate,'yyyy-mm-dd hh24:mi:ss') FROM dual
</select>
</mapper>
3.建造者模式
当一个类的构造函数参数个数超过4个,而且这些参数有些是可选的参数,考虑使用构造者模式。
public class Computer {
private String cpu;//必须
private String ram;//必须
private int usbCount;//可选
private String keyboard;//可选
private String display;//可选
}
//折叠构造函数模式
public class Computer {
...
public Computer(String cpu, String ram) {
this(cpu, ram, 0);
}
public Computer(String cpu, String ram, int usbCount) {
this(cpu, ram, usbCount, "罗技键盘");
}
public Computer(String cpu, String ram, int usbCount, String keyboard) {
this(cpu, ram, usbCount, keyboard, "三星显示器");
}
public Computer(String cpu, String ram, int usbCount, String keyboard, String display) {
this.cpu = cpu;
this.ram = ram;
this.usbCount = usbCount;
this.keyboard = keyboard;
this.display = display;
}
}
//第二种:Javabean 模式
public class Computer {
...
public String getCpu() {
return cpu;
}
public void setCpu(String cpu) {
this.cpu = cpu;
}
public String getRam() {
return ram;
}
public void setRam(String ram) {
this.ram = ram;
}
public int getUsbCount() {
return usbCount;
}
...
}
第一种主要是使用及阅读不方便。你可以想象一下,当你要调用一个类的构造函数时,你首先要决定使用哪一个,然后里面又是一堆参数,如果这些参数的类型很多又都一样,你还要搞清楚这些参数的含义,很容易就传混了。。。那酸爽谁用谁知道。
第二种方式在构建过程中对象的状态容易发生变化,造成错误。因为那个类中的属性是分步设置的,所以就容易出错。为什么会出错?你set元素1时需要有一些特殊操作,set2时又要依赖1的结果,你怎么让别人知道要先set哪个参数?
xml文件的配置明显就很多!
builder模式有4个角色。
-
Product: 最终要生成的对象,例如 Computer实例。
-
Builder: 构建者的抽象基类(有时会使用接口代替)。其定义了构建Product的抽象步骤,其实体类需要实现这些步骤。其会包含一个用来返回最终产品的方法
Product getProduct()
。 -
ConcreteBuilder: Builder的实现类。
-
Director: 决定如何构建最终产品的算法. 其会包含一个负责组装的方法
void Construct(Builder builder)
, 在这个方法中通过调用builder的方法,就可以设置builder,等设置完成后,就可以通过builder的getProduct()
方法获得最终的产品。在xml解析过程中build结构
还少了个XMLScriptBuilder,解析动态sql的
四、查询
执行器
-
BaseExecutor
-
-
- SimpleExecutor:每执行一次 update 或 select,就开启一个 Statement 对象,用完立刻关闭 Sttement 对象。(可以是 Statement 或 PrepareStatement 对象)
- ReuseExecutor:执行 update 或 select,以 sql 作为 key 查找 Statement 对象,存在就使用, 不存在就创建,用完后,不关闭 Statement 对象,而是放置于 Map内,供 下一次使用。(可以是 Statement 或 PrepareStatement 对象)
- BatchExecutor:执行 update(没有 select,JDBC 批处理不支持 select),将所有 sql 都 添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个 Statement 对象,每个 Statement 对象都是 addBatch()完毕后,等待逐一执行
-
-
CachingExecutor 先从缓存中获取查询结果,存在就返回,不存在,再委托给 Executor delegate 去数据库 取,delegate 可以是上面任一的 SimpleExecutor、ReuseExecutor、BatchExecutor。
装饰者模式CachingExecutor。
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager(); //新功能
缓存
MyBatis系统中默认定义了两级缓存, 一级 缓存和 二级缓存。
– 1、默认情况下,只有一级缓存(SqlSession级别的缓存,也称为本地缓存)开启,一级缓存默认实现类org.apache.ibatis.cache.impl.PerpetualCache。
– 2、二级缓存需要手动开启和配置,他是基于namespace级别的缓存。
– 3、为了提高扩展性。MyBatis定义了缓存接口Cache,我们可以通过实现Cache接口来自定义二级缓存。
CacheKey的构成
核心功能,可以动态更新Key的hash值。因为底层缓存数据是基于HashMap实现的
比较顺序:hashCode–>checksum–>count–>updateList,只要有一个不等则说明不是相同的Key
从查询的源码可以看出,mybatis在查询前,先看是否需要清除缓存,再通过CacheKey从缓存中查找是否有对应的key,有则返回对象,无则从数据库中查找,再看一级缓存的作用域范围,是STATEMENT则清除缓存,否则将结果集放到缓存中。
一级缓存演示& & 失效情况
同一次会话期间只要查询过的数据都会保存在当前SqlSession的一个Map中
一级缓存失效的四种情况
– 1、不同的SqlSession对应不同的一级缓存
– 2、同一个SqlSession但是查询条件不同
– 3、同一个SqlSession两次查询期间执行了任何一次增删改操作
– 4、同一个SqlSession两次查询期间手动清空了缓存
– sql标签的flushCache属性查询默认flushCache=false;增删改insert|update|delete会修改数据,默认flushCache=true,会同时清空一级和二级缓存。
– sqlSession.clearCache():只是用来清除一级缓存,不会清除二级缓存。
– 当在某一个作用域 (一级缓存Session/二级缓存Namespaces) 进行了 C/U/D 操作后,默认该作用域下所有有select中的缓存将被clear。
为什么要二级缓存
这就要说到为什么要以namespace级别来缓存。
MyBatis 一级缓存最大的共享范围就是一个SqlSession内部,那么如果多个 SqlSession 需要共享缓存,则需要开启二级缓存,开启二级缓存后,会使用 CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示
最常见的二级缓存用法,就是事务!
可以看源码。