Mybatis中SqlNode的組合模式

組合( Composite )模式就是把對象組合成樹形結構,以表示「部分-總體」的層次結構,用戶能夠像處理一個簡單對象同樣來處理一個複雜對象,從而使得調用者無需瞭解複雜元素的內部結構。前端

組合模式中的角色有:java

抽象組件(容器):定義了樹形結構中全部類的公共行爲,例如add(),remove()等方法。
樹葉:最終實現類,沒有子類。
樹枝:有子類的管理類,並經過管理方法調用其管理的子類的相關操做。
調用者:經過容器接口操做整個樹形結構。
具體組合模式的例子能夠參考 設計模式整理node

如今咱們來講一下SqlNode是什麼,來看這麼一段配置文件正則表達式

<select id="findByGameTypeCount" resultType="java.lang.Long"> select count(*)
from betdetails a inner join UserBetOrder b on a.orderId = b.id <where> <if test="gameType != null and gameType > 0"> a.gameType = #{gameType} and </if> <if test="currDrawno != null"> b.currentDrawno = #{currDrawno} and </if> <if test="orderId != null and orderId > 0"> a.orderId = #{orderId} and </if> <if test="status != null and status >= 0"> a.status = #{status} and </if> <if test="userId != null and userId > 0"> b.userId = #{userId} and </if> <if test="start != null"> a.createTime >= #{start} and </if> <if test="end != null"> a.createTime <= #{end} and </if> 1 = 1 </where></select>
<insert id="insertBetdetailsByBatch" parameterType="java.util.List"> insert into betdetails(id,orderId,actorIndex,createTime,ballIndex,ballValue,betAmount,rate1,rate2,rate3,gameType,status,betResult,awardAmount,ballName) values <foreach collection="list" item="item" index="index" separator=","> (#{item.id},#{item.orderId},#{item.actorIndex},#{item.createTime},#{item.ballIndex},#{item.ballValue},#{item.betAmount},#{item.rate1},#{item.rate2},#{item.rate3},#{item.gameType},#{item.status},#{item.betResult},#{item.awardAmount},#{item.ballName}) </foreach></insert>
這其中的<if><where><foreach>節點就是SqlNode節點,SqlNode是一個接口,表明着組合模式中的容器。只要是有SqlNode,那就表明着必定是一個動態的SQL,裏面就有可能會有參數#{}sql

public interface SqlNode {
//SqlNode接口中定義的惟一方法,該方法會根據用戶傳入的實參,解析該SqlNode所記錄的動態SQL節點,並調用DynamicContext.appendSql()方法將解析後的SQL片斷追加到
//DynamicContext.sqlBuilder中保存
//當SQL節點下的全部SqlNode完成解析後,就能夠從DynamicContext中獲取一條動態生成的完整的SQL語句 boolean apply(DynamicContext context);}
咱們先來看一下DynamicContext是什麼,它的核心字段以下express

private final ContextMap bindings; //參考上下文
//在SqlNode解析動態SQL時,會將解析後的SQL語句片斷添加到該屬性中保存,最終拼湊出一條完成的SQL語句private final StringBuilder sqlBuilder = new StringBuilder();
ContextMap是一個內部類,繼承於HashMap,重寫了get方法後端

static class ContextMap extends HashMap<String, Object> { private static final long serialVersionUID = 2977601501966151582L; //將用戶傳入的參數封裝成MetaObject對象(類實例中檢查類的屬性是否包含getter,setter方法) private MetaObject parameterMetaObject; public ContextMap(MetaObject parameterMetaObject) { this.parameterMetaObject = parameterMetaObject; } @Override public Object get(Object key) {設計模式

String strKey = (String) key;
//若是ContextMap中已經包含了該key,則直接返回
if (super.containsKey(strKey)) {      return super.get(strKey);    }
//若是不包含該key,從parameterMetaObject中查找對應屬性
if (parameterMetaObject != null) {      // issue #61 do not modify the context when reading      return parameterMetaObject.getValue(strKey);    }    return null;  }

}
public void appendSql(String sql) { sqlBuilder.append(sql); sqlBuilder.append(" ");}
SqlNode的實現類以下緩存

其中MixedSqlNode是樹枝,TextSqlNode是樹葉....app

咱們先來看一下TextSqlNode,TextSqlNode表示的是包含${}佔位符的動態SQL節點。它的接口實現方法以下

@Overridepublic boolean apply(DynamicContext context) {
//將動態SQL(帶${}佔位符的SQL)解析成完成SQL語句的解析器,即將${}佔位符替換成實際的變量值
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
//將解析後的SQL片斷添加到DynamicContext中 context.appendSql(parser.parse(text)); return true;}
BindingTokenParser是TextNode中定義的內部類,繼承了TokenHandler接口,它的主要做用是根據DynamicContext.bindings集合中的信息解析SQL語句節點中的${}佔位符。

private DynamicContext context;
private Pattern injectionFilter; //須要匹配的正則表達式
@Overridepublic String handleToken(String content) {
//獲取用戶提供的實參
Object parameter = context.getBindings().get("_parameter");
//若是實參爲null if (parameter == null) {

//將參考上下文的value key設爲null    context.getBindings().put("value", null);
//若是實參是一個經常使用數據類型的類(Integer.class,String.class,Byte.class等等)  } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
//將參考上下文的value key設爲該實參    context.getBindings().put("value", parameter);  }

//經過OGNL解析參考上下文的值
Object value = OgnlCache.getValue(content, context.getBindings()); String srtValue = (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
//檢測合法性 checkInjection(srtValue); return srtValue;}
private void checkInjection(String value) { if (injectionFilter != null && !injectionFilter.matcher(value).matches()) { throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern()); }
}
在OgnlCache中,對原生的OGNL進行了封裝。OGNL表達式的解析過程是比較耗時的,爲了提升效率,OgnlCache中使用了expressionCashe字段(ConcurrentHashMap<String,Object>類型)對解析後的OGNL表達式進行緩存。爲了說明OGNL,咱們先來看一個例子

@Data@ToStringpublic class User { private int id; private String name;}
public class OGNLDemo { public void testOgnl1() throws OgnlException {

OgnlContext context = new OgnlContext();        context.put("cn","China");        String value = (String) context.get("cn");        System.out.println(value);        User user = new User();        user.setId(100);        user.setName("Jack");        context.put("user",user);        Object u = context.get("user");        System.out.println(u);        Object ognl = Ognl.parseExpression("#user.id");        Object value1 = Ognl.getValue(ognl,context,context.getRoot());        System.out.println(value1);        User user1 = new User();        user1.setId(200);        user1.setName("Mark");        context.setRoot(user1);        Object ognl1 = Ognl.parseExpression("id");        Object value2 = Ognl.getValue(ognl1,context,context.getRoot());        System.out.println(value2);        Object ognl2 = Ognl.parseExpression("@@floor(10.9)");        Object value3 = Ognl.getValue(ognl2, context, context.getRoot());        System.out.println(value3);    }    public static void main(String[] args) throws OgnlException {
    OGNLDemo demo = new OGNLDemo();        demo.testOgnl1();    }

}
運行結果:

China
User(id=100, name=Jack)
100
200
10.0

private static final Map<String, Object> expressionCache = new ConcurrentHashMap<String, Object>();
public static Object getValue(String expression, Object root) { try {

//建立OgnlContext對象
Map<Object, OgnlClassResolver> context = Ognl.createDefaultContext(root, new OgnlClassResolver());
//使用OGNL執行expression表達式
return Ognl.getValue(parseExpression(expression), context, root);  } catch (OgnlException e) {    throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);  }

}
private static Object parseExpression(String expression) throws OgnlException {
//查找緩存
Object node = expressionCache.get(expression); if (node == null) {

//解析表達式
node = Ognl.parseExpression(expression);
//將表達式的解析結果添加到緩存中    expressionCache.put(expression, node);  }  return node;}

StaticTextSqlNode很簡單,就是直接返回SQL語句

public class StaticTextSqlNode implements SqlNode { private final String text; public StaticTextSqlNode(String text) { this.text = text; } @Override public boolean apply(DynamicContext context) {

context.appendSql(text);    return true;  }

}
IfSqlNode是解析<if>節點,字段含義以下

//用於解析<if>節點的test表達式的值
private final ExpressionEvaluator evaluator;
//記錄<if>節點中test表達式private final String test;
//記錄了<if>節點的子節點private final SqlNode contents;
接口方法以下

@Overridepublic boolean apply(DynamicContext context) {
//檢測test屬性中記錄的表達式 if (evaluator.evaluateBoolean(test, context.getBindings())) {

//若是test表達式爲true,則執行子節點的apply()方法    contents.apply(context);    return true; //返回test表達式的結果爲true  }  return false; //返回test表達式的結果爲false}

在ExpressionEvaluator中

public boolean evaluateBoolean(String expression, Object parameterObject) {
//用OGNL解析expression表達式
Object value = OgnlCache.getValue(expression, parameterObject);
//處理Boolean類型 if (value instanceof Boolean) { return (Boolean) value; }
//處理數字類型 if (value instanceof Number) { return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0; } return value != null;}
TrimSqlNode會根據子節點的解析結果,添加或刪除響應的前綴或後綴,好比有這麼一段配置

<insert id="insertNotNullBetdetails" parameterType="com.cloud.model.game.Betdetails"> insert into betdetails <trim prefix="(" suffix=")" suffixOverrides=","> <if test="id != null">id,</if> <if test="orderId != null">orderId,</if> <if test="actorIndex != null">actorIndex,</if> <if test="ballIndex != null">ballIndex,</if> <if test="ballValue != null">ballValue,</if> <if test="betAmount != null">betAmount,</if> <if test="createTime != null">createTime,</if> <if test="rate1 != null">rate1,</if> <if test="rate2 != null">rate2,</if> <if test="rate3 != null">rate3,</if> <if test="gameType != null">gameType,</if> <if test="status != null">status,</if> <if test="betResult != null">betResult,</if> <if test="awardAmount != null">awardAmount,</if> <if test="ballName != null">ballName,</if> </trim> <trim prefix="values (" suffix=")" suffixOverrides=","> <if test="id != null">#{id},</if> <if test="orderId != null">#{orderId},</if> <if test="actorIndex != null">#{actorIndex},</if> <if test="createTime != null">#{createTime},</if> <if test="ballIndex != null">#{ballIndex},</if> <if test="ballValue != null">#{ballValue},</if> <if test="betAmount != null">#{betAmount},</if> <if test="rate1 != null">#{rate1},</if> <if test="rate2 != null">#{rate2},</if> <if test="rate3 != null">#{rate3},</if> <if test="gameType != null">#{gameType},</if> <if test="status != null">#{status},</if> <if test="betResult != null">#{betResult},</if> <if test="awardAmount != null">#{awardAmount},</if> <if test="ballName != null">#{ballName},</if> </trim></insert>
TrimSqlNode中字段含義以下

private final SqlNode contents; //該<trim>節點的子節點private final String prefix; //記錄了前綴字符串(爲<trim>節點包裹的SQL語句添加的前綴)private final String suffix; //記錄了後綴字符串(爲<trim>節點包裹的SQL語句添加的後綴)
//若是<trim>節點包裹的SQL語句是空語句,刪除指定的前綴,如whereprivate final List<String> prefixesToOverride;
//若是<trim>節點包裹的SQL語句是空語句,刪除指定的後綴,如逗號private final List<String> suffixesToOverride;
它的接口方法以下

@Overridepublic boolean apply(DynamicContext context) {
//建立FilteredDynamicContext對象,FilteredDynamicContext是TrimSqlNode的內部類,繼承於DynamicContext
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
//調用子節點的apply()方法進行解析,注意收集SQL語句的是filteredDynamicContext
boolean result = contents.apply(filteredDynamicContext);
//處理前綴和後綴 filteredDynamicContext.applyAll(); return result;}
FilteredDynamicContext的字段屬性含義以下

private DynamicContext delegate; //底層封裝的DynamicContext對象private boolean prefixApplied; //是否已經處理過前綴private boolean suffixApplied; //是否已經處理事後綴private StringBuilder sqlBuffer; //用於記錄子節點解析後的結果
FilteredDynamicContext的applyAll()方法

public void applyAll() {
//獲取子節點解析後的結果,並所有轉化爲大寫 sqlBuffer = new StringBuilder(sqlBuffer.toString().trim()); String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH); if (trimmedUppercaseSql.length() > 0) {

//處理前綴
applyPrefix(sqlBuffer, trimmedUppercaseSql);
//處理後綴    applySuffix(sqlBuffer, trimmedUppercaseSql);  }

//將解析後的結果SQL片斷添加到DynamicContext的StringBuilder中 delegate.appendSql(sqlBuffer.toString());}
private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) { if (!prefixApplied) { //若是尚未處理過前綴 prefixApplied = true; //更新爲已處理 if (prefixesToOverride != null) { //若是須要刪除的前綴列表不爲null

//遍歷該前綴列表      for (String toRemove : prefixesToOverride) {
    //若是<trim>子節點收集上來的SQL語句以該前綴開頭        if (trimmedUppercaseSql.startsWith(toRemove)) {
      //從<trim>子節點收集上來的StringBuilder中刪除該前端
      sql.delete(0, toRemove.trim().length());          break;        }
  }
}
//若是有前綴字符串(好比說"("),將前綴字符串插入StringBuilder最前端    if (prefix != null) {
  sql.insert(0, " ");      sql.insert(0, prefix);    }

}
}
private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) { if (!suffixApplied) { //若是尚未處理事後綴 suffixApplied = true; //更新爲已處理後綴 if (suffixesToOverride != null) { //若是須要處理的後綴列表不爲null

//遍歷該後綴列表      for (String toRemove : suffixesToOverride) {
    //若是從<trim>子節點收集上來的SQL語句以該後綴結尾        if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {
      //獲取該後綴的起始位置          int start = sql.length() - toRemove.trim().length();
      //獲取該後綴的終止位置
      int end = sql.length();
      //從<trim>子節點收集上來的StringBuilder中刪除該後端          sql.delete(start, end);          break;        }
  }
}
//若是有後綴字符串(好比說")"),將前綴字符串拼接上StringBuilder最後端    if (suffix != null) {
  sql.append(" ");      sql.append(suffix);    }

}
}
WhereSqlNode和SetSqlNode都繼承於TrimSqlNode,他們只是在TrimSqlNode的屬性中指定了固定的標記。

public class WhereSqlNode extends TrimSqlNode { private static List<String> prefixList = Arrays.asList("AND ","OR ","ANDn", "ORn", "ANDr", "ORr", "ANDt", "ORt"); public WhereSqlNode(Configuration configuration, SqlNode contents) { super(configuration, contents, "WHERE", prefixList, null, null); }

}
public class SetSqlNode extends TrimSqlNode { private static List<String> suffixList = Arrays.asList(","); public SetSqlNode(Configuration configuration,SqlNode contents) { super(configuration, contents, "SET", null, null, suffixList); }

}
ForEachSqlNode,在動態SQL語句中,一般須要對一個集合進行迭代,Mybatis提供了<foreach>標籤實現該功能。在使用<foreach>標籤迭代集合時,不只可使用集合的元素和索引值,還能夠在循環開始以前或結束以後添加指定的字符串,也容許在迭代過程當中添加指定的分隔符。配置樣例以下

<insert id="insertBetdetailsByBatch" parameterType="java.util.List"> insert into betdetails(id,orderId,actorIndex,createTime,ballIndex,ballValue,betAmount,rate1,rate2,rate3,gameType,status,betResult,awardAmount,ballName) values <foreach collection="list" item="item" index="index" separator=","> (#{item.id},#{item.orderId},#{item.actorIndex},#{item.createTime},#{item.ballIndex},#{item.ballValue},#{item.betAmount},#{item.rate1},#{item.rate2},#{item.rate3},#{item.gameType},#{item.status},#{item.betResult},#{item.awardAmount},#{item.ballName}) </foreach></insert>
ForEachSqlNode中各個字段含義以下:

private final ExpressionEvaluator evaluator;private final String collectionExpression;private final SqlNode contents;private final String open;private final String close;private final String separator;private final String item;private final String index;private final Configuration configuration;​

相關文章
相關標籤/搜索