MyBatis核心特性
MyBatis和Hibernate 跟 DbUtils、 Spring JDBC 一样,都是对 JDBC 的一个封装,我们去看源码,最后一定会看到 Statement 和 ResultSet 这些对象。
1、 使用连接池对连接进行管理
2、 SQL 和代码分离,集中管理
3、 结果集映射
4、 参数映射和动态 SQL
5、 重复 SQL 的提取
6、 缓存管理
7、 插件机制
这么多的工具和不同的框架,在实际的项目里面应该怎么选择?
在一些业务比较简单的项目中,我们可以使用 Hibernate;如果需要更加灵活的 SQL,可以使用 MyBatis,对于底层的编码,或者性能要求非常高的场合,可以用 JDBC。
实际上在我们的项目中,MyBatis 和 Spring JDBC 是可以混合使用的
首先接口层是我们打交道最多的。核心对象是 SqlSession,它是上层应用和 MyBatis打交道的桥梁,SqlSession 上定义了非常多的对数据库的操作方法。接口层在接收到调用请求的时候,会调用核心处理层的相应模块来完成具体的数据库操作。
接下来是核心处理层。既然叫核心处理层,也就是跟数据库操作相关的动作都是在这一层完成的。
核心处理层主要做了这几件事:
(1)把接口中传入的参数解析并且映射成 JDBC 类型;
(2)解析 xml 文件中的 SQL 语句,包括插入参数,和动态 SQL 的生成;
(3)执行 SQL 语句;
(4)处理结果集,并映射成 Java 对象。
插件也属于核心层,这是由它的工作方式和拦截的对象决定的。
最后一个就是基础支持层。基础支持层主要是一些抽取出来的通用的功能(实现复用),用来支持核心处理层的功能。比如数据源、缓存、日志、xml 解析、反射、IO、事务等等这些功能。
缓存目的就是提升查询的效率和减少数据库的压力。跟 Hibernate 一样,MyBatis 也有一级缓存和二级缓存,并且预留了集成在第三方缓存的接口(比如缓存在Redis)。
? MyBatis 跟缓存相关的类都在 cache 包里面,其中有一个 Cache 接口,只有一个默认的实现类 PerpetualCache,它是用 HashMap 实现的。
? 除此之外,还有很多的装饰器,通过这些装饰器可以额外实现很多的功能:回收策略、日志记录、定时刷新等等。
?
“装饰者模式(Decorator Pattern) 是指在不改变原有对象的基础之上, 将功能附加到对象上, 提供了比继承更有弹
性的替代方案(扩展原有对象的功能) 。 ” 无论怎么装饰,经过多少层装饰,最后使用的还是基本的实现类(默认PerpetualCache)
所有的缓存实现类总体上可分为三类:基本缓存、淘汰算法缓存、装饰器缓存
缓存实现类 | 描述 | 作用 | 装饰条件 |
---|---|---|---|
基本缓存 | 缓存基本实现类 | 默认是 PerpetualCache, 也可以自定义比如 RedisCache、 EhCache 等, 具备基本功能的缓存类 | 无 |
LruCache | LRU 策略的缓存 | 当缓存到达上限时候, 删除最近最少使用的缓存 (Least Recently Use) | eviction="LRU"(默 认) |
FifoCache | FIFO 策略的缓存 | 当缓存到达上限时候, 删除最先入队的缓存 | eviction="FIFO" |
SoftCache WeakCache | 带清理策略的缓存 | 通过 JVM 的软引用和弱引用来实现缓存, 当 JVM 内存不足时, 会自动清理掉这些缓存, 基于 SoftReference 和 WeakReference | eviction="SOFT" eviction="WEAK |
? 一级缓存也叫本地缓存,MyBatis 的一级缓存是在会话(SqlSession)层面进行缓存的。MyBatis 的一级缓存是默认开启的,不需要任何的配置。
? 要在同一个会话里面共享一级缓存,这个对象肯定是在 SqlSession 里面创建的,作为 SqlSession 的一个属性 DefaultSqlSession 里面只有两个属性,Configuration 是全局的,所以缓存只可能放在 Executor 里面维护——SimpleExecutor/ReuseExecutor/BatchExecutor 的父类 BaseExecutor 的构造函数中持有了 PerpetualCache。
? 在同一个会话里面,多次执行相同的 SQL 语句,会直接从内存取到缓存的结果,不会再发送 SQL 到数据库。但是不同的会话里面,即使执行的 SQL 一模一样(通过一个Mapper 的同一个方法的相同参数调用),也不能使用到一级缓存。
? 一级缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话或者分布式环境下,会存在脏数据的问题。如果要解决这个问题,就要用到二级缓存。
? 二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是 namespace 级别的,可以被多个 SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。
? 作为一个作用范围更广的缓存,工作在一级缓存之前,也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存
? 要跨会话共享的话,SqlSession 本身和它里面的 BaseExecutor 已经满足不了需求了,实际上 MyBatis 用了一个装饰器的类来维护,就是 CachingExecutor。如果启用了二级缓存,MyBatis 在创建 Executor 对象的时候会对 Executor 进行装饰。
? CachingExecutor 对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有委派交给真正的查询器 Executor 实现类,比如 SimpleExecutor 来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户
第一步:在 mybatis-config.xml 中配置了(可以不配置,默认是 true):
<setting name="cacheEnabled" value="true"/>
只要没有显式地设置 cacheEnabled=false,都会用 CachingExecutor 装饰基本的执行器。
第二步:在 Mapper.xml 中配置
<!-- 声明这个 namespace 使用二级缓存 -->
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024" <!--最多缓存对象个数, 默认 1024-->
eviction="LRU" <!--回收策略-->
flushInterval="120000" <!--自动刷新时间 ms, 未配置时只有调用时刷新-->
readOnly="false"/> <!--默认是 false(安全) , 改为 true 可读写时, 对象必须支持序列化 -->
? 一级缓存默认是打开的,二级缓存需要配置才可以开启。那么我们必须思考一个问题,在什么情况下才有必要去开启二级缓存?
? 1、在查询为主的应用中使用,比如历史交易、历史订单的查询。因为所有的增删改都会刷新二级缓存,导致二级缓存失效。否则缓存就失去了意义。
? 2、如果多个 namespace 中有针对于同一个表的操作,比如 Blog 表,如果在一个namespace 中刷新了缓存,另一个 namespace 中没有刷新,就会出现读到脏数据的情况。所以,推荐在一个 Mapper 里面只操作单表的情况使用。
1.引入pom,maven:mybatis-redis 包
2.Mapper.xml 配置,type 使用 RedisCache:
3.普通Redis地址配置
最后二级缓存存在了Redis里面,具体不多说 比较少见。
因为 Spring 对 MyBatis 的一些操作进行的封装,我们不能直接看到它的本质,所以先看下不使用容器的时候
//读取配置
String resource = "mybatis/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
//创建SqlSession会话工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//创建会话
SqlSession session = sqlSessionFactory.openSession();
//执行SQL
try {
BookMarkDao mapper = session.getMapper(BookMarkDao.class);
BookMarkQuery bookMarkQuery = new BookMarkQuery();
bookMarkQuery.setUserId(new Long("123"));
List<BookMarkDO> bookMarkList = mapper.find(bookMarkQuery);
System.out.println(bookMarkList);
} finally {
session.close();
}
看到了 MyBatis 里面的几个核心对象:
SqlSessionFactoryBuiler、 SqlSessionFactory、 SqlSession 和 Mapper 对象。 这几个核心对象在 MyBatis 的整个工作流程里面的不同环节发挥作用。 如果说我们不用容器,自己去管理这些对象的话,我们必须思考一个问题:什么时候创建和销毁这些对象?
从每个对象的作用的角度来理解一下,只有理解了它们是干什么的,才知道什么时候应该创建,什么时候应该销毁
首先是 SqlSessionFactoryBuiler。它是用来构建 SqlSessionFactory 的, 而
SqlSessionFactory 只需要一个,所以只要构建了这一个 SqlSessionFactory,它的使命
就完成了,也就没有存在的意义了。所以它的生命周期只存在于方法的局部。
SqlSessionFactory 是用来创建 SqlSession 的,每次应用程序访问数据库,都需要
创建一个会话。 因为我们一直有创建会话的需要,所以 SqlSessionFactory 应该存在于
应用的整个生命周期中(作用域是应用作用域)。创建 SqlSession 只需要一个实例来做
这件事就行了,否则会产生很多的混乱,和浪费资源。所以我们要采用单例模式。
SqlSession 是一个会话,因为它不是线程安全的,不能在线程间共享。 所以我们在
请求开始的时候创建一个 SqlSession 对象,在请求结束或者说方法执行完毕的时候要及
时关闭它(一次请求或者操作中)。
Mapper(实际上是一个代理对象)是从 SqlSession 中获取的。
BookMarkDao mapper = session.getMapper(BookMarkDao.class);
它的作用是发送 SQL 来操作数据库的数据。它应该在一个 SqlSession 事务方法之
内。
对象 | 生命周期 |
---|---|
SqlSessionFactoryBuiler | 方法局部(method) |
SqlSessionFactory(单例) | 应用级别(application) |
SqlSession | 请求和操作(request/method) |
Mapper | 方法(method) |
第一步,我们通过建造者模式创建一个工厂类,配置文件的解析就是在这一步完成的,包括 mybatis-config.xml 和Mapper 适配器文件。
问题:解析的时候怎么解析的,做了什么,产生了什么对象,结果存放到了哪里。解析的结果决定着我们后面有什么对象可以使用,和到哪里去取。
第二步,通过 SqlSessionFactory 创建一个 SqlSession。
问题:SqlSession 是用来操作数据库的,返回了什么实现类,除了 SqlSession,还创建了什么对象,创建了什么环境?
第三步,获得一个 Mapper 对象。
问题:Mapper 是一个接口,没有实现类,是不能被实例化的,那获取到的这个Mapper 对象是什么对象?为什么要从 SqlSession 里面去获取?为什么传进去一个接口,然后还要用接口类型来接收?
第四步,调用接口方法。
问题:我们的接口没有创建实现类,为什么可以调用它的方法?那它调用的是什么方法?它又是根据什么找到我们要执行的 SQL 的?也就是接口方法怎么和 XML 映射器里面的 StatementID 关联起来的?此外,我们的方法参数是怎么转换成 SQL 参数的?获取到的结果集是怎么转换成对象的?
待分析的基础代码:
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlogById(1);
核心代码:
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
new 了一个 SqlSessionFactoryBuilder,非常明显的建造者模式,它里面定义了很多个 build 方法的重载,最终返回的是一个 SqlSessionFactory 对象(单例模式)。可以点进去 build 方法:
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
//。。。。
} finally {
//。。。。
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
核心两句话:
1.创建XMLConfigBuilder,调用parse进行解析
2.运用解析出来的Configuration,创建DefaultSqlSessionFactory
XMLConfigBuilder 是抽象类 BaseBuilder 的一个子类,专门用来解析全局配置文件,针对不同的构建目标还有其他的一些子类,比如:
XMLMapperBuilder:解析 Mapper 映射器
XMLStatementBuilder:解析增删改查标签
调用 XMLConfigBuilder的 parse()方法,它会返回一个 Configuration ,配置文件里面所有的信息都会放在 Configuration 里面 。查看parse方法:
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
//分步骤解析
//issue #117 read properties first
//1.properties
propertiesElement(root.evalNode("properties"));
//2.类型别名
typeAliasesElement(root.evalNode("typeAliases"));
//3.插件
pluginElement(root.evalNode("plugins"));
//4.对象工厂
objectFactoryElement(root.evalNode("objectFactory"));
//5.对象包装工厂
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//6.设置
settingsElement(root.evalNode("settings"));
// read it after objectFactory and objectWrapperFactory issue #631
//7.环境
environmentsElement(root.evalNode("environments"));
//8.databaseIdProvider
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//9.类型处理器
typeHandlerElement(root.evalNode("typeHandlers"));
//10.映射器
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
依次解析这里面的节点:configuration以及下属properties、settings、typeAliases等等,全部设置到内部属性Configuration里,也说明了写mybatis配置文件的顺序不能错。
其中这些标签配置里看下重点的mappers是怎么被解析的,我们的SQL、mapper是怎么解析的。(其它都是配置加载各种分支非重点)
主要是在上面代码:
XMLConfigBuilder#parse>parseConfiguration>mapperElement(root.evalNode("mappers"));里
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
扫描类型 | 含义 |
---|---|
resource | 相对路径 |
url | 绝对路径 |
package | 包 |
class | 单个接口 |
其中主要是两个,addMapper解析接口,XMLMapperBuilder 解析XML
? 无论是按 package 扫描,还是按接口扫描,最后都会调用到 MapperRegistry 的addMapper()方法。
MapperRegistry 里面维护的其实是一个 Map 容器,存储接口和代理工厂的映射关系。
? 在 addMapper()方法跟进去 里面创建了一个 MapperAnnotationBuilder,又一个parse方法,都是对注解的解析,比如@Options(设置自增key),@SelectKey(返回自增key),@ResultMap 等等
? 最后同样会解析成 MappedStatement 对象,也就是说在 XML 中配置,和使用注解配置,最后起到一样的效果
XML,具体是XMLMapperBuilder.parse()方法,源码:
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
主要有两个方法:
1.configurationElement()—— 解 析 所 有 的 子 标 签 , 其 中buildStatementFromContext()最终获得 MappedStatement 对象。
2.bindMapperForNamespace()——把 namespace(接口类型)和工厂类绑定起来
? 完成了 config 配置文件、Mapper 文件、Mapper 接口上的注解的解析 得到了一个最重要的对象Configuration,这里面存放了全部的配置信息,它在属性里面还有各种各样的容器。
? 最后,返回了一个 DefaultSqlSessionFactory,里面持有了 Configuration 的实例。
? 第二步,跟数据库的每一次连接,都需要创建一个会话,用openSession()方法来创建。
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
? DefaultSqlSessionFactory —— openSessionFromDataSource()这个会话里面,需要包含一个 Executor 用来执行 SQL。
? Executor 又要指定事务类型和执行器的类型。所以我们会先从 Configuration 里面拿到 Enviroment,Enviroment 里面就有事务工厂。
? 如果配置的是 JDBC,则会使用 Connection 对象的 commit()、rollback()、close()管理事务。
? 如果配置成 MANAGED,会把事务交给容器来管理,比如 JBOSS,Weblogic。(这里翻过车,SpringBoot+Resin不去配的话没得事物了,事物失效,Tomcat可以)
? 因为我们跑的是本地main程序,如果配置成 MANAGE 不会有任何事务。
? 如 果 是 Spring + MyBatis , 则 没 有 必 要 配 置 , 因 为 我 们 会 直 接 在applicationContext.xml 里面配置数据源和事务管理器,覆盖 MyBatis 的配置。
? Executor 的基本类型有三种:SIMPLE、BATCH、REUSE,默认是 SIMPLE(settingsElement()读取默认值),他们都继承了抽象类 BaseExecutor。
? 为什么要让抽象类实现接口,然后让具体实现类继承抽象类?(模板方法模式)
? 定义一个算法的骨架, 并允许子类为一个或者多个步骤提供实现。模板方法使得子类可以在不改变算法结构的情况下, 重新定义算法的某些步骤。 ”
? 三种类型的区别(通过 update()方法对比)?
? SimpleExecutor:每执行一次 update 或 select,就开启一个 Statement 对象,用完立刻关闭 Statement 对象。
? ReuseExecutor:执行 update 或 select,以 sql 作为 key 查找 Statement 对象,存在就使用,不存在就创建,用完后,不关闭 Statement 对象,而是放置于 Map 内,供下一次使用。简言之,就是重复使用 Statement 对象。
? BatchExecutor:执行 update(没有 select,JDBC 批处理不支持 select),将所有 sql 都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个 Statement 对象,每个 Statement 对象都是 addBatch()完毕后,等待逐一执行executeBatch()批处理。与 JDBC 批处理相同。
? 如果配置了 cacheEnabled=ture,会用装饰器模式对 executor 进行包装:new
CachingExecutor(executor)。
? 创建会话的过程,我们获得了一个 DefaultSqlSession,里面包含了一个Executor,它是 SQL 的执行者。
? 在解析 mapper 标签和 Mapper.xml 的时候已经把接口类型和类型对应的 MapperProxyFactory 放到了一个 Map 中。获取 Mapper 代理对象,实际上是从Map 中获取对应的工厂类后,调用MapperProxyFactory.newInstance() 方法创建对象
? 即JDK 动态代理:
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{ mapperInterface }, mapperProxy);
? 普通JDK 动态代理代理,在实现了 InvocationHandler 的代理类里面,需要传入一个被代理对象的实现类
? mybatis不需要实现类的原因:我们只需要根据接口类型+方法的名称,就可以找到Statement ID 了,而唯一要做的一件事情也是这件,所以不需要实现类
? 获得 Mapper 对象的过程,实质上是获取了一个 MapperProxy 的代理对象。MapperProxy 中有 sqlSession、mapperInterface、methodCache。
? 由于所有的 Mapper 都是 MapperProxy 代理对象,所以任意的方法都是执行MapperProxy 的 invoke()方法,debug进mapper(代理对象)的执行方法。
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);
}
//先从缓存里拿MapperMethod
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
private MapperMethod cachedMapperMethod(Method method) {
return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
这里可以看出Mapper方法的抽象是MapperMethod,先从methodCache缓存Map中取,没有则new一个。
然后执行MapperMethod的execute
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
//...
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("...");
}
return result;
}
分类INSERT、UPDATE、DELETE、SELECT执行,以查询方法为例跟进,进select的executeForMany里
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.selectList(command.getName(), param);
}
// issue #510 Collections & arrays support
if (!method.getReturnType().isAssignableFrom(result.getClass())) {
if (method.getReturnType().isArray()) {
return convertToArray(result);
} else {
return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
}
}
return result;
}
最终还是交给sqlSession,它的selectList去查询
selectOne()最终也是调用了 selectList()
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
在 SelectList()中,我们先根据 command name(Statement ID)从 Configuration中拿到 MappedStatement,这个 ms 上面有我们在 xml 中配置的所有属性,包括 id、statementType、sqlSource、useCache、入参、出参等等。
? 然后执行了 Executor 的 query()方法。 Executor 有三种基本类型,SIMPLE/REUSE/BATCH,还有一种包装类型,CachingExecutor。
? 如果启用了二级缓存,就会先调用 CachingExecutor 的 query()方法,里面有缓存相关的操作,然后才是再调用基本类型的执行器,比如默认的 SimpleExecutor。
? 此处惊奇的发现网上大量文章说要主动去开启二级缓存是错的!跟进源码Configuration属性boolean cacheEnabled = true;默认true,发现二级缓存默认开启,通过官方文档也证实了:
? 执行executor.query实际上是CachingExecutor的query,公司Springboot项目也是
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
根据MappedStatement、参数、limit、sql创建缓存的key。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
先从缓存取否则,执行delegate.query,这里就交给父类BaseExecutor执行query了
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
1)是否清空本地缓存
queryStack 用于记录查询栈,防止递归查询重复处理缓存。flushCache=true 的时候,会先清理本地缓存(一级缓存):clearLocalCache();
2)从一级缓存取
利用CacheKey。这个 CacheKey 就是缓存的 Key,localCache.getObject(key),从一级缓存取。如果没有缓存,会从数据库查询:queryFromDatabase()
3)从数据库查询
a)缓存
先在缓存localCache用个占位符占好。等之后执行查询后,移除占位符,放入数据。
b)查询
执行 Executor 的 doQuery();默认是 SimpleExecutor 的doQuery
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
这里会根据 MappedStatement 里面的 statementType 决定创建StatementHandler 的 类 型 。 默 认 是 PREPARED 。
1、STATEMENT:直接操作sql,不进行预编译,获取数据:$—Statement
2、PREPARED:预处理,参数,进行预编译,获取数据:#—–PreparedStatement:默认
3、CALLABLE:执行存储过程————CallableStatement
<update id="update5" statementType="PREPARED">
update tb_car set xh=#{xh} where id=#{id}
</update>
StatementHandler 里面包含了处理参数的 ParameterHandler 和处理结果集的ResultSetHandler。
这两个对象都是在上面 new 的时候创建的 。这三个对象都是可以被插件拦截的四大对象(还有个Executor),所以在创建之后都要用拦截器进行包装的方法。
用 new 出来的 StatementHandler 创建 Statement 对象——prepareStatement()方法对语句进行预编译,处理参数。
RoutingStatementHandler 的 query()方法。delegate 委派,最终由PreparedStatementHandler(上面statementType 决定的) 的 query()方法执行。
public <E> List<E> query(Statement statement, ResultHandler resultHandler) {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
前两是执行 JDBC 包中的 PreparedStatement ,原生SQL执行
MyBatis只要是最后的handleResultSets,处理结果;
ResultSetHandler 只有一个实现类:DefaultResultSetHandler。实现的步骤就是将Statement执行后的结果集,按照Mapper文件中配置的ResultType或ResultMap来封装成对应的对象,最后将封装的对象返回 。
通过不断while循环调用下一行结果,取匹配Map里的对象映射,对属性赋值
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
final List<Object> multipleResults = new ArrayList<Object>();
int resultSetCount = 0;
// 第一个结果集
ResultSetWrapper rsw = getFirstResultSet(stmt);
// 获取 resultMap
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
// 判断 ResultMap 是否为空,空则抛异常
validateResultMapsCount(rsw, resultMapCount);
// 处理第一个结果集
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
// 将结果集映射为对应的 ResultMap 对象
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
// 多个结果集
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
return collapseSingleResultList(multipleResults);
}
原文:https://www.cnblogs.com/chz-blogs/p/13234346.html