自己动手实现mybatis动态sql的方法
发现要坚持写博客真的是一件很困难的事情,各种原因都会导致顾不上博客。本来打算写自己动手实现orm,看看时间,还是先实现一个动态sql,下次有时间再补上orm完整的实现吧。
用过mybatis的人,估计对动态sql都不陌生,如果没有用过,就当看看热闹吧。我第一次接触mysql是在大四的时候,当时就觉得动态sql这东西很牛,很灵活,一直想搞明白怎么实现的,尽管当时已经能够写ioc,mvc和简单的orm框架(仿mybaits但是没有动态sql部分),但是仍然找不到mybatis核心的动态sql到底在哪实现的,怎么实现的,可能是那些代码太绕根本没法看懂,直到目前,我都没有勇气去看mybatis的动态sql部分,大概是天生对算法有莫名其妙的敬畏吧。
几年前因为想做一个配置平台,想用解析型语言替代java的实现,可以让配置人员在页面上方便的编写少量代码实现复杂的业务逻辑(包括数据库操作)。当时java已经有js解析引擎,但是大多数人都说效率太低,不知道我发什么疯就想到自己实现一个解析语言。不过实现自己的语言也是我一直的梦想,解析语言相对编译型语言入手简单,于是我就果断动手了,写完才知道,其实自己的实现估计还没有当时的js引擎效率高,那时的我真的是很年轻很简单。今天谈到的动态sql实现其实就是受到那时候解析语言的启发。
废话不多说直接开始聊动态sql,请看下面例子,先声明这里的例子并不是一个正确的sql的写法,只是想写一个尽量复杂的嵌套结构,如果把这种复杂的情况实现了,那么简单一点的就更加不在话下了。
deletefrompl_pagewidgetwherepagewidgetcodein #{item} #{b} anda=#{a}
要实现解析出上面例子的sql,首先一个难点类似是test属性里的条件怎么判断真假,不过这个难点在struts2中学到的ognl表达式面前就比较小儿科了。不知道有么有朋友遇到过一个比较奇葩的现象,就是有时候明明在mybatis动态sql中写如下表达式,但是当n=0的时候居然是满足条件的也就是test里的值是false,0居然不能满足这个表达式的条件,这里就是ognl库的原因了。没办法它就是这么玩的,当成特殊情况记住就可以了
test="n!=nullandn!=''"
ognl表达式使用很方便如下
importjava.util.HashMap; importjava.util.Map; importognl.Ognl; publicclassOgnlTest{ //输出结果:false publicstaticvoidmain(String[]args)throwsException{ Stringcon1="n!=nullandn!=''"; Maproot=newHashMap<>(); root.put("n",0); System.out.println(Ognl.getValue(con1,root)); } }
要实现解析上面例子的sql,第二个难点就是虽然这个sql披上一层xml的皮就是一个标准的sql,如下
deletefrompl_pagewidget wherepagewidgetcodein #{item} #{b} anda=#{a}
但是要解析上面的xml和我们平时不一样,这个xml是标签和文本混合的,正常我们开发中应该很少会用到解析这种xml。不过我们常用的解析xml的工具dom4j其实可以很好的解析这种sql,只不过很少可能用到。Element类的content()方法就可以返回一个Node的集合,再通过遍历这个集合,判断每个Node的类型就可以了。解决了这两个重点,只需要加上一点技巧就可以解析这种动态sql了。
我用到的技巧是根据java语法格式得到的启发。比如java中有局部变量和全局变量,不考虑引用传递这种情况,如果全局变量inti=1;方法里面传入这个全局变量,然后在方法里面修改,在方法里面看到的是改变后的值,但是在方法外面看到的仍然是1。这个现象其实学过java应该都知道。还有就是当方法调用的时候,方法里面可以看到全局变量,也可以看到局部变量,方法调用结束后局部变量会被清空释放(看垃圾搜集器高兴)。介绍了这些直接上代码了
importjava.io.StringReader; importjava.text.SimpleDateFormat; importjava.util.Arrays; importjava.util.Date; importjava.util.HashMap; importjava.util.List; importjava.util.Map; importjava.util.regex.Matcher; importjava.util.regex.Pattern; importorg.apache.commons.collections.MapUtils; importorg.apache.commons.lang.StringUtils; importorg.dom4j.Document; importorg.dom4j.Element; importorg.dom4j.Node; importorg.dom4j.Text; importorg.dom4j.io.SAXReader; importcom.rd.sql.Attrs; importcom.rd.sql.BaseNode; importcom.rd.sql.NodeFactory; publicclassSqlParser{ privateMapcurrParams=newHashMap (); /** deletefrompl_pagewidget wherepagewidgetcodein #{item} #{b} anda=#{a} */ publicstaticvoidmain(String[]args)throwsException{ Map map=newHashMap (); map.put("widgetcodes",Arrays.asList("1","2")); map.put("bs",Arrays.asList("3","4")); map.put("a",1); SqlParserparser=newSqlParser(); System.out .println(parser.parser("deletefrompl_pagewidget\n" +"\t \n" +"\t\twherepagewidgetcodein\n" +"\t\t \n" +"\t\t \n" +"\t\t#{item}\n" +"\t\t\n" +"\t\t \n" +"\t\t\t#{b}\n"+"\t\t\n" +"\t\t\n"+"\t\n" +"\t \n" +"\t\tanda=#{a}\n"+"\t\n",map)); System.out.println(parser.getParams()); } publicStringparser(Stringxml,Map params) throwsException{ //xml=""+xml; //给输入的动态sql套一层xml标签 xml=" "+xml+" "; SAXReaderreader=newSAXReader(false); Documentdocument=reader.read(newStringReader(xml)); Elementelement=document.getRootElement(); MapcurrParams=newHashMap (); StringBuildersb=newStringBuilder(); //开始解析 parserElement(element,currParams,params,sb); returnsb.toString(); } /** *使用递归解析动态sql *@paramele1待解析的xml标签 *@paramcurrParams *@paramglobalParams *@paramsb *@throwsException */ privatevoidparserElement(Elementele1,Map currParams, Map globalParams,StringBuildersb) throwsException{ //解析一个节点,比如解析到了一个if节点,假如test判断为true这里就返回true TempValval=parserOneElement(currParams,globalParams,ele1,sb); //得到解析的这个节点的抽象节点对象 BaseNodenode=val.getNode(); /** *实际上这句之上的语句只是解析了xml的标签,并没有解析标签里的内容,这里 *表示要解析内容之前,如果有前置操作做一点前置操作 */ node.pre(currParams,globalParams,ele1,sb); //判断是否还需要解析节点里的内容,例如if节点test结果为true booleanflag=val.isContinue(); //得到该节点下的所有子节点的集合,包含普通文本 List nodes=ele1.content(); if(flag&&!nodes.isEmpty()){ /** *这里表示要进一步解析节点里的内容了,可以把节点类比成一个方法的外壳 *里面的内容类比成方法里的具体语句,开始解析节点的内容之前 *先创建本节点下的局部参数的容器,最方便当然是map */ Map params=newHashMap (); /** *把外面传进来的局部参数,直接放入容器,由于本例中参数都是常用数据类型 *不会存在引用类型所以,这里相当于是一个copy,为了不影响外面传入的对象 *可以类比方法调用传入参数的情况 */ params.putAll(currParams); //循环所有子节点 for(inti=0;i params,Map globalParams) throwsException{ //获取foreach这种标签中用于记录循环的变量 StringindexStr=MapUtils.getString(params,Attrs.WHILE_INDEX); Integerindex=null; if(StringUtils.isNotEmpty(indexStr)){ index=MapUtils.getInteger(params,indexStr); } //匹配#{a}这种参数 Stringreg1="(#\\{)(\\w+)(\\})"; //匹配${a}这种参数 Stringreg2="(\\$\\{)(\\w+)(\\})"; Patternp1=Pattern.compile(reg1); Matcherm1=p1.matcher(str); Patternp2=Pattern.compile(reg2); Matcherm2=p2.matcher(str); StringwhileList=MapUtils.getString(params,Attrs.WHILE_LIST); Map allParams=getAllParams(params,globalParams); while(m1.find()){ StringtmpKey=m1.group(2); Stringkey=whileList==null?tmpKey:(whileList+"_"+tmpKey); key=index==null?key:(key+index); StringreKey="#{"+key+"}"; //如果在foreach类似的循环里,可能需要将参数#{xx}替换成#{xx_0},#{xx_1} str=str.replace(m1.group(0),reKey); currParams.put(key,allParams.get(tmpKey)); } while(m2.find()){ StringtmpKey=m2.group(2); Objectvalue=allParams.get(tmpKey); if(value!=null){ str=str.replace(m2.group(0),getValue(value)); } } returnstr; } privateStringgetValue(Objectvalue){ Stringresult=""; if(valueinstanceofDate){ SimpleDateFormatsdf=newSimpleDateFormat("yyyy-MM-ddHH:mm:ss"); result=sdf.format((Date)value); }else{ result=String.valueOf(value); } returnresult; } privateMap getAllParams(Map currParams, Map globalParams){ Map allParams=newHashMap (); allParams.putAll(globalParams); allParams.putAll(currParams); returnallParams; } //解析一个xml元素 privateTempValparserOneElement(Map currParams, Map globalParams,Elementele,StringBuildersb) throwsException{ //获取xml标签名 StringeleName=ele.getName(); //解析一个节点后是否继续,如遇到if这种节点,就需要判断test里是否为空 booleanisContinue=false; //声明一个抽象节点 BaseNodenode=null; if(StringUtils.isNotEmpty(eleName)){ //使用节点工厂根据节点名得到一个节点对象比如是if节点还是foreach节点 node=NodeFactory.create(eleName); //解析一下这个节点,返回是否还需要解析节点里的内容 isContinue=node.parse(currParams,globalParams,ele,sb); } returnnewTempVal(isContinue,ele,node); } publicMap getParams(){ returncurrParams; } /** *封装一个xml元素被解析后的结果 *@authorrongdi */ finalstaticclassTempVal{ privatebooleanisContinue; privateElementele; privateBaseNodenode; publicTempVal(booleanisContinue,Elementele,BaseNodenode){ this.isContinue=isContinue; this.ele=ele; this.node=node; } publicbooleanisContinue(){ returnisContinue; } publicvoidsetContinue(booleanisContinue){ this.isContinue=isContinue; } publicElementgetEle(){ returnele; } publicvoidsetEle(Elementele){ this.ele=ele; } publicBaseNodegetNode(){ returnnode; } publicvoidsetNode(BaseNodenode){ this.node=node; } } }
importorg.dom4j.Element; importjava.util.HashMap; importjava.util.Map; /** *抽象节点 *@authorrongdi */ publicabstractclassBaseNode{ publicabstractbooleanparse(MapcurrParams,Map globalParams,Elementele,StringBuildersb)throwsException; publicvoidpre(Map currParams,Map globalParams,Elementele,StringBuildersb)throwsException{ } publicvoidafter(Map currParams,Map globalParams,Elementele,StringBuildersb)throwsException{ } protectedMap getAllParams(Map currParams, Map globalParams){ Map allParams=newHashMap (); allParams.putAll(globalParams); allParams.putAll(currParams); returnallParams; } }
importjava.util.Map; importognl.Ognl; importorg.apache.commons.lang.StringUtils; importorg.dom4j.Element; /** *if节点 *@authorrongdi */ publicclassIfNodeextendsBaseNode{ @Override publicbooleanparse(MapcurrParams,Map globalParams,Elementele,StringBuildersb)throwsException{ //得到if节点的test属性 StringtestStr=ele.attributeValue("test"); booleantest=false; try{ if(StringUtils.isNotEmpty(testStr)){ //合并全局变量和局部变量 Map allParams=getAllParams(currParams,globalParams); //使用ognl判断true或者false test=(Boolean)Ognl.getValue(testStr,allParams); } }catch(Exceptione){ e.printStackTrace(); thrownewException("判断操作参数"+testStr+"不合法"); } if(ele.content()!=null&&ele.content().size()==0){ test=true; } returntest; } }
importjava.util.ArrayList; importjava.util.HashMap; importjava.util.List; importjava.util.Map; importjava.util.Set; importognl.Ognl; importorg.apache.commons.collections.MapUtils; importorg.apache.commons.lang.StringUtils; importorg.dom4j.Element; /** foreach节点属性如下 collection需要遍历的集合 item遍历集合后每个元素存放的变量 index遍历集合的索引数如0,1,2... separator遍历后以指定分隔符拼接 open遍历后拼接开始的符号如( close遍历后拼接结束的符号如) */ publicclassForeachNodeextendsBaseNode{ @Override publicbooleanparse(MapcurrParams,Map globalParams,Elementele,StringBuildersb)throwsException{ StringconditionStr=null; StringcollectionStr=ele.attributeValue("collection"); StringitemStr=ele.attributeValue("item"); Stringindex=ele.attributeValue("index"); StringseparatorStr=ele.attributeValue("separator"); StringopenStr=ele.attributeValue("open"); StringcloseStr=ele.attributeValue("close"); if(StringUtils.isEmpty(index)){ index="index"; } if(StringUtils.isEmpty(separatorStr)){ separatorStr=","; } if(StringUtils.isNotEmpty(openStr)){ currParams.put(Attrs.WHILE_OPEN,openStr); } if(StringUtils.isNotEmpty(closeStr)){ currParams.put(Attrs.WHILE_CLOSE,closeStr); } if(StringUtils.isNotEmpty(collectionStr)){ currParams.put(Attrs.WHILE_LIST,collectionStr); } currParams.put(Attrs.WHILE_SEPARATOR,separatorStr); if(index!=null){ /** *如果局部变量中存在当前循环变量的值,就表示已经不是第一次进入循环标签了,移除掉开始标记 *并将局部变量值加1 */ if(currParams.get(index)!=null){ currParams.remove(Attrs.WHILE_START); currParams.put(index+"_",(Integer)currParams.get(index+"_")+1); }else{//第一次进入循环标签内 currParams.put(Attrs.WHILE_START,true); currParams.put(index+"_",0); } currParams.put(index,(Integer)currParams.get(index+"_")); } booleancondition=true; Map allParams=getAllParams(currParams,globalParams); Objectcollection=null; if(StringUtils.isNotEmpty(collectionStr)){ //得到待循环的集合 collection=Ognl.getValue(collectionStr,allParams); //如果集合属性不为空,但是条件为null则默认加上一个边界条件 if(StringUtils.isEmpty(conditionStr)){ //这里只是用集合演示一下,也可以再加上数组,只不过改成.length而已 if(collectioninstanceofList){ conditionStr=index+"_<"+collectionStr+".size()"; }elseif(collectioninstanceofMap){ Mapmap=(Map)collection; Setset=map.entrySet(); Listlist=newArrayList(set); allParams.put("_list_",list); conditionStr=index+"_<_list_"+".size()"; } } } currParams.remove(Attrs.WHILE_END); if(StringUtils.isNotEmpty(conditionStr)){ //计算条件的值 condition=(Boolean)Ognl.getValue(conditionStr,allParams); Map tempMap=newHashMap<>(); tempMap.putAll(allParams); tempMap.put(index+"_",(Integer)currParams.get(index+"_")+1); currParams.put(Attrs.WHILE_END,!(Boolean)Ognl.getValue(conditionStr,tempMap)); } booleanflag=true; currParams.put(Attrs.WHILE_INDEX,index); currParams.put(Attrs.WHILE_FLAG,true); if(condition){ try{ if(StringUtils.isNotEmpty(itemStr)&&StringUtils.isNotEmpty(collectionStr)){ Objectvalue=null; intidx=Integer.parseInt(currParams.get(index+"_").toString()); if(collectioninstanceofList){ value=((List)collection).get(idx); currParams.put(itemStr,value); }elseif(collectioninstanceofMap){ Mapmap=(Map)collection; Set >set=map.entrySet(); List >list=newArrayList(set); currParams.put(itemStr,list.get(idx).getValue()); currParams.put(index,list.get(idx).getKey()); } } }catch(Exceptione){ thrownewException("从集合或者映射取值"+currParams.get(index)+"错误"+e.getMessage()); } }else{ flag=false; destroyVars(currParams,index,itemStr); } returnflag; } /** *如果是第一次进入循环标签,则拼上open的内容 */ @Override publicvoidpre(Map currParams,Map globalParams,Elementele,StringBuildersb)throwsException{ super.pre(currParams,globalParams,ele,sb); booleanstart=MapUtils.getBoolean(currParams,Attrs.WHILE_START,false); if(start){ Stringopen=MapUtils.getString(currParams,Attrs.WHILE_OPEN); sb.append(open); } } /** *如果是最后进入循环标签,则最后拼上close的内容 */ @Override publicvoidafter(Map currParams,Map globalParams,Elementele,StringBuildersb)throwsException{ super.after(currParams,globalParams,ele,sb); booleanend=MapUtils.getBoolean(currParams,Attrs.WHILE_END,false); Stringseparator=MapUtils.getString(currParams,Attrs.WHILE_SEPARATOR); if(!end&&StringUtils.isNotEmpty(separator)){ sb.append(separator); } if(end){ Stringclose=MapUtils.getString(currParams,Attrs.WHILE_CLOSE); if(sb.toString().endsWith(separator)){ sb.deleteCharAt(sb.length()-1); } sb.append(close); } } //释放临时变量 privatevoiddestroyVars(Map currParams,Stringindex,StringvarStr){ currParams.remove(Attrs.WHILE_INDEX); currParams.remove(Attrs.WHILE_FLAG); currParams.remove(Attrs.WHILE_SEPARATOR); currParams.remove(Attrs.WHILE_START); currParams.remove(Attrs.WHILE_END); currParams.remove(Attrs.WHILE_LIST); } } importorg.dom4j.Element; importjava.util.Map; publicclassSqlNodeextendsBaseNode{ @Override publicbooleanparse(Map currParams,Map globalParams,Elementele,StringBuildersb)throwsException{ returntrue; } } importjava.util.Arrays; importjava.util.List; importjava.util.Map; importjava.util.concurrent.ConcurrentHashMap; /** *节点工厂 */ publicclassNodeFactory{ privatestaticMap nodeMap=newConcurrentHashMap (); privatefinalstaticList whileList=Arrays.asList("foreach"); static{ nodeMap.put("if",newIfNode()); nodeMap.put("sql",newSqlNode()); nodeMap.put("foreach",newForeachNode()); } publicstaticbooleanisWhile(StringelementName){ returnwhileList.contains(elementName); } publicstaticvoidaddNode(StringnodeName,BaseNodenode){ nodeMap.put(nodeName,node); } publicstaticBaseNodecreate(StringnodeName){ returnnodeMap.get(nodeName); } } /** *各种标记 *@authorrongdi */ publicclassAttrs{ publicfinalstaticStringTRANSACTIONAL="transactional"; publicfinalstaticStringWHILE_START="while-start"; publicfinalstaticStringWHILE_END="while-end"; publicfinalstaticStringWHILE_OPEN="while-open"; publicfinalstaticStringWHILE_CLOSE="while-close"; publicfinalstaticStringWHILE_SEPARATOR="while-separator"; publicfinalstaticStringWHILE_INDEX="while-index"; publicfinalstaticStringWHILE_FLAG="while-flag"; publicfinalstaticStringWHILE_LIST="while-list"; publicfinalstaticStringWHEN_FLAG="when-flag"; publicstaticfinalStringPROCESS_VAR="process-var"; publicfinalstaticStringRESULT_FLAG="result-flag"; publicfinalstaticStringRETURN_FLAG="return-flag"; publicfinalstaticStringCONSOLE_VAR="console-var"; publicfinalstaticStringDO="do"; publicfinalstaticStringINDEX="index"; publicfinalstaticStringCONDITION="condition"; publicfinalstaticStringNAME="name"; publicfinalstaticStringVALUE="value"; publicstaticfinalStringTYPE="type"; publicstaticfinalStringFORMAT="format"; publicstaticfinalStringIF="if"; publicstaticfinalStringELSE="else"; publicfinalstaticStringFILE="file"; publicstaticfinalStringDATE="date"; publicstaticfinalStringNOW="now"; publicstaticfinalStringDECIMAL="decimal"; publicstaticfinalStringID="id"; publicstaticfinalStringPARAMS="params"; publicstaticfinalStringTARGET="target"; publicstaticfinalStringSINGLE="single"; publicstaticfinalStringPAGING="paging"; publicstaticfinalStringDESC="desc"; publicstaticfinalStringBREAK="break"; publicstaticfinalStringCONTINUE="continue"; publicstaticfinalStringCOLLECTION="collection"; publicstaticfinalStringVAR="var"; publicstaticfinalStringEXECUTOR="executor-1"; publicstaticfinalStringROLLBACK_FLAG="rollback-flag"; publicstaticfinalStringSERVICE="service"; publicstaticfinalStringREF="ref"; publicstaticfinalStringBIZS="bizs"; publicstaticfinalStringTITLES="titles"; publicstaticfinalStringCOLUMNS="columns"; publicstaticfinalStringCURRUSER="currUser"; publicstaticfinalStringCURRPERM="currPerm"; publicstaticfinalStringTASK_EXECUTOR="taskExecutor"; publicstaticfinalStringDELIMITER="delimiter"; publicstaticfinalStringOPERNAME="operName"; } currParams.remove(varStr); currParams.remove(index); currParams.remove(index+"_"); } }
附上pom文件
4.0.0 com.rd parser jar 1.0-SNAPSHOT myparser http://maven.apache.org dom4j dom4j 1.6.1 opensymphony ognl 2.6.11 commons-collections commons-collections 3.2.1 commons-lang commons-lang 2.6 junit junit 3.8.1 test src/main/java **/*.xml src/main/resources **/* ${project.basedir}/src/test/java ${project.basedir}/src/test/resources org.apache.maven.plugins maven-compiler-plugin 3.1 1.8 UTF-8
以上这篇自己动手实现mybatis动态sql的方法就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持毛票票。