MyBatis源碼學(xué)習(xí)

mybatis 不會(huì)直接和數(shù)據(jù)庫(kù)進(jìn)行打交道,mybatis 其實(shí)是對(duì) jdbc api 的進(jìn)一步封裝,最終和數(shù)據(jù)庫(kù)打交道的仍然是 jdbc 。

1. MyBatis基本構(gòu)成

  • SqlSessionFactoryBuilder(構(gòu)造器):它會(huì)根據(jù)配置信息或者代碼來(lái)生成SqlSessionFactory(工廠接口);
    • 生命周期:它的作用就是一個(gè)構(gòu)造器,一旦我們構(gòu)建了SqlSessionFactory,SqlSessionFactoryBuilder的作用就已經(jīng)完結(jié)。所以它的生命周期僅存在于方法局部。
  • SqlSessionFactory:依靠工廠來(lái)生成SqlSession(會(huì)話);
    • 生命周期:SqlSessionFactory的作用是創(chuàng)建SqlSession,而SqlSession就是一個(gè)會(huì)話,相當(dāng)于JDBC中的Connection對(duì)象。每次應(yīng)用程序需要訪問(wèn)數(shù)據(jù)庫(kù),我們就通過(guò)SqlSessionFactory創(chuàng)建SqlSession,所以SqlSessionFactory應(yīng)該在MyBatis應(yīng)用的整個(gè)生命周期中。而如果我們多次創(chuàng)建同一個(gè)數(shù)據(jù)庫(kù)的SqlSessionFactory,則每次創(chuàng)建SqlSessionFactory會(huì)打開(kāi)更多的數(shù)據(jù)庫(kù)連接(Connection)資源,那么連接資源就很快會(huì)被耗盡。因此SqlSessionFactory是一個(gè)全局單例,對(duì)應(yīng)一個(gè)數(shù)據(jù)庫(kù)連接池。
  • SqlSession:是一個(gè)既可以發(fā)送SQL去執(zhí)行并返回結(jié)果,也可以獲取Mapper的接口。
    • 生命周期:SqlSession相當(dāng)于一個(gè)JDBC的Connection對(duì)象,在一次請(qǐng)求事務(wù)會(huì)話后,我們會(huì)將其關(guān)閉。
  • SQL Mapper:它是由一個(gè)Java接口和XML文件(或注解)構(gòu)成的,需要給出對(duì)應(yīng)的SQL和映射規(guī)則。它負(fù)責(zé)發(fā)送SQL去執(zhí)行,并返回結(jié)果;
    • 生命周期:Mapper的作用是發(fā)送SQL,然后返回我們需要的結(jié)果,因此它應(yīng)該在一個(gè)SqlSession事務(wù)方法之內(nèi),是一個(gè)方法級(jí)別的東西,聲明周期與SqlSession相同。

2. SqlSessionFactoryBuilder

通過(guò)XMLConfigBuilder解析配置的XML文件,讀出配置參數(shù)并存入Configuration類(lèi)中。(MyBatis中幾乎所有的配置都是存在這里的)
使用了建造者模式,主要過(guò)程就是解析xml配置文件,new SqlSessionFactory

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    /**
     * 1. 根據(jù)inputStream等信息創(chuàng)建XMLConfigBuilder對(duì)象;
     * 2. XMLConfigBuilder會(huì)將XML配置文件的信息轉(zhuǎn)換為Document對(duì)象;
     */
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    /**
     * 3. parser.parse()會(huì)處理每個(gè)node并返回Configuration對(duì)象,解析此Node節(jié)點(diǎn)的子Node,獲取相關(guān)屬性:properties, settings, typeAliases,typeHandlers,
     *  objectFactory, objectWrapperFactory, plugins, environments,databaseIdProvider, mappers
     * 4. 如解析typeHandlers的過(guò)程就是將TypeHandler.class注冊(cè)到一個(gè)hashmap中
     * 5. 最后調(diào)用build方法 new DefaultSqlSessionFactory(config)
     */
    return build(parser.parse());
  } 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.
    }
  }
}

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

3. SqlSessionFactory

使用Configuration對(duì)象去創(chuàng)建SqlSessionFactory(默認(rèn)的實(shí)現(xiàn)為DefaultSqlSessionFactory)。
主要涉及二級(jí)緩存的Executor,直接new DefaultSqlSession

public SqlSession openSession(ExecutorType execType) {
  return openSessionFromDataSource(execType, null, false);
}

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    final Environment environment = configuration.getEnvironment();
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    /**
     * new Transaction, transactionFactory的實(shí)現(xiàn)類(lèi)有JdbcTransactionFactory, ManagedTransactionFactory,SpringManagedTransactionFactory
     * Spring與MyBatis整合后使用SpringManagedTransactionFactory,將事務(wù)委托給Spring
     */
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    /**
     * 創(chuàng)建Executor(事務(wù)包含在其中)
     * 如果開(kāi)啟了二級(jí)緩存,會(huì)創(chuàng)建CachingExecutor,一個(gè)裝飾器
     * 先去SqlSessionFactory級(jí)別的二級(jí)緩存中查,如果查到就使用,查不到則調(diào)用原有Executor的查詢方法
     */
    final Executor executor = configuration.newExecutor(tx, execType);
    // 直接new 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 {
    //最后清空錯(cuò)誤上下文
    ErrorContext.instance().reset();
  }
}

4. SqlSession

流程:

  • SqlSession -> Executor -> StatementHandler(調(diào)用prepare方法進(jìn)行預(yù)編譯) -> ParameterHandler(設(shè)置預(yù)編譯sql的參數(shù)) -> StatementHandler調(diào)用PreparedStatement執(zhí)行sql -> ResultHandler封裝結(jié)果
  • Mapper映射是通過(guò)動(dòng)態(tài)代理實(shí)現(xiàn)的,在MapperProxy中會(huì)根據(jù)SQL的類(lèi)型(insert、update、delete、select)調(diào)用SqlSession的對(duì)應(yīng)方法;
  • SqlSession中的insert、update、delete、select方法實(shí)際上是調(diào)用Executor的對(duì)應(yīng)方法;
    • SqlSession下的四大對(duì)象
    • Executor代表執(zhí)行器,由它來(lái)調(diào)度StatementHandler、ParameterHandler、ResultHandler等來(lái)執(zhí)行對(duì)應(yīng)的SQL;
    • StatementHandler的作用是使用數(shù)據(jù)庫(kù)的Statement(PreparedStatement(預(yù)編譯的,效率高,可以使用占位符代替參數(shù)從而多次執(zhí)行))來(lái)執(zhí)行sql操作;
    • ParameterHandler用于SQL對(duì)參數(shù)的處理,使用TypeHandler向PreparedStatement中設(shè)置參數(shù);
    • ResultHandler是進(jìn)行最后數(shù)據(jù)集(ResultSet)的封裝返回的處理;

4.1 select

這里以DefaultSqlSession為例。

DefaultSqlSession中有多個(gè)select方法,如selectOne, selectMap, selectList等,但都是以selectList為基礎(chǔ),如selectOne是調(diào)用selectList,然后list.get(0)得到結(jié)果。selectMap也是先selectList,然后遍歷得到的list,轉(zhuǎn)換為Map。

所以我們來(lái)看一下selectList

@Override
public <E> List<E> selectList(String statement, Object parameter) {
  return this.selectList(statement, parameter, RowBounds.DEFAULT);
}

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
    /**
     * 根據(jù)statement獲取MappedStatement
     * statement其實(shí)就是MappedStatement的id,為被調(diào)用Mapper的類(lèi)名,如com.example.shiro.mapper.UserMapper.selectByPrimaryKey,見(jiàn)下圖
     */
    MappedStatement ms = configuration.getMappedStatement(statement);
    /**
     * 調(diào)用之前創(chuàng)建的Executor query方法,沒(méi)有開(kāi)啟二級(jí)緩存則使用BaseExecutor
     * 注意這里傳入的ResultHandler為null,
     * 在后續(xù)query過(guò)程中,如果ResultHandler為null則先嘗試從緩存中取,
     * 如果ResultHandler不為null則不會(huì)嘗試從緩存中取
     */
    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();
  }
}
image.png

再來(lái)看一下BaseExecutor#query()方法

