聲明 : 此博客為博主原創,轉載請說明出處。
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);*/ }