Mybatis之深入学习——跟进中......

之前在spring mvc + mybatis项目中对mybatis的使用有了一定的掌握,但对于其内部的具体实现并不了解,因此在此开启对于mybatis更加深入的学习。

一、介绍

定义

MyBatis 是一个可以自定义SQL、存储过程和高级映射的持久层框架。MyBatis 摒除了大部分的JDBC代码、手工设置参数和结果集重获。
MyBatis 只使用简单的XML 和注解来配置和映射基本数据类型、Map 接口和POJO 到数据库记录。相对Hibernate和Apache OJB等“一站式”ORM解决方案而言,Mybatis 是一种“半自动化”的ORM实现。

核心组件

主要包括:
SqlSessionFactoryBuilder:会根据配置信息或代码来生成SqlSessionFactory;
SqlSessionFactory:依靠工厂来生成SqlSession;
SqlSession:是一个既可以发送SQL去执行并返回结果,也可以获取Mapper的接口;
SQL Mapper:是MyBatis新设计的组件,由一个Java接口和XML文件构成,需要给出对应的SQL和映射规则。它负责发送SQL去执行,并返回结果。

二、架构

MyBatis设计框架

API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。
数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。
框架支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。
引导层:配置和启动MyBatis配置信息的方法。

数据处理流程

数据处理流程

数据处理过程:
1.根据SQL的ID查找相应的MappedStatement对象。
2.根据传入参数对象解析MappedStatement对象,得到最终要执行的SQL和执行传入参数。
3.获取数据库连接,根据得到的最终SQL语句和执行传入参数到数据库执行,并得到执行结果。
4.根据MappedStatement对象中的结果映射对得到的执行结果进行转换处理,并得到最终的处理结果。
5.释放连接资源。

三、源码剖析

1.Mybatis Demo

以我mybatis入门的demo为例:

   private static SqlSessionFactoryBuilder sqlSessionFactoryBuilder;
   private static SqlSessionFactory sqlSessionFactory;
   private static void init() throws IOException {
       String resource = "mybatis-config.xml";
       Reader reader = Resources.getResourceAsReader(resource);
       sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
       sqlSessionFactory = sqlSessionFactoryBuilder.build(reader);
   }

应用程序的入口是SqlSessionFactoryBuilder,作用是通过XML配置文件创建Configuration对象,然后通过build方法创建SqlSessionFactory对象。
注:
没有必要每次访问Mybatis就创建一次SqlSessionFactoryBuilder,通常的做法是创建一个全局的对象

2. 入口类SqlSessionFactoryBuilder

public class SqlSessionFactoryBuilder {

    //Reader读取mybatis配置文件,传入构造方法
    public SqlSessionFactory build(Reader reader) {
        return build(reader, null, null);
    }

    public SqlSessionFactory build(Reader reader, String environment) {
        return build(reader, environment, null);
    }
  
    public SqlSessionFactory build(Reader reader, Properties properties) {
        return build(reader, null, properties);
    }
  
    //通过XMLConfigBuilder解析mybatis配置,从而创建SqlSessionFactory对象
    public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
        try {
            XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
            //构建的核心方法
            return build(parser.parse());
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error building SqlSession.", e);
        } finally {
            ErrorContext.instance().reset();
            try {
                reader.close();
            } catch (IOException e) {
                // Intentionally ignore. Prefer previous error.
            }
        }
    }
}

可以看到构建的核心是这一行:

return build(parser.parse());

parser的类是XMLConfigBuilder,XMLConfigBuilder 部分源码如下:

/**
 * mybatis 配置文件解析
 */
