如何根據動態SQL代碼自動生成DTO

#當前的情況java

通常作數據庫相關開發, 除非學習, 不然不多有人願意直接使用JDBC。原本Java代碼就比較囉嗦了,而直接用JDBC寫代碼之囉嗦簡直有些使人發狂!因此在實際開發過程當中,咱們一般都會使用一些框架/庫來幫助咱們操做數據庫。並且開源市場上的選擇也比較多,就我我的接觸到的有:Hibernate,MyBatis,JdbcTemplate,DbUtils,ActiveRecord,JavaLite等等。 這些框架都能大幅的提升開發效率,對於一些基本CRUD操做來講,雖然各有差別,但總的來講基本是夠用了。git

然而對於稍微複雜點的數據查詢來講,總免不了須要手工編寫SQL代碼,甚至還須要根據參數來動態拼接SQL。各類框架基本上都有一套本身拼接動態SQL的方案,也都能很輕鬆的將查詢出來的數據轉爲對象(DTO)。程序員

不過到目前爲止,這些框架雖然可以很輕鬆的幫助咱們完成數據的映射,可是這些DTO還得須要咱們手工一個個的去編寫。github

#存在的問題sql

一般咱們在寫完SQL的查詢代碼後, 須要有一個對應的DTO,將數據庫中查詢出的數據映射到DTO,以便於調用的程序可以更好的使用這些數據。固然,爲了省事,有時也會把數據直接存儲在像Map這樣的數據結構中。不過, Map這種方式雖然很輕便,可是會帶來幾個比重要的潛在問題:數據庫

  • 調用者須要記住Map裏面每一個key的名稱,這就會給程序員帶來一些所謂的記憶負擔
  • 太重的記憶負擔,就會致使系統的邏輯複雜,理解困難,維護更困難
  • SQL更改致使Key發生變化後,很難發現問題,須要程序員很是當心的處理這些更改

若是想要避免Map帶來的這些問題,咱們須要爲每一個SQL查詢都單獨編寫DTO。儘管書寫這些DTO並無什麼難度,可是很是枯燥乏味,特別是字段不少的時候更是如此;而且,若是SQL查詢的字段出現更改,也仍是要記得回來修改這個DTO。單獨編寫DTO雖然減輕了Map帶來的部分問題,同時也額外增長了新的工做量。數組

若是有一種方法可以在SQL代碼(包括動態拼接的SQL)編寫完成後,就自動的作到下面2點就很是完美了:數據結構

  1. 根據SQL代碼,直接生成對應的DTO
  2. 變動SQL代碼,自動修改對應的DTO

這樣,一方面解決了手工書寫DTO的麻煩; 另外一方面,當修改SQL致使某個字段發生更改時, 因爲自動生成的DTO也會同步修改,在那些引用到這個字段的地方,編譯器就會當即給出錯誤提示! 使得問題一產生就能當即被發現,這樣能夠避免了不少潛在的問題。框架

本文正是試圖要解決如何根據SQL代碼自動生成DTO的問題,省去手工編寫的麻煩,提升程序員的開發效率。eclipse

#解決的思路

理想老是很美好,現實老是很殘酷!

那麼,到底可否實現這個想法呢,咱們首先來初步分析一下自動產生DTO的可行性:

要實現自動產生DTO,其核心就是要拿到SQL查詢所對應的每一個列名及其數據類型。有了列名和數據類型,就能很容易寫一個方法來產生DTO了。

咱們知道,在通常狀況下,SQL查詢寫完以後,包括調用存儲過程和那些根據調用參數來動態拼接的SQL,雖然最終運行的SQL可能不盡相同,可是其查詢結果的字段部分都是相對固定的。

固然,也有極少狀況下會碰到字段都不肯定的查詢,不過在這種極端狀況下,即便手工也無法寫DTO了,反卻是用Map更合適, 咱們這裏不作討論。

那麼,怎麼才能拿到列名和類型呢?

一種方案是分析SQL代碼中SELECT部分的字段,不過其侷限性比較大:

  • 對於拼接的SQL代碼,分析難度比較大
  • 字段的類型也難以判斷
  • SELECT * ...; CALL statement 這樣常見的查詢方式分析起來難度也很大

上述方案對像Mybatis這種採用配置文件(xml)來寫SQL的方式,彷佛有些可行性,我沒有具體試驗過,但估計面臨的困難不會少。

另外一種方案是想辦法直接運行包含SQL的這些代碼:

咱們知道JDBC執行一個SQL查詢,會返回ResultSet對象,經過該對象中的方法getMetaData(),可以獲得此次查詢的一些元數據:如列名稱,列類型,以及該列所在的表名等,這些信息就已經足夠咱們來產生須要的那個類了。

