精盡MyBatis原始碼分析 - MyBatis初始化(四)之 SQL 初始化(下)
阿新 • • 發佈:2020-11-24
> 該系列文件是本人在學習 Mybatis 的原始碼過程中總結下來的,可能對讀者不太友好,請結合我的原始碼註釋([Mybatis原始碼分析 GitHub 地址](https://github.com/liu844869663/mybatis-3)、[Mybatis-Spring 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring)、[Spring-Boot-Starter 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring-boot-starter))進行閱讀
>
> MyBatis 版本:3.5.2
>
> MyBatis-Spring 版本:2.0.3
>
> MyBatis-Spring-Boot-Starter 版本:2.1.4
## MyBatis的初始化
在MyBatis初始化過程中,大致會有以下幾個步驟:
1. 建立`Configuration`全域性配置物件,會往`TypeAliasRegistry`別名註冊中心新增Mybatis需要用到的相關類,並設定預設的語言驅動類為`XMLLanguageDriver`
2. 載入`mybatis-config.xml`配置檔案、Mapper介面中的註解資訊和XML對映檔案,解析後的配置資訊會形成相應的物件並儲存到Configuration全域性配置物件中
3. 構建`DefaultSqlSessionFactory`物件,通過它可以建立`DefaultSqlSession`物件,MyBatis中`SqlSession`的預設實現類
因為整個初始化過程涉及到的程式碼比較多,所以拆分成了四個模組依次對MyBatis的初始化進行分析:
- [**《MyBatis初始化(一)之載入mybatis-config.xml》**](https://www.cnblogs.com/lifullmoon/p/14015009.html)
- [**《MyBatis初始化(二)之載入Mapper介面與XML對映檔案》**](https://www.cnblogs.com/lifullmoon/p/14015046.html)
- [**《MyBatis初始化(三)之SQL初始化(上)》**](https://www.cnblogs.com/lifullmoon/p/14015066.html)
- [**《MyBatis初始化(四)之SQL初始化(下)》**](https://www.cnblogs.com/lifullmoon/p/14015075.html)
由於在MyBatis的初始化過程中去解析Mapper介面與XML對映檔案涉及到的篇幅比較多,XML對映檔案的解析過程也比較複雜,所以才分成了後面三個模組,逐步分析,這樣便於理解
## 初始化(四)之SQL初始化(下)
在上一篇文件中詳細地講述了MyBatis在解析` ` 節點的過程中,是如何解析SQL語句的,如何實現動態SQL語句的,最終會生成一個`org.apache.ibatis.mapping.SqlSource`物件的,那麼接下來我們來看看`SqlSource`到底是什麼
主要包路徑:org.apache.ibatis.mapping、org.apache.ibatis.builder
主要涉及到的類:
- `org.apache.ibatis.builder.SqlSourceBuilder`:繼承了BaseBuilder抽象類,`SqlSource`構建器,負責將SQL語句中的`#{}`替換成相應的`?`佔位符,並獲取該`?`佔位符對應的 `ParameterMapping`物件
- `org.apache.ibatis.builder.ParameterExpression`:繼承了`HashMap`,引數表示式處理器,在`SqlSourceBuilder`處理`#{}`的內容時,需要通過其解析成key-value鍵值對
- `org.apache.ibatis.mapping.ParameterMapping`:儲存`#{}`中配置的屬性引數資訊
- `org.apache.ibatis.mapping.SqlSource`:SQL 資源介面,用於建立BoundSql物件(包含可執行的SQL語句與引數資訊)
- `org.apache.ibatis.mapping.BoundSql`:用於資料庫可執行的SQL語句的最終封裝物件
- `org.apache.ibatis.scripting.defaults.DefaultParameterHandler`:實現了ParameterHandler介面,用於將入參設定到`java.sql.PreparedStatement`預編譯物件中
用於將入參設定到`java.sql.PreparedStatement`預編譯物件中
我們先來回顧一下`org.apache.ibatis.scripting.xmltags.XMLScriptBuilder`的`parseScriptNode()`方法,將 SQL 指令碼(XML或者註解中定義的 SQL )解析成 `SqlSource` 物件
程式碼如下:
```java
public SqlSource parseScriptNode() {
// 解析 XML 或者註解中定義的 SQL
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
// 動態語句,使用了 ${} 也算
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
```
1. 如果是動態 SQL 語句,使用了 MyBatis 的自定義標籤(` `等)或者使用了 `${}` 都是動態 SQL 語句,則會建立`DynamicSqlSource`物件
2. 否則就是靜態 SQL 語句,建立 `RawSqlSource` 物件
`SqlSource`介面的實現類如下圖所示:
### SqlSourceBuilder
`org.apache.ibatis.builder.SqlSourceBuilder`:繼承了BaseBuilder抽象類,`SqlSource`構建器,負責將SQL語句中的`#{}`替換成相應的`?`佔位符,並獲取該`?`佔位符對應的 `org.apache.ibatis.mapping.ParameterMapping` 物件
#### 構造方法
```java
public class SqlSourceBuilder extends BaseBuilder {
private static final String PARAMETER_PROPERTIES = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName";
public SqlSourceBuilder(Configuration configuration) {
super(configuration);
}
}
```
其中`PARAMETER_PROPERTIES`字串定義了`#{}`中支援定義哪些屬性,在拋異常的時候用到
#### parse方法
解析原始的SQL(僅包含`#{}`定義的引數),轉換成StaticSqlSource物件
因為在`DynamicSqlSource`呼叫該方法前會將`MixedSqlNode`進行處理,呼叫其`apply`方法進行應用,根據`DynamicContext`上下文對MyBatis的自定義標籤或者包含`${}`的SQL生成的`SqlNode`進行邏輯處理或者注入值,生成一個SQL(僅包含`#{}`定義的引數)
程式碼如下:
```java
/**
* 執行解析原始 SQL ,成為 SqlSource 物件
*
* @param originalSql 原始 SQL
* @param parameterType 引數型別
* @param additionalParameters 上下文的引數集合,包含附加引數集合(通過 標籤生成的,或者` `標籤中的集合的元素)
* RawSqlSource傳入空集合
* DynamicSqlSource傳入 {@link org.apache.ibatis.scripting.xmltags.DynamicContext#bindings} 集合
* @return SqlSource 物件
*/
public SqlSource parse(String originalSql, Class> parameterType, Map additionalParameters) {
// <1> 建立 ParameterMappingTokenHandler 物件
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
// <2> 建立 GenericTokenParser 物件
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
/*
* <3> 執行解析
* 將我們在 SQL 定義的所有佔位符 #{content} 都替換成 ?
* 並生成對應的 ParameterMapping 物件儲存在 ParameterMappingTokenHandler 中
*/
String sql = parser.parse(originalSql);
// <4> 建立 StaticSqlSource 物件
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
```
該方法的入參`originalSql`為原始的SQL,也就是其所有的SqlNode節點已經應用了,也就是都呼叫了`apply`方法
包含的`${}`也已經注入了對應的值,所以這裡只剩`#{}`定義的入參了
1. 建立`ParameterMappingTokenHandler`處理器物件`handler`
2. 建立GenericTokenParser物件,用於處理`#{}`中的內容,通過`handler`將其轉換成`?`佔位符,並建立對應的`ParameterMapping`物件
3. 執行解析,獲取最終的 SQL 語句
4. 建立`StaticSqlSource`物件
#### ParameterMappingTokenHandler
`org.apache.ibatis.builder.SqlSourceBuilder`的內部類,用於解析`#{}`的內容,建立`ParameterMapping`物件,並將其替換成`?`佔位符
程式碼如下:
```java
private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
/**
* 我們在 SQL 語句中定義的佔位符對應的 ParameterMapping 陣列,根據順序來的
*/
private List parameterMappings = new ArrayList<>();
/**
* 引數型別
*/
private Class> parameterType;
/**
* additionalParameters 引數的對應的 MetaObject 物件
*/
private MetaObject metaParameters;
public ParameterMappingTokenHandler(Configuration configuration, Class> parameterType, Map additionalParameters) {
super(configuration);
this.parameterType = parameterType;
// 建立 additionalParameters 引數的對應的 MetaObject 物件
this.metaParameters = configuration.newMetaObject(additionalParameters);
}
public List getParameterMappings() {
return parameterMappings;
}
@Override
public String handleToken(String content) {
// <1> 構建 ParameterMapping 物件,並新增到 parameterMappings 中
parameterMappings.add(buildParameterMapping(content));
// <2> 返回 ? 佔位符
return "?";
}
/**
* 根據內容構建一個 ParameterMapping 物件
*
* @param content 我們在 SQL 語句中定義的佔位符
* @return ParameterMapping 物件
*/
private ParameterMapping buildParameterMapping(String content) {
// <1> 將字串解析成 key-value 鍵值對儲存
// 其中有一個key為"property",value就是對應的屬性名稱
Map propertiesMap = parseParameterMapping(content);
// <2> 獲得屬性的名字和型別
String property = propertiesMap.get("property"); // 名字
Class> propertyType; // 型別
if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params
propertyType = metaParameters.getGetterType(property);
} else if (typeHandlerRegistry.hasTypeHandler(parameterType)) { // 有對應的型別處理器,例如java.lang.string
propertyType = parameterType;
} else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) { // 設定的 Jdbc Type 是遊標
propertyType = java.sql.ResultSet.class;
} else if (property == null || Map.class.isAssignableFrom(parameterType)) { // 是 Map 集合
propertyType = Object.class;
} else { // 類物件
MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
if (metaClass.hasGetter(property)) {
// 通過反射獲取到其對應的 Java Type
propertyType = metaClass.getGetterType(property);
} else {
propertyType = Object.class;
}
}
// <3> 建立 ParameterMapping.Builder 構建者物件
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
// <3.1> 初始化 ParameterMapping.Builder 物件的屬性
Class> javaType = propertyType;
String typeHandlerAlias = null;
// 遍歷 SQL 配置的佔位符資訊,例如這樣配置:"name = #{name, jdbcType=VARCHAR}"
for (Map.Entry entry : propertiesMap.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
if ("javaType".equals(name)) {
javaType = resolveClass(value);
builder.javaType(javaType);
} else if ("jdbcType".equals(name)) {
builder.jdbcType(resolveJdbcType(value));
} else if ("mode".equals(name)) {
builder.mode(resolveParameterMode(value));
} else if ("numericScale".equals(name)) {
builder.numericScale(Integer.valueOf(value));
} else if ("resultMap".equals(name)) {
builder.resultMapId(value);
} else if ("typeHandler".equals(name)) {
typeHandlerAlias = value;
} else if ("jdbcTypeName".equals(name)) {
builder.jdbcTypeName(value);
} else if ("property".equals(name)) {
// Do Nothing
} else if ("expression".equals(name)) {
throw new BuilderException("Expression based parameters are not supported yet");
} else {
throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}. Valid properties are " + PARAMETER_PROPERTIES);
}
}
// <3.2> 如果 TypeHandler 型別處理器的別名非空
if (typeHandlerAlias != null) {
builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
}
// <3.3> 建立 ParameterMapping 物件
return builder.build();
}
private Map parseParameterMapping(String content) {
try {
return new ParameterExpression(content);
} catch (BuilderException ex) {
throw ex;
} catch (Exception ex) {
throw new BuilderException("Parsing error was found in mapping #{" + content
+ "}. Check syntax #{property|(expression), var1=value1, var2=value2, ...} ", ex);
}
}
}
```
構造方法:建立`additionalParameters`對應的MetaObject物件,便於操作上下文的引數集合,包含附加引數集合(通過 ` ` 標籤生成的,或者` `標籤中的集合的元素)
`handleToken(String content)`方法:
1. 呼叫`buildParameterMapping(content)`方法,解析`#{}`的內容建立`ParameterMapping`物件
2. 直接返回`?`佔位符
`buildParameterMapping(content)`方法:
1. 將字串解析成 key-value 鍵值對,通過`org.apache.ibatis.builder.ParameterExpression`進行解析,其中有一個key為"property",value就是對應的屬性名稱
2. 獲得屬性的名字和型別
3. 建立`ParameterMapping.Builder`構建者物件,設定引數的名稱與Java Type
1. 將上面第`1`步解析到key-value鍵值對設定到Builder中
2. 如果TypeHandler型別處理器的別名非空,則嘗試獲取其對應的型別處理器並設定到Builder中
3. 通過Builder建立`ParameterMapping`物件,如果沒有配置TypeHandler型別處理器,則根據引數Java Type和Jdbc Type從`TypeHandlerRegistry`註冊中心獲取並賦值到該物件中
### ParameterExpression
`org.apache.ibatis.builder.ParameterExpression`:繼承了`HashMap`,引數表示式處理器,在`ParameterMappingTokenHandler`處理`#{}`的內容時需要通過其解析成key-value鍵值對
構造方法:
```java
public class ParameterExpression extends HashMap {
private static final long serialVersionUID = -2417552199605158680L;
/**
* 從類的註釋中可以看出我們可以這樣定義佔位符
* 1. #{propertyName, javaType=string, jdbcType=VARCHAR}
* 2. #{(expression), javaType=string, jdbcType=VARCHAR}
*
* @param expression 我們定義的佔位符表示式
*/
public ParameterExpression(String expression) {
parse(expression);
}
}
```
在建構函式中呼叫其`parse(String expression)`方法
```java
private void parse(String expression) {
// 跳過前面的非法字元(ASCII 小於33),目的是去除空格,還有非法的字元,可以參照 ASCII 字元程式碼表看看
int p = skipWS(expression, 0);
if (expression.charAt(p) == '(') {
// 屬於第二種方式,我在官方沒有看到介紹,這裡也不做介紹了
expression(expression, p + 1);
} else {
// 將整個字串轉換成 key-value 儲存至 Map.Entry
property(expression, p);
}
}
```
先出去前面的空格或者非法字元,然後呼叫`property(String expression, int left)`方法
```java
// #{propertyName, javaType=string, jdbcType=VARCHAR}
private void property(String expression, int left) {
if (left < expression.length()) {
// 獲取到逗號或者冒號第一個位置,也就是分隔符
int right = skipUntil(expression, left, ",:");
// 從內容中擷取第一個逗號前面的字串,也上面第 1 種方式的 "name"
put("property", trimmedStr(expression, left, right));
// 解析字串一個逗號後面的字串,也就是該屬性的相關配置
jdbcTypeOpt(expression, right);
}
}
```
如果`left`開始位置小於字串的長度,那麼開始解析
1. 呼叫`skipUntil`方法,獲取從`left`開始`,`或者`:`第一個位置,也就是分隔符的位置
2. 這裡第一次進入的話就會先獲取第一個`,`的位置,那麼呼叫`trimmedStr`方法擷取前面的字串,也就是屬性名稱,然後存放一個`鍵值對(key為property,value為屬性名稱)`
3. 呼叫`jdbcTypeOpt(String expression, int p)`方法,繼續解析後面的字串,也就是該屬性的相關配置
```java
private void jdbcTypeOpt(String expression, int p) {
p = skipWS(expression, p);
if (p < expression.length()) {
if (expression.charAt(p) == ':') { // 屬於上面第 2 種方式,不做分析
jdbcType(expression, p + 1);
} else if (expression.charAt(p) == ',') {
// 將第一個 , 後面的字串解析成 key-value 儲存
option(expression, p + 1);
} else {
throw new BuilderException("Parsing error in {" + expression + "} in position " + p);
}
}
}
```
如果`p`(第一個`,`的位置)後面還有字串
則呼叫`option(String expression, int p)`方法將一個`,`後面的字串解析成key-value鍵值對儲存
```java
/**
* 將字串生成轉換成key-value的形式
* 例如 expression = "name, jdbcType = VARCHAR, javaType = string" 設定 p = 6
* 這樣將會往 Map 中儲存兩個鍵值對:"jdbcType"->"VARCHAR" "javaType"->"string"
*
* @param expression 字串
* @param p 字串從哪個位置轉換
*/
private void option(String expression, int p) {
int left = skipWS(expression, p);
if (left < expression.length()) {
// 獲取 = 的位置
int right = skipUntil(expression, left, "=");
// 擷取 = 前面的字串,對應的 key
String name = trimmedStr(expression, left, right);
left = right + 1;
// 獲取 , 的位置
right = skipUntil(expression, left, ",");
// 擷取 = 到 , 之間的字串,也就是對應的 value
String value = trimmedStr(expression, left, right);
// 將 key-value 儲存
put(name, value);
// 繼續遍歷後面的字串
option(expression, right + 1);
}
}
```
逐步解析,將字串解析成key-value鍵值對儲存,這裡儲存的都是屬性的相關配置,例如`JdbcType`配置
### ParameterMapping
`org.apache.ibatis.mapping.ParameterMapping`:儲存`#{}`中配置的屬性引數資訊,一個普通的實體類,程式碼如下:
```java
/**
* SQL 語句中 ? 佔位符對應的物件
*
* @author Clinton Begin
*/
public class ParameterMapping {
/**
* 全域性配置物件
*/
private Configuration configuration;
/**
* 屬性名稱
*/
private String property;
/**
* 引數模式
*/
private ParameterMode mode;
/**
* 屬性的 Java Type
* 一般可以直接通過入參物件知道,但是如果入參是 Map,需要顯式指定,以確保使用正確的型別處理器
*/
private Class> javaType = Object.class;
/**
* 屬性的 Jdbc Type
*/
private JdbcType jdbcType;
/**
* 對於數值型別,指定小數點後保留的位數
*/
private Integer numericScale;
/**
* 型別處理器
*/
private TypeHandler> typeHandler;
/**
* 如果 {@link mode} 為 OUT 或者 INOUT,且{@link jdbcType} 為 CURSOR(也就是 Oracle 的 REFCURSOR)
* 必須指定一個 resultMap 引用來將結果集 ResultMap 對映到引數的型別上
*/
private String resultMapId;
/**
* Jdbc Type 名稱
*/
private String jdbcTypeName;
private String expression;
private ParameterMapping() {
}
}
```
### SqlSource
`org.apache.ibatis.mapping.SqlSource`:SQL 資源介面,用於建立BoundSql物件(包含可執行的SQL語句與引數資訊),程式碼如下:
```java
/**
* Represents the content of a mapped statement read from an XML file or an annotation.
* It creates the SQL that will be passed to the database out of the input parameter received from the user.
*
* @author Clinton Begin
*/
public interface SqlSource {
/**
* 根據傳入的引數物件,返回 BoundSql 物件
*
* @param parameterObject 引數物件
* @return BoundSql 物件
*/
BoundSql getBoundSql(Object parameterObject);
}
```
#### StaticSqlSource
`org.apache.ibatis.builder.StaticSqlSource`:實現 SqlSource 介面,靜態的 SqlSource 實現類,程式碼如下:
```java
public class StaticSqlSource implements SqlSource {
/**
* 解析後的 SQL 語句,資料庫能執行
*/
private final String sql;
/**
* 上面 SQL 語句中佔位符對應的 ParameterMapping 引數集合
*/
private final List parameterMappings;
/**
* 全域性配置物件
*/
private final Configuration configuration;
public StaticSqlSource(Configuration configuration, String sql) {
this(configuration, sql, null);
}
public StaticSqlSource(Configuration configuration, String sql, List parameterMappings) {
this.sql = sql;
this.parameterMappings = parameterMappings;
this.configuration = configuration;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
}
```
在`SqlSourceBuilder`構建的SqlSource型別就是`StaticSqlSource`,用於獲取最終的靜態 SQL 語句
#### RawSqlSource
`org.apache.ibatis.scripting.defaults.RawSqlSource`:實現了SqlSource介面,靜態SQL語句對應的SqlSource物件,用於建立靜態 SQL 資源,程式碼如下:
```java
public class RawSqlSource implements SqlSource {
private final SqlSource sqlSource;
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class> parameterType) {
/*
* 因為靜態的 SQL 語句可以直接拿來解析,不需要根據入參就可以應用
* 所以呼叫 getSql 方法獲取靜態的 SQL 語句
*/
this(configuration, getSql(configuration, rootSqlNode), parameterType);
}
public RawSqlSource(Configuration configuration, String sql, Class> parameterType) {
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class> clazz = parameterType == null ? Object.class : parameterType;
// 通過 SqlSourceBuilder 將這個靜態的 SQL 進行轉換,變數替換成 ? 佔位符,並生成對應的 ParameterMapping 集合
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}
private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
DynamicContext context = new DynamicContext(configuration, null);
// 呼叫 StaticTextSqlNode 將 SQL 語句拼接起來
rootSqlNode.apply(context);
return context.getSql();
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return sqlSource.getBoundSql(parameterObject);
}
}
```
在建構函式中我們可以看到,會先呼叫`getSql`方法直接建立`SqlSource`
因為靜態的 SQL 語句,不需要根據入參來進行邏輯上的判斷處理,所以這裡在建構函式中就先初始化好 SqlSource,後續需要呼叫Mapper介面執行SQL的時候就減少了一定的時間
`getSql`方法:
1. 建立一個上下文物件`DynamicContext`,入參資訊為null
2. 呼叫`StaticTextSqlNode`的`apply`方法,將所有的SQL拼接在一起
3. 返回拼接好的SQL語句
構造方法:
1. 建立SqlSourceBuilder構建物件`sqlSourceParser`
2. 呼叫`sqlSourceParser`的`parse`方法對該SQL語句進行轉換,`#{}`全部替換成`?`佔位符,並建立對應的`ParameterMapping`物件
3. 第`2`步返回的`StaticSqlSource`物件設定到自己的`sqlSource`屬性中
`getBoundSql`方法:直接通過`StaticSqlSource`建立`BoundSql`物件
#### DynamicSqlSource
`org.apache.ibatis.scripting.defaults.DynamicSqlSource`:實現了SqlSource介面,動態SQL語句對應的SqlSource物件,用於建立靜態 SQL 資源,程式碼如下:
```java
public class DynamicSqlSource implements SqlSource {
private final Configuration configuration;
/**
* 根 SqlNode 物件
*/
private final SqlNode rootSqlNode;
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
// <1> 建立本次解析的動態 SQL 語句的上下文
DynamicContext context = new DynamicContext(configuration, parameterObject);
// <2> 根據上下文應用整個 SqlNode
rootSqlNode.apply(context);
// <3> 建立 SqlSourceBuilder 物件
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
// <4> 通過 SqlSourceBuilder 將應用後的 SQL 進行轉換,變數替換成 ? 佔位符,並生成對應的 ParameterMapping 集合
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
// <5> 建立 BoundSql 物件
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// <6> 新增附加引數到 BoundSql 物件中,因為上一步建立的`BoundSql`物件時候傳入的僅是入參資訊,沒有新增附加引數
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
}
```
在建構函式中僅僅是賦值,不像`RawSqlSource`的建構函式一樣直接可建立對應的SqlSource物件,因為動態SQL語句需要根據入參資訊,來解析SqlNode節點,所以這裡在`getBoundSql`方法中每次都會建立`StaticSqlSource`物件
`getBoundSql`方法:
1. 建立本次解析的動態 SQL 語句的上下文,設定入參資訊
2. 根據上下文應用整個 SqlNode,內部包含的所有SqlNode都會被應用,最終解析後的SQL會儲存上下文中
3. 建立 SqlSourceBuilder 構建物件`sqlSourceParser`
4. 呼叫`sqlSourceParser`的`parse`方法對第`2`步解析後的SQL語句進行轉換,`#{}`全部替換成`?`佔位符,並建立對應的`ParameterMapping`物件
5. 通過第`4`步返回的`StaticSqlSource`物件建立`BoundSql`物件
6. 新增附加引數到`BoundSql`物件中,因為上一步建立的`BoundSql`物件時候傳入的僅是入參資訊,沒有新增附加引數(通過` `標籤生成的,或者` `標籤中的集合的元素)
### BoundSql
`org.apache.ibatis.mapping.BoundSql`:用於資料庫可執行的SQL語句的最終封裝物件,一個普通的實體類,程式碼如下:
```java
public class BoundSql {
/**
* SQL 語句
*/
private final String sql;
/**
* 佔位符 ? 對應的入參資訊
*/
private final List parameterMappings;
/**
* 入參物件
*/
private final Object parameterObject;
/**
* 附加引數集合
*/
private final Map additionalParameters;
/**
* 附加引數的 MetaObject 物件,便於操作
*/
private final MetaObject metaParameters;
public BoundSql(Configuration configuration, String sql, List parameterMappings, Object parameterObject) {
this.sql = sql;
this.parameterMappings = parameterMappings;
this.parameterObject = parameterObject;
this.additionalParameters = new HashMap<>();
this.metaParameters = configuration.newMetaObject(additionalParameters);
}
public String getSql() {
return sql;
}
public List getParameterMappings() {
return parameterMappings;
}
public Object getParameterObject() {
return parameterObject;
}
public boolean hasAdditionalParameter(String name) {
String paramName = new PropertyTokenizer(name).getName();
return additionalParameters.containsKey(paramName);
}
public void setAdditionalParameter(String name, Object value) {
metaParameters.setValue(name, value);
}
public Object getAdditionalParameter(String name) {
return metaParameters.getValue(name);
}
}
```
### DefaultParameterHandler
`org.apache.ibatis.scripting.defaults.DefaultParameterHandler`:實現了ParameterHandler介面,預設實現類,僅提供這個實現類,用於將入參設定到`java.sql.PreparedStatement`預編譯物件中
回看到`org.apache.ibatis.scripting.xmltags.XMLLanguageDriver`語言驅動類中,實現了`createParameterHandler`方法,返回的引數處理器就是該物件
程式碼如下:
```java
public class DefaultParameterHandler implements ParameterHandler {
private final TypeHandlerRegistry typeHandlerRegistry;
/**
* MappedStatement 物件
*/
private final MappedStatement mappedStatement;
/**
* 入參
*/
private final Object parameterObject;
/**
* BoundSql 物件,實際的 SQL 語句
*/
private final BoundSql boundSql;
/**
* 全域性配置物件
*/
private final Configuration configuration;
public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
this.mappedStatement = mappedStatement;
this.configuration = mappedStatement.getConfiguration();
this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
this.parameterObject = parameterObject;
this.boundSql = boundSql;
}
@Override
public Object getParameterObject() {
return parameterObject;
}
@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
// 獲取 SQL 的引數資訊 ParameterMapping 物件
List parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
// 遍歷所有引數
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
/*
* OUT 表示引數僅作為出參,非 OUT 也就是需要作為入參
*/
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
// 獲取入參的屬性名
String propertyName = parameterMapping.getProperty();
/*
* 獲取入參的實際值
*/
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
// 在附加引數集合( 標籤生成的)中獲取
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
// 入參為 null 則該屬性也定義為 null
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
// 有型別處理器,則直接獲取入參物件
value = parameterObject;
} else {
// 建立入參對應的 MetaObject 物件並獲取該屬性的值
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 獲取定義的引數型別處理器
TypeHandler typeHandler = parameterMapping.getTypeHandler();
// 獲取定義的 Jdbc Type
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
// 如果沒有則設定成 'OTHER'
jdbcType = configuration.getJdbcTypeForNull();
}
try {
// 通過定義的 TypeHandler 引數型別處理器將 value 設定到對應的佔位符
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException(
"Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
}
```
往`PreparedStatement`中設定引數的大致邏輯如下:
1. 獲取SQL的引數資訊`ParameterMapping`物件的集合,然後對其遍歷
2. 如果引數的模式不為`ParameterMode.OUT`(預設為`ParameterMode.IN`),也就是說需要作為入參,那麼開始接下來的賦值
3. 獲取該引數對應的屬性名稱,並通過其獲取到對應的值
4. 獲取到`TypeHandler`型別處理器(在`ParameterMapping`構建的時候會建立對應的`TypeHandler`)
5. 獲取到Jdbc Type
6. 通過`TypeHandler`型別處理器,根據引數位置和Jdbc Type將屬性值設定到`PreparedStatement`中
這樣就完成對`PreparedStatement`的賦值,然後通過它執行SQL語句
### 總結
在MyBatis初始化的過程中,會將XML對映檔案中的` `節點解析成`MappedStatement`物件,其中會將節點中定義的SQL語句通過`XMLLanguageDriver`語言驅動類建立一個`SqlSource`物件,本文就是對該物件進行分析
通過`SqlSource`這個物件根據入參可以獲取到對應的`BoundSql`物件,`BoundSql`物件中包含了資料庫需要執行的SQL語句、`ParameterMapping`引數資訊、入參物件和附加的引數(通過` `標籤生成的,或者` `標籤中的集合的元素等等)
好了,對於MyBatis的整個初始化過程我們已經全部分析完了,其中肯定有不對或者迷惑的地方,歡迎指正!!!感謝大家的閱讀!!!:smile::smile::smile:
> 參考文章:**芋道原始碼**[《精盡 MyBatis 原始碼分析》](http://svip.iocoder.cn/categories/M