public class XMLConfigBuilder extends BaseBuilder {
    public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
        this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
    }
  
    //外部调用此方法对mybatis配置文件进行解析
    public Configuration parse() {
        if (parsed) {
            throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }
        parsed = true;
        //从根节点configuration
        parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
    }

    /**此方法解析configuration节点下的子节点
    *在configuration下面能配置的节点为以下10个节点
    */
    private void parseConfiguration(XNode root) {
        try {
            propertiesElement(root.evalNode("properties")); 
            typeAliasesElement(root.evalNode("typeAliases"));
            pluginElement(root.evalNode("plugins"));
            objectFactoryElement(root.evalNode("objectFactory"));
            objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
            settingsElement(root.evalNode("settings"));
            environmentsElement(root.evalNode("environments")); 
            databaseIdProviderElement(root.evalNode("databaseIdProvider"));
            typeHandlerElement(root.evalNode("typeHandlers"));
            mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
    }
}

跟进中...

三、SQL执行流程源码剖析

我们都是通过SqlSession去执行sql语句,Sqlsession对应着一次数据库会话。由于数据库会话不是永久的,因此Sqlsession的生命周期也不应该是永久的,相反,在你每次访问数据库时都需要创建。

获取SqlSession的步骤:

  1. 首先,SqlSessionFactoryBuilder去读取mybatis的配置文件;
  2. 然后构建一个DefaultSqlSessionFactory。
    源码如下:
/**
  * 一系列的构造方法最终都会调用此构建方法
  */
 public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
   try {
     /**通过XMLConfigBuilder解析配置文件,
     *解析的配置相关信息都会被封装为一个Configuration对象
     */
     XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
     //创建DefaultSessionFactory对象
     return build(parser.parse());
   } catch (Exception e) {
     throw ExceptionFactory.wrapException("Error building SqlSession.", e);
   } finally {
     ErrorContext.instance().reset();
     try {
       reader.close();
     } catch (IOException e) {
     }
   }
 }

 public SqlSessionFactory build(Configuration config) {
   return new DefaultSqlSessionFactory(config);
 }

在获取到SqlSessionFactory之后,就可以通过SqlSessionFactory去获取SqlSession对象:

 /**
  * 通常一系列openSession方法最终都会调用此方法
  */
 private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
   Transaction tx = null;
   try {
     /**通过Confuguration对象去获取Mybatis相关配置信息, 
     *Environment对象包含了数据源和事务的配置
     */
     final Environment environment = configuration.getEnvironment();
     final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
     tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
     //通过excutor真正执行sql, excutor是对于Statement的封装
     final Executor executor = configuration.newExecutor(tx, execType);
     //创建了一个DefaultSqlSession对象
     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();
   }
 }

而方法openSessionFromDataSource才是实际创建SqlSession的地方:

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {  
    Connection connection = null;  
    try {  
        final Environment environment = configuration.getEnvironment();  
        final DataSource dataSource = getDataSourceFromEnvironment(environment);  
         /**MyBatis对事务的处理相对简单,TransactionIsolationLevel中定义了几种隔离级别,
         *并不支持内嵌事务这样较复杂的场景,同时由于其是持久层的缘故,
         *所以真正在应用开发中会委托Spring来处理事务实现真正的与开发者隔离。
         *分析事务的实现是个入口,借此可以了解不少JDBC规范方面的事情。
         */
        TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);  
         connection = dataSource.getConnection();  
         if (level != null) {  
            connection.setTransactionIsolation(level.getLevel());
        }  
         connection = wrapConnection(connection);  
         Transaction tx = transactionFactory.newTransaction(connection,autoCommit);  
         Executorexecutor = configuration.newExecutor(tx, execType);  
         return newDefaultSqlSession(configuration, executor, autoCommit);  
     } catch (Exceptione) {  
        closeConnection(connection);  
        throwExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);  
    } finally {
        ErrorContext.instance().reset();
    }
}  

综上,创建sqlsession的主要步骤:

  1. 从配置中获取Environment;
  2. 从Environment中取得DataSource;
  3. 从Environment中取得TransactionFactory;
  4. 从DataSource里获取数据库连接对象Connection;
  5. 在取得的数据库连接上创建事务对象Transaction;
  6. 创建Executor对象(该对象非常重要,事实上sqlsession的所有操作都是通过它完成的);
  7. 创建sqlsession对象。

