当前位置: 首页 > 后端技术 > Java

java实训Mybatis动态Sql处理分析

时间:2023-04-01 16:16:37 Java

以下文章来自架构师必备的动态Sql介绍动态SQL是MyBatis强大的特性之一。如果你用过JDBC或者其他类似的框架,你应该明白根据不同的条件拼接SQL语句是多么痛苦。例如,拼接时一定不要忘记添加必要的空格,还要注意去掉列表最后一列名称中的逗号。使用动态SQL,您可以完全摆脱这种痛苦。使用动态SQL不是一件容易的事,但是MyBatis通过强大的动态SQL语言显着提高了此功能的易用性,可以在任何SQL映射语句中使用。Mybatis动态分析中有两个核心类SqlNode、SqlSource和ExpressionEvaluator。Mybatis动态Sql的使用分为动态Sql解析和动态Sql拼接执行两部分。封装SqlNodeSqlNode是在解析Xml文件时解析动态Sql,存放在MappedStatement的sqlSource属性中。对于嵌套的动态Sql,mybatis采用递归调用的方式进行解析。个人觉得这个东西还是比较绕的,所以博主准备了例子,源码,执行结果一起来讲解_java训练。Sql脚本分类在Mybatis中,Sql脚本分为静态Sql和动态Sql两种。下面我们通过具体的源码来看看两者的区别。StaticSql和dynamicSqlStaticSql说白了就是没有Sql脚本,判断力太强看不懂。//select是查询的一些属性//这个查询语句select*fromuserwhereid>#{user.id}isMybatis//StaticSql中的staticSql是不带任何条件的Sql语句select*fromuserwhereid>#{user.id}//这里有if判断条件,Mybatis调用带判断条件的SqldynamicSql。//动态Sql除了if之外还有foreach,where,trim等。具体去mybatis官网看ANDname=#{user.name}SqlNodeclassresultsystem这种结构在mybatis代码中可以多次看到。每个SqlNode负责自己的功能。单一责任。SqlNode应用的核心方法是通过ExpressionEvaluator解析OGNL表达式数据。接下来我们看看Mybatis是如何递归解析动态sql脚本的。//解析Sql脚本节点publicSqlSourceparseScriptNode(){//解析静态和动态脚本,并存储在MixedSqlNode中//这行代码很重要,后面我们会分析parseDynamicTags,这里是递归调用这个方法层层转换Sql脚本生成一个MixedSqlNode对象。MixedSqlNoderootSqlNode=parseDynamicTags(context);SqlSourcesqlSource=null;//是否为动态Sqlif(isDynamic){//动态Sql生成DynamicSqlSourcesqlSource=newDynamicSqlSource(configuration,rootSqlNode);}else{//否则为staticSqlSource=newSourcesqlRawSqlSource(configuration,rootSqlNode,parameterType));}returnsqlSource;}//AnhighlightedblockprotectedMixedSqlNodeparseDynamicTags(XNodenode){//创建一个SqlNode,这个列表存放了当前Sql脚本节点下所有的SqlNode信息Listcontents=newArrayList();NodeListchildren=node.getNode().getChildNodes();for(inti=0;iinSQLstatement.");}//call对应的handler进行节点处理,递归调用在这个handler.handleNode(child,contents);isDynamic=true;}}//创建MixedSqlNodereturnnewMixedSqlNode(contents);}//看看IfHandler是如何处理的它,IfHandler是XMLScriptBuilder的内部类privateclassIfHandlerimplementsNodeHandler{publicIfHandler(){//防止合成访问}//我们重点分析这个方法@OverridepublicvoidhandleNode(XNodenodeToHandle,ListtargetContents){//调用parseDynamicTags进行Node解析。这里是递归,再次调用上面的方法。MixedSqlNodemixedSqlNode=parseDynamicTags(nodeToHandle);//获取ifString对应的表达式test=nodeToHandle.getStringAttribute("test");//创建IfSqlNodeIfSqlNodeifSqlNode=newIfSqlNode(mixedSqlNode,test);targetContents.add(ifSqlNode);}下面我们根据Sql脚本和执行结果进行分析。//静态Sql脚本和嵌套动态Sql脚本.name!=nullanduser.name!=''">ANDname=#{user.name}ANDname=#{user.nameANDname=#{user.name}接下来我们分析一下执行结果:上面的递归结果已经用不同的颜色标出了,大家自己看吧。特别是,您需要查看IfSqlNode的属性。动态Sql分析动态Sql分析主要是将动态Sql转换成JDBC在进行数据库操作时可以识别的Sql脚本。在Mybatis中,Sql脚本主要是通过SqlSource解析出来的,换成JDBC可以识别的Sql脚本。我们先看类图。SqlSource:提供Sql解析的行为。RawSqlSource:编译静态Sql脚本,只生成一次StaticSqlSource。DynamicSqlSource:每次调用都会生成StaticSqlSource。每次调用传递的参数可能不同。每次都需要生成StaticSqlSource。ProviderSqlSource:第三方脚本语言的集成。FreeMarkerSqlSource:支持FreeMarker。StaticSqlSource:StaticSqlSource只封装了以上4种类型。博主不用这堂课会更清爽。这次主要分析StaticSqlSource、RawSqlSource、DynamicSqlSource。StaticSqlSource其实StaticSqlSource就是把其他几种Sql处理器的结果进行封装。让我们看一下源代码。//我们主要分析getBoundSqlpublicclassStaticSqlSourceimplementsSqlSource{privatefinalStringsql;privatefinalListparameterMappings;privatefinalConfiguration配置;publicStaticSqlSource(Configurationconfiguration,Stringsql){this(configuration,sql,null)};publicStaticSqlSource(Configurationconfiguration,Stringsql,ListparameterMappings){this.sql=sql;this.parameterMappings=parameterMappings;this.configuration=configuration;}//getBoundSql是创建一个BoundSql对象。@OverridepublicBoundSqlgetBoundSql(ObjectparameterObject){returnnewBoundSql(configuration,sql,parameterMappings,parameterObject);}}看完是不是很简单,其实有些代码并没有我们想象的那么难。RawSqlSource//我们重点分析RawSqlSource方法publicclassRawSqlSourceimplementsSqlSource{privatefinalSqlSourcesqlSource;publicRawSqlSource(Configurationconfiguration,SqlNoderootSqlNode,ClassparameterType){this(configuration,getSqlSource;Nodeql(configuration)parameter,Type)//这里实现了静态脚本的解析。所谓静态脚本解析就是把#{}解析成?解析Mapper.xml时执行静态Sql解析publicRawSqlSource(Configurationconfiguration,Stringsql,ClassparameterType){SqlSourceBuildersqlSourceParser=newSqlSourceBuilder(configuration);Classclazz=parameterType==null?Object.class:parameterType;//调用SqlSourceBuilder的parse方法解析SqlsqlSource=sqlSourceParser.parse(sql,clazz,newHashMap());}privatestaticStringgetSql(Configurationconfiguration,SqlNoderootSqlNode){DynamicContextcontext=newDynamicContext(configuration,null);rootSqlNode.apply(context);returncontext.getSql();}@OverridepublicBoundSqlgetBoundSql(对象参数erObject){returnsqlSource.getBoundSql(parameterObject);}}我们看一下SqlSourceBuilder的parse方法publicSqlSourceparse(StringoriginalSql,ClassparameterType,MapadditionalParameters){ParameterMappingTokenHandlerhandler=newParameterMappingTokenHandconfiguration,parameterType,additionalParameters);//在Sql脚本中找到#{}符号的脚本?GenericTokenParser中的代码比较复杂,博主没有研究过。//有兴趣的可以自己研究一下。GenericTokenParserparser=newGenericTokenParser("#{","}",handler);Stringsql=parser.parse(originalSql);returnnewStaticSqlSource(configuration,sql,handler.getParameterMappings());}DynamicSqlSource主要通过动态Sql解析DynamicSqlSource来完成。这里还是通过递归调用来进行sql分析。我们还是用上面的Sql来给大家讲解。publicclassDynamicSqlSourceimplementsSqlSource{privatefinalConfiguration配置;privatefinalSqlNoderootSqlNode;publicDynamicSqlSource(Configurationconfiguration,SqlNoderootSqlNode){this.configuration=configuration;this.rootSqlNode=rootSqlNode;}@OverridepublicBoundSqlgetBoundSql(ObjectparameterObject){//动态Sql解析上下文DynamicContextcontext=newDynamicContext(configuration,parameterObject);//rootSqlNode就是我们前面讲解的,将动态Sql解析成SqlNode对象。最外层是MixedSqlNode节点,存放//节点下的所有子节点。递归调用,根据传入参数的属性检查是否需要拼接sqlrootSqlNode.apply(context);//这段代码和上面的静态Sql连接代码一致。SqlSourceBuildersqlSourceParser=newSqlSourceBuilder(configuration);ClassparameterType=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方法。其他人有兴趣进行自己的研究。逻辑大致相同,只是实现有些不同。公共类MixedSqlNode实现SqlNode{privatefinalList内容;publicMixedSqlNode(Listcontents){this.contents=contents;}//获取循环SqlNode列表中的所有SqlNode,根据传入的参数和条件调用apply方法进行静态sql拼接。//列表中的SqlNode可能是一个简单的SqlNode对象,也可能是一个MixedSqlNode或者有更多的嵌套。//博主的例子是3个嵌套的If查询。根据博主的Sql脚本,这里会直接调用IfSqlNode的apply方法。//接下来我们看看IfSqlNode是如何实现的。@Overridepublicbooleanapply(DynamicContextcontext){for(SqlNodesqlNode:contents){sqlNode.apply(context);}returntrue;}}IfSqlNode的applypublicclassIfSqlNodeimplementsSqlNode{//ExpressionEvaluator会调用ognl解析表达式privatefinalExpressionEvaluator评估者;私有最终字符串测试;私有最终SqlNode内容;公共IfSqlNode(SqlNode内容,字符串测试){this.test=test;this.contents=contents;this.evaluator=newExpressionEvaluator();}@Overridepublicbooleanapply(DynamicContextcontext){//context.getBindings()存放的是请求参数,这里是一个HashMap,OGNl中的代码博主没研究过。//如果条件if为真,直接获取contents中SqlNode的apply方法进行动态脚本处理。if(evaluator.evaluateBoolean(test,context.getBindings())){contents.apply(context);returntrue;}returnfalse;}}这段代码有很多递归调用,博主认为不是非常透彻,所以看完之后一定要自己debug。总结Mybatis动态Sql从解析到执行分为两个过程。以下是这两个过程的简要总结。1、动态Sql生成SqlNode信息。这个过程发生在解析select、update等Sql语句的过程中。如果是静态Sql,会直接把#{}替换成?.2、获取BoundSql时触发动态Sql解析。会调用SqlNode的apply将Sql解析成静态Sql,然后把#{}替换成?,并绑定ParameterMapping映射。