声明 : 此博客为博主原创,转载请说明出处。
1. 项目需求背景
有一个这样的功能,前台传递 sql 形式的字符串 (符合mybatis的dtd格式),但是呢,前台是不想转义 大于号、小于号 等等
这些被mybatis的 xml 所引用的特殊字符串, 然后后台我们就可以存取到数据库当中去,保留这个格式的sql,然后在 【执行sql】
这个功能按钮上,前台点击就可以执行这段sql了。
JSON格式数据

{ "qry_db": "jlerp", "class_id": 222, "qry_id": null, "qry_type" : "sql", "qry_name": "查询会计科目余额aaa", "qry_file": "查询会计科目余额", "qry_db": "ccc", "qry_desc": "查询科目余额表中会计科目余额", "qry_sql": "select nvl(sum(gb.period_net_dr),0) >=period_net_dr\n from gl_balances gb, \n gl_code_combinations gcc\n where gb.code_combination_id=gcc.code_combination_id\n and gcc.summary_flag = 'N'\n and gb.set_of_books_id = 2\n and gb.period_name = #{period}\n and gcc.segment1 like '${segment1}%'\n<if test=\"segment2!=''\">\n and gcc.segment2=#{segment2}\n</if>\n and gcc.segment3 like '${segment3}%'", "in": [ { "dict_id": undefined, "authtype_id": "ou", "in_name": "b", "dict_name": "供应商aaa", "authtype_desc": null, "datatype": "number", "qry_id": null, "in_id": "period_num", "validate": null }, { "dict_id": null, "authtype_id": null, "in_name": "a", "dict_name": null, "authtype_desc": null, "datatype": "string", "qry_id": null, "in_id": "period_year", "validate": null }, { "dict_id": 3, "authtype_id": "ou", "in_name": "c", "dict_name": "部门", "authtype_desc": null, "datatype": "date", "qry_id": null, "in_id": "segment1", "validate": null }, { "dict_id": 4, "authtype_id": "dept", "in_name": "d", "dict_name": "公司", "authtype_desc": null, "datatype": "string", "qry_id": null, "in_id": "segment3", "validate": null }, { "dict_id": 5, "authtype_id": null, "in_name": "e", "dict_name": "项目", "authtype_desc": null, "datatype": "string", "qry_id": null, "in_id": "segment5", "validate": null } ], "out": [ { "datatype": null, "out_name": "net_bal", "link": {"qry_id": null, "link_qry_id": "200", param: [ { link_in_id:'vendor_id', link_in_id_value_type:'out', link_in_id_value:'vendor_id' }, {link_in_id:'date', link_in_id_value_type:'out', link_in_id_value:'vendor_id' }, {link_in_id:'name', link_in_id_value_type:'out', link_in_id_value:'vendor_id' }, ]}, "qry_id": null, "out_id": "net_bal", width: 0.12, render: "render1" } ] }
2. 分析
2.1 sql存到xml?
若我们把sql存放到 一个xml 当中,这样做最简单了,然后直接 刷新当前sqlSession , 即是再启动一次连接 (为什么要这么做?),众所周知,在项目启动的时候,mybatis的 sqlSessionFactory 会加载 configuration,这个configuration 可能是 从 [mybatis-config.xml] 当中读取到的,然后用来构造 sqlSessionFactory , 当然,我在这里就直接自己 new Configuration 来构造了,好了 我想说的是,无论怎样, configuration 会有 mapper 扫描包的信息,所以一旦有变动,我们必须 再次重新加载一次 此配置。 要不然,sqlSession.selectList( statementId,Class clazz ) 这个当中的 statmentId 你怎么找得到呢? 这个id 被放在配置类的 【MappedStatement】 当中了啊。
然后还要考虑到,前端传递的 sql 字符串当中 有 大于号 小于号,并且有 <if><foreach> 等等 OGNL 表达式,我们要是把其存放到 xml 当中去了,大于号 小于号是要转义成 > < 等的,那么我要洗个 regx 正则来完成吗? (PS:哎,能力有限,场景无限,这个方法我觉得不行),虽然我们加 <cache> 标签啊,指定sql 为存储过程等啊在xml 当中要方便很多,但是我们发现放进去会存在较大问题,这个还得慎重考虑。
2.2 研究下 注解形式SQL?
诶,我们发现啊 现在的 mybatis 其实还支持 注解形式的sql, 类似于 : @Select("<script>SELECT ...</script>")
在这个 SELECT 当中的可以填写的sql的 标签有 :
如果nodeHandlers
在课堂中检查方法org.apache.ibatis.builder.BuilderException
,将注意到支持的元素有:
- trim
- where
- set
- foreach
- if
- choose
- when
- otherwise
- bind
所以,我们写sql要注意点,这种注解 SQL 带来的不便就是不能与外界绑定 嵌套SQL 。但是这又有何妨呢?顶多我们把sql都写全。
然后,我们怎么仿照这种形式结合mybatis玩转呢? 下面步骤 3就来解释详细步骤。
3. 基于 String 形式的 sql 动态构建
首先,我们要把 sql 带上 <script> 标签,这样才能被解析为 dynamicSql (因为加上这个标签,才能帮助我们解析出 OGNL表达式的 结果),
其实,我们再来构建 configuration 的 mappedStatment 。为什么构建这玩意??? 大家可以想象一下 selectList 当中的 第一个入参 statementId
这是个撒玩意? 看源码
@Override public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
我们看上面的 来自 org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds)
第一个参数是什么? 是 configuration 当中的 mappedStatement 属性的key,这是个map 结构。所以笔者的用心在于 如何构建出这个 mappedStatement 呢?
不妨直接 看 mappedStatement 这个类的构建方法 -》
org.apache.ibatis.mapping.MappedStatement.Builder
public static class Builder { private MappedStatement mappedStatement = new MappedStatement(); public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) { mappedStatement.configuration = configuration; mappedStatement.id = id; mappedStatement.sqlSource = sqlSource; mappedStatement.statementType = StatementType.PREPARED; mappedStatement.parameterMap = new ParameterMap.Builder(configuration, "defaultParameterMap", null, new ArrayList<ParameterMapping>()).build(); mappedStatement.resultMaps = new ArrayList<ResultMap>(); mappedStatement.sqlCommandType = sqlCommandType; mappedStatement.keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; String logId = id; if (configuration.getLogPrefix() != null) { logId = configuration.getLogPrefix() + id; } mappedStatement.statementLog = LogFactory.getLog(logId); mappedStatement.lang = configuration.getDefaultScriptingLanguageInstance(); }
这个方法的这个 ,哦,不对,是静态内部类了,这个玩意能够构建一些你能想到的mappedStatement,比如你要加cache ,比如你要知道自增长返回主键,你要指定keyGenerate,你想把自己的
此sql 指定为是执行 CALLABLE 存储过程,这个类都能帮你完成。
至此,我们的可行性步骤分析出来了 :
前台传递 sql -----》 后台 mybatis的configuration 的mappedStatment 创建一个新的 -——》 sqlSession再次执行此 mappedStatment 的ID 即可。
如何构建 mappedStatent,我们可以直接使用 Builder 静态内部类来完成,并适当的指定自己的方式。那么我们就回到如何构建这个话题了,最基本的 我们需要一个 SqlSource 对吧,还有
configuration对象,以及自己想指定的 statementId , 还有个 此sql的类型,是 select?insert?update? 还是其他?
嗯,然后构建好了,我们就能 往当前 configuration当中注入了,待会,我们怎么构建sqlSource? 这个就是我们写成 <script> 标签的目的所在了。
我们可以使用
LanguageDriver languageDriver = configuration.getDefaultScriptingLanguageInstance();
来得到我们的sqlSource。OK,说了这么多,下面我就贴出代码,贴出用例图。
4. 分析图
这是一个mybatis执行过程图,下面我贴出我的 代码过程图 。
5. 代码
package root.report.util; import org.apache.ibatis.mapping.*; import org.apache.ibatis.scripting.LanguageDriver; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.RowBounds; import org.apache.ibatis.session.SqlSession; import org.apache.log4j.Logger; import org.apache.poi.ss.formula.functions.T; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * @Auther: pccw-lxf * @Date: 2018/11/9 16:14 * @Description: */ public class ExecuteSqlUtil { private static Logger log = Logger.getLogger(ExecuteSqlUtil.class); /** * * 功能描述: * 对符合 mybatis.dtd形式的sql进行动态sql解析并执行,返回Map结构的数据集 * @param: executeSql 要执行的sql , sqlSession 数据库会话,namespace 命名空间,mapper_id mapper的ID,bounds 分页参数 * @return: * @auther: * @date: 2018/11/9 16:17 */ public static List<?> executeDataBaseSql(String executeSql, SqlSession sqlSession, String namespace, String mapper_id, RowBounds bounds, Class<?> clazz,Object param){ // 1. 对executeSql 加上script标签 StringBuffer sb = new StringBuffer(); sb.append("<script>"); sb.append(executeSql); sb.append("</script>"); log.info(sb.toString()); Configuration configuration = sqlSession.getConfiguration(); LanguageDriver languageDriver = configuration.getDefaultScriptingLanguageInstance(); // 2. languageDriver 是帮助我们实现dynamicSQL的关键 SqlSource sqlSource = languageDriver.createSqlSource(configuration,sb.toString(),clazz); // 泛型化入参 newSelectMappedStatement(configuration,namespace+"."+mapper_id,sqlSource,clazz); List<?> list = sqlSession.selectList(namespace+"."+mapper_id,param,bounds); return list; } // private static MappedStatement newSelectMappedStatement(Configuration configuration,String msId, SqlSource sqlSource, final Class<?> resultType) { // 加强逻辑 : 一定要防止 MappedStatement 重复问题 MappedStatement msTest = null; try{ synchronized (configuration) { // 防止并发插入多次 msTest = configuration.getMappedStatement(msId); if (msTest != null) { configuration.getMappedStatementNames().remove(msTest.getId()); } } }catch (IllegalArgumentException e){ log.info("没有此mappedStatment,可以注入此mappedStatement到configuration当中"); } // 构建一个 select 类型的ms ,通过制定SqlCommandType.SELECT MappedStatement ms = new MappedStatement.Builder( // 注意!!-》 这里可以指定你想要的任何配置,比如cache,CALLABLE, configuration, msId, sqlSource, SqlCommandType.SELECT) // -》 注意,这里是SELECT,其他的UPDATE\INSERT 还需要指定成别的 .resultMaps(new ArrayList<ResultMap>() { { add(new ResultMap.Builder(configuration, "defaultResultMap", resultType, new ArrayList<ResultMapping>(0)).build()); } }) .build(); synchronized (configuration){ configuration.addMappedStatement(ms); // 加入到此中去 } return ms; } }
6. 测试调用结果:
测试所使用的代码是 :

public static void main(String[] args){ // ApplicationContext context = // PropertiesTest propertiesTest = new PropertiesTest(); // System.out.println("aaa"+propertiesTest.getUser()); SqlSession sqlSession = DbFactory.Open(DbFactory.FORM); String sql2 = "<script>select * from func_dict_value where dict_id=#{dict_id}" + " and value_code='${value_code}'"; String sql = "<script>select * from func_dict_value where dict_id=#{dict_id,jdbcType=INTEGER}" + " <if test=\"value_code == null\"></if>" + " <if test=\"value_code != null\">and value_code='${value_code}'</if></script>"; String sql3 = "<script>select a.dict_id,b.dict_name,a.value_code,a.value_name from func_dict_value a,func_dict b" + " where a.dict_id=#{dict_id,jdbcType=INTEGER} and a.dict_id=b.dict_id and a.dict_id>12</script>"; String sql4 = "<script>select * from func_dict_value where dict_id>=13 "+ " <if test=\"value_code == null\"></if>" + " <if test=\"value_code != null\">and value_code='${value_code}'</if></script>"; Configuration configuration = sqlSession.getConfiguration(); LanguageDriver languageDriver = configuration.getDefaultScriptingLanguageInstance(); // languageDriver.createParameterHandler(); SqlSource sqlSource = languageDriver.createSqlSource(configuration,sql4,Map.class); MappedStatement ms = newSelectMappedStatement(configuration,"testId",sqlSource,Map.class); SqlSource sqlSource2 = languageDriver.createSqlSource(configuration,sql3,Map.class); MappedStatement msCopy = newSelectMappedStatement(configuration,"testId",sqlSource2,Map.class); // configuration.setCacheEnabled(true); // configuration.addCache(ms.getCache()); Map<String,Object> parameterMap = new HashMap<>(); parameterMap.put("dict_id",13); parameterMap.put("value_code","支撑网"); List<Map<String,Object>> mapList = sqlSession.selectList("testId",parameterMap); System.out.println("map1->size"+mapList.size()); System.out.println("-----------"); String sql5 = "select a.dict_id,b.dict_name,a.value_code,a.value_name from func_dict_value a,func_dict b" + " where a.dict_id=#{dict_id,jdbcType=INTEGER} and a.dict_id=b.dict_id and a.dict_id>12"; List<Map<String,Object>> mapList2 = (List<Map<String,Object>>) ExecuteSqlUtil.executeDataBaseSql( sql5,sqlSession,"testDD","map11",null,Map.class,parameterMap); System.out.println("map2->size"+mapList2.size()); /* BoundSql boundSql = ms.getBoundSql(parameterMap); String finalSql = boundSql.getSql(); BoundSql boundSqlCopy = msCopy.getBoundSql(parameterMap); System.out.println("boundSqlCopy->"+boundSqlCopy.getSql()); System.out.println("boundSql->"+finalSql); System.out.println("我写的SQL->"+sql);*/ }