动态SQL的实现原理

4 篇文章 0 订阅

转自:http://learnworld.iteye.com/blog/1473491

 动态SQL是ibatis框架的一个重要特性,本文将深入分析ibatis框架中动态SQL的实现原理。动态sql的使用参见官方文档:Dynamic SQL。本文使用的ibatis版本为2.3.4。 

问题 
在介绍动态SQL的实现原理之前,让我们先来思考几个问题。 
1. 为什么需要动态SQL? 
通过动态sql可以提高运行程序的灵活性,使我们可以方便地实现多条件下的数据库操作。 

2. 如何描述动态SQL? 
对于变化的数据,通常是封装在配置文件中。对于动态SQL中的不同条件,可以采用不同标签表示其含义。通过各种条件标签的组合,描述需要表达的语义。 

3. 如何实现动态SQL? 
首先采用条件标签描述需要表达的语义,维护在配置文件中;初始化过程中,解析配置文件中的标签,生成sql配置对应的抽象语法树;请求处理过程中,根据参数对象解释该抽象语法树,生成当前请求的动态SQL语句。 

核心类图  

1. SqlResource 
该接口含义是作为sql对象的来源,通过该接口可以获取sql对象。其唯一的实现类是XmlSqlResource,表示通过xml文件生成sql对象。 

2. Sql 
该接口可以生成sql语句和获取sql相关的上下文环境(如ParameterMap、ResultMap等),有三个实现类: RawSql表示为原生的sql语句,在初始化即可确定sql语句;SimpleDynamicSql表示简单的动态sql,即sql语句中参数通过$property$方式指定,参数在sql生成过程中会被替换,不作为sql执行参数;DynamicSql表示动态sql,即sql描述文件中包含isNotNull、isGreaterThan等条件标签。 

3. SqlChild 
该接口表示sql抽象语法树的一个节点,包含sql语句的片段信息。该接口有两个实现类: SqlTag表示动态sql片段,即配置文件中的一个动态标签,内含动态sql属性值(如prepend、property值等);SqlText表示静态sql片段,即为原生的sql语句。每条动态sql通过SqlTag和SqlText构成相应的抽象语法树。 

4. SqlTagHandler 
该接口表示SqlTag(即不同的动态标签)对应的处理方式。比如实现类IsEmptyTagHandler用于处理<isEmpty>标签,IsEqualTagHandler用于处理<isEqual>标签等。 

5. SqlTagContext 
用于解释sql抽象语法树时使用的上下文环境。通过解释语法树每个节点,将生成的sql存入SqlTagContext。最终通过SqlTagContext获取完整的sql语句。 

从整体上看,动态sql的生成过程包含两步: 初始化过程用于解析sql配置,生成由SqlChild节点组成的抽象语法树;请求处理过程通过运行期的参数对象解释抽象语法树,生成实际的sql语句。下面通过源码分析整个过程。 

初始化过程 
初始化和配置文件解析一文中我们提到过,配置文件解析的核心逻辑是: sql配置文件 ->s ql映射文件 -> SqlStatement语句,本文将从SqlStatement的解析过程开始进行分析。 

1. SqlStatementParser.parseGeneralStatement()用于解析映射文件中的Sql配置,生成MappedStatement对象。部分源码如下: 

Java代码   收藏代码
  1. public void parseGeneralStatement(Node node, MappedStatement statement) {  
  2.         ...  
  3.         MappedStatementConfig statementConf = state.getConfig().newMappedStatementConfig(id, statement,  
  4.         new XMLSqlSource(state, node), parameterMapName, parameterClass, resultMapName, additionalResultMapNames,  
  5.         resultClass, additionalResultClasses, resultSetType, fetchSizeInt, allowRemappingBool, timeoutInt, cacheModelName,  
  6.         xmlResultName);  
  7.         ...  
  8. }  

上面这段代码核心逻辑是用于生成MappedStatement的相关配置信息,其中包含Sql配置的解析过程。 

2.  MappedStatementConfig.newMappedStatementConfig()调用了构造方法,部分源码如下: 
Java代码   收藏代码
  1. MappedStatementConfig(...){  
  2.     ...  
  3.     //sql处理过程  
  4.     Sql sql = processor.getSql(); //processor是XmlSqlSource对象  
  5.     setSqlForStatement(statement, sql);  
  6.     ...  
  7. }  

上面这两行就是初始化过程中sql处理的核心逻辑,首先通过配置文件生成Sql对象,接着将生成的Sql对象放入statement对象中。下面重点看一下processor.getSql()的实现过程。 

3.  XmlSqlSource.getSql()的完整源码如下: 
Java代码   收藏代码
  1. public Sql getSql() {  
  2.   boolean isDynamic = false;  
  3.   StringBuffer sqlBuffer = new StringBuffer();  
  4.   DynamicSql dynamic = new DynamicSql(state.getConfig().getClient().getDelegate());  
  5.   //通过配置文件生成DynamicSql  
  6.   isDynamic = parseDynamicTags(parentNode, dynamic, sqlBuffer, isDynamic, false);  
  7.   String sqlStatement = sqlBuffer.toString();  
  8.   //根据是否动态sql返回不同结果对象  
  9.   if (isDynamic) {  
  10.     return dynamic;  
  11.   } else {  
  12.     return new RawSql(sqlStatement);  
  13.   }  
  14. }  

这段代码的核心逻辑是生成DynamicSql对象,并根据是否动态sql返回不同结果对象。该方法的核心是parseDynamicTags()的实现。 

4.  sql解析过程中最关键的一步,通过配置文件生成Sql对象,parseDynamicTags()的完整实现如下: 
Java代码   收藏代码
  1. private boolean parseDynamicTags(Node node, DynamicParent dynamic, StringBuffer sqlBuffer, boolean isDynamic, boolean postParseRequired) {  
  2.   NodeList children = node.getChildNodes();  
  3.   //依次处理每个子节点  
  4.   for (int i = 0; i < children.getLength(); i++) {  
  5.     Node child = children.item(i);  
  6.     String nodeName = child.getNodeName();  
  7.     // 1. 处理sql文本  
  8.     if (child.getNodeType() == Node.CDATA_SECTION_NODE  
  9.         || child.getNodeType() == Node.TEXT_NODE) {  
  10.   
  11.       String data = ((CharacterData) child).getData();  
  12.       data = NodeletUtils.parsePropertyTokens(data, state.getGlobalProps());  
  13.   
  14.       //通过sql文本生成SqlText对象  
  15.       SqlText sqlText;  
  16.   
  17.       if (postParseRequired) {  
  18.         sqlText = new SqlText();  
  19.         sqlText.setPostParseRequired(postParseRequired);  
  20.         sqlText.setText(data);  
  21.       } else {  
  22.         //核心逻辑,解析sql文本  
  23.         sqlText = PARAM_PARSER.parseInlineParameterMap(state.getConfig().getClient().getDelegate().getTypeHandlerFactory(), data, null);  
  24.         sqlText.setPostParseRequired(postParseRequired);  
  25.       }  
  26.   
  27.       dynamic.addChild(sqlText); //当前节点加入父节点  
  28.   
  29.       sqlBuffer.append(data);  
  30.     // 2. 处理include标签  
  31.     } else if ("include".equals(nodeName)) {  
  32.       Properties attributes = NodeletUtils.parseAttributes(child, state.getGlobalProps());  
  33.       String refid = (String) attributes.get("refid");  
  34.       Node includeNode = (Node) state.getSqlIncludes().get(refid);  
  35.       if (includeNode == null) {  
  36.         String nsrefid = state.applyNamespace(refid);  
  37.         includeNode = (Node) state.getSqlIncludes().get(nsrefid);  
  38.         if (includeNode == null) {  
  39.           throw new RuntimeException("Could not find SQL statement to include with refid '" + refid + "'");  
  40.         }  
  41.       }  
  42.       //递归处理include节点  
  43.       isDynamic = parseDynamicTags(includeNode, dynamic, sqlBuffer, isDynamic, false);  
  44.     // 3. 处理动态标签  
  45.     } else {  
  46.       //获取动态标签对应的SqlTagHandler   
  47.       SqlTagHandler handler = SqlTagHandlerFactory.getSqlTagHandler(nodeName);  
  48.       if (handler != null) {  
  49.         isDynamic = true;  
  50.   
  51.         // 通过动态节点配置信息生成SqlTag对象  
  52.         SqlTag tag = new SqlTag();  
  53.         tag.setName(nodeName);  
  54.         tag.setHandler(handler);  
  55.   
  56.         Properties attributes = NodeletUtils.parseAttributes(child, state.getGlobalProps());  
  57.   
  58.         tag.setPrependAttr(attributes.getProperty("prepend"));  
  59.         tag.setPropertyAttr(attributes.getProperty("property"));  
  60.         tag.setRemoveFirstPrepend(attributes.getProperty("removeFirstPrepend"));  
  61.   
  62.         tag.setOpenAttr(attributes.getProperty("open"));  
  63.         tag.setCloseAttr(attributes.getProperty("close"));  
  64.   
  65.         tag.setComparePropertyAttr(attributes.getProperty("compareProperty"));  
  66.         tag.setCompareValueAttr(attributes.getProperty("compareValue"));  
  67.         tag.setConjunctionAttr(attributes.getProperty("conjunction"));  
  68.   
  69.         ...  
  70.         dynamic.addChild(tag);  //当前节点加入父节点  
  71.   
  72.         //递归处理子节点  
  73.         if (child.hasChildNodes()) {  
  74.           isDynamic = parseDynamicTags(child, tag, sqlBuffer, isDynamic, tag.isPostParseRequired());  
  75.         }  
  76.       }  
  77.     }  
  78.   }  
  79.   
  80.   return isDynamic;  
  81. }  

这段代码比较长,核心逻辑如下: 
依次处理当前节点的每个子节点,判断当前子节点类型,根据不同类型采用不同处理方式: 
4.1 处理sql文本节点  
对于sql文本节点,生成SqlText对象。这个过程需要解析sql语句,将其中的参数#param#替换为?,生成sql语句和ParameterMapping对象。sql文本解析的核心逻辑是InlineParameterMapParser.parseInlineParameterMap(),这里不再详述。 

4.2 处理include标签  
SqlMapParaser在处理<sql>标签时,会将处理结果放入XmlParserState.sqlIncludes这个map对象中。 这里主要通过当前include id在sqlIncludes中获取对应的包含节点信息,再递归处理包含节点。 

4.3 处理动态标签  
首先获取动态标签对应的SqlTagHandler,接着通过动态标签配置生成SqlTag对象,最后递归处理当前节点的每个子节点。 

小结  
动态sql初始化的 核心目标是通过递归方式构建DynamicSql对象 ,它是一个抽象语法树,由SqlText和SqlTag节点构成。而请求处理过程正是通过参数对象解释该抽象语法树,生成sql语句。 

请求处理过程  
整体设计和核心流程 一文中的SQL执行过程中,最后通过调用MappedStatement.executeQueryWithCallback()执行sql语句,而正是在这里生成当前请求的sql语句。 

1.  MappedStatement.executeQueryWithCallback()的部分源码如下: 
Java代码   收藏代码
  1. public Object executeQueryWithCallback(StatementScope statementScope, Transaction trans, Object parameterObject, Object resultObject)  
  2.     throws SQLException {  
  3.   ...  
  4.   Sql sql = getSql();  
  5.   ...  
  6.   String sqlString = sql.getSql(statementScope, parameterObject);  
  7.   ...  

这里首先获取当前MappedStatement对应的Sql对象,而Sql对象正是在上面初始化过程创建完成;接着调用Sql.getSql()方法,生成当前请求的sql语句。 

2.  Sql对象的类型可能是RawSql、StaticSql、SimpleDynamicSql或DynamicSql,这里我们重点关注DynamicSql,DynamicSql.getSql()的部分源码如下:  
Java代码   收藏代码
  1. public String getSql(StatementScope statementScope, Object parameterObject) {  
  2.     String sql = statementScope.getDynamicSql();  
  3.     if (sql == null) {  
  4.       // 生成sql语句  
  5.       process(statementScope, parameterObject);  
  6.       sql = statementScope.getDynamicSql();  
  7.     }  
  8.     return sql;  
  9.   }  

该方法通过调用process()方法,生成sql语句。 

3.  process()方法的部分源码如下: 
Java代码   收藏代码
  1. private void process(StatementScope statementScope, Object parameterObject) {  
  2.   SqlTagContext ctx = new SqlTagContext();  
  3.   List localChildren = children;  //抽象语法树,由SqlTag和SqlText组成  
  4.   // 通过参数对象解释抽象语法树  
  5.   processBodyChildren(statementScope, ctx, parameterObject, localChildren.iterator());  
  6.   
  7.   // 构建ParameterMap  
  8.   ParameterMap map = new ParameterMap(delegate);  
  9.   map.setId(statementScope.getStatement().getId() + "-InlineParameterMap");  
  10.   map.setParameterClass(((MappedStatement) statementScope.getStatement()).getParameterClass());  
  11.   map.setParameterMappingList(ctx.getParameterMappings());  
  12.   
  13.   //获取生成的sql语句  
  14.   String dynSql = ctx.getBodyText();  
  15.   
  16.   ...  
  17.   statementScope.setDynamicSql(dynSql);  
  18.   statementScope.setDynamicParameterMap(map);  
  19. }  

上面这段代码目标是构建sql执行语句和ParameterMap,构建sql执行语句通过processBodyChildren()方法完成。 

4.  构建sql执行语句过程中最关键的一步,解释抽象语法树。processBodyChildren()的完整源码如下: 
Java代码   收藏代码
  1. private void processBodyChildren(StatementScope statementScope, SqlTagContext ctx, Object parameterObject, Iterator localChildren, PrintWriter out) {  
  2.   while (localChildren.hasNext()) {  
  3.     SqlChild child = (SqlChild) localChildren.next();  
  4.     // 1. 解释SqlText节点  
  5.     if (child instanceof SqlText) {  
  6.       SqlText sqlText = (SqlText) child;  
  7.       String sqlStatement = sqlText.getText();  
  8.       if (sqlText.isWhiteSpace()) {  
  9.         out.print(sqlStatement);  
  10.       } else if (!sqlText.isPostParseRequired()) {  
  11.   
  12.         // 输出sqlStatement  
  13.         out.print(sqlStatement);  
  14.   
  15.         ParameterMapping[] mappings = sqlText.getParameterMappings();  
  16.         if (mappings != null) {  
  17.           for (int i = 0, n = mappings.length; i < n; i++) {  
  18.             ctx.addParameterMapping(mappings[i]);  
  19.           }  
  20.         }  
  21.       } else {  
  22.         ...  
  23.       }  
  24.     // 2. 解释SqlTag节点  
  25.     } else if (child instanceof SqlTag) {  
  26.       SqlTag tag = (SqlTag) child;  
  27.       SqlTagHandler handler = tag.getHandler();  
  28.       int response = SqlTagHandler.INCLUDE_BODY;  
  29.       do {  
  30.         StringWriter sw = new StringWriter();  
  31.         PrintWriter pw = new PrintWriter(sw);  
  32.         // 2.1 节点处理开始  
  33.         response = handler.doStartFragment(ctx, tag, parameterObject);  
  34.         if (response != SqlTagHandler.SKIP_BODY) {  
  35.           //递归处理子节点  
  36.           processBodyChildren(statementScope, ctx, parameterObject, tag.getChildren(), pw);  
  37.           pw.flush();  
  38.           pw.close();  
  39.           StringBuffer body = sw.getBuffer();  
  40.           // 2.2 节点处理结束  
  41.           response = handler.doEndFragment(ctx, tag, parameterObject, body);  
  42.           // 2.3 prepend处理  
  43.           handler.doPrepend(ctx, tag, parameterObject, body);  
  44.             
  45.           if (response != SqlTagHandler.SKIP_BODY) {  
  46.             if (body.length() > 0) {  
  47.               out.print(body.toString());  
  48.             }  
  49.           }  
  50.   
  51.         }  
  52.       } while (response == SqlTagHandler.REPEAT_BODY);  
  53.       ...   
  54.     }  
  55.   }  
  56. }  

这段代码是抽象语法树的解释过程,核心逻辑如下: 
4.1 处理SqlText节点  
SqlText节点主要包含sql语句和ParameterMapping信息,这些信息在初始化阶段已经处理完毕,解释时直接输出即可。 

4.2 处理SqlTag节点  
SqlTag节点包含sql动态配置信息,通过调用节点对应的SqlTagHandler进行解释处理。解释的流程控制通过response返回值完成,该常量在SqlTagHandler中定义,有以下三种值: INCLUDE_BODY表示当前节点生效;SKIP_BODY表示当前节点无效;REPEAT_BODY表示节点需要重复处理。下面以<isNull>标签举例,说明SqlTag节点处理过程。 
1) 节点处理开始  
Java代码   收藏代码
  1. public int doStartFragment(SqlTagContext ctx, SqlTag tag, Object parameterObject) {  
  2.     
  3.   ctx.pushRemoveFirstPrependMarker(tag);  
  4.   // 判断条件是否满足  
  5.   if (isCondition(ctx, tag, parameterObject)) {  
  6.     return SqlTagHandler.INCLUDE_BODY;  
  7.   } else {  
  8.     return SqlTagHandler.SKIP_BODY;  
  9.   }  
  10. }  

