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