@Override
 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
   /**
    * BoundSql中包含需要?jiǎng)討B(tài)生成的sql語(yǔ)句,以及對(duì)應(yīng)的參數(shù)
    */
   BoundSql boundSql = ms.getBoundSql(parameter);
   /**
    * 根據(jù)statementId, params, rowBounds來(lái)構(gòu)建一個(gè)key值,MyBatis認(rèn)為這幾個(gè)參數(shù)能夠代表同一個(gè)sql
    */
   CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
   return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}



 @SuppressWarnings("unchecked")
 @Override
 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.");
   }
   // 當(dāng)queryStack == 0時(shí)才清空緩存
   if (queryStack == 0 && ms.isFlushCacheRequired()) {
     clearLocalCache();
   }
   List<E> list;
   try {
     // 保證在執(zhí)行過(guò)程中不會(huì)清空緩存
     queryStack++;
    /**
     * localCache一級(jí)緩存,內(nèi)部為一個(gè)HashMap,線程不安全的
     * resultHandler DefaultSqlSession中傳入的ResultHandler為null
     * 注意:這里從cache中獲取到的結(jié)果強(qiáng)轉(zhuǎn)為list,queryFromDatabase會(huì)先傳入一個(gè)占位符,如果此時(shí)有另一個(gè)線程進(jìn)來(lái),再?gòu)?qiáng)轉(zhuǎn)則會(huì)拋異常,相當(dāng)于做了多線程操作的處理
     */
     list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
     if (list != null) {
       handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
     } else {
       // 緩存中沒(méi)有則從數(shù)據(jù)庫(kù)中查詢
       list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
     }
   } finally {
     queryStack--;
   }
   if (queryStack == 0) {
     //延遲加載隊(duì)列中所有元素
     for (DeferredLoad deferredLoad : deferredLoads) {
       deferredLoad.load();
     }
     // issue #601
     //清空延遲加載隊(duì)列
     deferredLoads.clear();
     if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
       // issue #482
       clearLocalCache();
     }
   }
   return list;
 }
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  List<E> list;
  /**
   * 在緩存中放入一個(gè)占位符 enum類(lèi)型
   * 當(dāng)?shù)谝粋€(gè)線程正常向數(shù)據(jù)庫(kù)中查詢時(shí),第二個(gè)線程也執(zhí)行了相同的查詢
   * 在BaseExecutor#query方法中(List<E>) localCache.getObject(key),此時(shí)報(bào)類(lèi)型轉(zhuǎn)換異常,防止多線程操作緩存
   */
  localCache.putObject(key, EXECUTION_PLACEHOLDER);
  try {
    /**
     * 調(diào)用StatementHandler#query方法
     * 如實(shí)現(xiàn)類(lèi)PreparedStatementHandler,就是調(diào)用PreparedStatement#excute方法
     */
    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
  } finally {
    // 刪除占位符
    localCache.removeObject(key);
  }
  //將查詢結(jié)果放入緩存
  localCache.putObject(key, list);
  if (ms.getStatementType() == StatementType.CALLABLE) {
    localOutputParameterCache.putObject(key, parameter);
  }
  // 直接返回緩存結(jié)果的引用
  return list;
}

可以看到,在queryFromDatabase中直接返回了緩存結(jié)果的引用,這樣就會(huì)出現(xiàn)臟讀,如下代碼:
在一次查詢后,修改查詢結(jié)果,下一次查詢時(shí)直接從緩存中查詢,結(jié)果會(huì)發(fā)現(xiàn)第二次的查詢結(jié)果也被修改了


@Transactional(rollbackFor = Exception.class)
public User getUser(Long id) {
    User user = userMapper.selectByPrimaryKey(id);
    log.info("User: {}", user.getUsername());
    user.setUsername("test-mybatis-cache");
    User user2 = userMapper.selectByPrimaryKey(id);
    log.info("User2: {}", user2.getUsername());


    return user;
}

緩存中返回的引用,在一次事務(wù)中MyBatis不會(huì)清空緩存,所以修改引用后,下次查詢得到的結(jié)果會(huì)有問(wèn)題

image.png

去掉@Transactional后變得正常了
image.png

其實(shí)SqlSession 一級(jí)緩存的查詢工作流程為:

  1. 對(duì)于某個(gè)查詢,根據(jù)statementId, params, rowBounds來(lái)構(gòu)建一個(gè)key值,根據(jù)這個(gè)key值去緩存Cache中取出對(duì)應(yīng)的key值存儲(chǔ)的緩存結(jié)果;
  2. 判斷從Cache中根據(jù)特定的key值取的數(shù)據(jù)數(shù)據(jù)是否為空,即是否命中;
  3. 如果命中,則直接將緩存結(jié)果返回;
  4. 如果沒(méi)命中:
    a. 去數(shù)據(jù)庫(kù)中查詢數(shù)據(jù),得到查詢結(jié)果;
    b. 將key和查詢到的結(jié)果分別作為key,value對(duì)存儲(chǔ)到Cache中;
    c. 將查詢結(jié)果返回;

MyBatis認(rèn)為,對(duì)于兩次查詢,如果以下條件都完全一樣,那么就認(rèn)為它們是完全相同的兩次查詢:

  1. 傳入的 statementId
  2. 查詢時(shí)要求的結(jié)果集中的結(jié)果范圍 (結(jié)果的范圍通過(guò)rowBounds.offset和rowBounds.limit表示);
  3. 這次查詢所產(chǎn)生的最終要傳遞給JDBC java.sql.Preparedstatement的Sql語(yǔ)句字符串(boundSql.getSql() )
  4. 傳遞給java.sql.Statement要設(shè)置的參數(shù)值

現(xiàn)在分別解釋上述四個(gè)條件:

  1. 傳入的statementId,對(duì)于MyBatis而言,你要使用它,必須需要一個(gè)statementId,它代表著你將執(zhí)行什么樣的Sql;
  2. MyBatis自身提供的分頁(yè)功能是通過(guò)RowBounds來(lái)實(shí)現(xiàn)的,它通過(guò)rowBounds.offset和rowBounds.limit來(lái)過(guò)濾查詢出來(lái)的結(jié)果集,這種分頁(yè)功能是基于查詢結(jié)果的再過(guò)濾,而不是進(jìn)行數(shù)據(jù)庫(kù)的物理分頁(yè);

