mybatis如何做到執行string形式的sql文件


聲明 : 此博客為博主原創,轉載請說明出處。

 

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"
        }
    ]
}
JSON格式數據

 

  

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 當中去了,大於號 小於號是要轉義成 &gt; &lt; 等的,那么我要洗個 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);*/
    }
測試代碼

 


  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM