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,實現的功能沒有變,但是代碼卻簡潔了,程序與程序之間的關系也更清晰了,這,也許就是面向對象的精髓吧~