在mybatis中,通过MapperProxy动态代理dao, 也就是说, 当执行dao中的方法的时,其实是对应的mapperProxy在代理。
那么,接下来我们来看看是如何获取MapperProxy对象:
首先,通过SqlSession从Configuration中获取:

 /**
  * 什么都不做,直接调用configuration中的getMapper方法
  */
 @Override
 public <T> T getMapper(Class<T> type) {
   return configuration.<T>getMapper(type, this);
 }

之后,Configuration源码:

/**
  * 直接调用MapperRegistry的方法
  */
 public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
   return mapperRegistry.getMapper(type, sqlSession);
 }

MapperRegistry源码如下:

 public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
   //MapperProxyFactory动态代理DAO接口
   final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
   if (mapperProxyFactory == null) {
     throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
   }
   try {
     //关键方法的实现
     return mapperProxyFactory.newInstance(sqlSession);
   } catch (Exception e) {
     throw new BindingException("Error getting mapper instance. Cause: " + e, e);
   }
 }

MapperProxyFactory源码:

 protected T newInstance(MapperProxy<T> mapperProxy) {
   //动态代理dao接口
   return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
 }
 
 public T newInstance(SqlSession sqlSession) {
   final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
   return newInstance(mapperProxy);
 }

上述解释了是如何动态代理DAO接口,接下来我们继续来看具体是怎么执行sql语句的,Sqlsession对数据库的操作都是通过Executor来完成的。与Sqlsession一样,Executor也是动态创建的:

 public Executor newExecutor(Transaction transaction, ExecutorType executorType) {  
          executorType = executorType == null ? defaultExecutorType : executorType;  
          executorType = executorType == null ?ExecutorType.SIMPLE : executorType;  
          Executor executor;  
          /**如果不开启cache的话,
          *创建的Executor只是3中基础类型之一
          */
          //BatchExecutor专门用于执行批量sql操作
          if(ExecutorType.BATCH == executorType) {
            executor = new BatchExecutor(this,transaction);
         } 
         //ReuseExecutor会重用statement执行sql操作
          else if(ExecutorType.REUSE == executorType) {
            executor = new ReuseExecutor(this,transaction);  
        }
         //SimpleExecutor只是简单执行sql
         else {  
            executor = newSimpleExecutor(this, transaction);
        }
          /**如果开启cache的话(默认开启),
          *就会创建CachingExecutor,它以前面创建的Executor作为唯一参数
          */
          if (cacheEnabled) {
           executor = new CachingExecutor(executor);  
        }
        executor = (Executor) interceptorChain.pluginAll(executor);  
        return executor;  
    }  

上述源码中,CachingExecutor在查询数据库前先查找缓存,若没找到的话调用delegate从数据库查询,并将查询结果存入缓存中。

上述中,每个MapperProxy对应一个dao接口, 在使用的时候,MapperProxy的具体实现:

  /**
   * MapperProxy在执行时会触发此方法
   */
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
      try {
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //MapperMethod执行sqlSession
    return mapperMethod.execute(sqlSession, args);
  }

MapperMethod:

  1. 根据参数和返回值类型选择不同的sqlsession方法来执行。
  2. 将mapper对象与sqlsession真正的关联起来。

其execute方法源码:

/**
   * 先判断CRUD类型,
   * 然后根据类型去选择到底执行sqlSession中的哪个方法
   */
  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    if (SqlCommandType.INSERT == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
    } else if (SqlCommandType.UPDATE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
    } else if (SqlCommandType.DELETE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
    } else if (SqlCommandType.SELECT == command.getType()) {
      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 {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
      }
    } else {
      throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName() 
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

对sqlsession方法的访问最终都会落到executor的相应方法上去。

SqlSession的CRUD方法,以selectList方法为例:

  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      //CRUD实际上是交给Excecutor去处理
      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();
    }
  }

