JSqlParse完整介紹
1、jsqlparse介紹
JSqlParse是一款很精簡的sql解析工具,它可以將常用的sql文本解析成具有層級結(jié)構(gòu)的“語法樹”,我們可以針對解析后的“樹節(jié)點(也即官網(wǎng)里說的有層次結(jié)構(gòu)的java類)”進行處理進而生成符合我們要求的sql形式。
官網(wǎng)給的介紹很簡潔:JSqlParser 解析 SQL 語句并將其轉(zhuǎn)換為 Java 類的層次結(jié)構(gòu)。生成的層次結(jié)構(gòu)可以使用訪問者模式進行訪問(官網(wǎng)地址:JSqlParser - Home)。
官網(wǎng)的介紹即是該中間件的全部,雖然介紹很短,但是其功能著實強悍。
2、jar包結(jié)構(gòu)介紹
這里我使用的是4.3版本,maven依賴如下:
<dependency> <groupId>com.github.jsqlparser</groupId> <artifactId>jsqlparser</artifactId> <version>4.3</version> </dependency>
JSqlParse的總體代碼量不大,結(jié)構(gòu)也很簡單,其項目整體結(jié)構(gòu)圖如下:
可以看到其總共只有五個大的包,各個包的功能定義也很清晰:
- expression:包含表達(dá)式相關(guān)的類和接口,可以簡單看做sql解析后的組成對象之一。如果需要對sql進行一些更改變換,基本都會涉及到這個包。
- parse:JSqlParse最核心的包,這個包里的類實現(xiàn)了sql的解析,進而我們才可以對解析后的sql(“java類”)做各種自定義處理。雖然這個包是最核心的包,但如果純粹從使用角度上來說可以不必太在意它,除非我們想深入了解sql解析的過程。
- schema:可以理解為模式,即定義一些和數(shù)據(jù)中概念相對應(yīng)的類,如表Table、列Column等。
- statement:sql語句也分很多種,如增刪改查等,這個包下就對應(yīng)各種解析后java類所組成的sql語句,其內(nèi)部結(jié)構(gòu)如下:
util:JSqlParse解析中用到的工具類,基本也不用太在意,不過有個TablesNamesFinder類則具有較強的參考價值。
其中該組件最厲害的地方是parse包的解析,即將sql解析成一組有血緣(或者成層級嵌套)的對象集,要了解這塊,需要對antlr有較深的理解才行。感興趣的可以專門去看一下。不過如果我們只是使用,就不需要專門了解語法的解析了,我們只需要知道如何對解析后的sql進行修改即可。下面我會先講解大致大體的如何去做,最后一節(jié)再講解其中的一些原理。
3、使用介紹
sql語句的修改是通過實現(xiàn)對應(yīng)的訪問者接口實現(xiàn)的,比如你想對from之后的table名稱進行處理,那么你只需要實現(xiàn) FromItemVisitor 接口并重寫 訪問Table的方法即可。如果你想對sql中的函數(shù)進行處理,那么你只需要實現(xiàn)ExpressionVisitor接口并重寫其中對應(yīng)的方法接口即可。
是不是很簡單,不過這里有個問題就是我們?nèi)绾伟盐覀冏远x的訪問者傳給解析后的sql對象。因為解析后的sql對象是具有層級的,我們要處理的對象很有可能在最內(nèi)層。如果你想自己遍歷解析后的sql對象,然后把訪問者傳給特定的對象,這個方法雖然可行,但只能用于于不包含嵌套或者嵌套層次不深的sql語句,一旦包含嵌套語句或者sql語句很復(fù)雜,你很難一層層的去處理。
正確的做法是從sql解析后的第一層開始,將每個遇到的相關(guān)訪問者接口都實現(xiàn)一遍,這樣在獲得解析后的sql對象后,直接就可以將自定義訪問者對象傳進去,也不需要我們自己一層層去剝開sql對象。我們只需要專注于自己需要的重寫的訪問者方法即可。展示下我實際中變更select語句用到的一些訪問者接口,貼出來給大家看下:
StatementVisitor, SelectVisitor, SelectItemVisitor, FromItemVisitor, GroupByVisitor, ExpressionVisitor,ItemsListVisitor
這些訪問者接口我也不是一次性全實現(xiàn)的,而是從最外層的StatementVisitor開始,一點點加的,后續(xù)如果有需要可能還會再加,這個過程是一個比較繁瑣的逐漸深入和查漏補缺的過程,所以在sql語法替換時一定要保持謹(jǐn)慎。但這也給出一個建議,千萬不要試圖追蹤各個模塊的迭代處理
情況,這樣很容易把你繞進去,你只需關(guān)注當(dāng)前所在的模塊即可,其它的通過accpet交給其它對應(yīng)的visitor去處理。
下面以更改select類型語句,將from之后table表名稱從table1改為table2,和將max函數(shù)修改為min函數(shù)作為目標(biāo),我們來實現(xiàn)下這個需求:
首先是流程代碼,如下:
public class Main { public static void main(String[] args) throws Exception{ //1、獲取原始sql輸入 String sql = "select max(age) from table1"; System.out.println("old sql:[{}]"+sql); //2、創(chuàng)建解析器 CCJSqlParserManager mgr = new CCJSqlParserManager(); //3、使用解析器解析sql生成具有層次結(jié)構(gòu)的java類 Statement stmt = mgr.parse(new StringReader(sql)); //4、將自定義訪問者傳入解析后的sql對象 stmt.accept(new MyJSqlVisitor()); //5、打印轉(zhuǎn)換后的sql語句 System.out.println("new sql:[{}]" + stmt.toString()); } }
其次是最核心的訪問者接口實現(xiàn)類,這里為了便于向大家展示sql修改的過程,我們一個個的添加接口:
首先是stmt.accept,這個對象接收的是一個StatementVisitor,所以我們在自定義的類MyJSqlVisitor中先實現(xiàn)這個接口,因為我們要改的是select類語句,所以我們可以找到對應(yīng)的visitor方法(至于為什么這個接口就是跟selet語句相關(guān),一個是根據(jù)方法名推斷,一個是debug查看,debug可以看到sql語句一層層的對象,再細(xì)就不啰嗦了,實戰(zhàn)個幾次就懂了)
public class MyJSqlVisitor implements StatementVisitor { @Override public void visit(Select select) { SelectBody selectBody = select.getSelectBody(); if (selectBody != null) { selectBody.accept(this); } } }
注意下,這里我只列出了一個實現(xiàn)的方法,是因為篇幅有限,我只截取了實現(xiàn)改動的方法,后續(xù)也是只展示實現(xiàn)了變動的代碼,接著可以看到selectBody也需要一個SelectVisitor類型的訪問者,所以我們再MyJSqlVisitor中添加實現(xiàn)該接口:
public class MyJSqlVisitor implements StatementVisitor, SelectVisitor { @Override public void visit(Select select) { SelectBody selectBody = select.getSelectBody(); if (selectBody != null) { selectBody.accept(this); } } @Override public void visit(PlainSelect plainSelect) { /** 處理select字段 */ List<SelectItem> selectItems = plainSelect.getSelectItems(); if (selectItems != null && selectItems.size() > 0) { selectItems.forEach(selectItem -> { selectItem.accept(this); }); } /** 處理表名或子查詢 */ FromItem fromItem = plainSelect.getFromItem(); if (fromItem!=null){ fromItem.accept(this); } } }
該接口對應(yīng)的visit方法中 selectItem和fromItem同時還需要SelectItemVisitor,F(xiàn)romItemVisitor兩種訪問者,所以我們先來實現(xiàn)SelectItemVisitor這個接口:
public class MyJSqlVisitor implements StatementVisitor, SelectVisitor ,SelectItemVisitor { @Override public void visit(Select select) { SelectBody selectBody = select.getSelectBody(); if (selectBody != null) { selectBody.accept(this); } } @Override public void visit(PlainSelect plainSelect) { /** 處理select字段 */ List<SelectItem> selectItems = plainSelect.getSelectItems(); if (selectItems != null && selectItems.size() > 0) { selectItems.forEach(selectItem -> { selectItem.accept(this); }); } /** 處理表名或子查詢 */ FromItem fromItem = plainSelect.getFromItem(); if (fromItem!=null){ fromItem.accept(this); } } // 這個方法我們并沒有考慮完全,比如select項目中可能有子查詢還有可能有case表達(dá)式,這些我們都沒考慮,這里只是先展示了一種思路。 @Override public void visit(SelectExpressionItem selectExpressionItem) { if (Function.class.isInstance(selectExpressionItem.getExpression())) { Function function = (Function) selectExpressionItem.getExpression(); function.accept(this); } } }
可以看到function.accept還需要一個ExpressionVisitor,這里我們接著實現(xiàn)它:
public class MyJSqlVisitor implements StatementVisitor, SelectVisitor ,SelectItemVisitor, ExpressionVisitor { @Override public void visit(Select select) { SelectBody selectBody = select.getSelectBody(); if (selectBody != null) { selectBody.accept(this); } } @Override public void visit(PlainSelect plainSelect) { /** 處理select字段 */ List<SelectItem> selectItems = plainSelect.getSelectItems(); if (selectItems != null && selectItems.size() > 0) { selectItems.forEach(selectItem -> { selectItem.accept(this); }); } /** 處理表名或子查詢 */ FromItem fromItem = plainSelect.getFromItem(); if (fromItem!=null){ fromItem.accept(this); } } // 這個方法我們并沒有考慮完全,比如select項目中可能有子查詢還有可能有case表達(dá)式,這些我們都沒考慮,這里只是先展示了一種思路。 @Override public void visit(SelectExpressionItem selectExpressionItem) { if (Function.class.isInstance(selectExpressionItem.getExpression())) { Function function = (Function) selectExpressionItem.getExpression(); function.accept(this); } } @Override public void visit(Function function) { if (function.getName().equalsIgnoreCase("max")){ function.setName("min"); } } }
至此,max轉(zhuǎn)min已經(jīng)結(jié)束,我們再回過頭實現(xiàn)FromItemVisitor接口:
public class MyJSqlVisitor implements StatementVisitor, SelectVisitor ,SelectItemVisitor, ExpressionVisitor,FromItemVisitor { @Override public void visit(Select select) { SelectBody selectBody = select.getSelectBody(); if (selectBody != null) { selectBody.accept(this); } } @Override public void visit(PlainSelect plainSelect) { /** 處理select字段 */ List<SelectItem> selectItems = plainSelect.getSelectItems(); if (selectItems != null && selectItems.size() > 0) { selectItems.forEach(selectItem -> { selectItem.accept(this); }); } /** 處理表名或子查詢 */ FromItem fromItem = plainSelect.getFromItem(); if (fromItem!=null){ fromItem.accept(this); } } // 這個方法我們并沒有考慮完全,比如select項目中可能有子查詢還有可能有case表達(dá)式,這些我們都沒考慮,這里只是先展示了一種思路。 @Override public void visit(SelectExpressionItem selectExpressionItem) { if (Function.class.isInstance(selectExpressionItem.getExpression())) { Function function = (Function) selectExpressionItem.getExpression(); function.accept(this); } } // 實現(xiàn)將max函數(shù)轉(zhuǎn)為min函數(shù) @Override public void visit(Function function) { if (function.getName().equalsIgnoreCase("max")){ function.setName("min"); } } //實現(xiàn)表名稱的更換 @Override public void visit(Table table) { if (table.getName().equalsIgnoreCase("table1")){ table.setName("table2"); } } }
至此,我們的兩個修改目標(biāo)已經(jīng)達(dá)成,運行main看下效果:
old sql:[{}]select max(age) from table1
new sql:[{}]SELECT min(age) FROM table2
Process finished with exit code 0
可以看到我們的目的實現(xiàn)了,不過這里請留意我們并沒有考慮子查詢等其它情況,這個demo只是展示一種修改思路,工作中具體的操作要考慮的比這細(xì)致的多。
使用建議:
1)一個個的添加接口,遇到什么類型的訪問者,加什么類型的實現(xiàn)接口,防止一次性加太多忘記實現(xiàn)邏輯。
2)不要試圖追蹤各個sql對象的迭代處理情況,這樣很容易把你繞進去,你只需關(guān)注當(dāng)前所在的方法模塊即可,其它的通過accpet交給其它對應(yīng)的visitor去處理即可。
3)不要試圖一次性實現(xiàn)所有的訪問者接口,根據(jù)需要進行實現(xiàn)
4)sql語法樹具有很強的層次性,當(dāng)被訪問者在進行處理時,要考慮到自己的子元素是不是也要進行迭代處理,如果需要的話,那么就調(diào)用對應(yīng)子元素的accpect方法,并將相關(guān)訪問者傳遞進去
5)如果沒有使用容器技術(shù),所有的訪問者接口盡量放在一個類中實現(xiàn),這樣當(dāng)有accept需要visitor對象的時候直接傳this就行。(我一開始沒有用容器管理bean,每個visitor接口我都單獨創(chuàng)建一個實現(xiàn)類,最后因為使用不到,造成迭代訪問時棧溢出錯誤)
4、核心原理介紹
這塊只是展示sql迭代訪問修改的原理,并不涉及將sql文本解析為對象類的原理。好了,進入正文。
要想理解sql迭代修改的原理,其實只要了解訪問者模式和多態(tài)這兩個知識點就行。如果不了解的可以先去查看對應(yīng)的知識點,然后再看下源碼仔細(xì)體會下。下面我會簡單介紹下,在前文我們也提過,要想修改sql,只需要實現(xiàn)對應(yīng)的訪問接口即可,然后將訪問者傳入被訪問的sql對象中。
在JSqlParse中,將解析后的sql對象看做被訪問者,我們自定義的visitor則看做訪問者。該組件同時將各類被訪問者和訪問者都抽象出了接口,我們代碼編輯時通過接口確定大體的執(zhí)行流程,在具體的代碼運行階段,就會通過多態(tài)尋找對應(yīng)的實現(xiàn)類。就拿demo中的statement來說,它是一個接口,但是運行的時候就會根據(jù)sql情況定位到具體的實現(xiàn)類,我們demo中對應(yīng)的具體實現(xiàn)類就是select對象,此時進入該對象查看具體的accept方法:
可以看到被訪問者調(diào)用的還是訪問者的visit方法,也就是我們對應(yīng)的重寫方法。以此類推,剩下的各個層級處理也是通過重復(fù)這個過程,所以想理解這個處理過程,一定要理解訪問者模式
到此這篇關(guān)于JSqlParse完整介紹的文章就介紹到這了,更多相關(guān)JSqlParse使用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于@SpringBootApplication詳解
這篇文章主要介紹了關(guān)于@SpringBootApplication的使用方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08Spring?Boot實現(xiàn)JWT?token自動續(xù)期的實現(xiàn)
本文主要介紹了Spring?Boot實現(xiàn)JWT?token自動續(xù)期,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-12-12聊聊maven的pom.xml中的exclusions標(biāo)簽的作用
這篇文章主要介紹了maven的pom.xml中的exclusions標(biāo)簽的作用,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12MybatisPlus中@TableField注解的使用詳解
這篇文章主要介紹了MybatisPlus中@TableField注解的使用詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09java實戰(zhàn)小技巧之優(yōu)雅的實現(xiàn)字符串拼接
字符串拼接是我們在Java代碼中比較經(jīng)常要做的事情,就是把多個字符串拼接到一起,這篇文章主要給大家介紹了關(guān)于java實戰(zhàn)小技巧之優(yōu)雅的實現(xiàn)字符串拼接的相關(guān)資料,需要的朋友可以參考下2021-08-08Spring MVC登錄注冊以及轉(zhuǎn)換json數(shù)據(jù)
本文主要介紹了Spring MVC登錄注冊以及轉(zhuǎn)換json數(shù)據(jù)的相關(guān)知識。具有很好的參考價值。下面跟著小編一起來看下吧2017-04-04java結(jié)合keytool如何實現(xiàn)非對稱簽名和驗證詳解
這篇文章主要給大家介紹了關(guān)于java結(jié)合keytool如何實現(xiàn)非對稱簽名和驗證的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-08-08spring-@Autowired注入與構(gòu)造函數(shù)注入使用方式
這篇文章主要介紹了spring-@Autowired注入與構(gòu)造函數(shù)注入使用方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12