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閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容