前段時間ibatis3.0發佈出來了,迫不及待,將其源碼下載拜讀。相對ibatis 2.x來說,3.0已是完全改變。具體我就不在這細說,論壇中有一個帖子介紹了ibatis 3.0的新特徵及使用。
由於其他模塊的源碼我還未細讀,在這篇中,先來討論Dynamic Sql在ibatis 3.0中的實現並比較2.x對應模塊的設計。
寫在前頭的話:
其實如從設計模式應用角度去看待ibatis 3.0中Dynamic Sql的實現,這篇跟我的上篇(HtmlParser設計解析(1)-解析器模式)相同,都是使用Interpreter模式。
這篇權當Interpreter模式的另一個demo,認我們體會這些開源項目中設計模式的使用。學習都是從模仿開始的,讓 我們吸收高人們的經驗,應用於我們實踐項目需求中。
從總結中提高:
一、對比2.x中與3.0的Sqlmap中dynamic sql配置
2.x:
- <select id="dynamicGetAccountList" parameterClass="Account" resultClass="Account">
- select ACC_ID as id,
- ACC_FIRST_NAME as firstName,
- ACC_LAST_NAME as lastName,
- ACC_EMAIL as emailAddress from ACCOUNT
- <dynamic prepend="WHERE">
- <isNotNull prepend="AND" property="emailAddress">
- ACC_EMAIL = #emailAddress#
- </isNotNull>
- <isNotNull property="idList" prepend=" or ACC_ID in ">
- <iterate property="idList" conjunction="," open="(" close=")" >
- #id#
- </iterate>
- </isNotNull>
- </dynamic>
- </select>
<select id="dynamicGetAccountList" parameterClass="Account" resultClass="Account"> select ACC_ID as id, ACC_FIRST_NAME as firstName, ACC_LAST_NAME as lastName, ACC_EMAIL as emailAddress from ACCOUNT <dynamic prepend="WHERE"> <isNotNull prepend="AND" property="emailAddress"> ACC_EMAIL = #emailAddress# </isNotNull> <isNotNull property="idList" prepend=" or ACC_ID in "> <iterate property="idList" conjunction="," open="(" close=")" > #id# </iterate> </isNotNull> </dynamic> </select>
3.0:
- <select id="dynamicGetAccountList" parameterType="Account" resultType="Account">
- select ACC_ID as id,
- ACC_FIRST_NAME as firstName,
- ACC_LAST_NAME as lastName,
- ACC_EMAIL as emailAddress from ACCOUNT
- <where>
- <if test="emailAddress != null">ACC_EMAIL = #{emailAddress}</if>
- <if test="idList != null">
- or ACC_ID IN
- <foreach item="id" index="index" open="(" close=")" separator="," collection="idList">
- #{idList[${index}]}
- </foreach>
- </if>
- </where>
- </select>
<select id="dynamicGetAccountList" parameterType="Account" resultType="Account"> select ACC_ID as id, ACC_FIRST_NAME as firstName, ACC_LAST_NAME as lastName, ACC_EMAIL as emailAddress from ACCOUNT <where> <if test="emailAddress != null">ACC_EMAIL = #{emailAddress}</if> <if test="idList != null"> or ACC_ID IN <foreach item="id" index="index" open="(" close=")" separator="," collection="idList"> #{idList[${index}]} </foreach> </if> </where> </select>
從上面這個簡單的比較中,第一感覺3.0了中其dynamic sql更加簡潔明瞭。
其二,test="emailAddress != null" 添加了OGNL的解釋支持,可以動態支持更多的判斷,這將不限於原2.x中提供
的判斷邏輯,更不需要爲每個判斷條件加個標籤進行配置。
例如:<if test="id > 10 && id < 20"> ACC_EMAIL = #{emailAddress}</if>
<if test="Account.emailAddress != null "> ACC_EMAIL = #{emailAddress}</if> ……
二、2.x Dynamic Sql的設計
2.1、2.x中dynamic流程。
這裏帖出,我先前在分析ibatis 2.3時畫的一個對dynamic sql的整體使用的時序圖,可能會顯得亂而複雜。
2.2、主要類設計
在這,我們只關注這幾個類:XMLSqlSource、DynamicSql、SqlTagHandler (具體類結構圖見後)
XMLSqlSource:相當於一個工廠類,其核心方法parseDynamicTags(),用於解析sql Tag,並判斷是否是動態SQL標籤。如果true,返回一個DynamicSql對象並創建多個SqlChildt對象添加至動態SQL列表中(addChild());false,返回RawSql對象(簡單的SQL語句) 。
DynamicSql:核心的動態SQL類。其動態條件判斷邏輯,參數映射等都發生在這個類中。
SqlTagHandle:動態條件判斷接口,其每個動態SQL標籤對應其一個子類。
接下來,我們具體看下在DynamicSql類中核心方法。
DynamicSql:
- private void processBodyChildren(StatementScope statementScope, SqlTagContext ctx, Object parameterObject, Iterator localChildren, PrintWriter out) {
- while (localChildren.hasNext()) { //XMLSqlSource 生成的動態SQL列表
- SqlChild child = (SqlChild) localChildren.next();
- if (child instanceof SqlText) {
- ... ... //組裝SQL語句及映射SQL參數
- } else if (child instanceof SqlTag) {
- SqlTag tag = (SqlTag) child;
- SqlTagHandler handler = tag.getHandler(); //得到動態SQL標籤處理器
- int response = SqlTagHandler.INCLUDE_BODY;
- do {
- response = handler.doStartFragment(ctx, tag, parameterObject); //處理開始片段
- if (response != SqlTagHandler.SKIP_BODY) { //是否跳過,意思該判斷的條件爲false
- processBodyChildren(statementScope, ctx, parameterObject, tag.getChildren(), pw); //遞歸處理
- StringBuffer body = sw.getBuffer();
- response = handler.doEndFragment(ctx, tag, parameterObject, body); //處理結束片段
- handler.doPrepend(ctx, tag, parameterObject, body); //組裝SQL
- }
- } while (response == SqlTagHandler.REPEAT_BODY);
- ... ... }
- }
private void processBodyChildren(StatementScope statementScope, SqlTagContext ctx, Object parameterObject, Iterator localChildren, PrintWriter out) {
while (localChildren.hasNext()) { //XMLSqlSource 生成的動態SQL列表
SqlChild child = (SqlChild) localChildren.next();
if (child instanceof SqlText) {
... ... //組裝SQL語句及映射SQL參數
} else if (child instanceof SqlTag) {
SqlTag tag = (SqlTag) child;
SqlTagHandler handler = tag.getHandler(); //得到動態SQL標籤處理器
int response = SqlTagHandler.INCLUDE_BODY;
do {
response = handler.doStartFragment(ctx, tag, parameterObject); //處理開始片段
if (response != SqlTagHandler.SKIP_BODY) { //是否跳過,意思該判斷的條件爲false
processBodyChildren(statementScope, ctx, parameterObject, tag.getChildren(), pw); //遞歸處理
StringBuffer body = sw.getBuffer();
response = handler.doEndFragment(ctx, tag, parameterObject, body); //處理結束片段
handler.doPrepend(ctx, tag, parameterObject, body); //組裝SQL
}
} while (response == SqlTagHandler.REPEAT_BODY);
... ... }
}
2.3、SqlTagHandle設計
首先看下SqlTagHandle處理類的結果圖:
- public abstract class ConditionalTagHandler extends BaseTagHandler {
- ... ...
- public abstract boolean isCondition(SqlTagContext ctx, SqlTag tag, Object parameterObject);
- public int doStartFragment(SqlTagContext ctx, SqlTag tag, Object parameterObject) {
- ctx.pushRemoveFirstPrependMarker(tag);
- if (isCondition(ctx, tag, parameterObject)) {
- return SqlTagHandler.INCLUDE_BODY;
- } else {
- return SqlTagHandler.SKIP_BODY;
- }
- }
- ... ...
- }
public abstract class ConditionalTagHandler extends BaseTagHandler {
... ...
public abstract boolean isCondition(SqlTagContext ctx, SqlTag tag, Object parameterObject);
public int doStartFragment(SqlTagContext ctx, SqlTag tag, Object parameterObject) {
ctx.pushRemoveFirstPrependMarker(tag);
if (isCondition(ctx, tag, parameterObject)) {
return SqlTagHandler.INCLUDE_BODY;
} else {
return SqlTagHandler.SKIP_BODY;
}
}
... ...
}
IsNullTagHandler:
- public class IsNullTagHandler extends ConditionalTagHandler {
- private static final Probe PROBE = ProbeFactory.getProbe();
- public boolean isCondition(SqlTagContext ctx, SqlTag tag, Object parameterObject) {
- if (parameterObject == null) {
- return true;
- } else {
- String prop = getResolvedProperty(ctx, tag);
- Object value;
- if (prop != null) {
- value = PROBE.getObject(parameterObject, prop);
- } else {
- value = parameterObject;
- }
- return value == null;
- }
- }
- }
public class IsNullTagHandler extends ConditionalTagHandler {
private static final Probe PROBE = ProbeFactory.getProbe();
public boolean isCondition(SqlTagContext ctx, SqlTag tag, Object parameterObject) {
if (parameterObject == null) {
return true;
} else {
String prop = getResolvedProperty(ctx, tag);
Object value;
if (prop != null) {
value = PROBE.getObject(parameterObject, prop);
} else {
value = parameterObject;
}
return value == null;
}
}
}
至於其他的相關類,不在這列出了,有興趣的可以找其源碼瞭解下。
2.4、總結ibatis 2.X Dynamic Sql 的設計
從上面的分析中,可以體會出作者的dynamic sql這模塊的設計思路。從裝載sqlmap.xml中各sql配置(時序圖中的1步),通過工廠創建DynamicSql和RawSql(時序圖中的3步),然後分發之不同的處理器。
在DynamicSql中則調用SqlTagHandle判斷其條件(時序圖中的10步)。而SqlTagHandle的設計使用策略者模式,讓其不同的子類來處理這個判斷邏輯。
通過一系列的加工,最終組裝一個Sql對象,將值set至MappedStatement(時序圖中的14步)中,然後MappedStatement對象執行executeQueryWithCallback查詢數據(時序圖中的17步),這兒會調用先前組裝的Sql對象(時序圖中的19步)。至於這其中的細節已不在這篇的研究這內。
三、3.0 Dynamic Sql的設計
至於3.0其基本流程跟2.x是一樣的,從裝載 -> 參數映射 -> 執行SQL -> 返回結果。我們直接切入主題,分析是核心部分。先從一個簡單的Dynamic Sql的測試用例開始。
3.1、 測試用例
dynamic sql test:
- @Test
- public void shouldTrimWHEREInsteadOfORForSecondCondition() throws Exception {
- /* SELECT * FROM BLOG
- <where>
- <if test="id != false"> and ID = #{id} </if>
- <if test="name != false"> or NAME = #{name} </if>
- </where>
- */
- final String expected = "SELECT * FROM BLOG WHERE NAME = ?";
- DynamicSqlSource source = createDynamicSqlSource(
- new TextSqlNode("SELECT * FROM BLOG"),
- new WhereSqlNode(mixedContents(
- new IfSqlNode(
- mixedContents(new TextSqlNode(" and ID = ? ")),"false"), new IfSqlNode(mixedContents(new TextSqlNode(" or NAME = ? ")), "true"))));
- BoundSql boundSql = source.getBoundSql(null);
- assertEquals(expected, boundSql.getSql());
- }
- private DynamicSqlSource createDynamicSqlSource(SqlNode... contents)
- throws IOException, SQLException {
- createBlogDataSource();
- final String resource = ".../MapperConfig.xml";
- final Reader reader = Resources.getResourceAsReader(resource);
- SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder()
- .build(reader);
- Configuration configuration = sqlMapper.getConfiguration();
- MixedSqlNode sqlNode = mixedContents(contents);
- return new DynamicSqlSource(configuration, sqlNode);
- }
- private MixedSqlNode mixedContents(SqlNode... contents) {
- return new MixedSqlNode(Arrays.asList(contents));
- }
@Test
public void shouldTrimWHEREInsteadOfORForSecondCondition() throws Exception {
/* SELECT * FROM BLOG
<where>
<if test="id != false"> and ID = #{id} </if>
<if test="name != false"> or NAME = #{name} </if>
</where>
*/
final String expected = "SELECT * FROM BLOG WHERE NAME = ?";
DynamicSqlSource source = createDynamicSqlSource(
new TextSqlNode("SELECT * FROM BLOG"),
new WhereSqlNode(mixedContents(
new IfSqlNode(
mixedContents(new TextSqlNode(" and ID = ? ")),"false"), new IfSqlNode(mixedContents(new TextSqlNode(" or NAME = ? ")), "true"))));
BoundSql boundSql = source.getBoundSql(null);
assertEquals(expected, boundSql.getSql());
}
private DynamicSqlSource createDynamicSqlSource(SqlNode... contents)
throws IOException, SQLException {
createBlogDataSource();
final String resource = ".../MapperConfig.xml";
final Reader reader = Resources.getResourceAsReader(resource);
SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder()
.build(reader);
Configuration configuration = sqlMapper.getConfiguration();
MixedSqlNode sqlNode = mixedContents(contents);
return new DynamicSqlSource(configuration, sqlNode);
}
private MixedSqlNode mixedContents(SqlNode... contents) {
return new MixedSqlNode(Arrays.asList(contents));
}
有經驗的人,我想一眼就能看出其3.0中的設計思想,從Test中可以看出,或者我上一篇介紹的HtmlParser NodeFilter。
YES,在ibatis 3.0 dynamic sql設計正是應用瞭解釋器模式,替換了原在這種需求下相對顯得笨拙的策略者模式。
下面具體看下類結構圖。
3.2、類結構圖
SqlNode Class Diagram:
SqlSource Class Diagram:
3.3、配置文件的解析
在這,我就順便提下ibatis解析組件對dynamic sql的解析方式,以代碼見分曉吧。
XMLStatementBuilder:
- public void parseStatementNode(XNode context) {
- ... ...
- List<SqlNode> contents = parseDynamicTags(context);
- MixedSqlNode rootSqlNode = new MixedSqlNode(contents);//再次包裝dynamic sql處理鏈
- SqlSource sqlSource = new DynamicSqlSource(configuration, rootSqlNode); //默認初始化DynamicSqlSource
- ... ...
- builderAssistant.addMappedStatement(id, sqlSource, statementType,
- sqlCommandType, fetchSize, timeout, parameterMap,
- parameterTypeClass, resultMap, resultTypeClass,
- resultSetTypeEnum, flushCache, useCache, keyGenerator,
- keyProperty); //將解析的所有屬性構建成相應的對象存入全局的申明對象(MappedStatement)中,後面只傳遞該對象。
- }
- private List<SqlNode> parseDynamicTags(XNode node) {
- List<SqlNode> contents = new ArrayList<SqlNode>();
- NodeList children = node.getNode().getChildNodes();
- for (int i = 0; i < children.getLength(); i++) {
- XNode child = node.newXNode(children.item(i));
- String nodeName = child.getNode().getNodeName();
- if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE
- || child.getNode().getNodeType() == Node.TEXT_NODE) {
- String data = child.getStringBody("");
- contents.add(new TextSqlNode(data));
- } else {
- NodeHandler handler = nodeHandlers.get(nodeName);
- if (handler == null) {
- throw new BuilderException("Unknown element <" + nodeName "> in SQL statement.");
- }
- handler.handleNode(child, contents);
- }
- }
- return contents;
- }
- private Map<String, NodeHandler> nodeHandlers = new HashMap<String, NodeHandler>() {
- {
- put("where", new WhereHandler());
- put("set", new SetHandler());
- put("foreach", new ForEachHandler());
- put("if", new IfHandler());
- ... ...
- }
- };
- private interface NodeHandler {
- void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
- }
- private class WhereHandler implements NodeHandler {
- public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
- List<SqlNode> contents = parseDynamicTags(nodeToHandle);// 遍歷
- MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);//對應測試用例中的mixedContents方法
- WhereSqlNode where = new WhereSqlNode(mixedSqlNode);
- targetContents.add(where);
- }
- }
- private class IfHandler implements NodeHandler {
- public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
- List<SqlNode> contents = parseDynamicTags(nodeToHandle);//遍歷
- MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
- String test = nodeToHandle.getStringAttribute("test");
- IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);//初始化對應的處理器
- targetContents.add(ifSqlNode);//
- }
- } // 其他的Handle詳見ibatis源碼~
public void parseStatementNode(XNode context) {
... ...
List<SqlNode> contents = parseDynamicTags(context);
MixedSqlNode rootSqlNode = new MixedSqlNode(contents);//再次包裝dynamic sql處理鏈
SqlSource sqlSource = new DynamicSqlSource(configuration, rootSqlNode); //默認初始化DynamicSqlSource
... ...
builderAssistant.addMappedStatement(id, sqlSource, statementType,
sqlCommandType, fetchSize, timeout, parameterMap,
parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, keyGenerator,
keyProperty); //將解析的所有屬性構建成相應的對象存入全局的申明對象(MappedStatement)中,後面只傳遞該對象。
}
private List<SqlNode> parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<SqlNode>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
String nodeName = child.getNode().getNodeName();
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE
|| child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
contents.add(new TextSqlNode(data));
} else {
NodeHandler handler = nodeHandlers.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName "> in SQL statement.");
}
handler.handleNode(child, contents);
}
}
return contents;
}
private Map<String, NodeHandler> nodeHandlers = new HashMap<String, NodeHandler>() {
{
put("where", new WhereHandler());
put("set", new SetHandler());
put("foreach", new ForEachHandler());
put("if", new IfHandler());
... ...
}
};
private interface NodeHandler {
void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
}
private class WhereHandler implements NodeHandler {
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> contents = parseDynamicTags(nodeToHandle);// 遍歷
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);//對應測試用例中的mixedContents方法
WhereSqlNode where = new WhereSqlNode(mixedSqlNode);
targetContents.add(where);
}
}
private class IfHandler implements NodeHandler {
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> contents = parseDynamicTags(nodeToHandle);//遍歷
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
String test = nodeToHandle.getStringAttribute("test");
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);//初始化對應的處理器
targetContents.add(ifSqlNode);//
}
} // 其他的Handle詳見ibatis源碼~
上面是其解析代碼的一部分,我想從這幾行代碼中,可以看出作者的思想了(遍歷XML各節點,以節點名查找相應對應的處理器,分發之該處理器執行"業務分析" — 策略者模式,這樣在XML中定義了多少標籤,這裏就需要多少個類與之對應,但如果策略類太多,這種方式就顯得笨拙了)。
以下就是其核心類的一部分源碼,先看再說。
3.4、DynamicSqlSource(核心類)
- public class DynamicSqlSource implements SqlSource {
- public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
- this.configuration = configuration;
- this.rootSqlNode = rootSqlNode;
- }
- public BoundSql getBoundSql(Object parameterObject) {
- DynamicContext context = new DynamicContext(parameterObject);//組裝後的結果存儲類
- rootSqlNode.apply(context);//調用SqlNode解釋sql,並組裝成完整的sql(SqlNode的客戶端調用就在這)
- SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
- Class parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
- SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType);
- BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
- for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
- boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
- }
- return boundSql;
- }
- }
public class DynamicSqlSource implements SqlSource {
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(parameterObject);//組裝後的結果存儲類
rootSqlNode.apply(context);//調用SqlNode解釋sql,並組裝成完整的sql(SqlNode的客戶端調用就在這)
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType);
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
}
3.5、SqlNode
- public interface SqlNode {
- public boolean apply(DynamicContext context);
- }
public interface SqlNode {
public boolean apply(DynamicContext context);
}
MixedSqlNode.class
- public class MixedSqlNode implements SqlNode {
- ... ....
- public boolean apply(DynamicContext context) {
- //遍歷組裝的解析內容
- for (SqlNode sqlNode : contents) {
- // 轉發至相關解釋器處理
- sqlNode.apply(context);
- }
- return true;
- }
- }
public class MixedSqlNode implements SqlNode {
... ....
public boolean apply(DynamicContext context) {
//遍歷組裝的解析內容
for (SqlNode sqlNode : contents) {
// 轉發至相關解釋器處理
sqlNode.apply(context);
}
return true;
}
}
IfSqlNode.class
- public class IfSqlNode implements SqlNode {
- ... ...
- public IfSqlNode(SqlNode contents, String test) {
- this.test = test;
- this.contents = contents;
- this.evaluator = new ExpressionEvaluator();
- }
- public boolean apply(DynamicContext context) {
- if (evaluator.evaluateBoolean(test, context.getBindings())) {//OGNL Expressions
- contents.apply(context);
- return true; //
- }
- return false;
- }
- }
public class IfSqlNode implements SqlNode {
... ...
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {//OGNL Expressions
contents.apply(context);
return true; //
}
return false;
}
}
TextSqlNode.class
- public class TextSqlNode implements SqlNode {
- private String text;
- public TextSqlNode(String text) {
- this.text = text;
- }
-