#0 系列目录#java
#1 引言# 本文主要讲解JDBC怎么演变到Mybatis的渐变过程,重点讲解了为何要将JDBC封装成Mybaits这样一个持久层框架。再而论述Mybatis做为一个数据持久层框架自己有待改进之处。git
#2 JDBC实现查询分析# 咱们先看看咱们最熟悉也是最基础的经过JDBC查询数据库数据,通常须要如下七个步骤:程序员
加载JDBC驱动;github
创建并获取数据库链接;算法
建立 JDBC Statements 对象;sql
设置SQL语句的传入参数;数据库
执行SQL语句并得到查询结果;apache
对查询结果进行转换处理并将处理结果返回;编程
释放相关资源(关闭Connection,关闭Statement,关闭ResultSet);设计模式
如下是具体的实现代码:
public static List<Map<String,Object>> queryForList(){ Connection connection = null; ResultSet rs = null; PreparedStatement stmt = null; List<Map<String,Object>> resultList = new ArrayList<Map<String,Object>>(); try { // 加载JDBC驱动 Class.forName("oracle.jdbc.driver.OracleDriver").newInstance(); String url = "jdbc:oracle:thin:@localhost:1521:ORACLEDB"; String user = "trainer"; String password = "trainer"; // 获取数据库链接 connection = DriverManager.getConnection(url,user,password); String sql = "select * from userinfo where user_id = ? "; // 建立Statement对象(每个Statement为一次数据库执行请求) stmt = connection.prepareStatement(sql); // 设置传入参数 stmt.setString(1, "zhangsan"); // 执行SQL语句 rs = stmt.executeQuery(); // 处理查询结果(将查询结果转换成List<Map>格式) ResultSetMetaData rsmd = rs.getMetaData(); int num = rsmd.getColumnCount(); while(rs.next()){ Map map = new HashMap(); for(int i = 0;i < num;i++){ String columnName = rsmd.getColumnName(i+1); map.put(columnName,rs.getString(columnName)); } resultList.add(map); } } catch (Exception e) { e.printStackTrace(); } finally { try { // 关闭结果集 if (rs != null) { rs.close(); rs = null; } // 关闭执行 if (stmt != null) { stmt.close(); stmt = null; } if (connection != null) { connection.close(); connection = null; } } catch (SQLException e) { e.printStackTrace(); } } return resultList; }
#3 JDBC演变到Mybatis过程# 上面咱们看到了实现JDBC有七个步骤,哪些步骤是能够进一步封装的,减小咱们开发的代码量。
##3.1 第一步优化:链接获取和释放##
数据库链接频繁的开启和关闭自己就形成了资源的浪费,影响系统的性能
。
解决问题:
数据库链接的获取和关闭咱们可使用数据库链接池来解决资源浪费的问题
。经过链接池就能够反复利用已经创建的链接去访问数据库了。减小链接的开启和关闭的时间。
可是如今链接池多种多样,可能存在变化
,有可能采用DBCP的链接池,也有可能采用容器自己的JNDI数据库链接池。
解决问题:
咱们能够经过DataSource进行隔离解耦
,咱们统一从DataSource里面获取数据库链接,DataSource具体由DBCP实现仍是由容器的JNDI实现均可以
,因此咱们将DataSource的具体实现经过让用户配置来应对变化。
##3.2 第二步优化:SQL统一存取##
咱们使用JDBC进行操做数据库时,SQL语句基本都散落在各个JAVA类中
,这样有三个不足之处:
第一,可读性不好,不利于维护以及作性能调优。
第二,改动Java代码须要从新编译、打包部署。
第三,不利于取出SQL在数据库客户端执行(取出后还得删掉中间的Java代码,编写好的SQL语句写好后还得经过+号在Java进行拼凑)。
解决问题:
咱们能够考虑不把SQL语句写到Java代码中,那么把SQL语句放到哪里呢?首先须要有一个统一存放的地方,咱们能够将这些SQL语句统一集中放到配置文件或者数据库里面(以key-value的格式存放)
。而后经过SQL语句的key值去获取对应的SQL语句。
既然咱们将SQL语句都统一放在配置文件或者数据库中,那么这里就涉及一个SQL语句的加载问题
。
##3.3 第三步优化:传入参数映射和动态SQL##
不少状况下,咱们均可以经过在SQL语句中设置占位符来达到使用传入参数的目的,这种方式自己就有必定局限性,它是按照必定顺序传入参数的,要与占位符一一匹配。可是,若是咱们传入的参数是不肯定的
(好比列表查询,根据用户填写的查询条件不一样,传入查询的参数也是不一样的,有时是一个参数、有时多是三个参数),那么咱们就得在后台代码中本身根据请求的传入参数去拼凑相应的SQL语句
,这样的话仍是避免不了在Java代码里面写SQL语句的命运
。既然咱们已经把SQL语句统一存放在配置文件或者数据库中了,怎么作到可以根据前台传入参数的不一样,动态生成对应的SQL语句呢
?
解决问题:
第一,咱们先解决这个动态问题,按照咱们正常的程序员思惟是,经过if和else这类的判断来进行是最直观的
,这个时候咱们想到了JSTL中的<if test=””></if>这样的标签,那么,能不能将这类的标签引入到SQL语句中呢?假设能够,那么咱们这里就须要一个专门的SQL解析器来解析这样的SQL语句,可是,if判断的变量来自于哪里呢?传入的值自己是可变的,那么咱们得为这个值定义一个不变的变量名称,并且这个变量名称必须和对应的值要有对应关系,能够经过这个变量名称找到对应的值,这个时候咱们想到了key-value的Map。解析的时候根据变量名的具体值来判断。
假如前面能够判断没有问题,那么假如判断的结果是true,那么就须要输出的标签里面的SQL片断,可是怎么解决在标签里面使用变量名称的问题呢?这里咱们须要使用一种有别于SQL的语法来嵌入变量(好比使用#变量名#)
。这样,SQL语句通过解析后就能够动态的生成符合上下文的SQL语句。
还有,怎么区分开占位符变量和非占位变量?有时候咱们单单使用占位符是知足不了的,占位符只能为查询条件占位,SQL语句其余地方使用不了。这里咱们可使用#变量名#表示占位符变量,使用$变量名$表示非占位符变量
。
##3.4 第四步优化:结果映射和结果缓存##
执行SQL语句、获取执行结果、对执行结果进行转换处理、释放相关资源是一整套下来的。假如是执行查询语句,那么执行SQL语句后,返回的是一个ResultSet结果集,这个时候咱们就须要将ResultSet对象的数据取出来,否则等到释放资源时就取不到这些结果信息了
。咱们从前面的优化来看,以及将获取链接、设置传入参数、执行SQL语句、释放资源这些都封装起来了,只剩下结果处理这块尚未进行封装,若是能封装起来,每一个数据库操做都不用本身写那么一大堆Java代码,直接调用一个封装的方法就能够搞定了。
解决问题:
咱们分析一下,通常对执行结果的有哪些处理,有可能将结果不作任何处理就直接返回,也有可能将结果转换成一个JavaBean对象返回、一个Map返回、一个List返回等等
,结果处理多是多种多样的。从这里看,咱们必须告诉SQL处理器两点:第一,须要返回什么类型的对象;第二,须要返回的对象的数据结构怎么跟执行的结果映射
,这样才能将具体的值copy到对应的数据结构上。
接下来,咱们能够进而考虑对SQL执行结果的缓存来提高性能
。缓存数据都是key-value的格式,那么这个key怎么来呢
?怎么保证惟一呢?即便同一条SQL语句几回访问的过程当中因为传入参数的不一样,获得的执行SQL语句也是不一样的。那么缓存起来的时候是多对。可是SQL语句和传入参数两部分合起来能够做为数据缓存的key值
。
##3.5 第五步优化:解决重复SQL语句问题##
因为咱们将全部SQL语句都放到配置文件中,这个时候会遇到一个SQL重复的问题
,几个功能的SQL语句其实都差很少,有些多是SELECT后面那段不一样、有些多是WHERE语句不一样。有时候表结构改了,那么咱们就须要改多个地方,不利于维护。
解决问题:
当咱们的代码程序出现重复代码时怎么办?将重复的代码抽离出来成为独立的一个类,而后在各个须要使用的地方进行引用
。对于SQL重复的问题,咱们也能够采用这种方式,经过将SQL片断模块化,将重复的SQL片断独立成一个SQL块,而后在各个SQL语句引用重复的SQL块
,这样须要修改时只须要修改一处便可。
#4 Mybaits有待改进之处#
Mybaits全部的数据库操做都是基于SQL语句,致使什么样的数据库操做都要写SQL语句
。一个应用系统要写的SQL语句实在太多了。
改进方法:
咱们对数据库进行的操做大部分都是对表数据的增删改查,不少都是对单表的数据进行操做,由这点咱们能够想到一个问题:单表操做可不能够不写SQL语句,经过JavaBean的默认映射器生成对应的SQL语句
,好比:一个类UserInfo对应于USER_INFO表, userId属性对应于USER_ID字段。这样咱们就能够经过反射能够获取到对应的表结构了,拼凑成对应的SQL语句显然不是问题
。
#5 MyBatis框架总体设计#
##5.1 接口层-和数据库交互的方式# MyBatis和数据库的交互有两种方式:
使用传统的MyBatis提供的API;
使用Mapper接口;
###5.1.1 使用传统的MyBatis提供的API### 这是传统的传递Statement Id 和查询参数给 SqlSession 对象,使用 SqlSession对象完成和数据库的交互
;MyBatis 提供了很是方便和简单的API,供用户实现对数据库的增删改查数据操做,以及对数据库链接信息和MyBatis 自身配置信息的维护操做。
上述使用MyBatis 的方法,是建立一个和数据库打交道的SqlSession对象,而后根据Statement Id 和参数来操做数据库
,这种方式当然很简单和实用,可是它不符合面向对象语言的概念和面向接口编程的编程习惯
。因为面向接口的编程是面向对象的大趋势,MyBatis 为了适应这一趋势,增长了第二种使用MyBatis 支持接口(Interface)调用方式。
###5.1.2 使用Mapper接口### MyBatis 将配置文件中的每个<mapper> 节点抽象为一个 Mapper 接口
,而这个接口中声明的方法和跟<mapper> 节点中的<select|update|delete|insert> 节点项对应
,即<select|update|delete|insert> 节点的id值为Mapper 接口中的方法名称,parameterType 值表示Mapper 对应方法的入参类型
,而resultMap 值则对应了Mapper 接口表示的返回值类型或者返回结果集的元素类型
。
根据MyBatis 的配置规范配置好后,经过SqlSession.getMapper(XXXMapper.class)
方法,MyBatis 会根据相应的接口声明的方法信息,经过动态代理机制生成一个Mapper 实例
,咱们使用Mapper 接口的某一个方法时,MyBatis 会根据这个方法的方法名和参数类型,肯定Statement Id,底层仍是经过SqlSession.select("statementId",parameterObject);或者SqlSession.update("statementId",parameterObject); 等等来实现对数据库的操做, MyBatis 引用Mapper 接口这种调用方式,纯粹是为了知足面向接口编程的须要。(其实还有一个缘由是在于,面向接口的编程,使得用户在接口上可使用注解来配置SQL语句,这样就能够脱离XML配置文件,实现“0配置”)。
##5.2 数据处理层## 数据处理层能够说是MyBatis 的核心,从大的方面上讲,它要完成两个功能:
经过传入参数构建动态SQL语句;
SQL语句的执行以及封装查询结果集成List<E>
###5.2.1 参数映射和动态SQL语句生成### 动态语句生成能够说是MyBatis框架很是优雅的一个设计,MyBatis 经过传入的参数值,使用 Ognl 来动态地构造SQL语句
,使得MyBatis 有很强的灵活性和扩展性。
参数映射指的是对于java 数据类型和jdbc数据类型之间的转换
:这里有包括两个过程:查询阶段,咱们要将java类型的数据,转换成jdbc类型的数据,经过 preparedStatement.setXXX() 来设值
;另外一个就是对resultset查询结果集的jdbcType 数据转换成java 数据类型
。
###5.2.2 SQL语句的执行以及封装查询结果集成List<E>###
动态SQL语句生成以后,MyBatis 将执行SQL语句,并将可能返回的结果集转换成List<E> 列表。MyBatis 在对结果集的处理中,支持结果集关系一对多和多对一的转换
,而且有两种支持方式,一种为嵌套查询语句的查询,还有一种是嵌套结果集的查询
。
##5.3 框架支撑层##
事务管理机制对于ORM框架而言是不可缺乏的一部分
,事务管理机制的质量也是考量一个ORM框架是否优秀的一个标准。
因为建立一个数据库链接所占用的资源比较大, 对于数据吞吐量大和访问量很是大的应用而言
,链接池的设计就显得很是重要。
为了提升数据利用率和减少服务器和数据库的压力,MyBatis 会对于一些查询提供会话级别的数据缓存
,会将对某一次查询,放置到SqlSession 中,在容许的时间间隔内,对于彻底相同的查询,MyBatis 会直接将缓存结果返回给用户,而不用再到数据库中查找。
传统的MyBatis 配置SQL 语句方式就是使用XML文件进行配置的,可是这种方式不能很好地支持面向接口编程的理念,为了支持面向接口的编程,MyBatis 引入了Mapper接口的概念,面向接口的引入,对使用注解来配置SQL 语句成为可能,用户只须要在接口上添加必要的注解便可,不用再去配置XML文件了
,可是,目前的MyBatis 只是对注解配置SQL 语句提供了有限的支持,某些高级功能仍是要依赖XML配置文件配置SQL 语句。
##5.4 引导层## 引导层是配置和启动MyBatis配置信息的方式
。MyBatis 提供两种方式来引导MyBatis :基于XML配置文件的方式和基于Java API 的方式
。
##5.5 主要构件及其相互关系## 从MyBatis代码实现的角度来看,MyBatis的主要的核心部件有如下几个:
SqlSession 做为MyBatis工做的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能
Executor MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
StatementHandler 封装了JDBC Statement操做,负责对JDBC statement 的操做,如设置参数、将Statement结果集转换成List集合。
ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所须要的参数,
ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
TypeHandler 负责java数据类型和jdbc数据类型之间的映射和转换
MappedStatement MappedStatement维护了一条<select|update|delete|insert>节点的封装,
SqlSource 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
BoundSql 表示动态生成的SQL语句以及相应的参数信息
Configuration MyBatis全部的配置信息都维持在Configuration对象之中。
它们的关系以下图所示:
#6 SqlSession工做过程分析#
SqlSession sqlSession = factory.openSession();
MyBatis封装了对数据库的访问,把对数据库的会话和事务控制放到了SqlSession对象中。
List<Employee> result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",params);
上述的"com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",是配置在EmployeesMapper.xml 的Statement ID,params 是传递的查询参数。
让咱们来看一下sqlSession.selectList()方法的定义:
public <E> List<E> selectList(String statement, Object parameter) { return this.selectList(statement, parameter, RowBounds.DEFAULT); } public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { //1.根据Statement Id,在mybatis 配置对象Configuration中查找和配置文件相对应的MappedStatement MappedStatement ms = configuration.getMappedStatement(statement); //2. 将查询任务委托给MyBatis 的执行器 Executor List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); return result; } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
MyBatis在初始化的时候,会将MyBatis的配置信息所有加载到内存中,使用org.apache.ibatis.session.Configuration实例来维护
。使用者可使用sqlSession.getConfiguration()方法来获取。MyBatis的配置文件中配置信息的组织格式和内存中对象的组织格式几乎彻底对应的
。上述例子中的
<select id="selectByMinSalary" resultMap="BaseResultMap" parameterType="java.util.Map" > select EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, SALARY from LOUIS.EMPLOYEES <if test="min_salary != null"> where SALARY < #{min_salary,jdbcType=DECIMAL} </if> </select>
加载到内存中会生成一个对应的MappedStatement对象,而后会以key="com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary" ,value为MappedStatement对象的形式维护到Configuration的一个Map中
。当之后须要使用的时候,只须要经过Id值来获取就能够了。
从上述的代码中咱们能够看到SqlSession的职能是:SqlSession根据Statement ID, 在mybatis配置对象Configuration中获取到对应的MappedStatement对象,而后调用mybatis执行器来执行具体的操做
。
/** * BaseExecutor 类部分代码 * */ public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { // 1. 根据具体传入的参数,动态地生成须要执行的SQL语句,用BoundSql对象表示 BoundSql boundSql = ms.getBoundSql(parameter); // 2. 为当前的查询建立一个缓存Key CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); } @SuppressWarnings("unchecked") 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 { // 3.缓存中没有值,直接从数据库中读取数据 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { queryStack--; } if (queryStack == 0) { for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } deferredLoads.clear(); // issue #601 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { clearLocalCache(); // issue #482 } } return list; } private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; localCache.putObject(key, EXECUTION_PLACEHOLDER); try { //4. 执行查询,返回List 结果,而后 将查询的结果放入缓存之中 list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { localCache.removeObject(key); } localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }
/** * *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(); //5. 根据既有的参数,建立StatementHandler对象来执行查询操做 StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); //6. 建立java.Sql.Statement对象,传递给StatementHandler对象 stmt = prepareStatement(handler, ms.getStatementLog()); //7. 调用StatementHandler.query()方法,返回List结果集 return handler.<E>query(stmt, resultHandler); } finally { closeStatement(stmt); } }
上述的Executor.query()方法几经转折,最后会建立一个StatementHandler对象,而后将必要的参数传递给StatementHandler
,使用StatementHandler来完成对数据库的查询,最终返回List结果集。
从上面的代码中咱们能够看出,Executor的功能和做用是:
根据传递的参数,完成SQL语句的动态解析,生成BoundSql对象,供StatementHandler使用;
为查询建立缓存,以提升性能;
建立JDBC的Statement链接对象,传递给StatementHandler对象,返回List查询结果;
接着上面的Executor第六步,看一下:prepareStatement() 方法的实现:
/** * *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); // 1.准备Statement对象,并设置Statement对象的参数 stmt = prepareStatement(handler, ms.getStatementLog()); // 2. StatementHandler执行query()方法,返回List结果 return handler.<E>query(stmt, resultHandler); } finally { closeStatement(stmt); } } private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; Connection connection = getConnection(statementLog); stmt = handler.prepare(connection); //对建立的Statement对象设置参数,即设置SQL 语句中 ? 设置为指定的参数 handler.parameterize(stmt); return stmt; }
以上咱们能够总结StatementHandler对象主要完成两个工做:
对于JDBC的PreparedStatement类型的对象,建立的过程当中,咱们使用的是SQL语句字符串会包含 若干个? 占位符,咱们其后再对占位符进行设值。 StatementHandler经过parameterize(statement)方法对Statement进行设值;
StatementHandler经过List<E> query(Statement statement, ResultHandler resultHandler)方法来完成执行Statement,和将Statement对象返回的resultSet封装成List;
/** * StatementHandler 类的parameterize(statement) 方法实现 */ public void parameterize(Statement statement) throws SQLException { //使用ParameterHandler对象来完成对Statement的设值 parameterHandler.setParameters((PreparedStatement) statement); }
/** * *ParameterHandler类的setParameters(PreparedStatement ps) 实现 * 对某一个Statement进行设置参数 */ public void setParameters(PreparedStatement ps) throws SQLException { ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null) { for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; String propertyName = parameterMapping.getProperty(); if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { value = null; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } // 每个Mapping都有一个TypeHandler,根据TypeHandler来对preparedStatement进行设置参数 TypeHandler typeHandler = parameterMapping.getTypeHandler(); JdbcType jdbcType = parameterMapping.getJdbcType(); if (value == null && jdbcType == null) jdbcType = configuration.getJdbcTypeForNull(); // 设置参数 typeHandler.setParameter(ps, i + 1, value, jdbcType); } } } }
从上述的代码能够看到,StatementHandler 的parameterize(Statement) 方法调用了 ParameterHandler的setParameters(statement) 方法, ParameterHandler的setParameters(Statement)方法负责 根据咱们输入的参数,对statement对象的 ? 占位符处进行赋值
。
/** * PreParedStatement类的query方法实现 */ public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { // 1.调用preparedStatemnt。execute()方法,而后将resultSet交给ResultSetHandler处理 PreparedStatement ps = (PreparedStatement) statement; ps.execute(); //2. 使用ResultHandler来处理ResultSet return resultSetHandler.<E> handleResultSets(ps); }
/** *ResultSetHandler类的handleResultSets()方法实现 * */ public List<Object> handleResultSets(Statement stmt) throws SQLException { final List<Object> multipleResults = new ArrayList<Object>(); int resultSetCount = 0; ResultSetWrapper rsw = getFirstResultSet(stmt); List<ResultMap> resultMaps = mappedStatement.getResultMaps(); int resultMapCount = resultMaps.size(); validateResultMapsCount(rsw, resultMapCount); while (rsw != null && resultMapCount > resultSetCount) { ResultMap resultMap = resultMaps.get(resultSetCount); //将resultSet handleResultSet(rsw, resultMap, multipleResults, null); rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; } String[] resultSets = mappedStatement.getResulSets(); 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); }
从上述代码咱们能够看出,StatementHandler 的List<E> query(Statement statement, ResultHandler resultHandler)方法的实现,是调用了ResultSetHandler的handleResultSets(Statement) 方法。ResultSetHandler的handleResultSets(Statement) 方法会将Statement语句执行后生成的resultSet 结果集转换成List<E> 结果集
:
public List<Object> handleResultSets(Statement stmt) throws SQLException { final List<Object> multipleResults = new ArrayList<Object>(); int resultSetCount = 0; ResultSetWrapper rsw = getFirstResultSet(stmt); List<ResultMap> resultMaps = mappedStatement.getResultMaps(); int resultMapCount = resultMaps.size(); validateResultMapsCount(rsw, resultMapCount); while (rsw != null && resultMapCount > resultSetCount) { ResultMap resultMap = resultMaps.get(resultSetCount); //将resultSet handleResultSet(rsw, resultMap, multipleResults, null); rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; } String[] resultSets = mappedStatement.getResulSets(); 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); }
#7 MyBatis初始化机制# ##7.1 MyBatis的初始化作了什么## 任何框架的初始化,无非是加载本身运行时所须要的配置信息
。MyBatis的配置信息,大概包含如下信息,其高层级结构以下:
MyBatis的上述配置信息会配置在XML配置文件中,那么,这些信息被加载进入MyBatis内部,MyBatis是怎样维护的呢?
MyBatis采用了一个很是直白和简单的方式---使用 org.apache.ibatis.session.Configuration
对象做为一个全部配置信息的容器,Configuration对象的组织结构和XML配置文件的组织结构几乎彻底同样
(固然,Configuration对象的功能并不限于此,它还负责建立一些MyBatis内部使用的对象,如Executor等,这将在后续的文章中讨论)。以下图所示:
MyBatis根据初始化好Configuration信息,这时候用户就可使用MyBatis进行数据库操做了。能够这么说,MyBatis初始化的过程,就是建立 Configuration对象的过程
。
MyBatis的初始化能够有两种方式:
基于XML配置文件:基于XML配置文件的方式是将MyBatis的全部配置信息放在XML文件中,MyBatis经过加载并XML配置文件,将配置文信息组装成内部的Configuration对象。
基于Java API:这种方式不使用XML配置文件,须要MyBatis使用者在Java代码中,手动建立Configuration对象,而后将配置参数set 进入Configuration对象中。
接下来咱们将经过 基于XML配置文件方式的MyBatis初始化,深刻探讨MyBatis是如何经过配置文件构建Configuration对象,并使用它。
##7.2 基于XML配置文件建立Configuration对象## 如今就从使用MyBatis的简单例子入手,深刻分析一下MyBatis是怎样完成初始化的,都初始化了什么。看如下代码:
String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession sqlSession = sqlSessionFactory.openSession(); List list = sqlSession.selectList("com.foo.bean.BlogMapper.queryAllBlogInfo");
有过MyBatis使用经验的读者会知道,上述语句的做用是执行com.foo.bean.BlogMapper.queryAllBlogInfo 定义的SQL语句,返回一个List结果集。总的来讲,上述代码经历了mybatis初始化 -->建立SqlSession -->执行SQL语句
返回结果三个过程。
上述代码的功能是根据配置文件mybatis-config.xml 配置文件,建立SqlSessionFactory对象,而后产生SqlSession,执行SQL语句。而mybatis的初始化就发生在第三句:SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
如今就让咱们看看第三句到底发生了什么。
SqlSessionFactoryBuilder根据传入的数据流生成Configuration对象,而后根据Configuration对象建立默认的SqlSessionFactory实例。
初始化的基本过程以下序列图所示:
由上图所示,mybatis初始化要通过简单的如下几步:
调用SqlSessionFactoryBuilder对象的build(inputStream)方法;
SqlSessionFactoryBuilder会根据输入流inputStream等信息建立XMLConfigBuilder对象;
SqlSessionFactoryBuilder调用XMLConfigBuilder对象的parse()方法;
XMLConfigBuilder对象返回Configuration对象;
SqlSessionFactoryBuilder根据Configuration对象建立一个DefaultSessionFactory对象;
SqlSessionFactoryBuilder返回 DefaultSessionFactory对象给Client,供Client使用。
SqlSessionFactoryBuilder相关的代码以下所示:
public SqlSessionFactory build(InputStream inputStream) { return build(inputStream, null, null); } public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { //2. 建立XMLConfigBuilder对象用来解析XML配置文件,生成Configuration对象 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); //3. 将XML配置文件内的信息解析成Java对象Configuration对象 Configuration config = parser.parse(); //4. 根据Configuration对象建立出SqlSessionFactory对象 return build(config); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } } // 今后处能够看出,MyBatis内部经过Configuration对象来建立SqlSessionFactory,用户也能够本身经过API构造好Configuration对象,调用此方法创SqlSessionFactory public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }
上述的初始化过程当中,涉及到了如下几个对象:
SqlSessionFactoryBuilder : SqlSessionFactory的构造器,用于建立SqlSessionFactory,采用了Builder设计模式
Configuration :该对象是mybatis-config.xml文件中全部mybatis配置信息
SqlSessionFactory:SqlSession工厂类,以工厂形式建立SqlSession对象,采用了Factory工厂设计模式
XmlConfigParser :负责将mybatis-config.xml配置文件解析成Configuration对象,共SqlSessonFactoryBuilder使用,建立SqlSessionFactory
当SqlSessionFactoryBuilder执行build()方法,调用了XMLConfigBuilder的parse()方法,而后返回了Configuration对象
。那么parse()方法是如何处理XML文件,生成Configuration对象的呢?将XML配置文件的信息转换为Document对象
,而XML配置定义文件DTD转换成XMLMapperEntityResolver对象
,而后将两者封装到XpathParser对象中
,XpathParser的做用是提供根据Xpath表达式获取基本的DOM节点Node信息的操做
。以下图所示:会从XPathParser中取出 <configuration>节点对应的Node对象,而后解析此Node节点的子Node
:properties, settings, typeAliases,typeHandlers, objectFactory, objectWrapperFactory, plugins, environments,databaseIdProvider, mappers:public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; //源码中没有这一句,只有 parseConfiguration(parser.evalNode("/configuration")); //为了让读者看得更明晰,源码拆分为如下两句 XNode configurationNode = parser.evalNode("/configuration"); parseConfiguration(configurationNode); return configuration; } /** * 解析 "/configuration"节点下的子节点信息,而后将解析的结果设置到Configuration对象中 */ private void parseConfiguration(XNode root) { try { //1.首先处理properties 节点 propertiesElement(root.evalNode("properties")); //issue #117 read properties first //2.处理typeAliases typeAliasesElement(root.evalNode("typeAliases")); //3.处理插件 pluginElement(root.evalNode("plugins")); //4.处理objectFactory objectFactoryElement(root.evalNode("objectFactory")); //5.objectWrapperFactory objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); //6.settings settingsElement(root.evalNode("settings")); //7.处理environments environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631 //8.database databaseIdProviderElement(root.evalNode("databaseIdProvider")); //9.typeHandlers typeHandlerElement(root.evalNode("typeHandlers")); //10.mappers mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
注意:在上述代码中,还有一个很是重要的地方,就是解析XML配置文件子节点<mappers>的方法mapperElements(root.evalNode("mappers")), 它将解析咱们配置的Mapper.xml配置文件,Mapper配置文件能够说是MyBatis的核心
,MyBatis的特性和理念都体如今此Mapper的配置和设计上。
解析子节点的过程这里就不一一介绍了,用户能够参照MyBatis源码仔细揣摩,咱们就看上述的environmentsElement(root.evalNode("environments")); 方法是如何将environments的信息解析出来,设置到Configuration对象中的:
/** * 解析environments节点,并将结果设置到Configuration对象中 * 注意:建立envronment时,若是SqlSessionFactoryBuilder指定了特定的环境(即数据源); * 则返回指定环境(数据源)的Environment对象,不然返回默认的Environment对象; * 这种方式实现了MyBatis能够链接多数据源 */ private void environmentsElement(XNode context) throws Exception { if (context != null) { if (environment == null) { environment = context.getStringAttribute("default"); } for (XNode child : context.getChildren()) { String id = child.getStringAttribute("id"); if (isSpecifiedEnvironment(id)) { //1.建立事务工厂 TransactionFactory TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); //2.建立数据源DataSource DataSource dataSource = dsFactory.getDataSource(); //3. 构造Environment对象 Environment.Builder environmentBuilder = new Environment.Builder(id) .transactionFactory(txFactory) .dataSource(dataSource); //4. 将建立的Envronment对象设置到configuration 对象中 configuration.setEnvironment(environmentBuilder.build()); } } } } private boolean isSpecifiedEnvironment(String id) { if (environment == null) { throw new BuilderException("No environment specified."); } else if (id == null) { throw new BuilderException("Environment requires an id attribute."); } else if (environment.equals(id)) { return true; } return false; }
将上述的MyBatis初始化基本过程的序列图细化:
##7.3 基于Java API手动加载XML配置文件建立Configuration对象,并使用SqlSessionFactory对象## 咱们可使用XMLConfigBuilder手动解析XML配置文件来建立Configuration对象,代码以下:
String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); // 手动建立XMLConfigBuilder,并解析建立Configuration对象 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, null,null); Configuration configuration=parse(); // 使用Configuration对象建立SqlSessionFactory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); // 使用MyBatis SqlSession sqlSession = sqlSessionFactory.openSession(); List list = sqlSession.selectList("com.foo.bean.BlogMapper.queryAllBlogInfo");
##7.4 涉及到的设计模式## 初始化的过程涉及到建立各类对象,因此会使用一些建立型的设计模式。在初始化的过程当中,Builder模式运用的比较多
。
###7.4.1 Builder模式应用1: SqlSessionFactory的建立### 对于建立SqlSessionFactory时,会根据状况提供不一样的参数,其参数组合能够有如下几种
:
因为构造时参数不定,能够为其建立一个构造器Builder,将SqlSessionFactory的构建过程和表示分开
:
MyBatis将SqlSessionFactoryBuilder和SqlSessionFactory相互独立。
###7.4.2 Builder模式应用2: 数据库链接环境Environment对象的建立### 在构建Configuration对象的过程当中,XMLConfigParser解析 mybatis XML配置文件节点<environment>节点时,会有如下相应的代码:
private void environmentsElement(XNode context) throws Exception { if (context != null) { if (environment == null) { environment = context.getStringAttribute("default"); } for (XNode child : context.getChildren()) { String id = child.getStringAttribute("id"); //是和默认的环境相同时,解析之 if (isSpecifiedEnvironment(id)) { TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); DataSource dataSource = dsFactory.getDataSource(); //使用了Environment内置的构造器Builder,传递id 事务工厂和数据源 Environment.Builder environmentBuilder = new Environment.Builder(id) .transactionFactory(txFactory) .dataSource(dataSource); configuration.setEnvironment(environmentBuilder.build()); } } } }
在Environment内部,定义了静态内部Builder类:
public final class Environment { private final String id; private final TransactionFactory transactionFactory; private final DataSource dataSource; public Environment(String id, TransactionFactory transactionFactory, DataSource dataSource) { if (id == null) { throw new IllegalArgumentException("Parameter 'id' must not be null"); } if (transactionFactory == null) { throw new IllegalArgumentException("Parameter 'transactionFactory' must not be null"); } this.id = id; if (dataSource == null) { throw new IllegalArgumentException("Parameter 'dataSource' must not be null"); } this.transactionFactory = transactionFactory; this.dataSource = dataSource; } public static class Builder { private String id; private TransactionFactory transactionFactory; private DataSource dataSource; public Builder(String id) { this.id = id; } public Builder transactionFactory(TransactionFactory transactionFactory) { this.transactionFactory = transactionFactory; return this; } public Builder dataSource(DataSource dataSource) { this.dataSource = dataSource; return this; } public String id() { return this.id; } public Environment build() { return new Environment(this.id, this.transactionFactory, this.dataSource); } } public String getId() { return this.id; } public TransactionFactory getTransactionFactory() { return this.transactionFactory; } public DataSource getDataSource() { return this.dataSource; } }
#8 MyBatis数据源与链接池# ##8.1 MyBatis数据源DataSource分类## MyBatis数据源实现是在如下四个包中:
MyBatis把数据源DataSource分为三种:
UNPOOLED 不使用链接池的数据源
POOLED 使用链接池的数据源
JNDI 使用JNDI实现的数据源
即:
相应地,MyBatis内部分别定义了实现了java.sql.DataSource接口的UnpooledDataSource,PooledDataSource类来表示UNPOOLED、POOLED类型的数据源
。 以下图所示:
对于JNDI类型的数据源DataSource,则是经过JNDI上下文中取值。
##8.2 数据源DataSource的建立过程## MyBatis数据源DataSource对象的建立发生在MyBatis初始化的过程当中
。下面让咱们一步步地了解MyBatis是如何建立数据源DataSource的。
在mybatis的XML配置文件中,使用<dataSource>元素来配置数据源:
type=”POOLED” :MyBatis会建立PooledDataSource实例
type=”UNPOOLED” :MyBatis会建立UnpooledDataSource实例
type=”JNDI” :MyBatis会从JNDI服务上查找DataSource实例,而后返回使用
MyBatis是经过工厂模式来建立数据源DataSource对象的
,MyBatis定义了抽象的工厂接口:org.apache.ibatis.datasource.DataSourceFactory,经过其getDataSource()方法返回数据源DataSource:public interface DataSourceFactory { void setProperties(Properties props); // 生产DataSource DataSource getDataSource(); }
上述三种不一样类型的type,则有对应的如下dataSource工厂:
POOLED PooledDataSourceFactory
UNPOOLED UnpooledDataSourceFactory
JNDI JndiDataSourceFactory
其类图以下所示:
将其放到Configuration对象内的Environment对象中
,供之后使用。##8.3 DataSource何时建立Connection对象## 当咱们须要建立SqlSession对象并须要执行SQL语句时,这时候MyBatis才会去调用dataSource对象来建立java.sql.Connection对象。也就是说,java.sql.Connection对象的建立一直延迟到执行SQL语句的时候
。
好比,咱们有以下方法执行一个简单的SQL语句:
String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession sqlSession = sqlSessionFactory.openSession(); sqlSession.selectList("SELECT * FROM STUDENTS");
前4句都不会致使java.sql.Connection对象的建立,只有当第5句sqlSession.selectList("SELECT * FROM STUDENTS")
,才会触发MyBatis在底层执行下面这个方法来建立java.sql.Connection对象:
protected void openConnection() throws SQLException { if (log.isDebugEnabled()) { log.debug("Opening JDBC Connection"); } connection = dataSource.getConnection(); if (level != null) { connection.setTransactionIsolation(level.getLevel()); } setDesiredAutoCommit(autoCommmit); }
##8.4 不使用链接池的UnpooledDataSource## 当 <dataSource>的type属性被配置成了”UNPOOLED”,MyBatis首先会实例化一个UnpooledDataSourceFactory工厂实例,而后经过.getDataSource()方法返回一个UnpooledDataSource实例对象引用,咱们假定为dataSource。
使用UnpooledDataSource的getConnection(),每调用一次就会产生一个新的Connection实例对象
。
UnPooledDataSource的getConnection()方法实现以下:
/* * UnpooledDataSource的getConnection()实现 */ public Connection getConnection() throws SQLException { return doGetConnection(username, password); } private Connection doGetConnection(String username, String password) throws SQLException { //封装username和password成properties Properties props = new Properties(); if (driverProperties != null) { props.putAll(driverProperties); } if (username != null) { props.setProperty("user", username); } if (password != null) { props.setProperty("password", password); } return doGetConnection(props); } /* * 获取数据链接 */ private Connection doGetConnection(Properties properties) throws SQLException { //1.初始化驱动 initializeDriver(); //2.从DriverManager中获取链接,获取新的Connection对象 Connection connection = DriverManager.getConnection(url, properties); //3.配置connection属性 configureConnection(connection); return connection; }
如上代码所示,UnpooledDataSource会作如下事情:
初始化驱动:判断driver驱动是否已经加载到内存中,若是尚未加载,则会动态地加载driver类,并实例化一个Driver对象,使用DriverManager.registerDriver()方法将其注册到内存中,以供后续使用。
建立Connection对象:使用DriverManager.getConnection()方法建立链接。
配置Connection对象:设置是否自动提交autoCommit和隔离级别isolationLevel。
返回Connection对象。
上述的序列图以下所示:
总结:从上述的代码中能够看到,咱们每调用一次getConnection()方法,都会经过DriverManager.getConnection()返回新的java.sql.Connection实例
。
##8.5 为何要使用链接池?##
首先让咱们来看一下建立一个java.sql.Connection对象的资源消耗。咱们经过链接Oracle数据库,建立建立Connection对象,来看建立一个Connection对象、执行SQL语句各消耗多长时间。代码以下:
public static void main(String[] args) throws Exception { String sql = "select * from hr.employees where employee_id < ? and employee_id >= ?"; PreparedStatement st = null; ResultSet rs = null; long beforeTimeOffset = -1L; //建立Connection对象前时间 long afterTimeOffset = -1L; //建立Connection对象后时间 long executeTimeOffset = -1L; //建立Connection对象后时间 Connection con = null; Class.forName("oracle.jdbc.driver.OracleDriver"); beforeTimeOffset = new Date().getTime(); System.out.println("before:\t" + beforeTimeOffset); con = DriverManager.getConnection("jdbc:oracle:thin:@127.0.0.1:1521:xe", "louluan", "123456"); afterTimeOffset = new Date().getTime(); System.out.println("after:\t\t" + afterTimeOffset); System.out.println("Create Costs:\t\t" + (afterTimeOffset - beforeTimeOffset) + " ms"); st = con.prepareStatement(sql); //设置参数 st.setInt(1, 101); st.setInt(2, 0); //查询,得出结果集 rs = st.executeQuery(); executeTimeOffset = new Date().getTime(); System.out.println("Exec Costs:\t\t" + (executeTimeOffset - afterTimeOffset) + " ms"); }
上述程序的执行结果为:
今后结果能够清楚地看出,建立一个Connection对象,用了250 毫秒;而执行SQL的时间用了170毫秒
。
建立一个Connection对象用了250毫秒!这个时间对计算机来讲能够说是一个很是奢侈的!
这仅仅是一个Connection对象就有这么大的代价,设想一下另一种状况:若是咱们在Web应用程序中,为用户的每个请求就操做一次数据库,当有10000个在线用户并发操做的话,对计算机而言,仅仅建立Connection对象不包括作业务的时间就要损耗10000×250ms= 250 0000 ms = 2500 s = 41.6667 min,居然要41分钟!!!若是对高用户群体使用这样的系统,简直就是开玩笑!
建立一个java.sql.Connection对象的代价是如此巨大,是由于建立一个Connection对象的过程,在底层就至关于和数据库创建的通讯链接,在创建通讯链接的过程,消耗了这么多的时间,而每每咱们创建链接后(即建立Connection对象后),就执行一个简单的SQL语句,而后就要抛弃掉,这是一个很是大的资源浪费!
对于须要频繁地跟数据库交互的应用程序,能够在建立了Connection对象,并操做完数据库后,能够不释放掉资源,而是将它放到内存中
,当下次须要操做数据库时,能够直接从内存中取出Connection对象,不须要再建立了,这样就极大地节省了建立Connection对象的资源消耗。因为内存也是有限和宝贵的,这又对咱们对内存中的Connection对象怎么有效地维护提出了很高的要求
。咱们将在内存中存放Connection对象的容器称之为链接池(Connection Pool)。下面让咱们来看一下MyBatis的线程池是怎样实现的。
##8.6 使用了链接池的PooledDataSource## 一样地,咱们也是使用PooledDataSource的getConnection()方法来返回Connection对象。如今让咱们看一下它的基本原理:
PooledDataSource将java.sql.Connection对象包裹成PooledConnection对象放到了PoolState类型的容器中维护
。 MyBatis将链接池中的PooledConnection分为两种状态:空闲状态(idle)和活动状态(active)
,这两种状态的PooledConnection对象分别被存储到PoolState容器内的idleConnections和activeConnections两个List集合中
:
idleConnections:
空闲(idle)状态PooledConnection对象被放置到此集合中,表示当前闲置的没有被使用的PooledConnection集合,调用PooledDataSource的getConnection()方法时,会优先今后集合中取PooledConnection对象。当用完一个java.sql.Connection对象时,MyBatis会将其包裹成PooledConnection对象放到此集合中。
activeConnections:
活动(active)状态的PooledConnection对象被放置到名为activeConnections的ArrayList中,表示当前正在被使用的PooledConnection集合,调用PooledDataSource的getConnection()方法时,会优先从idleConnections集合中取PooledConnection对象,若是没有,则看此集合是否已满,若是未满,PooledDataSource会建立出一个PooledConnection,添加到此集合中,并返回
。
PoolState链接池的大体结构以下所示:
下面让咱们看一下PooledDataSource 的getConnection()方法获取Connection对象的实现:
public Connection getConnection() throws SQLException { return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection(); } public Connection getConnection(String username, String password) throws SQLException { return popConnection(username, password).getProxyConnection(); }
上述的popConnection()方法,会从链接池中返回一个可用的PooledConnection对象,而后再调用getProxyConnection()方法最终返回Conection对象
。(至于为何会有getProxyConnection(),请关注下一节)。
如今让咱们看一下popConnection()方法到底作了什么:
先看是否有空闲(idle)状态下的PooledConnection对象,若是有,就直接返回一个可用的PooledConnection对象;不然进行第2步。
查看活动状态的PooledConnection池activeConnections是否已满;若是没有满,则建立一个新的PooledConnection对象,而后放到activeConnections池中,而后返回此PooledConnection对象;不然进行第三步;
看最早进入activeConnections池中的PooledConnection对象是否已通过期:若是已通过期,从activeConnections池中移除此对象,而后建立一个新的PooledConnection对象,添加到activeConnections中,而后将此对象返回;不然进行第4步。
线程等待,循环2步
/* * 传递一个用户名和密码,从链接池中返回可用的PooledConnection */ private PooledConnection popConnection(String username, String password) throws SQLException { boolean countedWait = false; PooledConnection conn = null; long t = System.currentTimeMillis(); int localBadConnectionCount = 0; while (conn == null) { synchronized (state) { if (state.idleConnections.size() > 0) { // 链接池中有空闲链接,取出第一个 conn = state.idleConnections.remove(0); if (log.isDebugEnabled()) { log.debug("Checked out connection " + conn.getRealHashCode() + " from pool."); } } else { // 链接池中没有空闲链接,则取当前正在使用的链接数小于最大限定值, if (state.activeConnections.size() < poolMaximumActiveConnections) { // 建立一个新的connection对象 conn = new PooledConnection(dataSource.getConnection(), this); @SuppressWarnings("unused") //used in logging, if enabled Connection realConn = conn.getRealConnection(); if (log.isDebugEnabled()) { log.debug("Created connection " + conn.getRealHashCode() + "."); } } else { // Cannot create new connection 当活动链接池已满,不能建立时,取出活动链接池的第一个,即最早进入链接池的PooledConnection对象 // 计算它的校验时间,若是校验时间大于链接池规定的最大校验时间,则认为它已通过期了,利用这个PoolConnection内部的realConnection从新生成一个PooledConnection // PooledConnection oldestActiveConnection = state.activeConnections.get(0); long longestCheckoutTime = oldestActiveConnection.getCheckoutTime(); if (longestCheckoutTime > poolMaximumCheckoutTime) { // Can claim overdue connection state.claimedOverdueConnectionCount++; state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime; state.accumulatedCheckoutTime += longestCheckoutTime; state.activeConnections.remove(oldestActiveConnection); if (!oldestActiveConnection.getRealConnection().getAutoCommit()) { oldestActiveConnection.getRealConnection().rollback(); } conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this); oldestActiveConnection.invalidate(); if (log.isDebugEnabled()) { log.debug("Claimed overdue connection " + conn.getRealHashCode() + "."); } } else { //若是不能释放,则必须等待有 // Must wait try { if (!countedWait) { state.hadToWaitCount++; countedWait = true; } if (log.isDebugEnabled()) { log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection."); } long wt = System.currentTimeMillis(); state.wait(poolTimeToWait); state.accumulatedWaitTime += System.currentTimeMillis() - wt; } catch (InterruptedException e) { break; } } } } //若是获取PooledConnection成功,则更新其信息 if (conn != null) { if (conn.isValid()) { if (!conn.getRealConnection().getAutoCommit()) { conn.getRealConnection().rollback(); } conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password)); conn.setCheckoutTimestamp(System.currentTimeMillis()); conn.setLastUsedTimestamp(System.currentTimeMillis()); state.activeConnections.add(conn); state.requestCount++; state.accumulatedRequestTime += System.currentTimeMillis() - t; } else { if (log.isDebugEnabled()) { log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection."); } state.badConnectionCount++; localBadConnectionCount++; conn = null; if (localBadConnectionCount > (poolMaximumIdleConnections + 3)) { if (log.isDebugEnabled()) { log.debug("PooledDataSource: Could not get a good connection to the database."); } throw new SQLException("PooledDataSource: Could not get a good connection to the database."); } } } } } if (conn == null) { if (log.isDebugEnabled()) { log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); } throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); } return conn; }
对应的处理流程图以下所示:
如上所示,对于PooledDataSource的getConnection()方法内,先是调用类PooledDataSource的popConnection()方法返回了一个PooledConnection对象,而后调用了PooledConnection的getProxyConnection()来返回Connection对象
。
当咱们的程序中使用完Connection对象时,若是不使用数据库链接池,咱们通常会调用 connection.close()方法
,关闭connection链接,释放资源。以下所示:
private void test() throws ClassNotFoundException, SQLException { String sql = "select * from hr.employees where employee_id < ? and employee_id >= ?"; PreparedStatement st = null; ResultSet rs = null; Connection con = null; Class.forName("oracle.jdbc.driver.OracleDriver"); try { con = DriverManager.getConnection("jdbc:oracle:thin:@127.0.0.1:1521:xe", "louluan", "123456"); st = con.prepareStatement(sql); //设置参数 st.setInt(1, 101); st.setInt(2, 0); //查询,得出结果集 rs = st.executeQuery(); //取数据,省略 //关闭,释放资源 con.close(); } catch (SQLException e) { con.close(); e.printStackTrace(); } }
调用过close()方法的Connection对象所持有的资源会被所有释放掉,Connection对象也就不能再使用
。
那么,若是咱们使用了链接池,咱们在用完了Connection对象时,须要将它放在链接池中,该怎样作呢?
为了和通常的使用Conneciton对象的方式保持一致,咱们但愿当Connection使用完后,调用.close()方法,而实际上Connection资源并无被释放,而实际上被添加到了链接池中。这样能够作到吗?答案是能够。上述的要求从另一个角度来描述就是:可否提供一种机制,让咱们知道Connection对象调用了什么方法,从而根据不一样的方法自定义相应的处理机制。刚好代理机制就能够完成上述要求
.
怎样实现Connection对象调用了close()方法,而实际是将其添加到链接池中:
这是要使用代理模式,为真正的Connection对象建立一个代理对象,代理对象全部的方法都是调用相应的真正Connection对象的方法实现。当代理对象执行close()方法时,要特殊处理,不调用真正Connection对象的close()方法,而是将Connection对象添加到链接池中
。
MyBatis的PooledDataSource的PoolState内部维护的对象是PooledConnection类型的对象,而PooledConnection则是对真正的数据库链接java.sql.Connection实例对象的包裹器
。
PooledConnection对象内持有一个真正的数据库链接java.sql.Connection实例对象和一个java.sql.Connection的代理
,其部分定义以下:
class PooledConnection implements InvocationHandler { //...... //所建立它的datasource引用 private PooledDataSource dataSource; //真正的Connection对象 private Connection realConnection; //代理本身的代理Connection private Connection proxyConnection; //...... }
PooledConenction实现了InvocationHandler接口,而且,proxyConnection对象也是根据这个它来生成的代理对象:
public PooledConnection(Connection connection, PooledDataSource dataSource) { this.hashCode = connection.hashCode(); this.realConnection = connection; this.dataSource = dataSource; this.createdTimestamp = System.currentTimeMillis(); this.lastUsedTimestamp = System.currentTimeMillis(); this.valid = true; this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this); }
实际上,咱们调用PooledDataSource的getConnection()方法返回的就是这个proxyConnection对象。当咱们调用此proxyConnection对象上的任何方法时,都会调用PooledConnection对象内invoke()方法
。
让咱们看一下PooledConnection类中的invoke()方法定义:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); //当调用关闭的时候,回收此Connection到PooledDataSource中 if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) { dataSource.pushConnection(this); return null; } else { try { if (!Object.class.equals(method.getDeclaringClass())) { checkConnection(); } return method.invoke(realConnection, args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } }
从上述代码能够看到,当咱们使用了pooledDataSource.getConnection()返回的Connection对象的close()方法时,不会调用真正Connection的close()方法,而是将此Connection对象放到链接池中
。
##8.7 JNDI类型的数据源DataSource## 对于JNDI类型的数据源DataSource的获取就比较简单,MyBatis定义了一个JndiDataSourceFactory工厂来建立经过JNDI形式生成的DataSource。下面让咱们看一下JndiDataSourceFactory的关键代码:
if (properties.containsKey(INITIAL_CONTEXT) && properties.containsKey(DATA_SOURCE)) { //从JNDI上下文中找到DataSource并返回 Context ctx = (Context) initCtx.lookup(properties.getProperty(INITIAL_CONTEXT)); dataSource = (DataSource) ctx.lookup(properties.getProperty(DATA_SOURCE)); } else if (properties.containsKey(DATA_SOURCE)) { //从JNDI上下文中找到DataSource并返回 dataSource = (DataSource) initCtx.lookup(properties.getProperty(DATA_SOURCE)); }
#9 MyBatis事务管理机制# ##9.1 概述## 对数据库的事务而言,应该具备如下几点:建立(create)、提交(commit)、回滚(rollback)、关闭(close)
。对应地,MyBatis将事务抽象成了Transaction接口:
MyBatis的事务管理分为两种形式:
这二者的类图以下所示:
##9.2 事务的配置、建立和使用##
咱们在使用MyBatis时,通常会在MyBatisXML配置文件中定义相似以下的信息:
<environment>节点定义了链接某个数据库的信息,其子节点<transactionManager> 的type 会决定咱们用什么类型的事务管理机制
。
MyBatis事务的建立是交给TransactionFactory 事务工厂来建立的,若是咱们将<transactionManager>的type 配置为"JDBC",那么,在MyBatis初始化解析<environment>节点时,会根据type="JDBC"建立一个JdbcTransactionFactory工厂,其源码以下:
/** * 解析<transactionManager>节点,建立对应的TransactionFactory * @param context * @return * @throws Exception */ private TransactionFactory transactionManagerElement(XNode context) throws Exception { if (context != null) { String type = context.getStringAttribute("type"); Properties props = context.getChildrenAsProperties(); /* * 在Configuration初始化的时候,会经过如下语句,给JDBC和MANAGED对应的工厂类 * typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class); * typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class); * 下述的resolveClass(type).newInstance()会建立对应的工厂实例 */ TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance(); factory.setProperties(props); return factory; } throw new BuilderException("Environment declaration requires a TransactionFactory."); }
如上述代码所示,若是type = "JDBC",则MyBatis会建立一个JdbcTransactionFactory.class 实例;若是type="MANAGED",则MyBatis会建立一个MangedTransactionFactory.class实例。
MyBatis对<transactionManager>节点的解析会生成TransactionFactory实例;而对<dataSource>解析会生成datasouce实例,做为<environment>节点,会根据TransactionFactory和DataSource实例建立一个Environment对象
,代码以下所示:
private void environmentsElement(XNode context) throws Exception { if (context != null) { if (environment == null) { environment = context.getStringAttribute("default"); } for (XNode child : context.getChildren()) { String id = child.getStringAttribute("id"); //是和默认的环境相同时,解析之 if (isSpecifiedEnvironment(id)) { //1.解析<transactionManager>节点,决定建立什么类型的TransactionFactory TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); //2. 建立dataSource DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); DataSource dataSource = dsFactory.getDataSource(); //3. 使用了Environment内置的构造器Builder,传递id 事务工厂TransactionFactory和数据源DataSource Environment.Builder environmentBuilder = new Environment.Builder(id) .transactionFactory(txFactory) .dataSource(dataSource); configuration.setEnvironment(environmentBuilder.build()); } } } }
Environment表示着一个数据库的链接,生成后的Environment对象会被设置到Configuration实例中
,以供后续的使用。
上述一直在讲事务工厂TransactionFactory来建立的Transaction,如今让咱们看一下MyBatis中的TransactionFactory的定义吧。
事务工厂Transaction定义了建立Transaction的两个方法:一个是经过指定的Connection对象建立Transaction
,另外是经过数据源DataSource来建立Transaction
。与JDBC 和MANAGED两种Transaction相对应,TransactionFactory有两个对应的实现的子类:
经过事务工厂TransactionFactory很容易获取到Transaction对象实例。咱们以JdbcTransaction为例,看一下JdbcTransactionFactory是怎样生成JdbcTransaction的,代码以下:
public class JdbcTransactionFactory implements TransactionFactory { public void setProperties(Properties props) { } /** * 根据给定的数据库链接Connection建立Transaction * @param conn Existing database connection * @return */ public Transaction newTransaction(Connection conn) { return new JdbcTransaction(conn); } /** * 根据DataSource、隔离级别和是否自动提交建立Transacion * * @param ds * @param level Desired isolation level * @param autoCommit Desired autocommit * @return */ public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) { return new JdbcTransaction(ds, level, autoCommit); } }
如上说是,JdbcTransactionFactory会建立JDBC类型的Transaction,即JdbcTransaction。相似地,ManagedTransactionFactory也会建立ManagedTransaction。下面咱们会分别深刻JdbcTranaction 和ManagedTransaction,看它们究竟是怎样实现事务管理的。
JdbcTransaction直接使用JDBC的提交和回滚事务管理机制
。它依赖与从dataSource中取得的链接connection 来管理transaction 的做用域,connection对象的获取被延迟到调用getConnection()方法。若是autocommit设置为on,开启状态的话,它会忽略commit和rollback。
直观地讲,就是JdbcTransaction是使用的java.sql.Connection 上的commit和rollback功能,JdbcTransaction只是至关于对java.sql.Connection事务处理进行了一次包装(wrapper),Transaction的事务管理都是经过java.sql.Connection实现的
。JdbcTransaction的代码实现以下:
public class JdbcTransaction implements Transaction { private static final Log log = LogFactory.getLog(JdbcTransaction.class); //数据库链接 protected Connection connection; //数据源 protected DataSource dataSource; //隔离级别 protected TransactionIsolationLevel level; //是否为自动提交 protected boolean autoCommmit; public JdbcTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit) { dataSource = ds; level = desiredLevel; autoCommmit = desiredAutoCommit; } public JdbcTransaction(Connection connection) { this.connection = connection; } public Connection getConnection() throws SQLException { if (connection == null) { openConnection(); } return connection; } /** * commit()功能 使用connection的commit() * @throws SQLException */ public void commit() throws SQLException { if (connection != null && !connection.getAutoCommit()) { if (log.isDebugEnabled()) { log.debug("Committing JDBC Connection [" + connection + "]"); } connection.commit(); } } /** * rollback()功能 使用connection的rollback() * @throws SQLException */ public void rollback() throws SQLException { if (connection != null && !connection.getAutoCommit()) { if (log.isDebugEnabled()) { log.debug("Rolling back JDBC Connection [" + connection + "]"); } connection.rollback(); } } /** * close()功能 使用connection的close() * @throws SQLException */ public void close() throws SQLException { if (connection != null) { resetAutoCommit(); if (log.isDebugEnabled()) { log.debug("Closing JDBC Connection [" + connection + "]"); } connection.close(); } } protected void setDesiredAutoCommit(boolean desiredAutoCommit) { try { if (connection.getAutoCommit() != desiredAutoCommit) { if (log.isDebugEnabled()) { log.debug("Setting autocommit to " + desiredAutoCommit + " on JDBC Connection [" + connection + "]"); } connection.setAutoCommit(desiredAutoCommit); } } catch (SQLException e) { // Only a very poorly implemented driver would fail here, // and there's not much we can do about that. throw new TransactionException("Error configuring AutoCommit. " + "Your driver may not support getAutoCommit() or setAutoCommit(). " + "Requested setting: " + desiredAutoCommit + ". Cause: " + e, e); } } protected void resetAutoCommit() { try { if (!connection.getAutoCommit()) { // MyBatis does not call commit/rollback on a connection if just selects were performed. // Some databases start transactions with select statements // and they mandate a commit/rollback before closing the connection. // A workaround is setting the autocommit to true before closing the connection. // Sybase throws an exception here. if (log.isDebugEnabled()) { log.debug("Resetting autocommit to true on JDBC Connection [" + connection + "]"); } connection.setAutoCommit(true); } } catch (SQLException e) { log.debug("Error resetting autocommit to true " + "before closing the connection. Cause: " + e); } } protected void openConnection() throws SQLException { if (log.isDebugEnabled()) { log.debug("Opening JDBC Connection"); } connection = dataSource.getConnection(); if (level != null) { connection.setTransactionIsolation(level.getLevel()); } setDesiredAutoCommit(autoCommmit); } }
ManagedTransaction让容器来管理事务Transaction的整个生命周期,意思就是说,使用ManagedTransaction的commit和rollback功能不会对事务有任何的影响,它什么都不会作,它将事务管理的权利移交给了容器来实现
。看以下Managed的实现代码你们就会一目了然:
/** * * 让容器管理事务transaction的整个生命周期 * connection的获取延迟到getConnection()方法的调用 * 忽略全部的commit和rollback操做 * 默认状况下,能够关闭一个链接connection,也能够配置它不能够关闭一个链接 * 让容器来管理transaction的整个生命周期 * @see ManagedTransactionFactory */ public class ManagedTransaction implements Transaction { private static final Log log = LogFactory.getLog(ManagedTransaction.class); private DataSource dataSource; private TransactionIsolationLevel level; private Connection connection; private boolean closeConnection; public ManagedTransaction(Connection connection, boolean closeConnection) { this.connection = connection; this.closeConnection = closeConnection; } public ManagedTransaction(DataSource ds, TransactionIsolationLevel level, boolean closeConnection) { this.dataSource = ds; this.level = level; this.closeConnection = closeConnection; } public Connection getConnection() throws SQLException { if (this.connection == null) { openConnection(); } return this.connection; } public void commit() throws SQLException { // Does nothing } public void rollback() throws SQLException { // Does nothing } public void close() throws SQLException { if (this.closeConnection && this.connection != null) { if (log.isDebugEnabled()) { log.debug("Closing JDBC Connection [" + this.connection + "]"); } this.connection.close(); } } protected void openConnection() throws SQLException { if (log.isDebugEnabled()) { log.debug("Opening JDBC Connection"); } this.connection = this.dataSource.getConnection(); if (this.level != null) { this.connection.setTransactionIsolation(this.level.getLevel()); } } }
注意:若是咱们使用MyBatis构建本地程序,即不是WEB程序
,若将type设置成"MANAGED",那么,咱们执行的任何update操做,即便咱们最后执行了commit操做,数据也不会保留,不会对数据库形成任何影响
。由于咱们将MyBatis配置成了“MANAGED”,即MyBatis本身无论理事务,而咱们又是运行的本地程序,没有事务管理功能
,因此对数据库的update操做都是无效的。
#10 MyBatis关联查询# MyBatis 提供了高级的关联查询功能,能够很方便地将数据库获取的结果集映射到定义的Java Bean 中。下面经过一个实例,来展现一下Mybatis对于常见的一对多和多对一关系复杂映射是怎样处理的。
设计一个简单的博客系统,一个用户能够开多个博客,在博客中能够发表文章,容许发表评论,能够为文章加标签。博客系统主要有如下几张表构成:
Author表:做者信息表,记录做者的信息,用户名和密码,邮箱等。
Blog表:博客表,一个做者能够开多个博客,即Author和Blog的关系是一对多。
Post表:文章记录表,记录文章发表时间,标题,正文等信息;一个博客下能够有不少篇文章,Blog 和Post的关系是一对多。
Comments表:文章评论表,记录文章的评论,一篇文章能够有不少个评论:Post和Comments的对应关系是一对多。
Tag表:标签表,表示文章的标签分类,一篇文章能够有多个标签,而一个标签能够应用到不一样的文章上,因此Tag和Post的关系是多对多的关系;(Tag和Post的多对多关系经过Post_Tag表体现)
Post_Tag表:记录 文章和标签的对应关系。
通常状况下,咱们会根据每一张表的结构 建立与此相对应的JavaBean(或者Pojo),来完成对表的基本CRUD操做。
上述对单个表的JavaBean定义有时候不能知足业务上的需求。在业务上,一个Blog对象应该有其做者的信息和一个文章列表,以下图所示:
若是想获得这样的类的实例,则最起码要有一下几步:
经过Blog 的id 到Blog表里查询Blog信息,将查询到的blogId 和title 赋到Blog对象内;
根据查询到到blog信息中的authorId 去 Author表获取对应的author信息,获取Author对象,而后赋到Blog对象内;
根据 blogId 去 Post表里查询 对应的 Post文章列表,将List<Post>对象赋到Blog对象中;
这样的话,在底层最起码调用三次查询语句,请看下列的代码:
/* * 经过blogId获取BlogInfo对象 */ public static BlogInfo ordinaryQueryOnTest(String blogId) { BigDecimal id = new BigDecimal(blogId); SqlSession session = sqlSessionFactory.openSession(); BlogInfo blogInfo = new BlogInfo(); //1.根据blogid 查询Blog对象,将值设置到blogInfo中 Blog blog = (Blog)session.selectOne("com.foo.bean.BlogMapper.selectByPrimaryKey",id); blogInfo.setBlogId(blog.getBlogId()); blogInfo.setTitle(blog.getTitle()); //2.根据Blog中的authorId,进入数据库查询Author信息,将结果设置到blogInfo对象中 Author author = (Author)session.selectOne("com.foo.bean.AuthorMapper.selectByPrimaryKey",blog.getAuthorId()); blogInfo.setAuthor(author); //3.查询posts对象,设置进blogInfo中 List posts = session.selectList("com.foo.bean.PostMapper.selectByBlogId",blog.getBlogId()); blogInfo.setPosts(posts); //以JSON字符串的形式将对象打印出来 JSONObject object = new JSONObject(blogInfo); System.out.println(object.toString()); return blogInfo; }
从上面的代码能够看出,想获取一个BlogInfo对象比较麻烦,总共要调用三次数据库查询,获得须要的信息,而后再组装BlogInfo对象。
##10.1 嵌套语句查询## mybatis提供了一种机制,叫作嵌套语句查询
,能够大大简化上述的操做,加入配置及代码以下:
<resultMap type="com.foo.bean.BlogInfo" id="BlogInfo"> <id column="blog_id" property="blogId" /> <result column="title" property="title" /> <association property="author" column="blog_author_id" javaType="com.foo.bean.Author" select="com.foo.bean.AuthorMapper.selectByPrimaryKey"> </association> <collection property="posts" column="blog_id" ofType="com.foo.bean.Post" select="com.foo.bean.PostMapper.selectByBlogId"> </collection> </resultMap> <select id="queryBlogInfoById" resultMap="BlogInfo" parameterType="java.math.BigDecimal"> SELECT B.BLOG_ID, B.TITLE, B.AUTHOR_ID AS BLOG_AUTHOR_ID FROM LOULUAN.BLOG B where B.BLOG_ID = #{blogId,jdbcType=DECIMAL} </select>
/* * 经过blogId获取BlogInfo对象 */ public static BlogInfo nestedQueryOnTest(String blogId) { BigDecimal id = new BigDecimal(blogId); SqlSession session = sqlSessionFactory.openSession(); BlogInfo blogInfo = new BlogInfo(); blogInfo = (BlogInfo)session.selectOne("com.foo.bean.BlogMapper.queryBlogInfoById",id); JSONObject object = new JSONObject(blogInfo); System.out.println(object.toString()); return blogInfo; }
经过上述的代码彻底能够实现前面的那个查询。这里咱们在代码里只须要 blogInfo = (BlogInfo)session.selectOne("com.foo.bean.BlogMapper.queryBlogInfoById",id);一句便可获取到复杂的blogInfo对象。
嵌套语句查询的原理:
在上面的代码中,Mybatis会执行如下流程:
先执行 queryBlogInfoById 对应的语句从Blog表里获取到ResultSet结果集;
取出ResultSet下一条有效记录,而后根据resultMap定义的映射规格,经过这条记录的数据来构建对应的一个BlogInfo 对象。
当要对BlogInfo中的author属性进行赋值的时候,发现有一个关联的查询,此时Mybatis会先执行这个select查询语句,获得返回的结果,将结果设置到BlogInfo的author属性上;
对BlogInfo的posts进行赋值时,也有上述相似的过程。
重复2步骤,直至ResultSet. next () == false;
如下是blogInfo对象构造赋值过程示意图:
这种关联的嵌套查询,有一个很是好的做用就是:能够重用select语句,经过简单的select语句之间的组合来构造复杂的对象
。上面嵌套的两个select语句com.foo.bean.AuthorMapper.selectByPrimaryKey和com.foo.bean.PostMapper.selectByBlogId彻底能够独立使用。
N+1问题:
它的弊端也比较明显:即所谓的N+1问题。关联的嵌套查询显示获得一个结果集,而后根据这个结果集的每一条记录进行关联查询。
如今假设嵌套查询就一个(即resultMap 内部就一个association标签),现查询的结果集返回条数为N,那么关联查询语句将会被执行N次,加上自身返回结果集查询1次,共须要访问数据库N+1次。若是N比较大的话,这样的数据库访问消耗是很是大的!因此使用这种嵌套语句查询的使用者必定要考虑慎重考虑,确保N值不会很大。
以上面的例子为例,select 语句自己会返回com.foo.bean.BlogMapper.queryBlogInfoById 条数为1 的结果集,因为它有两条关联的语句查询,它须要共访问数据库 1*(1+1)=3次数据库。
##10.2 嵌套结果查询## 嵌套语句的查询会致使数据库访问次数不定,进而有可能影响到性能
。Mybatis还支持一种嵌套结果的查询:即对于一对多,多对多,多对一的状况的查询,Mybatis经过联合查询,将结果从数据库内一次性查出来
,而后根据其一对多,多对一,多对多的关系和ResultMap中的配置,进行结果的转换,构建须要的对象。
从新定义BlogInfo的结果映射 resultMap:
<resultMap type="com.foo.bean.BlogInfo" id="BlogInfo"> <id column="blog_id" property="blogId"/> <result column="title" property="title"/> <association property="author" column="blog_author_id" javaType="com.foo.bean.Author"> <id column="author_id" property="authorId"/> <result column="user_name" property="userName"/> <result column="password" property="password"/> <result column="email" property="email"/> <result column="biography" property="biography"/> </association> <collection property="posts" column="blog_post_id" ofType="com.foo.bean.Post"> <id column="post_id" property="postId"/> <result column="blog_id" property="blogId"/> <result column="create_time" property="createTime"/> <result column="subject" property="subject"/> <result column="body" property="body"/> <result column="draft" property="draft"/> </collection> </resultMap>
对应的sql语句以下:
<select id="queryAllBlogInfo" resultMap="BlogInfo"> SELECT B.BLOG_ID, B.TITLE, B.AUTHOR_ID AS BLOG_AUTHOR_ID, A.AUTHOR_ID, A.USER_NAME, A.PASSWORD, A.EMAIL, A.BIOGRAPHY, P.POST_ID, P.BLOG_ID AS BLOG_POST_ID , P.CREATE_TIME, P.SUBJECT, P.BODY, P.DRAFT FROM BLOG B LEFT OUTER JOIN AUTHOR A ON B.AUTHOR_ID = A.AUTHOR_ID LEFT OUTER JOIN POST P ON P.BLOG_ID = B.BLOG_ID </select>
/* * 获取全部Blog的全部信息 */ public static BlogInfo nestedResultOnTest() { SqlSession session = sqlSessionFactory.openSession(); BlogInfo blogInfo = new BlogInfo(); blogInfo = (BlogInfo)session.selectOne("com.foo.bean.BlogMapper.queryAllBlogInfo"); JSONObject object = new JSONObject(blogInfo); System.out.println(object.toString()); return blogInfo; }
嵌套结果查询的执行步骤:
根据表的对应关系,进行join操做,获取到结果集;
根据结果集的信息和BlogInfo 的resultMap定义信息,对返回的结果集在内存中进行组装、赋值,构造BlogInfo;
返回构造出来的结果List<BlogInfo> 结果。
对于关联的结果查询,若是是多对一的关系
,则经过形如 <association property="author" column="blog_author_id" javaType="com.foo.bean.Author"> 进行配置,Mybatis会经过column属性对应的author_id 值去从内存中取数据,而且封装成Author对象;
若是是一对多的关系,就如Blog和Post之间的关系
,经过形如 <collection property="posts" column="blog_post_id" ofType="com.foo.bean.Post">进行配置,MyBatis经过 blog_Id去内存中取Post对象,封装成List<Post>;
对于关联结果的查询,只须要查询数据库一次,而后对结果的整合和组装所有放在了内存中。
#11 MyBatis一级缓存实现# ##11.1 什么是一级缓存? 为何使用一级缓存?## 每当咱们使用MyBatis开启一次和数据库的会话,MyBatis会建立出一个SqlSession对象表示一次数据库会话
。
在对数据库的一次会话中,咱们有可能会反复地执行彻底相同的查询语句,若是不采起一些措施的话,每一次查询都会查询一次数据库,而咱们在极短的时间内作了彻底相同的查询,那么它们的结果极有可能彻底相同,因为查询一次数据库的代价很大,这有可能形成很大的资源浪费。
为了解决这一问题,减小资源的浪费,MyBatis会在表示会话的SqlSession对象中创建一个简单的缓存,将每次查询到的结果结果缓存起来,当下次查询的时候,若是判断先前有个彻底同样的查询,会直接从缓存中直接将结果取出,返回给用户,不须要再进行一次数据库查询了。
以下图所示,MyBatis会在一次会话的表示----一个SqlSession对象中建立一个本地缓存(local cache),对于每一次查询,都会尝试根据查询的条件去本地缓存中查找是否在缓存中,若是在缓存中,就直接从缓存中取出,而后返回给用户;不然,从数据库读取数据,将查询结果存入缓存并返回给用户
。
对于会话(Session)级别的数据缓存,咱们称之为一级数据缓存,简称一级缓存。
##11.2 MyBatis中的一级缓存是怎样组织的?(即SqlSession中的缓存是怎样组织的?)## 因为MyBatis使用SqlSession对象表示一次数据库的会话,那么,对于会话级别的一级缓存也应该是在SqlSession中控制的
。
实际上, MyBatis只是一个MyBatis对外的接口,SqlSession将它的工做交给了Executor执行器这个角色来完成,负责完成对数据库的各类操做
。当建立了一个SqlSession对象时,MyBatis会为这个SqlSession对象建立一个新的Executor执行器,而缓存信息就被维护在这个Executor执行器中
,MyBatis将缓存和对缓存相关的操做封装成了Cache接口中。SqlSession、Executor、Cache
之间的关系以下列类图所示:
如上述的类图所示,Executor接口的实现类BaseExecutor中拥有一个Cache接口的实现类PerpetualCache,则对于BaseExecutor对象而言,它将使用PerpetualCache对象维护缓存
。
综上,SqlSession对象、Executor对象、Cache对象
之间的关系以下图所示:
因为Session级别的一级缓存实际上就是使用PerpetualCache维护的,那么PerpetualCache是怎样实现的呢?
PerpetualCache实现原理其实很简单,其内部就是经过一个简单的HashMap<k,v> 来实现的,没有其余的任何限制
。以下是PerpetualCache的实现代码:
package org.apache.ibatis.cache.impl; import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.ReadWriteLock; import org.apache.ibatis.cache.Cache; import org.apache.ibatis.cache.CacheException; /** * 使用简单的HashMap来维护缓存 * @author Clinton Begin */ public class PerpetualCache implements Cache { private String id; private Map<Object, Object> cache = new HashMap<Object, Object>(); public PerpetualCache(String id) { this.id = id; } public String getId() { return id; } public int getSize() { return cache.size(); } public void putObject(Object key, Object value) { cache.put(key, value); } public Object getObject(Object key) { return cache.get(key); } public Object removeObject(Object key) { return cache.remove(key); } public void clear() { cache.clear(); } public ReadWriteLock getReadWriteLock() { return null; } public boolean equals(Object o) { if (getId() == null) throw new CacheException("Cache instances require an ID."); if (this == o) return true; if (!(o instanceof Cache)) return false; Cache otherCache = (Cache) o; return getId().equals(otherCache.getId()); } public int hashCode() { if (getId() == null) throw new CacheException("Cache instances require an ID."); return getId().hashCode(); } }
##11.3 一级缓存的生命周期有多长?##
MyBatis在开启一个数据库会话时,会建立一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉
。
若是SqlSession调用了close()方法
,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用;
若是SqlSession调用了clearCache()
,会清空PerpetualCache对象中的数据,可是该对象仍可以使用;
SqlSession中执行了任何一个update操做(update()、delete()、insert())
,都会清空PerpetualCache对象的数据,可是该对象能够继续使用
;
##11.4 SqlSession 一级缓存的工做流程##
根据statementId,params,rowBounds来构建一个key值
,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果;##11.5 Cache接口的设计以及CacheKey的定义## 以下图所示,MyBatis定义了一个org.apache.ibatis.cache.Cache接口做为其Cache提供者的SPI(Service Provider Interface)
,全部的MyBatis内部的Cache缓存,都应该实现这一接口
。MyBatis定义了一个PerpetualCache实现类实现了Cache接口,实际上,在SqlSession对象里的Executor对象内维护的Cache类型实例对象,就是PerpetualCache子类建立的
。
(MyBatis内部还有不少Cache接口的实现,一级缓存只会涉及到这一个PerpetualCache子类,Cache的其余实现将会放到二级缓存中介绍)。
咱们知道,Cache最核心的实现其实就是一个Map,将本次查询使用的特征值做为key,将查询结果做为value存储到Map中。如今最核心的问题出现了:怎样来肯定一次查询的特征值?
换句话说就是:怎样判断某两次查询是彻底相同的查询?
也能够这样说:如何肯定Cache中的key值?
MyBatis认为,对于两次查询,若是如下条件都彻底同样,那么就认为它们是彻底相同的两次查询:
传入的 statementId
查询时要求的结果集中的结果范围 (结果的范围经过rowBounds.offset和rowBounds.limit表示)
此次查询所产生的最终要传递给JDBC java.sql.Preparedstatement的Sql语句字符串(boundSql.getSql() )
传递给java.sql.Statement要设置的参数值
如今分别解释上述四个条件:
传入的statementId,对于MyBatis而言,你要使用它,必须须要一个statementId,它表明着你将执行什么样的Sql
;
MyBatis自身提供的分页功能是经过RowBounds来实现的,它经过rowBounds.offset和rowBounds.limit来过滤查询出来的结果集,这种分页功能是基于查询结果的再过滤,而不是进行数据库的物理分页;
因为MyBatis底层仍是依赖于JDBC实现的,那么,对于两次彻底如出一辙的查询,MyBatis要保证对于底层JDBC而言,也是彻底一致的查询才行。而对于JDBC而言,两次查询,只要传入给JDBC的SQL语句彻底一致,传入的参数也彻底一致,就认为是两次查询是彻底一致的。
上述的第3个条件正是要求保证传递给JDBC的SQL语句彻底一致;第4条则是保证传递给JDBC的参数也彻底一致;即三、4两条MyBatis最本质的要求就是:调用JDBC的时候,传入的SQL语句要彻底相同,传递给JDBC的参数值也要彻底相同
。
综上所述,CacheKey由如下条件决定:statementId + rowBounds + 传递给JDBC的SQL + 传递给JDBC的参数值
;
对于每次的查询请求,Executor都会根据传递的参数信息以及动态生成的SQL语句,将上面的条件根据必定的计算规则,建立一个对应的CacheKey对象。
咱们知道建立CacheKey的目的,就两个:
根据CacheKey做为key,去Cache缓存中查找缓存结果;
若是查找缓存命中失败,则经过此CacheKey做为key,将从数据库查询到的结果做为value,组成key,value对存储到Cache缓存中;
CacheKey的构建被放置到了Executor接口的实现类BaseExecutor中,定义以下:
/** * 所属类: org.apache.ibatis.executor.BaseExecutor * 功能 : 根据传入信息构建CacheKey */ public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { if (closed) throw new ExecutorException("Executor was closed."); CacheKey cacheKey = new CacheKey(); //1.statementId cacheKey.update(ms.getId()); //2. rowBounds.offset cacheKey.update(rowBounds.getOffset()); //3. rowBounds.limit cacheKey.update(rowBounds.getLimit()); //4. SQL语句 cacheKey.update(boundSql.getSql()); //5. 将每个要传递给JDBC的参数值也更新到CacheKey中 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); for (int i = 0; i < parameterMappings.size(); i++) { // mimic DefaultParameterHandler logic ParameterMapping parameterMapping = parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; String propertyName = parameterMapping.getProperty(); if (boundSql.hasAdditionalParameter(propertyName)) { value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { value = null; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } //将每个要传递给JDBC的参数值也更新到CacheKey中 cacheKey.update(value); } } return cacheKey; }
刚才已经提到,Cache接口的实现,本质上是使用的HashMap<k,v>,而构建CacheKey的目的就是为了做为HashMap<k,v>中的key值。而HashMap是经过key值的hashcode 来组织和存储的,那么,构建CacheKey的过程实际上就是构造其hashCode的过程
。下面的代码就是CacheKey的核心hashcode生成算法,感兴趣的话能够看一下:
public void update(Object object) { if (object != null && object.getClass().isArray()) { int length = Array.getLength(object); for (int i = 0; i < length; i++) { Object element = Array.get(object, i); doUpdate(element); } } else { doUpdate(object); } } private void doUpdate(Object object) { //1. 获得对象的hashcode; int baseHashCode = object == null ? 1 : object.hashCode(); //对象计数递增 count++; checksum += baseHashCode; //2. 对象的hashcode 扩大count倍 baseHashCode *= count; //3. hashCode * 拓展因子(默认37)+拓展扩大后的对象hashCode值 hashcode = multiplier * hashcode + baseHashCode; updateList.add(object); }
MyBatis认为的彻底相同的查询,不是指使用sqlSession查询时传递给算起来Session的全部参数值完彻底全相同,你只要保证statementId,rowBounds,最后生成的SQL语句,以及这个SQL语句所须要的参数彻底一致就能够了。
##11.6 一级缓存的性能分析##
读者有可能就以为不妥了:若是我一直使用某一个SqlSession对象查询数据,这样会不会致使HashMap太大,而致使 java.lang.OutOfMemoryError错误啊?
读者这么考虑也不无道理,不过MyBatis的确是这样设计的。
MyBatis这样设计也有它本身的理由:
a. 通常而言SqlSession的生存时间很短。通常状况下使用一个SqlSession对象执行的操做不会太多,执行完就会消亡;
b. 对于某一个SqlSession对象而言,只要执行update操做(update、insert、delete),都会将这个SqlSession对象中对应的一级缓存清空掉,因此通常状况下不会出现缓存过大,影响JVM内存空间的问题;
c. 能够手动地释放掉SqlSession对象中的缓存。
MyBatis的一级缓存就是使用了简单的HashMap,MyBatis只负责将查询数据库的结果存储到缓存中去, 不会去判断缓存存放的时间是否过长、是否过时,所以也就没有对缓存的结果进行更新这一说了。
根据一级缓存的特性,在使用的过程当中,我认为应该注意:
对于数据变化频率很大,而且须要高时效准确性的数据要求,咱们使用SqlSession查询的时候,要控制好SqlSession的生存时间,SqlSession的生存时间越长,它其中缓存的数据有可能就越旧,从而形成和真实数据库的偏差;同时对于这种状况,用户也能够手动地适时清空SqlSession中的缓存;
对于只执行、而且频繁执行大范围的select操做的SqlSession对象,SqlSession对象的生存时间不该过长。
#12 MyBatis二级缓存实现# MyBatis的二级缓存是Application级别的缓存
,它能够提升对数据库查询的效率,以提升应用的性能。 ##12.1 MyBatis的缓存机制总体设计以及二级缓存的工做模式##
如上图所示,当开一个会话时,一个SqlSession对象会使用一个Executor对象来完成会话操做,MyBatis的二级缓存机制的关键就是对这个Executor对象作文章
。若是用户配置了"cacheEnabled=true"
,那么MyBatis在为SqlSession对象建立Executor对象时,会对Executor对象加上一个装饰者:CachingExecutor
,这时SqlSession使用CachingExecutor对象来完成操做请求。CachingExecutor对于查询请求,会先判断该查询请求在Application级别的二级缓存中是否有缓存结果
,若是有查询结果,则直接返回缓存结果;若是缓存中没有,再交给真正的Executor对象来完成查询操做,以后CachingExecutor会将真正Executor返回的查询结果放置到缓存中
,而后在返回给用户。
CachingExecutor是Executor的装饰者,以加强Executor的功能,使其具备缓存查询的功能,这里用到了设计模式中的装饰者模式
,CachingExecutor和Executor的接口的关系以下类图所示:
##12.2 MyBatis二级缓存的划分## MyBatis并非简单地对整个Application就只有一个Cache缓存对象,它将缓存划分的更细,便是Mapper级别的,即每个Mapper均可以拥有一个Cache对象,具体以下:
MyBatis将Application级别的二级缓存细分到Mapper级别
,即对于每个Mapper.xml,若是在其中使用了<cache> 节点,则MyBatis会为这个Mapper建立一个Cache缓存对象,以下图所示:
注:上述的每个Cache对象,都会有一个本身所属的namespace命名空间,而且会将Mapper的 namespace做为它们的ID;
若是你想让多个Mapper公用一个Cache的话,你可使用<cache-ref namespace="">节点,来指定你的这个Mapper使用到了哪个Mapper的Cache缓存。
##12.3 使用二级缓存,必需要具有的条件## MyBatis对二级缓存的支持粒度很细,它会指定某一条查询语句是否使用二级缓存
。
虽然在Mapper中配置了<cache>,而且为此Mapper分配了Cache对象,这并不表示咱们使用Mapper中定义的查询语句查到的结果都会放置到Cache对象之中
,咱们必须指定Mapper中的某条选择语句是否支持缓存,即以下所示,在<select> 节点中配置useCache="true",Mapper才会对此Select的查询支持缓存特性
,不然,不会对此Select查询,不会通过Cache缓存。以下所示,Select语句配置了useCache="true",则代表这条Select语句的查询会使用二级缓存。
<select id="selectByMinSalary" resultMap="BaseResultMap" parameterType="java.util.Map" useCache="true">
总之,要想使某条Select查询支持二级缓存,你须要保证:
MyBatis支持二级缓存的总开关:全局配置变量参数 cacheEnabled=true
该select语句所在的Mapper,配置了<cache> 或<cached-ref>节点,而且有效
该select语句的参数 useCache=true
##12.4 一级缓存和二级缓存的使用顺序## 请注意,若是你的MyBatis使用了二级缓存,而且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:二级缓存 ———> 一级缓存 ——> 数据库
。
##12.5 二级缓存实现的选择## MyBatis对二级缓存的设计很是灵活,它本身内部实现了一系列的Cache缓存实现类,并提供了各类缓存刷新策略如LRU,FIFO等等
;另外,MyBatis还容许用户自定义Cache接口实现,用户是须要实现org.apache.ibatis.cache.Cache接口,而后将Cache实现类配置在<cache type="">节点的type属性上便可;除此以外,MyBatis还支持跟第三方内存缓存库如Memecached的集成,总之,使用MyBatis的二级缓存有三个选择:
MyBatis自身提供的缓存实现;
用户自定义的Cache接口实现;
跟第三方内存缓存库的集成;
##12.6 MyBatis自身提供的二级缓存的实现## MyBatis自身提供了丰富的,而且功能强大的二级缓存的实现,它拥有一系列的Cache接口装饰者,能够知足各类对缓存操做和更新的策略。
MyBatis定义了大量的Cache的装饰器来加强Cache缓存的功能,以下类图所示。
对于每一个Cache而言,都有一个容量限制,MyBatis各供了各类策略来对Cache缓存的容量进行控制,以及对Cache中的数据进行刷新和置换。MyBatis主要提供了如下几个刷新和置换策略:
LRU:(Least Recently Used),最近最少使用算法,即若是缓存中容量已经满了,会将缓存中最近最少被使用的缓存记录清除掉,而后添加新的记录;
FIFO:(First in first out),先进先出算法,若是缓存中的容量已经满了,那么会将最早进入缓存中的数据清除掉;
Scheduled:指定时间间隔清空算法,该算法会以指定的某一个时间间隔将Cache缓存中的数据清空;
#13 如何细粒度地控制你的MyBatis二级缓存# ##13.1 一个关于MyBatis的二级缓存的实际问题## 现有AMapper.xml中定义了对数据库表 ATable 的CRUD操做,BMapper定义了对数据库表BTable的CRUD操做;
假设 MyBatis 的二级缓存开启,而且 AMapper 中使用了二级缓存,AMapper对应的二级缓存为ACache;
除此以外,AMapper 中还定义了一个跟BTable有关的查询语句,相似以下所述:
<select id="selectATableWithJoin" resultMap="BaseResultMap" useCache="true"> select * from ATable left join BTable on .... </select>
执行如下操做:
好,问题就出如今第3步上:
因为AMapper的“selectATableWithJoin” 对应的SQL语句须要和BTable进行join查找,而在第 2 步BTable的数据已经更新了,可是第 3 步查询的值是第 1 步的缓存值,已经极有可能跟真实数据库结果不同,即ACache中缓存数据过时了!
总结来看,就是:
对于某些使用了 join链接的查询,若是其关联的表数据发生了更新,join链接的查询因为先前缓存的缘由,致使查询结果和真实数据不一样步;
从MyBatis的角度来看,这个问题能够这样表述:
对于某些表执行了更新(update、delete、insert)操做后,如何去清空跟这些表有关联的查询语句所形成的缓存;
##13.2 当前MyBatis二级缓存的工做机制##
MyBatis二级缓存的一个重要特色:即松散的Cache缓存管理和维护
一个Mapper中定义的增删改查操做只能影响到本身关联的Cache对象
。如上图所示的Mapper namespace1中定义的若干CRUD语句,产生的缓存只会被放置到相应关联的Cache1中,即Mapper namespace2,namespace3,namespace4 中的CRUD的语句不会影响到Cache1。
能够看出,Mapper之间的缓存关系比较松散,相互关联的程度比较弱。
如今再回到上面描述的问题,若是咱们将AMapper和BMapper共用一个Cache对象
,那么,当BMapper执行更新操做时,能够清空对应Cache中的全部的缓存数据,这样的话,数据不是也能够保持最新吗?
确实这个也是一种解决方案,不过,它会使缓存的使用效率变的很低!
AMapper和BMapper的任意的更新操做都会将共用的Cache清空,会频繁地清空Cache,致使Cache实际的命中率和使用率就变得很低了,因此这种策略实际状况下是不可取的。
最理想的解决方案就是:
**对于某些表执行了更新(update、delete、insert)操做后,如何去清空跟这些表有关联的查询语句所形成的缓存;**这样,就是以很细的粒度管理MyBatis内部的缓存,使得缓存的使用率和准确率都能大大地提高。
##13.3 mybatis-enhanced-cache插件的设计和工做原理## 该插件主要由两个构件组成:EnhancedCachingExecutor和EnhancedCachingManager
。源码地址:https://github.com/LuanLouis/mybatis-enhanced-cache。
EnhancedCachingExecutor是针对于Executor的拦截器,拦截Executor的几个关键的方法;EnhancedCachingExecutor主要作如下几件事:
每当有Executor执行query操做时, 1.1 记录下该查询StatementId和CacheKey,而后将其添加到EnhancedCachingManager中; 1.2 记录下该查询StatementId和此StatementId所属Mapper内的Cache缓存对象引用,添加到EnhancedCachingManager中;
每当Executor执行了update操做时,将此update操做的StatementId传递给EnhancedCachingManager,让EnhancedCachingManager根据此update的StatementId的配置,去清空指定的查询语句所产生的缓存;
另外一个构件:EnhancedCachingManager,它也是本插件的核心,它维护着如下几样东西:
整个MyBatis的全部查询所产生的CacheKey集合(以statementId分类);
全部的使用过了的查询的statementId 及其对应的Cache缓存对象的引用;
update类型的StatementId和查询StatementId集合的映射,用于当Update类型的语句执行时,根据此映射决定应该清空哪些查询语句产生的缓存;
以下图所示:
原理很简单,就是 当执行了某个update操做时,根据配置信息去清空指定的查询语句在Cache中所产生的缓存数据。
##13.4 mybatis-enhanced-cache 插件的使用实例##
<plugins> <plugin interceptor="org.luanlouis.mybatis.plugin.cache.EnhancedCachingExecutor"> <property name="dependency" value="dependencys.xml"/> <property name="cacheEnabled" value="true"/> </plugin> </plugins>
其中,<property name="dependency"> 中的value属性是 StatementId之间的依赖关系的配置文件路径。
<?xml version="1.0" encoding="UTF-8"?> <dependencies> <statements> <statement id="com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey"> <observer id="com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments" /> </statement> </statements> </dependencies>
<statement>节点配置的是更新语句的statementId,其内的子节点<observer> 配置的是当更新语句执行后,应当清空缓存的查询语句的StatementId。子节点<observer>能够有多个。
如上的配置,则说明,若是"com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey" 更新语句执行后,由 “com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments” 语句所产生的放置在Cache缓存中的数据都都会被清空。
<?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.louis.mybatis.dao.DepartmentsMapper" > <cache></cache> <resultMap id="BaseResultMap" type="com.louis.mybatis.model.Department" > <id column="DEPARTMENT_ID" property="departmentId" jdbcType="DECIMAL" /> <result column="DEPARTMENT_NAME" property="departmentName" jdbcType="VARCHAR" /> <result column="MANAGER_ID" property="managerId" jdbcType="DECIMAL" /> <result column="LOCATION_ID" property="locationId" jdbcType="DECIMAL" /> </resultMap> <sql id="Base_Column_List" > DEPARTMENT_ID, DEPARTMENT_NAME, MANAGER_ID, LOCATION_ID </sql> <update id="updateByPrimaryKey" parameterType="com.louis.mybatis.model.Department" > update HR.DEPARTMENTS set DEPARTMENT_NAME = #{departmentName,jdbcType=VARCHAR}, MANAGER_ID = #{managerId,jdbcType=DECIMAL}, LOCATION_ID = #{locationId,jdbcType=DECIMAL} where DEPARTMENT_ID = #{departmentId,jdbcType=DECIMAL} </update> <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" > select <include refid="Base_Column_List" /> from HR.DEPARTMENTS where DEPARTMENT_ID = #{departmentId,jdbcType=DECIMAL} </select> </mapper>
<?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.louis.mybatis.dao.EmployeesMapper"> <cache eviction="LRU" flushInterval="100000" size="10000"/> <resultMap id="BaseResultMap" type="com.louis.mybatis.model.Employee"> <id column="EMPLOYEE_ID" jdbcType="DECIMAL" property="employeeId" /> <result column="FIRST_NAME" jdbcType="VARCHAR" property="firstName" /> <result column="LAST_NAME" jdbcType="VARCHAR" property="lastName" /> <result column="EMAIL" jdbcType="VARCHAR" property="email" /> <result column="PHONE_NUMBER" jdbcType="VARCHAR" property="phoneNumber" /> <result column="HIRE_DATE" jdbcType="DATE" property="hireDate" /> <result column="JOB_ID" jdbcType="VARCHAR" property="jobId" /> <result column="SALARY" jdbcType="DECIMAL" property="salary" /> <result column="COMMISSION_PCT" jdbcType="DECIMAL" property="commissionPct" /> <result column="MANAGER_ID" jdbcType="DECIMAL" property="managerId" /> <result column="DEPARTMENT_ID" jdbcType="DECIMAL" property="departmentId" /> </resultMap> <sql id="Base_Column_List"> EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID </sql> <select id="selectWithDepartments" parameterType="java.lang.Integer" resultMap="BaseResultMap" useCache="true" > select * from HR.EMPLOYEES t left join HR.DEPARTMENTS S ON T.DEPARTMENT_ID = S.DEPARTMENT_ID where EMPLOYEE_ID = #{employeeId,jdbcType=DECIMAL} </select> </mapper>
public class SelectDemo3 { private static final Logger loger = Logger.getLogger(SelectDemo3.class); public static void main(String[] args) throws Exception { InputStream inputStream = Resources.getResourceAsStream("mybatisConfig.xml"); SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build(inputStream); SqlSession sqlSession = factory.openSession(true); SqlSession sqlSession2 = factory.openSession(true); //3.使用SqlSession查询 Map<String,Object> params = new HashMap<String,Object>(); params.put("employeeId",10); //a.查询工资低于10000的员工 Date first = new Date(); //第一次查询 List<Employee> result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params); sqlSession.commit(); checkCacheStatus(sqlSession); params.put("employeeId", 11); result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params); sqlSession.commit(); checkCacheStatus(sqlSession); params.put("employeeId", 12); result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params); sqlSession.commit(); checkCacheStatus(sqlSession); params.put("employeeId", 13); result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params); sqlSession.commit(); checkCacheStatus(sqlSession); Department department = sqlSession.selectOne("com.louis.mybatis.dao.DepartmentsMapper.selectByPrimaryKey",10); department.setDepartmentName("updated"); sqlSession2.update("com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey", department); sqlSession.commit(); checkCacheStatus(sqlSession); } public static void checkCacheStatus(SqlSession sqlSession) { loger.info("------------Cache Status------------"); Iterator<String> iter = sqlSession.getConfiguration().getCacheNames().iterator(); while(iter.hasNext()) { String it = iter.next(); loger.info(it+":"+sqlSession.getConfiguration().getCache(it).getSize()); } loger.info("------------------------------------"); } }
结果分析:
从上述的结果能够看出,前四次执行了“com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments”语句,EmployeesMapper对应的Cache缓存中存储的结果缓存有1个增长到4个。
当执行了"com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey"后,EmployeeMapper对应的缓存Cache结果被清空了,即"com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey"更新语句引发了EmployeeMapper中的"com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments"缓存的清空。