那麼,怎麼纔可以運行這些包含SQL的代碼呢?

對於那些固定的SQL語句還稍微好說點,咱們拿到這個固定的SQL,調用JDBC就能拿到MetaData,而後就能夠很容易的根據這些信息來生成DTO。可是,對於那些複雜的須要根據一系列參數來動態產生的SQL查詢,在參數設置好前是沒法直接運行的,也就沒法獲得MetaData,得不到MetaData咱們就沒法生成DTO。

怎麼辦?

前面已經討論了,即使是動態SQL,不管輸入什麼樣的參數,雖然執行的SQL語句可能不同,可是最終產生結果列倒是固定的。 咱們當前須要解決的問題不正是要獲取這些列信息嗎? 既然如此,那咱們就構造一系列默認的參數值。這些參數並無實際用處,僅僅是爲了讓咱們正在編輯SQL代碼得以正常運行,以便拿到須要的MetaData,至於可否查詢到數據並不緊要。

一般咱們編寫的SQL代碼,有2種存在形式:一是直接在Java代碼中, 另一種是放在配置文件中。這裏不討論哪一種形式更好,之後我會單獨再找地方來討論。這裏主要討論的是在Java代碼中拼接的SQL, 如何實現一個代碼生成器來自動生成這些DTO:

要全自動化的解決這個問題,咱們先來看看這個代碼生成器所要面臨的一些挑戰及應對的思路:

  • 如何標識一段須要生成DTO的SQL代碼

首先,咱們須要標識出這段代碼,以便於代碼生成器能夠運行這段須要生成DTO代碼。而一般狀況下,咱們的數據接口都是方法級別的,所以咱們能夠經過對方法進行註解,用註解來標識這個方法要返回一個DTO對象是個不錯的選擇。

  • 如何定義DTO的類名

一種很容易想到的方法就是經過SQL代碼所在的類名+方法名自動組合出一個名稱, 固然有時爲了靈活控制,應該容許程序員指定一個名字。

  • 如何執行代碼

執行代碼的關鍵是構造一批可以調用註解方法的合適參數。固然首先須要對註解的方法進行代碼分析,提取方法參數名及類型。代碼分析能夠用相似JavaCC這樣的工具,或者一些語法分析器,這裏不作細究。下面主要探討下默認參數的構造:

爲了簡化問題,默認狀況下咱們能夠按以下規則進行構造:

數字型參數,默認爲:0, 例如:public Object find(int arg){...} 構造 int arg=0;  
字符串參數,默認爲:"",     構造 String arg="";  
布爾型參數,默認爲:false,  構造 boolean arg=false;  
數組型參數,默認爲:類型[0], 構造 int[] arg=new int[0];  
對象型參數,默認爲:new 類型(), 例如:public Object find(User arg){...} 構造 User arg=new User();

固然,對於一些簡單參數的狀況下,上面構造規則基本上都可以奏效。 可是,對於有些參數:好比參數是一個接口,或者是一個須要動態鏈接的表名,又或者是SQL拼接代碼的邏輯要求參數必須是某些特殊值等等,默認構造出的參數就會致使程序沒法執行。

可是,怎麼纔可以讓咱們的代碼生成器可以繼續執行下去呢? 好像確實沒有什麼能自動處理的辦法,只好把這個問題交給程序員來處理了,讓程序員來幫助代碼生成器完成參數的初始化。

咱們能夠在註解上提供一個參數, 該參數主要完成對默認規則下沒法初始化的參數進行設置。 固然,這個參數中的初始化代碼也能夠覆蓋默認規則,以便於咱們在編輯階段就能夠測試執行不一樣的SQL流程。

  • 如何生成DTO

通過以上一系列的處理,咱們終於能自動的把包含SQL查詢代碼的方法運行起來了。不過,如今咱們還沒獲得想要的MetaData,還沒法生成DTO。

一種可能的方式是包裝一個JDBC,截獲本次方法調用時執行的SQL查詢, 但面臨的問題是,若是方法中有屢次查詢就比較麻煩了。

另外一種方式依賴於框架的支持,能夠截獲到方法的return語句,獲取其執行的SQL語句, 有了SQL語句,生成DTO就沒有什麼難度了。

  • 如何修改代碼

爲了儘可能減小程序員的工做,咱們的代碼生成器在生成完DTO後, 還須要將方法的返回值自動修改爲這個DTO類。

  • 如何處理SQL的變動

簡單的作法是:一旦有某個SQL代碼發生變化,就把全部的DTO都按照前面的方法從新生成一遍。 不過,很顯然當查詢方法不少的時候,DTO代碼生成的過程將緩慢到難以忍受。

