从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,实现的功能没有变,但是代码却简洁了,程序与程序之间的关系也更清晰了,这,也许就是面向对象的精髓吧~

 

     

posted @ 2015-02-25 21:21  biakia  阅读(5920)  评论(0编辑  收藏  举报