ibatis 3.0 Dynamic Sql 设计解析(并与二.x的差异)

ibatis 3.0 Dynamic Sql 设计解析(并与2.x的差异)

      前段时间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>

 

   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>

      从上面这个简单的比较中,第一感觉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的整体使用的时序图,可能会显得乱而复杂。

ibatis 3.0 Dynamic Sql 设计解析(并与二.x的差异)

   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);
		... ...    }
}

 

    2.3、SqlTagHandle设计

    首先看下SqlTagHandle处理类的结果图:


ibatis 3.0 Dynamic Sql 设计解析(并与二.x的差异)

 

    ConditionalTagHandler:

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;
		}
	}
}

   至于其他的相关类,不在这列出了,有兴趣的可以找其源码了解下。

 

   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));
	}
  

  有经验的人,我想一眼就能看出其3.0中的设计思想,从Test中可以看出,或者我上一篇介绍的HtmlParser NodeFilter。

    YES,在ibatis 3.0 dynamic sql设计正是应用了解释器模式,替换了原在这种需求下相对显得笨拙的策略者模式。

 

   下面具体看下类结构图。

 

   3.2、类结构图

   SqlNode Class Diagram:

ibatis 3.0 Dynamic Sql 设计解析(并与二.x的差异)

   SqlSource Class Diagram:

 

ibatis 3.0 Dynamic Sql 设计解析(并与二.x的差异)

   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源码~

    上面是其解析代码的一部分,我想从这几行代码中,可以看出作者的思想了(遍历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;
	}
}

 

   3.5、SqlNode

 

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;
	}
}

   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;
	}
}

   TextSqlNode.class

public class TextSqlNode implements SqlNode {
	private String text;

	public TextSqlNode(String text) {
		this.text = text;
	}

	public boolean apply(DynamicContext context) {
		GenericTokenParser parser = new GenericTokenParser("${", "}", new BindingTokenParser(context));
		context.appendSql(parser.parse(text));//组装sql
		return true;
	}

	private static class BindingTokenParser implements GenericTokenParser.TokenHandler {
		private DynamicContext context;
		public BindingTokenParser(DynamicContext context) {
			this.context = context;
		}
		public String handleToken(String content) {
			try {
				Object value = Ognl.getValue(content, context.getBindings());
				return String.valueOf(value);
			} catch (OgnlException e) {
				throw new BuilderException("Error evaluating expression '" + content + "'. Cause: " + e, e);
			}
		}
	}
}

   通过这些代码,再结合上面的测试用例就能够明白个七七八八,简单说就是解释器模式(Interpreter)的一个demo。ibatis 3.0 Dynamic Sql 设计解析(并与二.x的差异)

   其中SqlNode接口中方法非常简单,就一个apply(),接受一个DynamicContext的参数。

TextSqlNode 在这扮演终结表达式角色,在这个解释类中再没有contents.apply(context);的方法出现,到此结束,退出遍历,添加一个SQL条件,因在此之前其他解释器已判定所有的指定条件是否符合。

   其他的解释类都是非终结表达式角色,为TextSqlNode做保护判断,是否允许进行这个"地带"。

 

   3.6、总结

   通过上面的分析,我们不难看出,其结构非常简单、清晰。

   需求是:用户通过指定的标签按指定的规则组装业务逻辑。这里必须是指定,因为从上代码中看,其这模块不适用用户自定义扩展。

   解决方案是:XMLStatementBuilder读取配置(每个标签对就一个配置解析类 - 采用策略者模式),生成一个SqlSource的对象。再次,Executor执行时需要得到一个BoundSql对象,这时调用SqlNode对象将符合用户条件的组装成完整SQL(每个标签也同时对应一个解释器或者说条件判断器 - 解释器模式 ),最后从Executor从BoundSql读取需要的值,执行客户端的操作。OVER。其中灵活之处在SqlNode客户端,可由用户自行构造对象链。

 

   我想这时对ibatis 3.0 的dynamic sql设计应有所了解。当然,在这只是粗略的体现其作者的思想,详细、完整还需要看完整源码。

   相对于2.x版本来说,其大致思想及流程是不变的,只是采取不同的方式去处理。相对2.x,3.0 dynamic sql这模块显得更为轻巧,在解释配置时,就将层次分析清楚,然后运用解释器模式有效的配合了,对配置的解释及生成完整的SQL。就像剥竹笋一样,一层一层,清晰可见。

 

   以上是本人粗略里分析的ibatis的dynamic sql这模块并与之2.x的进行简单的比较,简洁的体现ibatis作者在改版时的设计思想的变化,有对照及总结才有提高。我这权当抛砖引玉,如其中有什么错误,请大家指出,并请大家多多指教。ibatis 3.0 Dynamic Sql 设计解析(并与二.x的差异)

 

1 楼 grandboy 2009-08-25  
还没有来得及读源码呢,盼着正式版本快点出来,好在项目里应用。准备把 Spring3和IBatis3一块应用到新的项目里。