动态Sql介绍
动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。
Mybatis动态解析里面有2个核心的类SqlNode、SqlSource、ExpressionEvaluator。Mybatis动态Sql使用分为2个部分:动态Sql解析、动态Sql拼接执行。
封装SqlNode
SqlNode是在解析Xml文件的时候对动态Sql进行解析,并存在MappedStatement的sqlSource属性中。对于嵌套动态Sql,mybatis用递归调用来进行解析。这块东西个人觉得还是比较绕,所以这块博主准备事例、源码、执行结果一起讲解。
Sql脚本分类
在Mybatis中Sql脚本分为2种类型:静态Sql和动态Sql。下面我们通过具体的源码来看下2者区分。
静态Sql和动态Sql
静态Sql说白了就没有太任何判断了解的Sql脚本。
//Select是查询的一些属性
SqlNode类结果体系
看mybatis代码很多时候可以看到这种结构。每个SqlNode负责自己那块功能。职责单一。SqlNode的核心方法apply就是通过ExpressionEvaluator来解析OGNL表达式数据的。接下来我们看看Mybatis是如何递归解析动态sql脚本的。
//解析Sql脚本节点 publicSqlSourceparseScriptNode(){ //解析静态和动态脚本,并存在MixedSqlNode里面 //这行代码很关键,后面我们会去分析parseDynamicTags这里就是一层一层递归调用该方法把Sql脚本生成MixedSqlNode对象。 MixedSqlNoderootSqlNode=parseDynamicTags(context); SqlSourcesqlSource=null; //是否为动态Sql if(isDynamic){ //动态Sql则生成DynamicSqlSource sqlSource=newDynamicSqlSource(configuration,rootSqlNode); }else{ //否则为静态SqlSource sqlSource=newRawSqlSource(configuration,rootSqlNode,parameterType); } returnsqlSource; }
//Anhighlightedblock protectedMixedSqlNodeparseDynamicTags(XNodenode){ //创建个SqlNode,这个列表存了当前Sql脚本节点下的所有的SqlNode信息 Listcontents=newArrayList (); NodeListchildren=node.getNode().getChildNodes(); for(inti=0;i< children.getLength(); i++) { XNode child = node.newXNode(children.item(i)); //判断子元素或属性中的文本内容 || 子元素文档中的 CDATA 部(不会由解析器解析的文本) if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) { String data = child.getStringBody(""); //解析data TextSqlNode textSqlNode = new TextSqlNode(data); //判断当前的Sql脚本是否为动态脚本 if (textSqlNode.isDynamic()) { contents.add(textSqlNode); isDynamic = true; } else { contents.add(new StaticTextSqlNode(data)); } //如果子元素为代表元素,则需要解析子元素 } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628 //获取元素的名字 String nodeName = child.getNode().getNodeName(); //根据元素名获取到元素节点的处理器,Mybatis提供了8中元素处理器,ChooseHandler、IfHandler、OtherwiseHandler //TrimHandler、BindHandler、WhereHandler、SetHandler、ForEachHandler。博主会给大家分析下IfHandler NodeHandler handler = nodeHandlerMap.get(nodeName); if (handler == null) { throw new BuilderException("Unknown element <" + nodeName + ">inSQLstatement."); } //调用对应的handler进行节点处理,递归调用就在这块 handler.handleNode(child,contents); isDynamic=true; } } //创建MixedSqlNode returnnewMixedSqlNode(contents); }
//下面我们看下IfHandler是如何处理,IfHandler是XMLScriptBuilder的内部类 privateclassIfHandlerimplementsNodeHandler{ publicIfHandler(){ //PreventSyntheticAccess } //我们着重分析这个方法 @Override publicvoidhandleNode(XNodenodeToHandle,ListtargetContents){ //调用parseDynamicTags进行节点解析。这里就是递归,又调用了上面的方法。 MixedSqlNodemixedSqlNode=parseDynamicTags(nodeToHandle); //获取if对应的表达式 Stringtest=nodeToHandle.getStringAttribute("test"); //创建IfSqlNode IfSqlNodeifSqlNode=newIfSqlNode(mixedSqlNode,test); targetContents.add(ifSqlNode); } }
下面我们根据Sql脚本和执行结果来分析。
//静态Sql脚本和嵌套的动态Sql脚本
下面我们分析下执行结果:
上面递归结果已经用不通颜色标记了,大家自己看下。特别需要看下IfSqlNode的属性。
动态Sql解析
动态Sql解析主要是执行数据库操作的时候把动态Sql转换成JDBC能识别的Sql脚本。Mybatis中主要是通过SqlSource来解析Sql脚本,替换成JDBC能识别的Sql脚本。我们先看下类图。
SqlSource:提供了Sql解析的行为。
RawSqlSource:静态Sql脚本的编译,只生成一次StaticSqlSource。
DynamicSqlSource:每次调用都会生成StaticSqlSource。每次调用传入参数可能不一样。需要每次生成StaticSqlSource。
ProviderSqlSource:第三方脚本语言的集成。
FreeMarkerSqlSource:对FreeMarker的支持。
StaticSqlSource:StaticSqlSource只是对上面4中类型做了层封装。博主没有这个类会更清爽些。
我们这次主要对StaticSqlSource、RawSqlSource、和DynamicSqlSource进行分析。
StaticSqlSource
其实StaticSqlSource就是对其他几种类型Sql处理器结果进行包装。我们看下源码。
//我们主要分析下getBoundSql publicclassStaticSqlSourceimplementsSqlSource{ privatefinalStringsql; privatefinalListparameterMappings; privatefinalConfigurationconfiguration; publicStaticSqlSource(Configurationconfiguration,Stringsql){ this(configuration,sql,null); } publicStaticSqlSource(Configurationconfiguration,Stringsql,List parameterMappings){ this.sql=sql; this.parameterMappings=parameterMappings; this.configuration=configuration; } //getBoundSql就是创建一个BoundSql对象。 @Override publicBoundSqlgetBoundSql(ObjectparameterObject){ returnnewBoundSql(configuration,sql,parameterMappings,parameterObject); } }
看完是不是非常简单,其实有些代码确实没有我们想象中那么难。
RawSqlSource
//我们着重分析RawSqlSource方法 publicclassRawSqlSourceimplementsSqlSource{ privatefinalSqlSourcesqlSource; publicRawSqlSource(Configurationconfiguration,SqlNoderootSqlNode,Class>parameterType){ this(configuration,getSql(configuration,rootSqlNode),parameterType); } //这里实现了对静态脚本的解析,所谓的静态脚本解析就是把#{}解析成?静态Sql解析是在解析Mapper.xml的时候执行的 publicRawSqlSource(Configurationconfiguration,Stringsql,Class>parameterType){ SqlSourceBuildersqlSourceParser=newSqlSourceBuilder(configuration); Class>clazz=parameterType==null?Object.class:parameterType; //通过调用SqlSourceBuilder的parse方法来解析Sql sqlSource=sqlSourceParser.parse(sql,clazz,newHashMap()); } privatestaticStringgetSql(Configurationconfiguration,SqlNoderootSqlNode){ DynamicContextcontext=newDynamicContext(configuration,null); rootSqlNode.apply(context); returncontext.getSql(); } @Override publicBoundSqlgetBoundSql(ObjectparameterObject){ returnsqlSource.getBoundSql(parameterObject); } }
下面我们来看下SqlSourceBuilder的parse方法
publicSqlSourceparse(StringoriginalSql,Class>parameterType,MapadditionalParameters){ ParameterMappingTokenHandlerhandler=newParameterMappingTokenHandler(configuration,parameterType,additionalParameters); //找到Sql脚本中#{}符号的脚本用?号进行替代。GenericTokenParser里面代码比较复杂,博主也没有研究。 //有兴趣自己可以研究下。 GenericTokenParserparser=newGenericTokenParser("#{","}",handler); Stringsql=parser.parse(originalSql); returnnewStaticSqlSource(configuration,sql,handler.getParameterMappings()); }
DynamicSqlSource
动态Sql解析主要由DynamicSqlSource来完成。这里面又是通过递归调进行sql解析。我们还是延用上面的Sql给大家讲解。
publicclassDynamicSqlSourceimplementsSqlSource{ privatefinalConfigurationconfiguration; privatefinalSqlNoderootSqlNode; publicDynamicSqlSource(Configurationconfiguration,SqlNoderootSqlNode){ this.configuration=configuration; this.rootSqlNode=rootSqlNode; } @Override publicBoundSqlgetBoundSql(ObjectparameterObject){ //动态Sql解析上下文 DynamicContextcontext=newDynamicContext(configuration,parameterObject); //rootSqlNode就是我们前面讲解的,把动态Sql解析成SqlNode对象。外层为MixedSqlNode节点,节点存储了 //节点下的所有子节点。里面递归调用并根据传入参数的属性检查是否需要拼接sql rootSqlNode.apply(context); //这块代码和上面静态Sql接代码一致。 SqlSourceBuildersqlSourceParser=newSqlSourceBuilder(configuration); Class>parameterType=parameterObject==null?Object.class:parameterObject.getClass(); //把我们动态Sql中的#{}替换成? SqlSourcesqlSource=sqlSourceParser.parse(context.getSql(),parameterType,context.getBindings()); BoundSqlboundSql=sqlSource.getBoundSql(parameterObject); for(Map.Entryentry:context.getBindings().entrySet()){ boundSql.setAdditionalParameter(entry.getKey(),entry.getValue()); } returnboundSql; } }
动态Sql解析apply方法博主只根据场景介绍下MixedSqlNode和IfSqlNode的apply方法。其他有兴趣自己去研究下。逻辑大体一致,实现有些区别。
publicclassMixedSqlNodeimplementsSqlNode{ privatefinalListcontents; publicMixedSqlNode(List contents){ this.contents=contents; } //获取循环SqlNode列表的所有SqlNode,调用apply方法根据传入参数和条件进行静态sql的拼接。 //列表中的SqlNode可能是一个简单的SqlNode对象,也可能是一个MixedSqlNode或者有更多的嵌套。 //博主的例子就是3个嵌套If查询。根据博主的Sql脚本,这里直接会调用IfSqlNode的apply方法。 //我们接下来看下IfSqlNode是如何实现的。 @Override publicbooleanapply(DynamicContextcontext){ for(SqlNodesqlNode:contents){ sqlNode.apply(context); } returntrue; } }
IfSqlNode的apply
publicclassIfSqlNodeimplementsSqlNode{ //ExpressionEvaluator会调用ognl来对表达式进行解析 privatefinalExpressionEvaluatorevaluator; privatefinalStringtest; privatefinalSqlNodecontents; publicIfSqlNode(SqlNodecontents,Stringtest){ this.test=test; this.contents=contents; this.evaluator=newExpressionEvaluator(); } @Override publicbooleanapply(DynamicContextcontext){ //context.getBindings()里面就存储这请求参数,这里是一个HashMap,OGNl里面代码博主没有研究。 //如果条件if成立,直接获取contents中的SqlNode的apply方法进行动态脚本处理。 if(evaluator.evaluateBoolean(test,context.getBindings())){ contents.apply(context); returntrue; } returnfalse; } }
这块代码很多递归调用,博主自认为讲的不太透彻,所以大家看完务必自己去调试下。
总结
Mybatis动态Sql从解析到执行分为2个过程下面对这个2个过程进行简单总结。
1.动态Sql生成SqlNode信息,这个过程发生在对select、update等Sql语句解析过程。如果是静态Sql直接会把#{}替换成?。
2.动态Sql解析在获取BoundSql时候触发。会调用SqlNode的apply进行Sql解析成静态Sql,然后把#{}替换成?,并绑定ParameterMapping映射。
-
SQL
+关注
关注
1文章
759浏览量
44060 -
源码
+关注
关注
8文章
633浏览量
29134 -
脚本
+关注
关注
1文章
387浏览量
14829
原文标题:Mybatis动态Sql处理
文章出处:【微信号:AndroidPush,微信公众号:Android编程精选】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论