從JDBC到commons-dbutils

一、前言mysql

    玩過Java web的人應該都接觸過JDBC,正是有了它,Java程序才能輕鬆地訪問數據庫。JDBC不少人都會,可是爲何我還要寫它呢?我曾經一度用爛了JDBC,一度認爲JDBC不過如此,後來,我對面向對象的理解漸漸深刻,慢慢地學會了如何抽象JDBC代碼,再後來,我遇到了commons-dbutils這個輕量級工具包,發現這個工具包也是對JDBC代碼的抽象,並且比我寫的代碼更加優化。在這個過程當中,我體會到了抽象的魅力,我也但願經過這篇文章,把個人體會分享出來。web

   文章大體按必定的邏輯進行:JDBC如何使用-----這樣使用有什麼問題------如何改進-----分析commons-dbutils的原理sql

 

二、JDBC如何使用數據庫

    這一小節經過一個例子來講明JDBC如何使用。設計模式

    咱們大體能夠講JDBC的整個操做流程分爲4步:數組

    一、獲取數據庫鏈接緩存

    二、建立statementide

    三、執行sql語句並處理返回結果工具

    四、釋放不須要的資源性能

下面是一個小例子(省略了try-catch代碼):

String username="root";
String password="123";
String url="jdbc:mysql://localhost/test";
Connection con=null;
Statement st=null;
ResultSet rs=null;

//一、獲取鏈接
Class.forName("com.mysql.jdbc.Driver");         
con=DriverManager.getConnection(url,username,password); //二、建立statement String sql="select * from test_user"; st=con.createStatement(); //三、執行sql語句並處理返回結果 rs=st.executeQuery(sql); while(rs.next()) { //對結果進行處理 } //4、釋放資源 rs.close(); st.close(); con.close();

以上的例子是查詢的一種用法,除了用Statement外,還能夠用PreparedStatement,後者是前者的子類,在前者的基礎上增長了預編譯和防止sql注入的功能。另外,查詢和增刪改是不一樣的用法,查詢會返回ResultSet而增刪改不會。

 

三、這樣寫代碼有什麼問題

      3.一、這樣寫代碼會形成大量重複勞動,好比獲取鏈接,若是每一個執行sql的方法都要寫一遍相同的代碼,那麼這樣的重複代碼將充斥整個DAO層。

      3.二、這樣的代碼可讀性比較差,幾十行代碼真正和業務相關的其實就幾行

      3.三、大量重複代碼會形成一個問題,那就是可維護性變差,一旦某個常量改變了,那麼就須要把每一個方法都改一遍

      3.四、數據庫鏈接是重量級資源,每調用一次方法都去建立一個鏈接,性能會存在瓶頸

 

四、如何改進

      針對前面的問題中的一、二、3,改進的方法就是抽象,把可重用的代碼抽象出去,單獨組成一個模塊,模塊與模塊之間實現解耦。因爲整個JDBC操做流程分爲4步,所以能夠從這4步中下手去抽象。

     4.一、獲取數據庫鏈接

       我當時的解決方案是一次初始化不少鏈接放入list,而後用的時候取,如今的通用方法就是鏈接池,好比DBCP、C3P0等等。有興趣的人能夠去看看它們的源代碼,看看是如何實現的

    4.二、建立statement

       我當時使用PreparedStatement進行處理,由於PreparedStatement會緩存已經編譯過的sql

   4.三、執行sql語句並處理返回結果

     這塊可使用反射,將獲得的結果封裝成Java bean對象

   4.四、釋放資源

     使用動態代理,改變connection的close方法的行爲,將connection放回鏈接池

 

五、commons-dbutils的原理

     雖然我作出了改進,但距離真正的解耦還差得遠,而commons-dbutils做爲commons開源項目組中的一個成員,在這方面作得還算不錯,經過閱讀它的源代碼,能夠學習如何抽象和解耦JDBC的操做流程。

    5.一、總體結構

    先看一下它有哪些類:

   

   

一共有27個類,但真正經常使用的是三大組件十幾個類:門面組件、結果處理組件和行處理組件,其中門面組件提供程序入口,並進行一些參數檢驗等,結果處理組件則是核心所在,由於返回的結果能夠是map,能夠是list能夠是JavaBean,這一塊的變化很大,因此抽象出一個組件出來應對這些變化,行處理組件是從結果處理組件中分離出來的,它是結果處理組件的基礎,不管哪一種處理器,最終都要與一行數據打交道,所以,單獨抽象出這一組件。

類名 描述
門面組件
QueryRunner 執行增刪改查的入口
結果處理組件
ResultSetHandler 用於處理ResultSet的接口
AbstractKeyedHandler 將返回結果處理成鍵值對的抽象類
KeyedHandler

處理數據庫返回結果,封裝成一個Map,數據庫表的一個列名爲key,一般能夠用主鍵,數據庫中的一行結果以Map的形式做爲value

BeanMapHandler 處理數據庫返回結果,封裝成一個Map,和KeyedHandler的惟一的不一樣是,每一行結果以Javabean的形式做爲value
AbstractListHandler 將返回結果處理成鏈表的抽象類
ArrayListHandler

將返回結果處理成鏈表,這個鏈表的每一個

元素都是一個Object數組,保存了數據庫中對應的一行數據

ColumnListHandler

若是要取單獨一列數據,能夠用這個handler,用戶指定列名,它返回這個

列的一個list

MapListHandler

ArrayListHandler不一樣的是,鏈表的每一個元素是個Map,這個Map表明數據庫裏的一行數據

ArrayHandler

將一行數據處理成object數組

BeanHandler

將一行數據處理成一個Java bean

BeanListHandler

將全部數據處理成一個list,list的元素時Java bean

MapHandler

將一行結果處理成一個Map

MapListHandler

將全部結果處理成一個list,list的元素時Map

ScalarHandler

這個類經常用於取單個數據,好比某一數據集的總數等等

行處理組件

RowProcessor 用於處理數據庫中一行數據的接口
BasicRowProcessor 基本的行處理器實現類
BeanProcessor 經過反射將數據庫數據轉換成Javabean
工具類
DbUtils 包含不少JDBC工具方法

 

 

     5.2 執行流程

      不管是增刪改查,都須要調用QueryRunner的方法,所以QueryRunner就是執行的入口。它的每一個方法,都須要用戶提供connection、handler、sql以及sql的參數,而返回的則是用戶想要的結果,這多是一個List,一個Javabean或者僅僅是一個Integer。

      一、以查詢爲例,QueryRunner內部的每個查詢方法都會調用私有方法,先去建立 PreparedStatement,而後執行sql獲得ResultSet,而後用handler對結果進行處理,最後釋放鏈接,代碼以下:

   

 1  private <T> T query(Connection conn, boolean closeConn, String sql, ResultSetHandler<T> rsh, Object... params)
 2             throws SQLException {
 3         if (conn == null) {
 4             throw new SQLException("Null connection");
 5         }
 6 
 7         if (sql == null) {
 8             if (closeConn) {
 9                 close(conn);
10             }
11             throw new SQLException("Null SQL statement");
12         }
13 
14         if (rsh == null) {
15             if (closeConn) {
16                 close(conn);
17             }
18             throw new SQLException("Null ResultSetHandler");
19         }
20 
21         PreparedStatement stmt = null;
22         ResultSet rs = null;
23         T result = null;
24 
25         try {
26             stmt = this.prepareStatement(conn, sql); //建立statement
27             this.fillStatement(stmt, params);  //填充參數
28             rs = this.wrap(stmt.executeQuery()); //對rs進行包裝
29             result = rsh.handle(rs);  //使用結果處理器進行處理
30 
31         } catch (SQLException e) {
32             this.rethrow(e, sql, params);
33 
34         } finally {
35             try {
36                 close(rs);
37             } finally {
38                 close(stmt);
39                 if (closeConn) {
40                     close(conn);
41                 }
42             }
43         }
44 
45         return result;
46     }

 

         二、每一個handler的實現類都是以抽象類爲基礎,看代碼(以AbstractListHandler爲例):

            

 1     @Override
 2     public List<T> handle(ResultSet rs) throws SQLException {
 3         List<T> rows = new ArrayList<T>();
 4         while (rs.next()) {
 5             rows.add(this.handleRow(rs));
 6         }
 7         return rows;
 8     }
 9 
10     /**
11      * Row handler. Method converts current row into some Java object.
12      *
13      * @param rs <code>ResultSet</code> to process.
14      * @return row processing result
15      * @throws SQLException error occurs
16      */
17     protected abstract T handleRow(ResultSet rs) throws SQLException;
handle方法都是同樣的,這個方法也是QueryRunner內部執行的方法,而不同的在handleRow這個方法的實現上。這裏用到了模板方法的設計模式,
將不變的抽象到上層,易變的下方到下層。


三、每一個handleRow的實現都不同,但最終都會使用行處理器組件,行處理器是BasicRowProcessor,有toArray,toBean,toBeanList,toMap這些方法
toArray和toMap是經過數據庫的元數據來實現的,而toBean和toBeanList則是經過反射實現,具體能夠去看源代碼實現,應該是比較好理解的。

5.三、和數據源的結合
從上面能夠看出,dbutils抽象了二、三、4(JDBC 4步驟),而沒有把鏈接的獲取抽象,其實,鏈接的獲取和維護自己就有其餘組件提供,也就是datasource
數據源,dbutils只負責二、三、4,不應它管就無論,這樣才能作到解耦。在構造QueryRunner的時候,能夠選擇傳入一個數據源,這樣,在調用方法的時候,
就不須要傳入connection了。


5.四、總結
使用dbutils再加上DBCP數據源,能夠極大的簡化重複代碼,提升代碼可讀性和可維護性,如下是使用dbutils的一個小例子:
 1 /**
 2      * 獲取經常使用地址
 3      * */
 4     public List<CommonAddr> getCommAddrList(int memID) {
 5         String sql = "SELECT `addrID`, `addr`, `phone`, `receiver`, `usedTime` "
 6                 + "FROM `usr_cm_address` WHERE `memID`=? order by usedTime desc";
 7         
 8         try {
 9             return runner.query(sql, new BeanListHandler<CommonAddr>(CommonAddr.class),memID);
10         } catch (SQLException e1) {
11             logger.error("getCommAddrList error,e={}",e1);
12         }
13         return null;
14     }

  若是用最原始的JDBC來寫,光把數據庫結果轉換成List估計都要十幾行代碼吧。

 

 

六、尾聲

   從JDBC到dbutils,實現的功能沒有變,可是代碼卻簡潔了,程序與程序之間的關係也更清晰了,這,也許就是面向對象的精髓吧~

相關文章
相關標籤/搜索