其中isCondition()方法实现如下: 
Java代码   收藏代码
  1. public boolean isCondition(SqlTagContext ctx, SqlTag tag, Object parameterObject) {  
  2.   if (parameterObject == null) {  
  3.     return true;  
  4.   } else {  
  5.     // 获取参数值  
  6.     String prop = getResolvedProperty(ctx, tag);  
  7.     Object value;  
  8.     if (prop != null) {  
  9.       value = PROBE.getObject(parameterObject, prop);  
  10.     } else {  
  11.       value = parameterObject;  
  12.     }  
  13.     return value == null;  //判断是否为null  
  14.   }  
  15. }  

首先获取参数值,再判断参数值是否满足条件,最后确定当前节点是否生效(即是否是当前请求的sql语句的组成部分)。 

2) 节点处理结束  
Java代码   收藏代码
  1. public int doEndFragment(SqlTagContext ctx, SqlTag tag, Object parameterObject, StringBuffer bodyContent) {  
  2.   if (tag.isCloseAvailable()  && !(tag.getHandler() instanceof IterateTagHandler)) {  
  3.     if (bodyContent.toString().trim().length() > 0) {  
  4.       bodyContent.append(tag.getCloseAttr());  
  5.     }  
  6.   }  
  7.   return SqlTagHandler.INCLUDE_BODY;  
  8. }  

该步功能是在sql中增加动态标签对应的close属性值。 

3) prepend处理  
Java代码   收藏代码
  1. public void doPrepend(SqlTagContext ctx, SqlTag tag, Object parameterObject, StringBuffer bodyContent) {  
  2.     
  3.   if (tag.isOpenAvailable() && !(tag.getHandler() instanceof IterateTagHandler)) {  
  4.     if (bodyContent.toString().trim().length() > 0) {  
  5.       bodyContent.insert(0, tag.getOpenAttr());  
  6.     }  
  7.   }  
  8.     
  9.   if (tag.isPrependAvailable()) {  
  10.     if (bodyContent.toString().trim().length() > 0) {  
  11.       if (tag.getParent() != null && ctx.peekRemoveFirstPrependMarker(tag)) {  
  12.         ctx.disableRemoveFirstPrependMarker();  
  13.       }else {  
  14.         bodyContent.insert(0, tag.getPrependAttr());  
  15.       }  
  16.     }  
  17.   }  

该步功能是在sql中增加动态标签对应的open和prepend属性值。 

小结  
请求处理过程核心目标是 通过参数对象解释抽象语法树 ,生成当前请求的sql语句。该过程重点是对SqlTag节点的解析,通过调用该节点对应的SqlTagHandler完成处理。 

总结  
从设计上看,dynamic sql的实现主要涉及三个模式: 
解释器模式 : 初始化过程中构建出抽象语法树,请求处理时根据参数对象解释语法树,生成sql语句。 
工厂模式:  : 为动态标签的处理方式创建工厂类(SqlTagHandlerFactory),根据标签名称获取对应的处理方式。 
策略模式:  : 将动态标签处理方式抽象为接口,针对不同标签有相应的实现类。解释抽象语法树时,定义统一的解释流程,再调用标签对应的处理方式完成解释中的各个子环节。 
最后,以一张图总结动态sql的实现原理: 
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值