精盡MyBatis原始碼分析 - MyBatis初始化(四)之 SQL 初始化(下)

月圓吖發表於2020-11-24

該系列文件是本人在學習 Mybatis 的原始碼過程中總結下來的,可能對讀者不太友好,請結合我的原始碼註釋(Mybatis原始碼分析 GitHub 地址Mybatis-Spring 原始碼分析 GitHub 地址Spring-Boot-Starter 原始碼分析 GitHub 地址)進行閱讀

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的初始化過程中去解析Mapper介面與XML對映檔案涉及到的篇幅比較多,XML對映檔案的解析過程也比較複雜,所以才分成了後面三個模組,逐步分析,這樣便於理解

初始化(四)之SQL初始化(下)

在上一篇文件中詳細地講述了MyBatis在解析<select /> <insert /> <update /> <delete /> 節點的過程中,是如何解析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<String, String>,參數列達式處理器,在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.XMLScriptBuilderparseScriptNode()方法,將 SQL 指令碼(XML或者註解中定義的 SQL )解析成 SqlSource 物件

程式碼如下:

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 的自定義標籤(<if /> <foreach />等)或者使用了 ${} 都是動態 SQL 語句,則會建立DynamicSqlSource物件
  2. 否則就是靜態 SQL 語句,建立 RawSqlSource 物件

SqlSource介面的實現類如下圖所示:

SqlSource

SqlSourceBuilder

org.apache.ibatis.builder.SqlSourceBuilder:繼承了BaseBuilder抽象類,SqlSource構建器,負責將SQL語句中的#{}替換成相應的?佔位符,並獲取該?佔位符對應的 org.apache.ibatis.mapping.ParameterMapping 物件

構造方法

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(僅包含#{}定義的引數)

程式碼如下:

/**
* 執行解析原始 SQL ,成為 SqlSource 物件
*
* @param originalSql          原始 SQL
* @param parameterType        引數型別
* @param additionalParameters 上下文的引數集合,包含附加引數集合(通過 <bind /> 標籤生成的,或者`<foreach />`標籤中的集合的元素)
*                             RawSqlSource傳入空集合
*                             DynamicSqlSource傳入 {@link org.apache.ibatis.scripting.xmltags.DynamicContext#bindings} 集合
* @return SqlSource 物件
*/
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> 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物件,並將其替換成?佔位符

程式碼如下:

private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {

    /**
     * 我們在 SQL 語句中定義的佔位符對應的 ParameterMapping 陣列,根據順序來的
     */
    private List<ParameterMapping> parameterMappings = new ArrayList<>();
    /**
     * 引數型別
     */
    private Class<?> parameterType;
    /**
     * additionalParameters 引數的對應的 MetaObject 物件
     */
    private MetaObject metaParameters;

    public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType, Map<String, Object> additionalParameters) {
        super(configuration);
        this.parameterType = parameterType;
        // 建立 additionalParameters 引數的對應的 MetaObject 物件
        this.metaParameters = configuration.newMetaObject(additionalParameters);
    }

    public List<ParameterMapping> 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<String, String> 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<String, String> 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<String, String> 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物件,便於操作上下文的引數集合,包含附加引數集合(通過 <bind /> 標籤生成的,或者<foreach />標籤中的集合的元素)

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<String, String>,參數列達式處理器,在ParameterMappingTokenHandler處理#{}的內容時需要通過其解析成key-value鍵值對

構造方法:

public class ParameterExpression extends HashMap<String, String> {

	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)方法

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)方法

// #{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)方法,繼續解析後面的字串,也就是該屬性的相關配置

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鍵值對儲存

/**
 * 將字串生成轉換成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:儲存#{}中配置的屬性引數資訊,一個普通的實體類,程式碼如下:

/**
 * 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語句與引數資訊),程式碼如下:

/**
 * 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 實現類,程式碼如下:

public class StaticSqlSource implements SqlSource {

	/**
	 * 解析後的 SQL 語句,資料庫能執行
	 */
	private final String sql;
	/**
	 * 上面 SQL 語句中佔位符對應的 ParameterMapping 引數集合
	 */
	private final List<ParameterMapping> parameterMappings;
    /**
     * 全域性配置物件
     */
	private final Configuration configuration;

	public StaticSqlSource(Configuration configuration, String sql) {
		this(configuration, sql, null);
	}

	public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> 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 資源,程式碼如下:

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. 呼叫StaticTextSqlNodeapply方法,將所有的SQL拼接在一起
  3. 返回拼接好的SQL語句

構造方法:

  1. 建立SqlSourceBuilder構建物件sqlSourceParser
  2. 呼叫sqlSourceParserparse方法對該SQL語句進行轉換,#{}全部替換成?佔位符,並建立對應的ParameterMapping物件
  3. 2步返回的StaticSqlSource物件設定到自己的sqlSource屬性中

getBoundSql方法:直接通過StaticSqlSource建立BoundSql物件

DynamicSqlSource

org.apache.ibatis.scripting.defaults.DynamicSqlSource:實現了SqlSource介面,動態SQL語句對應的SqlSource物件,用於建立靜態 SQL 資源,程式碼如下:

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. 呼叫sqlSourceParserparse方法對第2步解析後的SQL語句進行轉換,#{}全部替換成?佔位符,並建立對應的ParameterMapping物件
  5. 通過第4步返回的StaticSqlSource物件建立BoundSql物件
  6. 新增附加引數到BoundSql物件中,因為上一步建立的BoundSql物件時候傳入的僅是入參資訊,沒有新增附加引數(通過<bind />標籤生成的,或者<foreach />標籤中的集合的元素)

BoundSql

org.apache.ibatis.mapping.BoundSql:用於資料庫可執行的SQL語句的最終封裝物件,一個普通的實體類,程式碼如下:

public class BoundSql {
  /**
   * SQL 語句
   */
  private final String sql;
  /**
   * 佔位符 ? 對應的入參資訊
   */
  private final List<ParameterMapping> parameterMappings;
  /**
   * 入參物件
   */
  private final Object parameterObject;
  /**
   * 附加引數集合
   */
  private final Map<String, Object> additionalParameters;
  /**
   * 附加引數的 MetaObject 物件,便於操作
   */
  private final MetaObject metaParameters;

  public BoundSql(Configuration configuration, String sql, List<ParameterMapping> 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<ParameterMapping> 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方法,返回的引數處理器就是該物件

程式碼如下:

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<ParameterMapping> 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
					  // 在附加引數集合(<bind />標籤生成的)中獲取
						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對映檔案中的<select /> <insert /> <update /> <delete />節點解析成MappedStatement物件,其中會將節點中定義的SQL語句通過XMLLanguageDriver語言驅動類建立一個SqlSource物件,本文就是對該物件進行分析

通過SqlSource這個物件根據入參可以獲取到對應的BoundSql物件,BoundSql物件中包含了資料庫需要執行的SQL語句、ParameterMapping引數資訊、入參物件和附加的引數(通過<bind />標籤生成的,或者<foreach />標籤中的集合的元素等等)

好了,對於MyBatis的整個初始化過程我們已經全部分析完了,其中肯定有不對或者迷惑的地方,歡迎指正!!!感謝大家的閱讀!!!???

參考文章:芋道原始碼《精盡 MyBatis 原始碼分析》

相關文章