從JDBC到commons-dbutils


1、前言

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

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

 

2、JDBC如何使用

    這一小節通過一個例子來說明JDBC如何使用。

    我們大致可以講JDBC的整個操作流程分為4步:

    1、獲取數據庫連接

    2、創建statement

    3、執行sql語句並處理返回結果

    4、釋放不需要的資源

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

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

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

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

 

3、這樣寫代碼有什么問題

      3.1、這樣寫代碼會造成大量重復勞動,比如獲取連接,如果每個執行sql的方法都要寫一遍相同的代碼,那么這樣的重復代碼將充斥整個DAO層。

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

      3.3、大量重復代碼會造成一個問題,那就是可維護性變差,一旦某個常量改變了,那么就需要把每個方法都改一遍

      3.4、數據庫連接是重量級資源,每調用一次方法都去創建一個連接,性能會存在瓶頸

 

4、如何改進

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

     4.1、獲取數據庫連接

       我當時的解決方案是一次初始化很多連接放入list,然后用的時候取,現在的通用方法就是連接池,比如DBCP、C3P0等等。有興趣的人可以去看看它們的源代碼,看看是如何實現的

    4.2、創建statement

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

   4.3、執行sql語句並處理返回結果

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

   4.4、釋放資源

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

 

5、commons-dbutils的原理

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

    5.1、整體結構

    先看一下它有哪些類:

   

   

一共有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。

      1、以查詢為例,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     }

 

         2、每個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這個方法的實現上。這里用到了模板方法的設計模式,
將不變的抽象到上層,易變的下方到下層。


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

5.3、和數據源的結合
從上面可以看出,dbutils抽象了2、3、4(JDBC 4步驟),而沒有把連接的獲取抽象,其實,連接的獲取和維護本身就有其他組件提供,也就是datasource
數據源,dbutils只負責2、3、4,不該它管就不管,這樣才能做到解耦。在構造QueryRunner的時候,可以選擇傳入一個數據源,這樣,在調用方法的時候,
就不需要傳入connection了。


5.4、總結
使用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估計都要十幾行代碼吧。

 

 

6、尾聲

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

 

     


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM