前言
Mybatis小編已經陸續出了兩篇博客了,之前我們講解了Executor處理器,快取體系,卻沒有講解與jdbc互動的相關操作,這其實就是Mybatis的StatementHandler做的事情,一個SQL請求會經過會話,然后是執行器, 由StatementHandler執行jdbc最終到達資料庫,今天小編帶大家詳細認識一下StatementHandler他是怎樣的結構以及如何發揮作用的,
StatementHandler
定義
JDBC處理器,基于JDBC構建JDBC Statement,并設定引數,然后執行Sql,每呼叫會話當中一次sql,都會有與之相對應的且唯一的Statement實體(命中快取除外),
結構
StatementHandler 介面原始碼:
public interface StatementHandler {
//基于JDBC宣告statement
Statement prepare(Connection connection, Integer transactionTimeout)
throws SQLException;
//為statement設定方法
void parameterize(Statement statement)
throws SQLException;
//添加批處理
void batch(Statement statement)
throws SQLException;
//執行update 方法
int update(Statement statement)
throws SQLException;
//執行query 方法
<E> List<E> query(Statement statement, ResultHandler resultHandler)
throws SQLException;
//查詢游標
<E> Cursor<E> queryCursor(Statement statement)
throws SQLException;
//獲取動態sql
BoundSql getBoundSql();
//獲取引數處理器
ParameterHandler getParameterHandler();
}
StatementHandler 有三個子類
- SimpleStatementHandler:對應JDBC中的Statement
- PreparedStatementHandler:對應JDBC中的PreparedStatement
- CallableStatementHandler:對應JDBC中的CallableStatement,
下面是結構圖:

大部分情況下都是前處理器,所以接下小編就針對PreparedStatementHandler來講解其流程,(其他的大家自行研究)
PreparedStatementHandler處理流程
首先看一下呼叫的時序圖:

總共執行程序分為三個階段:
- 預處理:這里預處理不僅僅是通過Connection創建Statement,還包括設定引數,
- 執行:包含執行SQL和處理結果映射兩部分,
- 關閉:直接關閉Statement,
原始碼閱讀
假設使用SimpleExecutor來呼叫的
@Override
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最終呼叫的地方RoutingStatementHandler
//為什么使用configuration創建,這里第一可以理解為簡單工廠,統一創建,第二是對插件做攔截
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//創建statement然后是引數處理
stmt = prepareStatement(handler, ms.getStatementLog());
//執行查詢操作
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
使用RoutingStatementHandler來創建的根據StatementType來new一個相應的statementHandler(小編覺得這個好像沒什么特別大的作用,可能當初作者還想再這個Handler里面做其他操作吧),里面statementType默認為PREPARED,可以通過@Options(statementType = StatementType.PREPARED)來配置,@Options放在對應介面的方法上,
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}
創建statement并且引數處理
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
//獲取連接
Connection connection = getConnection(statementLog);
//獲取具體的StatementHandler這里就是PreparedStatementHandler的instantiateStatement方法,
//然后再base里面做了超時時間以及設定回傳行數,設定的引數可以做mappedStatement配置
stmt = handler.prepare(connection, transaction.getTimeout());
//引數處理,具體參加后續引數部分怎么進行引數映射
handler.parameterize(stmt);
return stmt;
}
PreparedStatementHandler的instantiateStatement方法
protected Statement instantiateStatement(Connection connection) throws SQLException {
String sql = boundSql.getSql();
if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
String[] keyColumnNames = mappedStatement.getKeyColumns();
if (keyColumnNames == null) {
return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
} else {
return connection.prepareStatement(sql, keyColumnNames);
}
} else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
return connection.prepareStatement(sql);
} else {
return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
}
}
具體執行方法:
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
//處理結果集,后續詳解
return resultSetHandler.handleResultSets(ps);
}
上圖涉及到了引數處理以及結果集封裝以及原始碼閱讀中小編沒有具體講,這是由于涉及資料庫欄位和JavaBean之間的相互映射,相對復雜,所以分別使用ParameterHandler與ResultSetHandler兩個專門的組件實作,接下來就一起了解一下引數處理與結果集封裝的處理流程,
引數處理
引數處理包括了引數轉換,引數映射以及引數的賦值,小編先來說一下引數的轉換:
引數轉換
所有引數轉換我們運用的到了一個類:ParamNameResolver,分為兩種情況
- 單個引數:假如說沒有加@Param注解則不做轉換直接交給執行器做對應的查詢,如果有注解,就轉換成一個map
- 多個引數:按照順序轉換,并且轉換成一個map,key按照順序就是param1,param2,如果假如了@Param,key就為對應引數的名稱,jdk8之后基于反射可以用到對應引數的名稱(需要打開引數編譯),jdk8之前則為arg0,arg1,
ParamNameResolver引數轉換類原始碼閱讀
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
//沒有引數,或引數數量為0回傳null
if (args == null || paramCount == 0) {
return null;
//如果沒有@param注解并且引數數量為1,則直接回傳args[0]也就是引數本身
} else if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
} else {
//其他都封裝成一個map,ParamMap為mybatis自定義的其實就是繼承了hashMap,對get方法重寫了一下
final Map<String, Object> param = new ParamMap<>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
// ensure not to overwrite parameter named with @Param
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
多個引數情況下運行的結果的

所以在我們介面類上可以這樣寫:
//這兩種都可以,不過得注意順序同時注意arg是從0開始的param是從1開始的(小編覺得還是加上@Param注解比較好)
@Update("update users set name=#{arg1} where id=#{arg0}")
@Update("update users set name=#{param2} where id=#{param1}")
int setName(Integer id, String name);
對了小編說了可以加入編譯引數然后不寫注解也可以決議到引數名稱,那怎么加呢?請看下圖:

或者在pom的maven配置中 加入
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
然后記得maven clean一下重新編譯,當然不建議這么做,
結果如下圖

引數映射與賦值
上面說的一個和多個引數由ParamNameResolver轉換,但是無論是一個或多個引數的情況下都會出現引數是JavaBean物件或者是原始型別,這里交由ParameterHandler處理,如果只有一個原始型別的話那引數占用符基本可以隨便寫,多參的情況下根據map的key映射,如果是JavaBean物件會根據屬性的名稱進行映射比方說#{user.id},
ParameterHandler原始碼閱讀實作為DefaultParameterHandler
@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
//boundSql中包含了sql,然后引數使用?作為占位符
//原先sql里面的#{param1}等封裝為parameterMappings為引數映射,有幾個引數就會映射幾個
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
//不為空
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
//判斷是否是存盤程序的出參,不需要管
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
//獲得屬性名稱也就是param1或arg0或者是name等
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
//將ParamNameResolver轉換出來的引數封裝為metaObject,然后就可以直接拿到值了
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
//獲取jdbcType
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
//如果沒設定jdbc型別則基于值型別做引數設定否則根據jdbc型別轉換
//這里需要TypeHandler來處理然后進行賦值
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
對于上面小編的注釋更直觀的圖如下:

上面MetaObject 是很厲害的可以封裝javaBean物件,原始型別還有陣列List等等,這邊小編后續講解MetaObject ,因為這個很復雜,
引數的賦值通過TypeHandler 為PrepareStatement設定值,通常情況下一般的資料型別MyBatis都有與之相對應的TypeHandler,
結果集封裝
指讀取ResultSet資料,并將每一行轉換成相對應的物件,用戶可在轉換的程序當中可以通過ResultContext來控制是否要繼續轉換,轉換后的物件都會暫存在ResultHandler中最后統一封裝成list回傳給呼叫方,

結果集封裝比較復雜下次講解,
總結
今天主要講了與jdbc打交道的statementHandler他有三個主要的子類,也分別封裝了jdbc的statement,然后我們根據最常用的PrepareStatement說了一下其主要的流程,分為三個階段:預處理,執行,關閉,其中有四個主要步驟:準備statement,設定引數,執行以及結果值處理,之后到設定引數的重要組件包括ParamNameResolver(引數轉換器),ParameterHandler(引數的映射)以及TypeHandler(引數賦值所需的型別處理),最后簡單說了一下結果集封裝,結果集封裝是最復雜的下次小編接著為大家詳細說明了一下,先到這兒,一起加油努力!
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/279688.html
標籤:其他
下一篇:SAAS前端組織特點