Executor分成两大类,一类是CacheExecutor,另一类是普通Executor。
普通Executor:

  1. BatchExecutor专门用于执行批量sql操作。
  2. ReuseExecutor会重用statement执行sql操作。
  3. SimpleExecutor只是简单执行sql没有什么特别的。
    以SimpleExecutor为例:
public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds,ResultHandler resultHandler) throws SQLException {  
    Statement stmt = null;  
    try {  
        Configuration configuration = ms.getConfiguration();  
        StatementHandler handler = configuration.newStatementHandler(this, ms,parameter, rowBounds,resultHandler);  
        stmt =prepareStatement(handler);  
        returnhandler.query(stmt, resultHandler);  
    } finally {  
        closeStatement(stmt);  
    }  
}  

通过一层一层的调用,最终会来到doQuery方法,以SimpleExecutor为例:

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());
      //StatementHandler封装了Statement, 通过StatementHandler 去处理
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

四、动态SQL

什么是动态SQL? 有什么作用?

传统的使用JDBC的方法,在组合复杂的的SQL语句时,需要拼接,容易导致错误。Mybatis的动态SQL功能正是为了解决这种问题应用而生, 其通过 if, choose, when, otherwise, trim, where, set, foreach标签,可组合成非常灵活的SQL语句,从而提高开发人员的效率。

if

<select id="findUserById" resultType="user">
    select * from user where 
        <if test="id != null">
               id=#{id}
        </if>
    and deleteFlag=0;
</select>

上面例子: 如果传入的id 不为空, 那么才会SQL才拼接id = #{id}。
但如果传入的id为null, 那么你这最终的SQL语句:

 select * from user where and deleteFlag=0

语句有错,无法通过解析!
此时需要引入where

where

<select id="findUserById" resultType="user">
    select * from user 
        <where>
            <if test="id != null">
                id=#{id}
            </if>
            and deleteFlag=0;
        </where>
</select>

mybatis中,当where标签遇到AND或OR时,会去除AND或OR。

set

<update id="updateUser" parameterType="com.dy.entity.User">
    update user
        <set>
            <if test="name != null">name = #{name},</if> 
            <if test="password != null">password = #{password},</if> 
            <if test="age != null">age = #{age},</if> 
        </set>
        <where>
            <if test="id != null">
                id = #{id}
            </if>
            and deleteFlag = 0;
        </where>
</update>

foreach

java中有for, 可通过for循环, 同样在mybatis中有foreach, 可通过它实现循环,循环的对象主要是java容器和数组。

<select id="selectPostIn" resultType="domain.blog.Post">
    SELECT *
    FROM POST P
    WHERE ID in
    <foreach item="item" index="index" collection="list"
        open="(" separator="," close=")">
        #{item}
    </foreach>
</select>

五、缓存机制源码分析

1. 介绍

当一条SQL语句被标记为“可缓存”后,第一次执行时会将从数据库获取的所有数据存储在一段高速缓存中,之后执行同样语句时会从高速缓存中读取结果,而不是再次在数据库中去命中。

Mybatis提供查询缓存,用于减轻数据压力,提高数据库性能。

Mybaits提供一级缓存,和二级缓存:

  1. 一级缓存的作用域是同一个SqlSession,在同一个sqlSession中两次执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。当一个sqlSession结束后该sqlSession中的一级缓存也就不存在了。
    Mybatis默认开启一级缓存。

  2. 二级缓存是多个SqlSession共享的,其作用域是mapper的同一个namespace,不同的sqlSession两次执行相同namespace下的sql语句且向sql中传递参数也相同即最终执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。

Mybatis中一级缓存和二级缓存的结构如下:

Mybatis缓存机制

2. 源码剖析

2.1 一级缓存

一级缓存的作用域是SqlSession,那么我们就先看从SqlSession入手,类DefaultSqlSession是接口SqlSession的实现类, 其中方法selectList:

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
   try {
     MappedStatement ms = configuration.getMappedStatement(statement);
     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();
   }
}

可以看到SqlSession调用接口Executor中的方法。接下来我们看下DefaultSqlSession中的executor接口属性是如何得到的:

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, autoCommit);
     return new DefaultSqlSession(configuration, executor);
   } 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();
   }
}

可以看到,Executor接口的实现类是由Configuration构造的:

public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
   executorType = executorType == null ? defaultExecutorType : executorType;
   executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
   Executor executor;
   if (ExecutorType.BATCH == executorType) {
     executor = new BatchExecutor(this, transaction);
   } else if (ExecutorType.REUSE == executorType) {
     executor = new ReuseExecutor(this, transaction);
   } else {
     executor = new SimpleExecutor(this, transaction);
   }
   if (cacheEnabled) {
     executor = new CachingExecutor(executor, autoCommit);
   }
   executor = (Executor) interceptorChain.pluginAll(executor);
   return executor;
}

根据不同的ExecutorType创建Executor:

  1. 如果属性cacheEnabled为true的话,那么通过装饰器CachingExecutor包装executor,这个装饰器是 。
  2. 属性cacheEnabled是配置文件中节点settings中子节点cacheEnabled的值,默认为true。
    接下来,CachingExecutor执行sql的操作是什么,类CachingExecutor中方法query:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
   //cache是个二级缓存
   Cache cache = ms.getCache();
   if (cache != null) {
     flushCacheIfRequired(ms);
     if (ms.isUseCache() && resultHandler == null) {
       ensureNoOutParams(ms, parameterObject, boundSql);
       if (!dirty) {
         cache.getReadWriteLock().readLock().lock();
         try {
           @SuppressWarnings("unchecked")
           List<E> cachedList = (List<E>) cache.getObject(key);
           if (cachedList != null) return cachedList;
         } finally {
           cache.getReadWriteLock().readLock().unlock();
         }
       }
       List<E> list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
       tcm.putObject(cache, key, list); // issue #578. Query must be not synchronized to prevent deadlocks
       return list;
     }
   }
   return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

上述代码中是类SimpleExecutor,由于SimpleExecutor没有覆盖父类中方法query,因此最终执行了类SimpleExecutor的父类BaseExecutor中的方法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++;
     //localCache就是一级缓存
     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();
     }
     deferredLoads.clear(); // issue #601
     if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
       clearLocalCache(); // issue #482
     }
   }
   return list;
}

类BaseExecutor中的属性localCache是类PerpetualCache的实例。类PerpetualCache 同样实现了Mybatis的Cache缓存接口的实现类,内部通过使用Map 类型的属性存储缓存数据。
localCache就是一级缓存。
在执行新增或更新或删除操作,一级缓存就会被清除,接下来我们来看看其原理。首先Mybatis在新增或删除时,都是通过调用方法update,即,新增或删除操作在Mybatis中都被视为更新操作。
类DefaultSqlSession中方法update:

public int update(String statement, Object parameter) {
   try {
     dirty = true;
     MappedStatement ms = configuration.getMappedStatement(statement);
     //调用了CachingExecutor的update方法
     return executor.update(ms, wrapCollection(parameter));
   } catch (Exception e) {
     throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
   } finally {
     ErrorContext.instance().reset();
   }
}

调用了CachingExecutor的update方法。

public int update(MappedStatement ms, Object parameterObject) throws SQLException {
   //方法flushCacheIfRequired清除的是二级缓存
   flushCacheIfRequired(ms);
   return delegate.update(ms, parameterObject);
}

CachingExecutor委托给类SimpleExecutor的方法update,SimpleExecutor没有覆盖父类BaseExecutor的方法update。BaseExecutor的方法update:

public int update(MappedStatement ms, Object parameter) throws SQLException {
   ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
   if (closed) throw new ExecutorException("Executor was closed.");
   //清除一级缓存LocalCache
   clearLocalCache();
   return doUpdate(ms, parameter);
}