由于MyBatis底層還是依賴于JDBC實(shí)現(xiàn)的,那么,對(duì)于兩次完全一模一樣的查詢,MyBatis要保證對(duì)于底層JDBC而言,也是完全一致的查詢才行。而對(duì)于JDBC而言,兩次查詢,只要傳入給JDBC的SQL語(yǔ)句完全一致,傳入的參數(shù)也完全一致,就認(rèn)為是兩次查詢是完全一致的。
上述的第3個(gè)條件正是要求保證傳遞給JDBC的SQL語(yǔ)句完全一致;第4條則是保證傳遞給JDBC的參數(shù)也完全一致;
即3、4兩條MyBatis最本質(zhì)的要求就是:
調(diào)用JDBC的時(shí)候,傳入的SQL語(yǔ)句要完全相同,傳遞給JDBC的參數(shù)值也要完全相同。

4.2 insert & delete

// DefaultSqlSession#insert


@Override
public int insert(String statement) {
  return insert(statement, null);
}

@Override
public int insert(String statement, Object parameter) {
  return update(statement, parameter);
}


// DefaultSqlSession#delete
@Override
public int delete(String statement) {
  return update(statement, null);
}

@Override
public int delete(String statement, Object parameter) {
  return update(statement, parameter);
}

可以看到insert和delete方法其實(shí)就是調(diào)用了update(會(huì)清空一級(jí)緩存),下面來(lái)看一下update方法

4.3 update

@Override
public int update(String statement) {
  return update(statement, null);
}

@Override
public int update(String statement, Object parameter) {
  try {
    /**
     * dirty置為true,在commit和rollback方法中會(huì)判斷isCommitOrRollbackRequired(),
     * 如果dirty為true則表明需要commit,會(huì)調(diào)用transaction.commit();
     */
    dirty = true;
    MappedStatement ms = configuration.getMappedStatement(statement);
    // 直接調(diào)用Executor#update
    return executor.update(ms, wrapCollection(parameter));
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

看一下BaseExecutor#update

@Override
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.clear();
   */
  clearLocalCache();
  // doUpdate,一個(gè)模板方法,交給子類(lèi)實(shí)現(xiàn)
  return doUpdate(ms, parameter);
}

SimpleExecutor#doUpdate

@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
  Statement stmt = null;
  try {
    Configuration configuration = ms.getConfiguration();
    /**
     * 創(chuàng)建StatementHandler
     * StatementHandler負(fù)責(zé)處理Mybatis與JDBC之間Statement的交互,如PreparedStatement
     */
    StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
    /**
     * 1. 創(chuàng)建JDBC連接Connection(使用連接池)
     * 2. 調(diào)用StatementHandler#prepare方法,通過(guò)Connection創(chuàng)建PreparedStatement
     * 3. 調(diào)用StatementHandler#parameterize方法,向PreparedStatement中設(shè)置sql參數(shù),TypeHandler就是在這里生效的 
     */
    stmt = prepareStatement(handler, ms.getStatementLog());
    /**
     * 直接調(diào)用PreparedStatement#execute方法,執(zhí)行sql并返回受影響行數(shù)
     */
    return handler.update(stmt);
  } finally {
    /**
     * 關(guān)閉statement
     */
    closeStatement(stmt);
  }
}

總體來(lái)說(shuō),BaseExecutor#update方法比較簡(jiǎn)單,無(wú)非就是先清空本地一級(jí)緩存,再調(diào)用PreparedStatement執(zhí)行sql。

4.4 commit & rollback

