项目使用mybatis操作数据库。最初支持的数据库是oracle,后来需求要同时支持mysql。问题来了,oracle和mysql或多或少有差别,最初的设计没有考虑到兼容支持多种数据库,设计上严重依赖oracle(比如序列生成主键)。之前《mybatis Oracle到Mysql迁移 记录》有稍微记录了一下,使用的方法是:1)建表和函数|存储过程 模拟oracle的sequence,2)使用spring profile切换配置,一个项目里有2套xml。
组长说这样不行2套xml太麻烦。但是用VendorDatabaseIdProvider另外写有差异的sql又有点乱。说要弄一个规范,执行的时候再按照数据库类型生成sql。那意思就是要做编译,一个自定义的sql语法转换成特定数据库的sql语法?这样有2个地方可以进行修改:一种SqlSessionFactoryBean生产configuration后的mappingstatements里修改,一种mybatis的StatementHandle或Executor 的插件拦截。能力有限写不出性能很好的编译器,使用第二种的话更影响执行速度。第一种的话mappingstatments, sqlNode的字段都是私有需要反射,强行不好。 然后点来点去点来点去点到xml,瞄到动态标签和ognl。白想那么多了,可以使用动态标签静态函数生成特定的sql片段。但是mybatis不支持自定义标签,需要改源码。使用foreach标签比较繁琐还是可以用的<foreach item="item" collection="#{'1': @Generator@nextvalSql('序列名')}">${item}</foreach> (可能还有其他比较简洁的写法,试了几种目前没想到)。最后还是决定自定义一个标签再搭配一些sql转换的静态方法。
关于mybatis解析sql过程可以参考Mybatis解析动态sql原理分析 这篇文章。
目标: 自定义标签<value expr="@Generator@nextvalSql('序列名')"/> 返回表达式的字符串。
5个步骤
1)修改org.apache.ibatis.builder.xml下的mybatis-3-mapping.dtd,加入value的校验规则,参看include标签的规则定义。下面是片段
<!ELEMENT include EMPTY> <!ATTLIST include refid CDATA #REQUIRED > <!ELEMENT value EMPTY> <!ATTLIST value expr CDATA #REQUIRED > <!ELEMENT sql (#PCDATA | include | value | trim | where | set | foreach | choose | if | bind)*> <!ATTLIST sql id CDATA #REQUIRED lang CDATA #IMPLIED databaseId CDATA #IMPLIED >
2)实现接口org.apache.ibatis.scripting.xmltags.SqlNode ValueSqlNode 翻译处理value标签。
public class ValueSqlNode implements SqlNode { private final String expression; public ValueSqlNode(String expression) { this.expression=expression; } @Override public boolean apply(DynamicContext context) { context.appendSql(OgnlCache.getValue(expression, context.getBindings()).toString()); return true; } }
3)XMLScriptBuilder添加nodeHandler
NodeHandler nodeHandlers(String nodeName) { Map<String, NodeHandler> map = new HashMap<String, NodeHandler>(); map.put("trim", new TrimHandler()); map.put("where", new WhereHandler()); map.put("set", new SetHandler()); map.put("foreach", new ForEachHandler()); map.put("if", new IfHandler()); map.put("choose", new ChooseHandler()); map.put("when", new IfHandler()); map.put("otherwise", new OtherwiseHandler()); map.put("bind", new BindHandler()); map.put("value",new ValueHandler()); return map.get(nodeName); } private class ValueHandler implements NodeHandler { public ValueHandler() { // Prevent Synthetic Access 求问这个何解? } @Override public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) { final String expression = nodeToHandle.getStringAttribute("expr"); final ValueSqlNode node = new ValueSqlNode(expression); targetContents.add(node); } }
4)一些sql转换的静态方法。代码也很简单:Generator.java , Tanslator
5)使用,用正则匹配替换原来的sql片段
批量处理插入
<insert id="xx" flushCache="true" parameterType="java.util.List" >
<value expr="@Generator@batchBegin()"/>
<foreach collection="list" item="item" index="index" separator=";" close=";">
INSERT INTO xxxx(xxx,xxx) values
(#{item.xxxx},#{item.xxxx})
</foreach>
<value expr="@Generator@batchEnd()"/>
</insert>
字符串连接
JS.SCHEMA_CODE like '%'||#{schemaCode}||'%'
改成
JS.SCHEMA_CODE like <value expr="@Generator@concat('\'%\'','#{schemaCode}','\'%\'')"/>
关键字处理
T."ORDER"
改成
T.<value expr="@Generator@keywordEscape('ORDER')"/>
返回记录个数限制
AND ROWNUM<2
改成
<value expr="@Generator@limit(n,true)"/>
ROWNUM<2
改成
<value expr="@Generator@limit(n,false)"/>
获取序列下一个值
SEQ_FILE_ID.NEXTVAL
改成
<value expr="@Generator@nextval('SEQ_FILE_ID')"/>
获取序列下n个值
SELECT SEQ_OPERATE_PARAMS_ID.NEXTVAL FROM DUAL CONNECT BY LEVEL<=#{n}
改成
<value expr="@Generator@nextvalN('SEQ_OPERATE_PARAMS_ID','#{n}')"/>
日期格式
to_date( #{qRuntimeLastBegin},'yyyy-MM-dd HH24:mi:ss')
改成
<value expr="@Generator@strToDate('#{qRuntimeLastBegin}','yyyy-MM-dd HH:mm:ss')"/>
to_date( #{qRuntimeLastBegin},'yyyy-MM-dd')
改成
<value expr="@Generator@strToDate('#{qRuntimeLastBegin}','yyyy-MM-dd')"/>
当前日期
sysdate
改成
<value expr="@Generator@sysdate()"/>