另一種更合理的作法是:咱們在生成DTO時增長一個指紋字段,其值能夠用SQL代碼中所包含的信息來產生,例如:代碼長度+代碼的hashCode.代碼生成器在決定是否須要處理這個方法前,先計算該方法的指紋和存在於DTO裏面的指紋進行比較,若是相同就跳過,不然就認爲本方法的SQL發生了變動,須要更新DTO。

#具體的實現

到此爲止,基本上DTO代碼生成器的主要障礙都有了相應的處理辦法。最後,咱們用一個具體的實現來作個簡單示例。

這裏須要引入2個項目:

這是一個功能強大且很是容易使用的ORM框架,經過@DB(jdbc_url,username,password)註解來引入數據庫。

這是一個相應的Eclipse插件,它能夠:

  1. @DB註解的接口,在文件保存時 ,自動生成表的CRUD操做
  2. @Select註解的方法,在文件保存時 ,自動生成DTO
  3. 很輕鬆的書寫多行字符串

插件安裝和設置能夠參考: https://github.com/11039850/monalisa-orm/wiki/Code-Generator

下面是一個根據動態SQL自動生成DTO示例,完整的例子工程能夠參考: https://github.com/11039850/monalisa-example

package test.dao;
	
    public class UserBlogDao {
        //@Select 註解指示該方法需自動生成DTO
        //默認類名: Result + 方法名, 默認包名:數據訪問類的包名+"."+數據訪問類的名稱(小寫)
        //可選參數:name 指定生成結果類的名稱,若是未指定該參數,則採用默認類名
        //可選參數:build 初始化調用參數的Java片斷代碼,替換默認的參數構造規則
        @Select(name="test.result.UserBlogs") 
	
        //!!! 保存後會自動修改該函數的返回值爲: List -> List<UserBlogs>
        //第一次編寫時,因爲結果類還不存在, 爲了保證可以編譯正常,
        //函數的返回值 和 查詢結果要用 泛值 替代, 保存後,插件會自動修改.
        //函數的返回值 和 查詢結果 泛值的對應關係分三類以下:
        //1. List查詢
        //public DataTable   method_name(...){... return Query.getList();   }    或
        //public List        method_name(...){... return Query.getList();   }    
        //
        //2. Page查詢
        //public Page   method_name(...){... return Query.Page();      }
        //
        //3. 單條記錄
        //public Object method_name(...){... return Query.getResult(); }
        //
        public List  selectUserBlogs(int user_id){ 
            Query q=TestDB.DB.createQuery();
	
            q.add(""/**~{
                SELECT a.id,a.name,b.title, b.content,b.create_time
                    FROM user a, blog b   
                    WHERE a.id=b.user_id AND a.id=?
            }*/, user_id);	
	        
            return q.getList(); 
        } 
    }

上述代碼保存後,插件就會自動生成一個DTO類:test.result.UserBlogs, 並自動將方法修改爲以下的聲明:

public List<UserBlogs>  selectUserBlogs(int user_id){ 
            ...
            return q.getList(UserBlogs.class); 
        }

固然,若是對selectUserBlogs方法作了任何的修改(包括只是加了一個空格),保存文件後,插件也會自動更新UserBlogs。

同時,爲了方便咱們調試,插件也會在Eclipse的控制檯窗口輸出相似下面的信息:

2016-06-27 17:00:31 [I] ****** Starting generate result classes from: test.dao.UserBlogDao ******	
2016-06-27 17:00:31 [I] Create class: test.result.UserBlogs, from: [selectUserBlogs(int)]
SELECT a.id,a.name,b.title, b.content,b.create_time
    FROM user a, blog b    
    WHERE a.id=b.user_id AND a.id=0

順便補充一下:

在Java代碼中書寫SQL,很是使人討厭的一件事情就是Java語言中字符串的鏈接問題。使得大段的SQL代碼中間要插不少的換行/轉義符號,寫起來很麻煩,看着也不舒服。monalisa-eclipse插件順便也解決了多行字符串的書寫問題。

例如:

System.out.println(""/**~{
	    SELECT * 
	    	FROM user
	    	WHERE name="zzg"
	}*/);

將會輸出:

SELECT * 
	    FROM user
	    WHERE name="zzg"

固然,爲了快速書寫,能夠在Eclipse中把多行字符串的語法設置爲一個代碼模板。關於多行語法的更多細節能夠參考: https://github.com/11039850/monalisa-orm/wiki/Multiple-line-syntax

到這裏,動態SQL代碼自動生成DTO的思路和實現例子基本上就介紹完了, 歡迎你們提出各類有理無理的意見,一塊兒討論、進步,謝謝!

相關文章
相關標籤/搜索