@Override
public void commit(boolean force) {
  try {
    /**
     * isCommitOrRollbackRequired(),根據(jù)dirty、autoCommit、force判斷是否需要提交或回滾
     * 先清空緩存,再transaction.commit()
     */
    executor.commit(isCommitOrRollbackRequired(force));
    /**
     * dirty置為false,下次無(wú)需提交或回滾
     */
    dirty = false;
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

@Override
public void rollback(boolean force) {
  try {
    /**
     * isCommitOrRollbackRequired(),根據(jù)dirty、autoCommit、force判斷是否需要提交或回滾
     * 先清空緩存,再transaction.rollback()
     */
    executor.rollback(isCommitOrRollbackRequired(force));
    /**
     * dirty置為false,下次無(wú)需提交或回滾
     */
    dirty = false;
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error rolling back transaction.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}
  • 當(dāng)使用MyBatis-Spring時(shí),在org.mybatis.spring.SqlSessionTemplate中會(huì)調(diào)用SqlSession#commit,如果添加了@Transactional注解,在commit后sql才會(huì)生效,如果有沒(méi)有添加注解commit前sql就已經(jīng)生效了。這里無(wú)論是否添加了@Transactional注解都需要執(zhí)行SqlSession#commit主要是考慮到有的數(shù)據(jù)庫(kù)必須在close前調(diào)用commit或rollback;
  • 當(dāng)使用MyBatis-Spring時(shí),@Transactional注解會(huì)使用Spring的事務(wù),則不會(huì)調(diào)用SqlSession#rollback方法;

5. MyBatis-Spring

文檔:http://mybatis.org/spring/zh/getting-started.html

  • 一個(gè)使用 MyBatis-Spring 的其中一個(gè)主要原因是它允許 MyBatis 參與到 Spring 的事務(wù)管理中。而不是給 MyBatis 創(chuàng)建一個(gè)新的專(zhuān)用事務(wù)管理器,MyBatis-Spring 借助了 Spring 中的 DataSourceTransactionManager 來(lái)實(shí)現(xiàn)事務(wù)管理。
  • 一旦配置好了 Spring 的事務(wù)管理器,你就可以在 Spring 中按你平時(shí)的方式來(lái)配置事務(wù)。并且支持 @Transactional 注解和 AOP 風(fēng)格的配置。在事務(wù)處理期間,一個(gè)單獨(dú)的 SqlSession 對(duì)象將會(huì)被創(chuàng)建和使用。當(dāng)事務(wù)完成時(shí),這個(gè) session 會(huì)以合適的方式提交或回滾。
  • 不能在 Spring 管理的 SqlSession 上調(diào)用 SqlSession.commit(),SqlSession.rollback() 或 SqlSession.close() 方法。如果這樣做了,就會(huì)拋出 UnsupportedOperationException 異常。在使用注入的映射器時(shí),這些方法也不會(huì)暴露出來(lái)。
  • DefaultSqlSession中的一級(jí)緩存就是一個(gè)HashMap,它不是線程安全的,MyBatis-Spring中SqlSessionTemplate是線程安全的,它將SqlSession存儲(chǔ)在org.springframework.transaction.support.TransactionSynchronizationManager中,TransactionSynchronizationManager中使用ThreadLocal變量保存SqlSession。每個(gè)線程過(guò)來(lái)都是一個(gè)獨(dú)立的SqlSession,所以能夠保證線程安全。https://my.oschina.net/u/3145456/blog/1841572
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");

6. SQL Mapper

  • 它是由一個(gè)Java接口和XML文件(或注解)構(gòu)成的,需要給出對(duì)應(yīng)的SQL和映射規(guī)則。它負(fù)責(zé)發(fā)送SQL去執(zhí)行,并返回結(jié)果;
  • 在Spring啟動(dòng)時(shí)(getBean),會(huì)初始化所有的Mapper類(lèi),并生成對(duì)應(yīng)的代理類(lèi)MapperProxy。
  • 當(dāng)執(zhí)行Mapper中的方法時(shí)(如userMapper.insert(user)),會(huì)調(diào)用Mapper的代理類(lèi)MapperProxy(所以Mapper需要是接口),MapperProxy會(huì)調(diào)用SqlSessionTemplate的對(duì)應(yīng)方法,如下:
@Override
public int insert(String statement, Object parameter) {
  return this.sqlSessionProxy.insert(statement, parameter);
}
  • 而這里的sqlSessionProxy是在SqlSessionTemplate的構(gòu)造函數(shù)中創(chuàng)建的動(dòng)態(tài)代理類(lèi)(主要處理了SqlSession的線程安全問(wèn)題,最終還是直接調(diào)用DefaultSqlSession的對(duì)應(yīng)方法)
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
    PersistenceExceptionTranslator exceptionTranslator) {

  notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
  notNull(executorType, "Property 'executorType' is required");

  this.sqlSessionFactory = sqlSessionFactory;
  this.executorType = executorType;
  this.exceptionTranslator = exceptionTranslator;
  // 創(chuàng)建SqlSession的動(dòng)態(tài)代理類(lèi),需要看下SqlSessionInterceptor
  this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
      new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}
private class SqlSessionInterceptor implements InvocationHandler {
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    /**
     * 1. 嘗試去TransactionSynchronizationManager的Threadlocal中尋找SqlSession(線程安全的)
     *  1.1 如果能夠獲取到,則直接返回(此時(shí)需要計(jì)數(shù)器加一,用于記錄SqlSession被獲取了多少次)
     *  1.2 如果獲取不到
     *    1.2.1 創(chuàng)建新的SqlSession
     *    1.2.2 如果當(dāng)前存在事務(wù),則向TransactionSynchronizationManager的Threadlocal中注冊(cè)新創(chuàng)建的SqlSession
     *    1.2.3 如果沒(méi)有事務(wù),則不會(huì)向TransactionSynchronizationManager注冊(cè),所以每次都是新的SQLSession
     */
    SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
        SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
    try {
      // 可以看到invoke函數(shù)的參數(shù)中proxy是沒(méi)有被用到的,這里直接傳入的是動(dòng)態(tài)獲取的SqlSession
      Object result = method.invoke(sqlSession, args);
      if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
        // force commit even on non-dirty sessions because some databases require
        // a commit/rollback before calling close()
        sqlSession.commit(true);
      }
      return result;
    } catch (Throwable t) {
      Throwable unwrapped = unwrapThrowable(t);
      if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
        // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        sqlSession = null;
        Throwable translated = SqlSessionTemplate.this.exceptionTranslator
            .translateExceptionIfPossible((PersistenceException) unwrapped);
        if (translated != null) {
          unwrapped = translated;
        }
      }
      throw unwrapped;
    } finally {
      if (sqlSession != null) {
        /**
         * 關(guān)閉SqlSession
         * 如果TransactionSynchronizationManager中存在SqlSession,減少一個(gè)計(jì)數(shù)(holder.released()),并不直接close SqlSession
         * 如果TransactionSynchronizationManager不存在,直接調(diào)用SqlSession#close方法
         */
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
      }
    }
  }
}
  • 關(guān)閉SqlSession:
    • 如果不存在事務(wù),每次執(zhí)行一個(gè)Mapper的方法時(shí),都會(huì)創(chuàng)建一個(gè)新的SqlSession,執(zhí)行完畢后關(guān)閉;
    • 如果存在事務(wù),在事務(wù)的過(guò)程中,使用的是相同的SqlSession,事務(wù)結(jié)束后,會(huì)關(guān)閉SqlSession;
    • 也就是說(shuō),在事務(wù)中才會(huì)用到SqlSession的一級(jí)緩存,而無(wú)事務(wù)時(shí),沒(méi)法觸發(fā)一級(jí)緩存。

7. MyBatis兩級(jí)緩存

  1. 默認(rèn)開(kāi)啟一級(jí)緩存,PerpetualCache對(duì)象就是使用HashMap來(lái)做的(一級(jí)緩存只是相對(duì)于SqlSession而言
  2. 在參數(shù)和SQL完全一樣的情況下,我們使用同一個(gè)SqlSession對(duì)象調(diào)用同一個(gè)Mapper的方法,往往只執(zhí)行一次SQL,如果沒(méi)有聲明需要刷新,并且緩存沒(méi)有超時(shí)的情況下,SqlSession都只會(huì)取出當(dāng)前緩存的數(shù)據(jù),而不會(huì)再次發(fā)送SQl到數(shù)據(jù)庫(kù)。
  3. 如果使用不同的SqlSession對(duì)象,因?yàn)椴煌腟qlSession都是相互隔離的,所以用相同的Mapper、參數(shù)和方法,會(huì)發(fā)送多次SQL到數(shù)據(jù)庫(kù)。
  4. 二級(jí)緩存是在Mapper級(jí)別的,默認(rèn)是不開(kāi)啟的,且要求返回的POJO必須是可序列化的,即實(shí)現(xiàn)Serializable接口。實(shí)現(xiàn)Serializable接口主要是因?yàn)榫彺娌灰欢ㄊ窃趦?nèi)存中,也可能在磁盤(pán)中,所以需要進(jìn)行序列化和反序列化。(開(kāi)啟二級(jí)緩存后默認(rèn)insert、update、delete會(huì)刷新緩存,緩存使用LRU或FIFO等最近最少使用算法來(lái)回收)
  5. 二級(jí)緩存是SqlSessionFactory層面的,生命周期與SqlSessionFactory、Configuration對(duì)象相同
  6. 默認(rèn)系統(tǒng)緩存是MyBatis所在服務(wù)器的本地緩存,如果想使用redis等緩存服務(wù)器,MyBatis也支持自定義緩存,需要實(shí)現(xiàn)org.apache.ibatis.cache.Cache。

一級(jí)緩存的具體實(shí)現(xiàn)已經(jīng)在上面闡述過(guò)了,所以這里只討論下二級(jí)緩存。

二級(jí)緩存與一級(jí)緩存其機(jī)制相同,默認(rèn)也是采用 PerpetualCache,HashMap存儲(chǔ),不同在于其存儲(chǔ)作用域?yàn)?Mapper(Namespace),并且可自定義存儲(chǔ)源。

7.1 select

CachingExecutor是一個(gè)裝飾器,豐富了如SimpleExecutor的功能,提供了二級(jí)緩存的支持。
CachingExecutor#query

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  /**
   * 1. 從MappedStatement中獲取cache
   *  1.1 Spring boot中想要開(kāi)啟了二級(jí)緩存需要在Mapper上添加@CacheNamespace注解,如果添加了該注解,則這里可以獲取到cache
   *  1.2 MappedStatement是在啟動(dòng)時(shí),注冊(cè)到org.apache.ibatis.session.Configuration中的
   *    1.2.1 每個(gè)Mapper的每個(gè)方法都會(huì)生成一個(gè)MappedStatement,且Mapper中保存了一個(gè)cache對(duì)象
   *      a. 如果沒(méi)有開(kāi)啟二級(jí)緩存,cache對(duì)象為空
   *      b. 如果開(kāi)啟了二級(jí)緩存,則每個(gè)Mapper的不同方法共享同一個(gè)cache對(duì)象,即mybatis的二級(jí)緩存是Mapper級(jí)別的
   */
  Cache cache = ms.getCache();
  if (cache != null) {
    /**
     * 是否需要刷新緩存,默認(rèn)情況下,select不需要刷新緩存
     */
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, parameterObject, boundSql);
      /**
       * tcm.getObject(cache, key)實(shí)際上就是去參數(shù)中的cache獲取緩存
       */
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        /**
         * 如果沒(méi)有查詢到,則會(huì)去被代理的Executor(如SimpleExecutor)查詢
         * delegate.query也會(huì)去查詢一級(jí)緩存
         */
        list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        /**
         * 該方法并沒(méi)有直接將查詢的結(jié)果對(duì)象存儲(chǔ)到其封裝的二級(jí)緩存Cache對(duì)象中,
         * 而是暫時(shí)保存到entriesToAddOnCommit集合中,
         * 在事務(wù)提交時(shí)(CachingExecutor#commit)才會(huì)將這些結(jié)果從entriesToAddOnCommit集合中添加到二級(jí)緩存中
         */
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  /**
   * 未開(kāi)啟二級(jí)緩存,則直接調(diào)用原Executor
   */
  return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

下圖可以看到,開(kāi)啟了二級(jí)緩存后,每個(gè)Mapper的不同方法共享同一個(gè)cache對(duì)象,不同Mapper的cache對(duì)象不同


image.png

image.png

7.2 insert, delete, update

CachingExecutor#update
當(dāng)SqlSession執(zhí)行,insert, delete, update時(shí),如果開(kāi)啟了二級(jí)緩存會(huì)調(diào)用CachingExecutor#update方法

@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
  //是否需要刷新緩存,insert,delete,update要刷新緩存
  flushCacheIfRequired(ms);
  // 直接調(diào)用原Executor
  return delegate.update(ms, parameterObject);
}

開(kāi)啟二級(jí)緩存時(shí),SqlSession的聲明周期與之前相同:

  • 如果不存在事務(wù),每次執(zhí)行一個(gè)Mapper的方法時(shí),都會(huì)創(chuàng)建一個(gè)新的SqlSession,執(zhí)行完畢后關(guān)閉;
  • 如果存在事務(wù),在事務(wù)的過(guò)程中,使用的是相同的SqlSession,事務(wù)結(jié)束后,會(huì)關(guān)閉SqlSession;

7.3 二級(jí)緩存的缺點(diǎn)

  1. 容易出現(xiàn)臟數(shù)據(jù):
    a. 由于二級(jí)緩存是Mapper級(jí)別的,如UserMapper中出現(xiàn)了查詢Role表的SQL,因?yàn)镽oleMapper與UserMapper的二級(jí)緩存不同,所以使用RoleMapper更新Role表并不會(huì)刷新UserMapper中查詢Role表的SQL;
    b. 同理,當(dāng)在Mapper中出現(xiàn)關(guān)聯(lián)查詢時(shí),其他Mapper修改了關(guān)聯(lián)的數(shù)據(jù)表,則一定會(huì)出現(xiàn)臟數(shù)據(jù);
  2. 緩存粒度只到Mapper,無(wú)法獲取更細(xì)粒度的緩存;
  3. 分布式場(chǎng)景下,必然會(huì)出現(xiàn)臟數(shù)據(jù);
    a. 一級(jí)緩存如果為開(kāi)啟事務(wù),則每一個(gè)sql對(duì)應(yīng)一個(gè)SqlSession對(duì)應(yīng)一個(gè)一級(jí)緩存,所以不會(huì)出現(xiàn)臟數(shù)據(jù);如果開(kāi)啟了事務(wù),則在兩次查詢的間隙有他人修改,可能會(huì)出現(xiàn)臟數(shù)據(jù)(未強(qiáng)制加鎖);
  4. 所以說(shuō)使用二級(jí)緩存,還不如自己在業(yè)務(wù)層做一次緩存;

8. MyBatis延遲加載

當(dāng)真正使用到這個(gè)數(shù)據(jù)時(shí)才會(huì)發(fā)送sql語(yǔ)句。(級(jí)聯(lián)時(shí),默認(rèn)將關(guān)聯(lián)的屬性都查詢出來(lái),如果開(kāi)啟了延遲加載,則使用到關(guān)聯(lián)屬性時(shí)才會(huì)查找)
實(shí)現(xiàn)原理:使用動(dòng)態(tài)代理,會(huì)生成一個(gè)動(dòng)態(tài)代理對(duì)象,里面保存著相關(guān)的SQL和參數(shù),一旦我們使用這個(gè)代理對(duì)象的方法,它會(huì)進(jìn)入到動(dòng)態(tài)代理對(duì)象的代理方法里,方法里面會(huì)通過(guò)發(fā)送sql和參數(shù),就可以把對(duì)應(yīng)的結(jié)果從數(shù)據(jù)庫(kù)中查找回來(lái)。

