Mybatis3.4.x技術內幕(十七):Mybatis之動態Sql設計本來(上)

上一篇博文中,介紹了可複用的sql片斷,經過<include>標籤進行引入,而<include>標籤內通常存放的是靜態sql,其實,sql片斷也是能夠放置動態sql標籤內容。java

1. Mybatis支持的動態sql及基本用法

org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.nodeHandlers(String)部分源碼。node

NodeHandler nodeHandlers(String nodeName) {
    Map<String, NodeHandler> map = new HashMap<String, NodeHandler>();
    map.put("trim", new TrimHandler());
    map.put("where", new WhereHandler());
    map.put("set", new SetHandler());
    map.put("foreach", new ForEachHandler());
    map.put("if", new IfHandler());
    map.put("choose", new ChooseHandler());
    map.put("when", new IfHandler());
    map.put("otherwise", new OtherwiseHandler());
    map.put("bind", new BindHandler());
    return map.get(nodeName);
  }

Mybatis所支持的動態sql標籤:trim|where|set|foreach|if|choose|when|otherwise|bind。面試

<select id="findStudents" parameterType="customMap" resultType="StudentResult">
		select * from STUDENTS where 1 = 1 
		<choose>
			<when test="name != null">
				and name = #{name}
			</when>
			<when test="email != null">
				and EMAIL = #{email}
			</when>
			<otherwise>
				and PHONE = "123"
			</otherwise>
		</choose>
	</select>

    <select id="countAll" resultType="int">
		select count(1) from (
			select 
			stud_id as studId
			, name, email
			, dob
			, phone
		from students
		<where>
			<if test="id != null">
				AND STUD_ID &lt; 310
			</if>
		</where>
		) tmp 
	</select>

    <select id="findAllStudents" resultMap="StudentResult" parameterMap="customMap">
		<bind name="status" value="'status'"/>
		SELECT * FROM STUDENTS WHERE STUD_ID > #{id}, #{status},${driver}
	</select>

	<insert id="insertStudents" useGeneratedKeys="true" keyProperty="studId" parameterType="java.util.ArrayList">
		INSERT INTO
		STUDENTS(STUD_ID, NAME, EMAIL, DOB, PHONE)
		VALUES
		<foreach collection="list" item="item" index="index" separator=","> 
        	(#{item.studId},#{item.name},#{item.email},#{item.dob}, #{item.phone}) 
    	</foreach> 
	</insert>

爲了不篇幅過長,咱們簡單列舉了幾個動態sql的基本用法,咱們的重點依然是剖析Mybatis動態sql的底層設計原理。sql

2. SqlSource

在Mybatis中,每個select|insert|update|delete標籤,都會被解析爲一個MappedStatement對象,SqlSource就是MappedStatement對象中一個屬性,其最終執行的sql字符串就是由SqlSource提供的。express

public final class MappedStatement {
    private SqlSource sqlSource;
}

org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode()部分源碼:apache

SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

(Made In IntelliJ Idea IDE)設計模式

DynamicSqlSource:處理動態sql。網絡

RawSqlSource:處理靜態sql,其內部裝飾StaticSqlSource。app

StaticSqlSource:處理靜態sql,不管是靜態sql,仍是動態sql,最終的處理結果,都是靜態sqlless

ProviderSqlSource:處理註解Annotation形式的sql。

DynamicSqlSource和StaticSqlSource的最大區別在於:StaticSqlSource的String sql,能夠直接獲取使用,而DynamicSqlSource的String sql須要逐一根據條件解析並拼接出最終的sql,方能使用。

3. DynamicSqlSource以及SqlNode

public class DynamicSqlSource implements SqlSource {

  private Configuration configuration;
  private SqlNode rootSqlNode;
}
public interface SqlNode {
  boolean apply(DynamicContext context);
}

boolean apply(DynamicContext context):該方法的含義爲,將sql的處理結果,append到DynamicContext context對象中,DynamicContext能夠理解爲StringBuilder對象的功能,它的做用就是計算sql片斷並append到一塊兒,造成最終的sql。對該方法的理解很是重要,只有理解了這個方法,才能真正明白一個完整sql是如何組裝出來的

下面的僞代碼,展現了SqlNode.apply(DynamicContext)方法設計的核心原理。

StringBuilder sb = new StringBuilder();
		IfSqlNode.apply(StringBuilder sb) {
			sb.append("select ");
		}
		SetSqlNode.apply(StringBuilder sb) {
			sb.append("* from ss ");
		}
		sb.toString();
		//output: select * from ss

DynamicSqlSource爲動態sql源,而SqlNode則具體表明瞭動態sql源中具體的動態sql類型。

(Made In IntelliJ Idea IDE)

上面的SqlNode,基本上見名知意,咱們着重解釋一下容易迷惑的兩個SqlNode。

VarDeclSqlNode:處理動態sql標籤<bind>的SqlNode類。

public class VarDeclSqlNode implements SqlNode {

  private final String name;
  private final String expression;

  public VarDeclSqlNode(String var, String exp) {
    name = var;
    expression = exp;
  }

  @Override
  public boolean apply(DynamicContext context) {
    final Object value = OgnlCache.getValue(expression, context.getBindings());
    // 因爲沒有sql可append,僅是把bind標籤的變量名和值保存至上下文參數列表內
    context.bind(name, value);
    return true;
  }
}

MixedSqlNode:意爲混合的SqlNode,它保存了其餘多種SqlNode的集合,能夠看作是一個List<SqlNode>列表,事實也確實如此。

DynamicSqlSource中的SqlNode rootSqlNode屬性,一般都是MixedSqlNode對象(徹底是靜態sql時,多是一個StaticTextSqlNode),而MixedSqlNode對象又保存了全部的List<SqlNode>集合,這也是經過一個rootSqlNode,就能找到全部SqlNode的深層緣由。

4. SqlNode的組合設計模式

public class ForEachSqlNode implements SqlNode {
 private SqlNode contents;
}

SqlNode,採用了組合設計模式,組合設計模式能夠用來表示經典的樹型結構,有人不由要問,組合設計模式,它的屬性,應該List<SqlNode>集合,怎麼會是單一的SqlNode呢?

前面說的MixedSqlNode,就表明了List<SqlNode>集合,因此,它是換湯不換藥的經典組合設計模式。

舉例:ForEachSqlNode內部,多是一個StaticTextSqlNode,看XML就一目瞭然。

<foreach collection="list" item="item" index="index" separator=","> 
        	(#{item.studId},#{item.name},#{item.email},#{item.dob}, #{item.phone}) 
</foreach>

5. NodeHandler

SqlNode是由NodeHandler建立出來的。

(Made In EDrawMax)

private class ChooseHandler implements NodeHandler {
    public ChooseHandler() {
      // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      List<SqlNode> whenSqlNodes = new ArrayList<SqlNode>();
      List<SqlNode> otherwiseSqlNodes = new ArrayList<SqlNode>();
      handleWhenOtherwiseNodes(nodeToHandle, whenSqlNodes, otherwiseSqlNodes);
      SqlNode defaultSqlNode = getDefaultSqlNode(otherwiseSqlNodes);
      ChooseSqlNode chooseSqlNode = new ChooseSqlNode(whenSqlNodes, defaultSqlNode);
      targetContents.add(chooseSqlNode);
    }

    private void handleWhenOtherwiseNodes(XNode chooseSqlNode, List<SqlNode> ifSqlNodes, List<SqlNode> defaultSqlNodes) {
      List<XNode> children = chooseSqlNode.getChildren();
      for (XNode child : children) {
        String nodeName = child.getNode().getNodeName();
        NodeHandler handler = nodeHandlers(nodeName);
        if (handler instanceof IfHandler) {
          handler.handleNode(child, ifSqlNodes);
        } else if (handler instanceof OtherwiseHandler) {
          handler.handleNode(child, defaultSqlNodes);
        }
      }
    }
//...
}

上面的例子,能夠清楚看出,<choose>標籤是和<when>、<otherwise>標籤配合使用的,建立ChooseSqlNode時,就同時建立了when、otherwise的邏輯,而when會轉換爲if標籤處理,otherwise則轉換爲SqlNode處理,通常是StaticTextSqlNode。

map.put("if", new IfHandler());
map.put("when", new IfHandler());

因篇幅問題,咱們再也不逐一描述,讀者可自行查看。

6. LanguageDriver

LanguageDriver是一個輔助工具類,用於建立SqlSource。

(Made In IntelliJ Idea IDE)
XMLLanguageDriver:用於建立動態、靜態SqlSource。

RawLanguageDriver:在確保只有靜態sql時,可使用,不得含有任何動態sql的內容,不然,請使用XMLLanguageDriver。它實際上是對XMLLanguageDriver建立的結果進行惟靜態sql檢查而已,發現有動態sql的內容,就拋異常。

/**
 * As of 3.2.4 the default XML language is able to identify static statements
 * and create a {@link RawSqlSource}. So there is no need to use RAW unless you
 * want to make sure that there is not any dynamic tag for any reason.
 * 
 * @since 3.2.0
 * @author Eduardo Macarron
 */
public class RawLanguageDriver extends XMLLanguageDriver {

  @Override
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    SqlSource source = super.createSqlSource(configuration, script, parameterType);
    checkIsNotDynamic(source);
    return source;
  }
// ...
}

總結:本篇博文,介紹了Mybatis動態sql的基本結構,基本用法,基本設計原理,下一篇將仔細分析Mybatis使用OGNL計算表達式以及處理#{name}和${name}的時機和它們之間的異同。面試過程當中,面試官可能會問你在Mybatis中,#{name}和${name}有什麼區別

版權提示:文章出自開源中國社區,若對文章感興趣,可關注個人開源中國社區博客(http://my.oschina.net/zudajun)。(通過網絡爬蟲或轉載的文章,常常丟失流程圖、時序圖,格式錯亂等,仍是看原版的比較好)

相關文章
相關標籤/搜索