方法clearLocalCache清除一级缓存LocalCache:

public void clearLocalCache() {
   if (!closed) {
     localCache.clear();
     localOutputParameterCache.clear();
   }
}

可以看到:
如果sqlsession没有关闭的话,进行新增、删除、修改这类更新操作,那么就清除一级缓存,即SqlSession的缓存。

2.2 二级缓存

二级缓存的作用域是全局,即,二级缓存已脱离SqlSession的控制,二级缓存在SqlSession关闭或提交之后才会生效。

二级缓存的工作机制:

  1. 一个SqlSession对象会通过使用一个Executor对象来完成会话操作,Mybatis的二级缓存机制的关键就在于这个Executor对象。
  2. 如果用户配置了属性"cacheEnabled=true",那么Mybatis在为SqlSession的对象创建Executor对象时,会对Executor对象加上装饰器CachingExecutor,此时SqlSession通过使用CachingExecutor对象完成操作请求。
  3. CachingExecutor对于查询请求,首先判断该查询请求在Application级别的二级缓存中是否有缓存结果。
    3.1如果有查询结果,则直接返回缓存结果;
    3.2 如果缓存中没有,再交给真正的Executor对象来完成查询操作,之后CachingExecutor会将真正Executor返回的查询结果放置到缓存中,最后再返回给用户。

下图是二级缓存工作模式:


二级缓存工作模式

缓存配置操作:

  1. mybatis全局配置文件中的setting中的cacheEnabled需为true。
  2. mapper配置文件中需要加入<cache>节点。
  3. mapper配置文件中的select节点需要加上属性useCache需要为true。

类XMLMappedBuilder用来解析每个mapper配置文件的解析类,每一个mapper配置都会实例化一个XMLMapperBuilder类,其中的解析方法:

private void configurationElement(XNode context) {
   try {
     String namespace = context.getStringAttribute("namespace");
     if (namespace.equals("")) {
         throw new BuilderException("Mapper's namespace cannot be empty");
     }
     builderAssistant.setCurrentNamespace(namespace);
     cacheRefElement(context.evalNode("cache-ref"));
     //解析缓存cache方法
     cacheElement(context.evalNode("cache"));
     parameterMapElement(context.evalNodes("/mapper/parameterMap"));
     resultMapElements(context.evalNodes("/mapper/resultMap"));
     sqlElement(context.evalNodes("/mapper/sql"));
     buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
   } catch (Exception e) {
     throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
   }
}

方法cacheElement解析缓存cache:

private void cacheElement(XNode context) throws Exception {
   if (context != null) {
     String type = context.getStringAttribute("type", "PERPETUAL");
     Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
     String eviction = context.getStringAttribute("eviction", "LRU");
     Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
     Long flushInterval = context.getLongAttribute("flushInterval");
     Integer size = context.getIntAttribute("size");
     boolean readWrite = !context.getBooleanAttribute("readOnly", false);
     Properties props = context.getChildrenAsProperties();
     builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props);
   }
}

解析完cache标签之后会使用类builderAssistant的userNewCache方法:

public Cache useNewCache(Class<? extends Cache> typeClass,
    Class<? extends Cache> evictionClass,
    Long flushInterval,
    Integer size,
    boolean readWrite,
    Properties props) {
        typeClass = valueOrDefault(typeClass, PerpetualCache.class);
        evictionClass = valueOrDefault(evictionClass, LruCache.class);
        Cache cache = new CacheBuilder(currentNamespace)
       .implementation(typeClass)
       .addDecorator(evictionClass)
       .clearInterval(flushInterval)
       .size(size)
       .readWrite(readWrite)
       .properties(props)
       .build();
   configuration.addCache(cache);
   currentCache = cache;
   return cache;
}