MyBatis延遲加載是通過(guò)動(dòng)態(tài)代理實(shí)現(xiàn)的,當(dāng)調(diào)用配置為延遲加載的屬性方法時(shí)(如getXXX()),此時(shí)會(huì)調(diào)用動(dòng)態(tài)代理對(duì)象的get方法,會(huì)發(fā)送sql到db查詢數(shù)據(jù),這些操作是通過(guò)SqlSession來(lái)執(zhí)行的。由于在和某些框架集成時(shí),SqlSession的生命周期交給了框架來(lái)管理,因此當(dāng)對(duì)象超出SqlSession生命周期調(diào)用時(shí),會(huì)由于鏈接關(guān)閉等問(wèn)題而拋出異常。因而在與Spring集成時(shí),需要注意SqlSession的聲明周期。

8.1 延遲加載的優(yōu)缺點(diǎn)

  • 優(yōu)點(diǎn):先從單表查詢,需要時(shí)再去查詢另一張表(兩次查詢),如果并沒(méi)有用到另一張表中的數(shù)據(jù),可以加快查詢速度。
    • 如果使用延遲加載,使用關(guān)聯(lián)查詢的話,在數(shù)據(jù)量很大的情況下,關(guān)聯(lián)查詢jion兩張大表,效率會(huì)很低。而如果延遲加載第二次延遲查詢命中索引概率大的話,效率會(huì)更高。
    • 一定要進(jìn)行關(guān)聯(lián)查詢嗎?
      • 關(guān)聯(lián)查詢效率很低,尤其是兩張大表jion的情況下,所以要盡可能避免這種情況!
      • 如果非要兩張大表jion的話,可以不作為實(shí)時(shí)場(chǎng)景,讓它作為一個(gè)定時(shí)任務(wù)去跑,第一次跑的數(shù)據(jù)量大可能耗時(shí)長(zhǎng),后面采用按時(shí)間增量更新的策略,根據(jù)時(shí)間切分的好后面每次跑的數(shù)據(jù)量就不會(huì)太大。
      • 如果一張大表跟一張小表jion,那么可以將小表緩存,單查一張大表將查詢得到的結(jié)果(肯定比全量jion數(shù)據(jù)要少)再去jion。
      • 大表單查很慢,可以多個(gè)線程去查,如果大表做了分庫(kù)分表或者按時(shí)間分區(qū),查詢方式就又有不同。
      • 盡量將一個(gè)大sql拆分為多個(gè)小sql,大sql會(huì)長(zhǎng)時(shí)間占用連接,影響其他sql。
  • 缺點(diǎn):因?yàn)橹挥挟?dāng)需要用到數(shù)據(jù)時(shí),才會(huì)進(jìn)行數(shù)據(jù)庫(kù)查詢,這樣在大批量數(shù)據(jù)查詢時(shí),因?yàn)椴樵児ぷ饕惨臅r(shí)間,所以可能造成用戶等待時(shí)間變長(zhǎng),造成用戶體驗(yàn)下降。

9. 如何將jdbc查詢的結(jié)果轉(zhuǎn)換為相應(yīng)對(duì)象的?

  1. 當(dāng)調(diào)用SimpleExecutor#query()方法時(shí),如果沒(méi)有命中緩存,則會(huì)queryFromDatabase,最終會(huì)使用如PreparedStatement執(zhí)行查詢;
  2. ResultSetHandler#handleResultSets()方法,將resultSet轉(zhuǎn)為對(duì)應(yīng)的對(duì)象。
    a. 獲取resultSet中的所有列名,并獲得列名對(duì)應(yīng)的值;
    b. 查找是否有匹配的TypeHandler,如果有的話,調(diào)用TypeHandler的getResult() → getNullableResult()方法,獲得通過(guò)TypeHandler轉(zhuǎn)換后的屬性值;
    c. 利用反射new對(duì)象,根據(jù)列名獲得對(duì)象的setXX()方法,再使用反射調(diào)用待生成對(duì)象的setXX()方法,將屬性設(shè)置進(jìn)去;
    d. 將設(shè)置了屬性的對(duì)象存入ResultHandler中;
    e. 這樣僅僅是處理了一條記錄,如果查詢結(jié)果有多條記錄,還會(huì)循環(huán)這個(gè)過(guò)程。
    f. 利用ResultHandler處理對(duì)象。
    i. 如DefaultResultHandler,內(nèi)部維護(hù)了一個(gè)List,查詢得到的n條記錄都會(huì)存在這里
    ii. 最終返回的數(shù)據(jù)會(huì)判斷List中的個(gè)數(shù),如果只有一個(gè)就get(0),只返回一個(gè)對(duì)象。如果有多個(gè)會(huì)返回這個(gè)List
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容