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