目前,mapper配置文件中的cache节点被解析到了XMLMapperBuilder实例中的builderAssistant属性中的currentCache值里。
接下来类XMLMapperBuilder会解析节点select,通过使用XMLStatementBuilder进行解析(也包括其他节点insert,update,delete):

public void parseStatementNode() {
   String id = context.getStringAttribute("id");
   String databaseId = context.getStringAttribute("databaseId");

   if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) return;

   Integer fetchSize = context.getIntAttribute("fetchSize");
   Integer timeout = context.getIntAttribute("timeout");
   String parameterMap = context.getStringAttribute("parameterMap");
   String parameterType = context.getStringAttribute("parameterType");
   Class<?> parameterTypeClass = resolveClass(parameterType);
   String resultMap = context.getStringAttribute("resultMap");
   String resultType = context.getStringAttribute("resultType");
   String lang = context.getStringAttribute("lang");
   LanguageDriver langDriver = getLanguageDriver(lang);

   Class<?> resultTypeClass = resolveClass(resultType);
   String resultSetType = context.getStringAttribute("resultSetType");
   StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
   ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

   String nodeName = context.getNode().getNodeName();
   SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
   boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
   boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
   boolean useCache = context.getBooleanAttribute("useCache", isSelect);
   boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

   XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
   includeParser.applyIncludes(context.getNode());

   // 解析selectKey
   processSelectKeyNodes(id, parameterTypeClass, langDriver);

   // 解析SQL
   SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
   String resultSets = context.getStringAttribute("resultSets");
   String keyProperty = context.getStringAttribute("keyProperty");
   String keyColumn = context.getStringAttribute("keyColumn");
   KeyGenerator keyGenerator;
   String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
   keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
   if (configuration.hasKeyGenerator(keyStatementId)) {
     keyGenerator = configuration.getKeyGenerator(keyStatementId);
   } else {
     keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
         configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
         ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
   }

   builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
       fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
       resultSetTypeEnum, flushCache, useCache, resultOrdered,
       keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

上述源码前半部分都在解析一些标签的属性,可以看到最后一行使用builderAssistant添加MappedStatement,其中builderAssistant属性是构造XMLStatementBuilder的时候通过XMLMappedBuilder传入的,接下来,我们看如何设置二级缓存:

private void setStatementCache(
    boolean isSelect,
    boolean flushCache,
    boolean useCache,
    Cache cache,
    MappedStatement.Builder statementBuilder) {
        flushCache = valueOrDefault(flushCache, !isSelect);
        useCache = valueOrDefault(useCache, isSelect);
        statementBuilder.flushCacheRequired(flushCache);
        statementBuilder.useCache(useCache);
        statementBuilder.cache(cache);
}

最终mapper配置文件中的<cache/>被设置到了类XMLMapperBuilder的属性builderAssistant中,XMLMapperBuilder中使用XMLStatementBuilder遍历CRUD节点,遍历CRUD节点的时候将这个cache节点设置到这些CRUD节点中,这个cache就是所谓的二级缓存。
在使用二级缓存之后:查询数据的话,先从二级缓存中拿数据,如果没有的话,去一级缓存中拿,一级缓存也没有的话再查询数据库。有了数据之后在丢到TransactionalCache这个对象的entriesToAddOnCommit属性中。

接下来我们来验证为什么SqlSession commit或close之后,二级缓存才会生效:
类DefaultSqlSession的方法commit:

public void commit(boolean force) {
   try {
     executor.commit(isCommitOrRollbackRequired(force));
     dirty = false;
   } catch (Exception e) {
     throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
   } finally {
     ErrorContext.instance().reset();
   }
}

类CachingExecutor的方法commit:

public void commit(boolean required) throws SQLException {
   delegate.commit(required);
   tcm.commit();
   dirty = false;
}

类TransactionalCacheManager的方法commit:

public void commit() {
   for (TransactionalCache txCache : transactionalCaches.values()) {
     txCache.commit();
   }
}

类TransactionalCache的方法commit:

public void commit() {
   delegate.getReadWriteLock().writeLock().lock();
   try {
     if (clearOnCommit) {
       delegate.clear();
     } else {
       for (RemoveEntry entry : entriesToRemoveOnCommit.values()) {
         entry.commit();
       }
     }
     for (AddEntry entry : entriesToAddOnCommit.values()) {
       entry.commit();
     }
     reset();
   } finally {
     delegate.getReadWriteLock().writeLock().unlock();
   }
}

可以看到调用了AddEntry的方法commit:

public void commit() {
    cache.putObject(key, value);
}

原来方法AddEntry中的commit方法会把数据丢到cache中,也就是丢到二级缓存中。
而之所以为何调用close方法后,二级缓存才会生效,是因为close方法内部会调用commit方法。

四、JDBC演变到Mybatis过程

JDBC实现查询所需步骤:

加载JDBC驱动;
建立并获取数据库连接;
创建 JDBC Statements 对象;
设置SQL语句的传入参数;
执行SQL语句并获得查询结果;
对查询结果进行转换处理并将处理结果返回;
释放相关资源(关闭Connection,关闭Statement,关闭ResultSet);

1. 连接获取和释放

问题描述:
数据库连接频繁的开启和关闭本身就造成了资源的浪费,影响系统的性能。

优化方案:
数据库连接的获取和关闭我们可以使用数据库连接池来解决资源浪费的问题。通过连接池就可以反复利用已经建立的连接去访问数据库了。减少连接的开启和关闭的时间。

2. SQL统一存取

问题描述:
使用JDBC进行操作数据库时,SQL语句基本都散落在各个JAVA类中,这样有三个不足之处:

  1. 可读性很差,不利于维护以及做性能调优。
  2. 改动Java代码需要重新编译、打包部署。
  3. 不利于取出SQL在数据库客户端执行。

优化方案:
可以考虑不把SQL语句写到Java代码中,那么把SQL语句放到哪里呢?首先需要有一个统一存放的地方,我们可以将这些SQL语句统一集中放到配置文件或者数据库里面(以key-value的格式存放)。然后通过SQL语句的key值去获取对应的SQL语句。

3. 传入参数映射和动态SQL

问题描述:
既然我们已经把SQL语句统一存放在配置文件或者数据库中了,怎么做到能够根据前台传入参数的不同,动态生成对应的SQL语句呢?

优化方案:
需要使用一种有别于SQL的语法来嵌入变量(比如使用#变量名#)。这样,SQL语句经过解析后就可以动态的生成符合上下文的SQL语句。可以使用#变量名#表示占位符变量,使用变量名表示非占位符变量。

4. 结果映射和结果缓存

问题描述:
执行SQL语句、获取执行结果、对执行结果进行转换处理、释放相关资源是一整套下来的。假如是执行查询语句,那么执行SQL语句后,返回的是一个ResultSet结果集,这个时候我们就需要将ResultSet对象的数据取出来,不然等到释放资源时就取不到这些结果信息了。

优化方案:
必须告诉SQL处理器两点:第一,需要返回什么类型的对象;第二,需要返回的对象的数据结构怎么跟执行的结果映射,这样才能将具体的值copy到对应的数据结构上。

5. 解决重复SQL语句问题

问题描述:
由于我们将所有SQL语句都放到配置文件中,这个时候会遇到一个SQL重复的问题,几个功能的SQL语句其实都差不多,有些可能是SELECT后面那段不同、有些可能是WHERE语句不同。有时候表结构改了,那么我们就需要改多个地方,不利于维护。

优化方案:
当我们的代码程序出现重复代码时怎么办?将重复的代码抽离出来成为独立的一个类,然后在各个需要使用的地方进行引用。对于SQL重复的问题,我们也可以采用这种方式,通过将SQL片段模块化,将重复的SQL片段独立成一个SQL块,然后在各个SQL语句引用重复的SQL块,这样需要修改时只需要修改一处即可。

未完跟进中......

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容