一個jsqlparse+git做的小工具幫我節省時間摸魚

背景

前些時間做了個小工具解決了團隊內數據庫腳本檢驗&多測試環境自動執行的問題,感覺挺有意思,在這跟大家分享一下。

工具誕生之前的流程是這樣:

1.開發人員先在開發環境編寫腳本&執行;

2.執行沒問題之後記錄到代碼目錄下的upgrade目錄;

3.提測時手動將upgrade目錄下的腳本文件在測試庫執行。

 

大概長這樣

 

 這套流程在我之前就有了,剛進來的時候感覺有點low,畢竟老東家解決這類問題是通過一款自研的數據庫自動化運維平臺-iDB,其誕生的目的是“解決絕大部分重複、複雜的數據庫運維工作 ,滿足業務對數據庫信息查詢和快速變更需求,藉此提升研發效率,保證數據庫操作符合審計要求,有可追溯的變更和審覈日誌”,內心一度排斥過一段時間,後來轉念一想這套手工機制對於目前的團隊規模來說是夠用,況且沒有配備dba,搞一套iDB上來誰審覈誰呢?想到這,心裏自然也就釋懷了,不用過於追求太時尚的工具、技術,夠用就可以了。

問題出現

伴隨着近兩年業務快速發展,團隊也迎來了擴編,往往在這種時候就容易出一些低級故障,俗話說越忙越亂。

遇到過哪些問題呢?

0.代碼提測了,但是腳本忘記執行了,測試走流程的時候發現有報錯然後反饋給開發處理,耽誤進度;

1.刷數據未加where條件,導致測試環境崩潰,有一次還波及了線上,幸好只是一張配置表,從其他私有化環境快速同步一份即可,但也是心驚肉跳,要是業務表可能團隊要團滅了;

2.一些高危sql,比如drop table if exists,原意是想順暢的建一張新表,但是誰能保證同樣的語句不會再出現?

3.一些高級語法導致部分私有化環境不兼容,一般情況下開發執行一些數據庫操作都是直接通過navicat等工具在開發庫執行,然後再把工具生成的腳本記錄下來,但是也有當時沒整理,發版時再整理的情況,這時只能手寫sql了,不排除會寫一些高級的語法,導致發版時部分環境失敗的情況,因爲數據庫版本有差異(有些客戶分配的庫,統一版本比較難);

人肉解決

最開始研發經理是專門安排了一個測試同學去處理這事,他需要定時做以下事項:

1.檢查是否有新腳本提交,如果有就繼續後續流程;

2.檢查是否爲高危腳本,如果高危就線下告知相關的開發整改,否則就繼續後續流程;

3.在navicat等工具中執行,如果執行失敗了就線下告知相關的開發處理。

 

起初幾天確實是解決了前面提到的一些問題,但是人畢竟不是機器,會忘記、會疲倦、會煩躁、會走神,兩週以後“人肉”方案又出現一些新問題:

1.忙的時候會忘;

2.頻率不好把握,幾小時一次太慢了,幾分鐘一次人會崩潰;

3.這個活太low了,臨時幹幾天還行,長期沒人願意幹;

 

機器解決

機器不知疲倦、一絲不苟、戒驕戒躁,最適合幹這類重複性而且枯燥的活,鑑於此我利用半天時間構思&開發了一個小工具用來解決這一問題,解救了那個悲慘的測試同學。

 這個工具需要具備以下特點:

1.定時拉取代碼判斷腳本文件是否有變化;

2.如果腳本文件變化了解析腳本看是否有語法錯誤,如果有語法錯誤發送郵件給提交人;

3.如果沒有語法錯誤判斷是否有高危語句,如果有高危發郵件給提交人和研發組長&經理們;

4.一切正常,開始執行sql語句,執行結果需要發郵件給相關人員,需要避免重複執行;

 

接下來一步步看如何解決上面的問題:

1.定時拉取代碼

這個比較簡單,因爲是運行在我的開發機器上,定時使用Runtime執行git pull即可。

Process process = Runtime.getRuntime().exec("git pull ",null,new File(代碼目錄));

2.判斷腳本文件是否發生變化

記錄腳本文件的md5,拉取代碼以後計算md5是否發生變化。

3.語法解析

利用jsqlparser工具將腳本文件內容解析爲Statements對象,代表一組解析之後的sql語句對象,如果有語法錯誤jsqlparser會拋出異常,異常信息中包含具體的行號和錯誤信息,以下面這組sql語句爲例:

String sql = "ALTER TABLE `wf_position` ADD COLUMN `c1` VARCHAR (10);" +
                "ALTER TABLE `wf_position` ADD COLUNM `c2` VARCHAR (10)";
CCJSqlParserUtil.parseStatements(sql);

解析的時候會拋出如下異常,很明顯是因爲COLUNM寫錯了

net.sf.jsqlparser.JSQLParserException: Encountered unexpected token: "`c2`" <S_QUOTED_IDENTIFIER>
    at line 1, column 93.
Was expecting:
    "COMMENT"
  at net.sf.jsqlparser.parser.CCJSqlParserUtil.parseStatements(CCJSqlParserUtil.java:188)

至此我們已經做完了語法解析,但是怎麼根據拋出的語法錯誤找到對應的提交人呢?這裏分兩步完成:

3.1正則匹配出異常堆棧中的line

Pattern pattern = Pattern.compile("line (\\d+), column (\\d+)");
try{
    CCJSqlParserUtil.parseStatements(sql);
}catch(exception){
  String message = exception.getMessage();
  Matcher m = pattern.matcher(message);
  int line = -1;
  int column = -1;
  while(m.find()){
      int groupCount = m.groupCount();
      if(groupCount > 0){
          line = Integer.parseInt(m.group(1));
          column = Integer.parseInt(m.group(2));
          break;
      }
  }
}

3.2前一步的line可以對應到腳本文件中的行,利用git blame命令可以獲得對應行的提交記錄,裏面包含提交者的姓名和郵箱  

String blameParams = scriptFile.getName()+" -L "+lineNum+","+lineNum;
Process process = Runtime.getRuntime().exec("git blame "+blameParams,null,new File(scriptFile所在目錄));

輸出格式如下,紅框所示區域就是提交者的郵箱(組內約定git user.name必須攜帶郵箱,所以這裏能拿到)  

 

 接下來就各種的截取,最終提取郵箱,比較簡單,這裏就不囉嗦了。

4.高危判斷

遍歷所有的Statement對象,目前主要識別三類:

1.drop table

2.update不帶where條件

3.delete不帶where條件

Statements statements = CCJSqlParserUtil.parseStatements(sql);
List<RiskScript> riskScripts = new ArrayList<>()
for(Statement statement : statements.getStatements()){
    RiskScript riskScript = new RiskScript();
    
    //drop table
    if(statement instanceof Drop 
      && (((Drop) statement).getType().equals("table") 
      || ((Drop) statement).getType().equals("TABLE"))){
        riskScript.setErrorMsg("drop table高危,放棄自動執行,請確認,如有需要請手動執行");
        riskScript.setSql(statement.toString());
        this.riskScripts.add(riskScript);
        continue;
    }
    
    //update不帶where條件
    if(statement instanceof Update 
        && ((Update) statement).getWhere() == null){
        riskScript.setErrorMsg("update 不帶where條件,放棄自動執行,請確認,如有需要請手動執行");
        riskScript.setSql(statement.toString());
        this.riskScripts.add(riskScript);
        continue;
    }
    
    //delete不帶where條件
    if(statement instanceof Delete && 
       ((Delete) statement).getWhere() == null){
        riskScript.setErrorMsg("delete 不帶where條件,放棄自動執行,請確認,如有需要請手動執行");
        riskScript.setSql(statement.toString());
        this.riskScripts.add(riskScript);
        continue;
    }

}

5.避免重複執行

這個比較簡單,每次執行完以後記錄下每條sql的執行歷史,執行前判斷。

效果展示

 

 

總結

是不是應該引入一個高大上的數據庫自動化運維平臺呢?我的判斷是暫時不需要,究其原因我認爲有以下幾點:

1.雖說沒有專人審覈那麼精細,但依賴工具把一些高危的sql已經排除在外,已然是夠用了,想想老東家爲什麼需要dba嚴格審覈是因爲toC的數據量較大,字段類型、索引等對性能的影響不容小覷,而目前toB的業務,數據量不會特別大,字段類型、索引等因素對性能的影響姑且可以忽略,起碼現階段差別不大;

2.每個迭代產生的腳本變更較多,如果引入太繁瑣的流程,對開發效率是一種制約,不求設計出精妙的表結構,只願你不要寫出“團滅”的腳本;

 

推薦閱讀

https://jsqlparser.sourceforge.net/home.php

https://www.w3cschool.cn/doc_git/git-git-blame.html

https://www.cnblogs.com/zhengyun_ustc/p/idb.html

小區裏隨手一拍

  

  

 

 

  

  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章