DbUtils使用与源码分析

作者: coderstone | 来源:发表于2017-10-24 10:02 被阅读161次

DbUtils

1 准备工作

  1. 建立数据库demo
  2. 运行示例项目中的demo.sql文件里面的sql语句
  3. 往dept与emp表里面插入一些测试数据
说明: 
1. DbUtils的使用不需要建立有关系的表来演示。
2. DbUtils并不会处理关系映射
3. 整个演示案例,只会对emp表进行操作,这里搞两个表是为了大家测试用
4. 下面的文章针对是DbUtils 1.7这个版本
5. 提到*DbUtils*时指的是这个库,而*DbUtils类*时指的是此库里面的DbUtils类

emp表的sql代码如下:

create table emp
(
    id int(4) auto_increment primary key,
    username varchar(10) default 'NULL' null,
    did int(4) unsigned not null,
    salary decimal(10,2) default 'NULL' null,
);

2 DbUtils是什么

DbUtils是Apache下的一个小类库,对JDBC操作进行了封装,以简化易于出错,代码很无聊的JDBC操作代码

DbUtils代码的优点:

  1. No possibility for resource leaks. Correct JDBC coding isn't difficult but it is time-consuming and tedious.
    This often leads to connection leaks that may be difficult to track down.
  2. Cleaner, clearer persistence code. The amount of code needed to persist data in a database is drastically reduced.
    The remaining code clearly expresses your intention without being cluttered with resource cleanup.
  3. Automatically populate JavaBean properties from ResultSets. You don't need to manually copy column values
    into bean instances by calling setter methods. Each row of the ResultSet can be represented by one fully populated bean instance.

DbUtils不是:

  1. An Object/Relational bridge - there are plenty of good O/R tools already. DbUtils is for developers looking to use JDBC without all the mundane pieces.
  2. A Data Access Object (DAO) framework - DbUtils can be used to build a DAO framework though.
  3. An object oriented abstraction of general database objects like a Table, Column, or PrimaryKey.
  4. A heavyweight framework of any kind - the goal here is to be a straightforward and easy to use JDBC helper library.

3 DbUtils的三个代表类

JDBC代码的处理本质上只有CRUD四种形式的代码,稍微再细分一下大致可以分为

  1. 修改(update)与delete操作
  2. 增加操作(insert),因为这里牵涉到插入数据时数据库层面自动生成
    数据的的获取问题,比如自增长字段等情况
  3. 查询操作,主要牵涉到查询出来的数据用什么承载的问题,
    数组还是HashMap还是普通的PO对象

3.1 QueryRunner

QueryRunner的update方法可以用来处理CUD操作:(毕竟不是所有的插入操作需要处理数据库生成值的问题),代码如下

QueryRunner queryRunner = new QueryRunner();
queryRunner.update(connection, "update emp set username='111' where id = 3");

其中connection对象是通过常见的DriverManager.getConnection方法得到的一个JDBC连接
QueryRunner类的update方法重载情况见下图:

QueryRunner.Update重载情况

之所以有些update方法不需要Connection对象是因为在实例化QueryRunner的时候
可以传递DataSource对象给QueryRunner,然后这些Update方法就利用传递进来的DataSource
对象获得连接以处理CUD操作。QueryRunner的构造函数如下图:

QueryRunner_constructor.png

传递DataSource对象给QueryRunner主要的目的是可以让DbUtils与连接池整合使用。

整个QueryRunner类的方法有下面几类

方法名 主要作用
update 处理普通的CUD操作
insert 处理插入,可以处理数据库生成的值
query 查询数据
execute 调用存储过程
batch 批量CUD
insertBatch 批量插入

3.2 DbUtils

我们知道JDBC代码很重要的地方是需要对一些资源进行关闭,比如Connection
PreparedStatement对象的关闭等。

3.2.1 资源关闭

在上面的示例代码中,QueryRunner的update方法内部会创建PreparedStatement对象,并且自动
在异常处理的finally代码块中调用关闭PreparedStatement对象,而Connection对象并没有关闭,需要我们自己
编写代码关闭。

这种资源关闭的方法,DbUtils类已经帮我们写好了,直接编写DbUtils.close(connection)
方法就可以关闭Connection对象了。QueryRunner对象的update方法自动关闭PreparedStatement对象
也是调用DbUtils的相关方法。

DbUtils类的关闭相关的方法如下图

DbUtils_close.png

DbUtils类的关闭(close)与安静的关闭有什么区别。源代码如下:

    public static void close(Connection conn) throws SQLException {
        if (conn != null) {
            conn.close();
        }
    }

    public static void closeQuietly(Connection conn) {
        try {
            close(conn);
        } catch (SQLException e) { // NOPMD
            // quiet
        }
    }

3.2.2 DbUtils类的主要作用

DbUtils类的方法体系如下表:

方法名 主要作用
close(Quietly) 关闭或安静的关闭Connection,Statement,ResultSet
loadDriver 加载数据库驱动类
commitAndClose 事务提交并关闭连接
rollback 回滚事务
rollbackAndClose(Quietly) 回滚并关闭Connection
printStackTrace 把SqlException异常栈输出到指定的PrintWriter(默认输出到System.err)
printWarnings 把Connection对象的warnings输出到指定的PrintWriter对象上。

3.3 ResultSetHandler

了解了CUD操作后,接下来就了解下DbUtils是如何帮助我们简化查询操作的。查询最重要的事情有2个,
一个是如何编写sql语句,一个是如何把查询到的数据封装起来。而DbUtils会帮我们处理后面的任务。

3.3.1 查询的基本操作

为了后续操作做准备,先创建一个实体类Emp,大致的代码情况如下:

public class Emp {
    private  Integer id;
    private String username;
    private BigDecimal salary;
    //getter,setter,toString ommited 
}

下面的代码,依据主键查询一条记录,并封装为一个Emp对象,代码如下:

QueryRunner queryRunner = new QueryRunner();
        Emp result = queryRunner.query(connection,
                "select * from emp where id = 3",
                    new ResultSetHandler<Emp>() {
                        public Emp handle(ResultSet rs) throws SQLException {
                                Emp emp = null;
                            if (rs.next()) {
                                emp = new Emp();
                                emp.setId(rs.getInt("id"));
                                emp.setUsername(rs.getString("username"));
                                emp.setSalary(rs.getBigDecimal("salary"));
                            }
                            return emp;
                        }
        });
        System.out.println(result);
        DbUtils.close(connection);

注意:通过ResultSetHandler传递过来的ResultSet还没有被处理过,所以需要调用next方法

利用DbUtils处理查询,是靠QueryRunner.query() 方法完成的。此方法的第三个参数ResultSetHandler是一个
回调型接口,此接口的代码如下:

    public interface ResultSetHandler<T> {
        T handle(ResultSet rs) throws SQLException;
    }

此接口的是用来把JDBC中的ResultSet转换为其它的数据类型,比如上面的代码就是把ResultSet转换为Emp这个实体bean
上面的代码,是通过匿名内部类的方式来实现ResultSetHandler这个接口来完成示例的。

QueryRunner的查询相关的所有方法如下图:

QueryRunner_query.png

这里的8个query方法,有4个已经废弃,主要原因就是因为可变长度的方法参数一般应该作为方法的最后一个参数。
被废弃的4个没有按照这个规定设计方法签名。

很明显整个QueryRunner的query方法体系最重要的就是ResultSetHandler接口的处理,上面我们通过匿名内部的形式
已经进行了处理,那这种方式好不好?有没有更好的处理方式呢?这就是我们接下来要了解的内容。

3.3.2 ResultSetHandler体系

上面的查询代码,不够简洁,需要我们自己实现ResultSetHandler接口,自己处理数据到实体的
映射。而我们上面的映射规则是有规律的,就是同名字段的数据映射到对应的实体属性上。
既然有规律,那么就可以把这种工作抽象出来,固定化,而不需要我们每次都自己编写。
DbUtils已经帮我们完成了这样的工作。比如下面的代码。

QueryRunner queryRunner = new QueryRunner();
        BeanHandler<Emp> handler = new BeanHandler<Emp>(Emp.class);
        
        Emp result = queryRunner.query(connection,
                "select * from emp where id = 3",
                handler
                );
        System.out.println(result);
        DbUtils.close(connection);

很明显这样写之后(重点看BeanHandler这行代码),代码大大简化,也更具有通用性。那么DbUtils到底提供了多少
ResultSetHandler的实现呢?整个类型层次结构如下图:

ResultSetHandler_Hirachicy.png

类图如下:

ResultSetHandler_classDiagram.png

在这14个类中,BaseResultSetHandler类型,主要的作用是你想扩展DbUtils,创建自己的ResultSetHandler实现时
使用,简化自己的工作。BaseResultSetHandler遵循DRY原则,把一些常见而通用的方法实现,你直接调用就可以了。

而其它的12个实现中,有2个抽象类AbstractListHandler与AbstractKeyedHandler,所以我们真正可以使用的,只有10个类型
那么这10个类型到底各有什么作用呢?

从数据库返回的记录的行数,我们可以分为单行与多行记录,而这些返回的数据
可以用数组来处理,也可以用List、Map、实体等来处理。这10个类就是应付这些情况的。

  • 单行某列的处理:ScalarHandler

假定sql: select id,username from emp where id = 3

利用这个Handler可以取得某一列的值,比如id列的数据或者username列的数据

  • 单行数据的处理:

假定sql语句为select id,username as myname from emp where id = 3

返回的结果 3, aaa

类型 说明 结果示例
ArrayHandler 返回一个数组 Object[] [3,"aaa"]
MapHandler 返回一个HashMap对象,Map<String,Object> {"id":3,"myname":aaa}
BeanHandler 返回某个实体对象 new Emp(3,"aaa")
  • 多行数据的处理

假定sql语句为select id,username as myname from emp

返回的结果

  • 3, aaa
  • 4, bbb
类型 说明 结果示例
ArrayListHandler List<Object[]> {[3,"aaa"],[4,"bbb"]}
MapListHandler List<Map<String,Object>> {{"id":3,"myname":"aaa"}, {"id":4,"myname":"bbb"}}
ColumnListHandler List<T>,如果想查询myname列的数据的话 {"aaa","bbb"}
KeyedHandler Map<K,Map<String,Object>> 假定实例化KeyedHandler时,构造函数传递的是id列,那么结果为:{3={id=3, myname=aaa}, 4={id=4, myname=bbb}}如果传递的是myname列,那么结果为:{aaa={id=3, myname=aaa}, bbb={id=4, myname=bbb}}
BeanMapHandler Map<K,Emp> 假定实例化KeyedHandler时,构造函数传递的是id列,那么结果为:{3=Emp{id=3, username='aaa'}, 4=Emp{id=4, username='bbb'}}如果传递的是myname列,那么结果为:{aaa=Emp{id=3, username='aaa'}, bbb=Emp{id=4, username='bbb'}}
BeanListHandler List<Emp>这样的结果 {{new Emp(3,"aaa")},{new Emp(3,"aaa")}}

KeyedHandler的使用示例代码如下:

 QueryRunner queryRunner = new QueryRunner();
        KeyedHandler<Integer> handler = new KeyedHandler<Integer>("id");
        Map<Integer,Map<String,Object>> result = queryRunner.query(connection,
                "select id,username myname from emp ",
                handler
        );
        System.out.println(result);
        DbUtils.close(connection);

BeanMapHandler的使用示例代码如下:

        QueryRunner queryRunner = new QueryRunner();
        BeanMapHandler<Integer, Emp> handler = new BeanMapHandler<Integer, Emp>(Emp.class,"id");
        Map<Integer, Emp> result = queryRunner.query(connection,
                "select id,username  from emp ",
                handler
        );
        System.out.println(result);
        DbUtils.close(connection);

3.3.3 映射处理

这里属于对DbUtils源码层面的分析,没兴趣,可以跳过这个小节


3.3.3.1 ResultSetHandler体系

分析整个映射处理所牵涉到的类以及流程,只需要关注ArrayHandler与BeanHandler的实现即可了解整个映射
处理了。我们先从ArrayHandler开始了解。假定你的代码类似这样

        QueryRunner queryRunner = new QueryRunner(ConnectionUtil.getDataSource());
        ArrayHandler handler = new ArrayHandler();
        Object[] result = queryRunner.query("select id,username from emp where id = 3",handler);

上面的代码从调用QueryRunner类的query方法到返回一个对象数组,时序图如下:

Query_Return_An_Array.png

而ArrayHandler的Handle方法源代码如下:

 public Object[] handle(ResultSet rs) throws SQLException {
        return rs.next() ? this.convert.toArray(rs) : EMPTY_ARRAY;
    }

这里的convert变量就是采用的ArrayHandler的默认转换器,也就是RowProcessor接口的实现类BasicRowProcessor

这里的rs.next()而不是while(rs.next())这样的代码,也充分说明了ArrayHandler就是只处理一条记录,哪怕你的SQL语句
可能导致返回多条记录

3.3.3.2 RowProcessor体系

RowProcessor接口的结构如下:

RowProcessor.png

BasicRowProcessor的toArray方法的源代码如下:

 public Object[] toArray(ResultSet rs) throws SQLException {
        ResultSetMetaData meta = rs.getMetaData();
        int cols = meta.getColumnCount();
        Object[] result = new Object[cols];

        for (int i = 0; i < cols; i++) {
            result[i] = rs.getObject(i + 1);
        }

        return result;
    }

3.3.3.3 BeanProcessor

接下来了解查询一条记录返回一个实体对象(也就是bean对象的)的整个流程。查询的代码如下:

        QueryRunner queryRunner = new QueryRunner();
        BeanHandler<Emp> handler = new BeanHandler<Emp>(Emp.class);
        Emp result = queryRunner.query(connection,
                "select * from emp where id = 3",
                handler
                );

整个处理的流程图如下:

Query_Return_A_Bean.png

BeanHandler的Handle方法代码如下:

return rs.next() ? this.convert.toBean(rs, this.type) : null;

这里的convert指的就是BasicRowProcessor对象

BasicRowProcessor的toBean方法的源代码如下:

  public <T> T toBean(ResultSet rs, Class<? extends T> type) throws SQLException {
        return this.convert.toBean(rs, type);
    }

这里的convert默认指的就是BeanProcessor对象

BeanProcessor的toBean代码如下:

    public <T> T toBean(ResultSet rs, Class<? extends T> type) throws SQLException {
        T bean = this.newInstance(type);
        return this.populateBean(rs, bean);
    }

上面的代码中, T bean = this.newInstance(type)这行代码只是利用Class创建一个实例
等价于T bean = type.newInstance();之所以要额外再创建一个叫newInstance的方法主要处理了一些异常
而this.populateBean(rs,bean)才是真正数据映射发生点。关于这点留到下一小节来说明

从把数据库的一条记录转换为数组或者一个实体对象来看,这里面牵涉到QueryRunner、ResultSetHandler、RowProcessor、BasicRowProcessor、BeanProcessor
这几个类,他们之间的类结构如下图:

ResultSetHandler_RowProcessor_BeanProcessor.png

从上面的类图以及分析来看,整个查询的流程,可以用下面的图来表示:

QueryFlow.png

3.3.3.4 数据库记录到Bean的映射

我们知道,我们可以通过ResultSetMetaData对象可以得到sql查询结果的列的数量,列的别名与名字。通过ResultSet对象
可以得到某列的数据,而实体Bean这一端,我们可以通过反射或者内省机制得到实体类的字段,每个字段的类型以及调用字段对应的
setter方法等。

基于这些信息,那么我们就知道,想进行数据的映射处理,需要完成下面的几个任务

  1. sql查询结果列与实体字段的映射
  2. 找到的实体字段调用相关的setter方法赋值,找不到的不用处理
  3. 赋值的时候需要考虑类型是否兼容的问题,比如sql查询结果的某列的值为字符串“abc”,你就
    不能把其赋值给一个Integer类型的字段,即便两者的名字是一样的(匹配映射成功)

基于这些信息,我们就能较好的理解DbUtils库中的代码,在DbUtils中,数据库记录到Bean的映射是由BeanProcessor的populateBean方法来完成,
此方法的代码如下:

    public <T> T populateBean(ResultSet rs, T bean) throws SQLException {
        PropertyDescriptor[] props = this.propertyDescriptors(bean.getClass());
        ResultSetMetaData rsmd = rs.getMetaData();
        int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);

        return populateBean(rs, bean, props, columnToProperty);
    }
  1. 在此方法中,PropertyDescriptor[] props = this.propertyDescriptors(bean.getClass()); 是通过java的内省机制得到bean的
    各个属性的信息(字段与其getter,setter).
  2. ResultSetMetaData rsmd = rs.getMetaData(); 这是得到查询的元数据信息,通过ResultSetMetaData可以得到查询的列数、名字、别名
    等信息
  3. 'int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);' 这行代码是找到查询语句与bean的字段的映射结果,具体
    实现逻辑在后面有讲解:
  4. 最后一行代码是真正给bean对象赋值

上面的代码中,我个人认为比较重要的是就是映射逻辑的实现。
假定你编写的sql是select id,username,salary,did from emp,而你的bean类型中有
id,username,xx这3个字段以及相应的getter,setter方法。

映射的处理过程中,以sql查询结果列为准,由于bean的xx字段,在sql语句中没有,所以压根就不处理这个xx的字段。
由于sql查询的列数为4列,但由于通过ResultSet取查询数据时,是从1而不是从0开始的。所以在这里构建一个有5个元素的数组
此数组的第一个元素并不存放任何内容。并且构建一个有5个元素的映射结果数组(就是下图中中间的列),这个里面存放的值是右边数组中的列在
最左边数组中索引位置。

ColumnToPropertyMapping.png

3.3.3.5 从QueryRunner的query方法到ResultSetHandler之前


        PreparedStatement stmt = null;
        ResultSet rs = null;
        T result = null;

        stmt = this.prepareStatement(conn, sql);
        this.fillStatement(stmt, params);
        rs = this.wrap(stmt.executeQuery());
        result = rsh.handle(rs);

上面的代码中,唯一值得一说的就是rs = this.wrap(stmt.executeQuery()); 这行代码主要的作用是对ResultSet
对象进行一定的处理,比如在调用ResultSet的getObject方法之后再进行处理。这里采用的是一种类似装饰模式的形式来实现的。

在DbUtils库里面的wrappers包下面已经自带了2个ResultSet的装饰类(切面类)。这里是利用JDK自带的动态代理机制实现的。

4 整合连接池

在上面的使用过程中,有2个不方便的地方,一个是调用query与update这两个方法时,要自己传递一个Connection
对象,并且需要自己记得关闭Connection,这很容易忘记,而这会导致致命的链接资源被浪费,甚至数据库链接过载的问题。
这也是实例化QueryRunner方法传递DataSource对象,调用query与update方法不需要Connection的对象的作用所在。

在这里我们采用alibaba的druid连接池(粗暴使用,关于此连接池的更合理使用,请查看参考资料),首先我们自己编写一个
工具类,用来得到相关的DataSource对象。代码如下:

public class ConnectionUtil {

    private  static DataSource dataSource;

    static {
        DruidDataSource dds  = new DruidDataSource();
        dds.setDriverClassName("org.mariadb.jdbc.Driver");
        dds.setUsername("root");
        dds.setPassword("");
        dds.setUrl("jdbc:mariadb://127.0.0.1:3306/demo?useUnicode=true&characterEncoding=UTF-8");
        dds.setInitialSize(5);
        dds.setMinIdle(1);
        dds.setMaxActive(10);
        dataSource = dds;

    }

    public static DataSource getDataSource(){
        return  dataSource;
    }
}

使用此连接池的代码如下:

        QueryRunner queryRunner = new QueryRunner(ConnectionUtil.getDataSource());
        KeyedHandler<String> handler = new KeyedHandler<String>("username");
        Map<String,Map<String,Object>> result = queryRunner.query(
                "select id,username  from emp ",
                handler
        );
        System.out.println(result);

重点注意QueryRunner的构造函数以及QueryRunner的query方法所在的代码

当与连接池结合使用之后,你调用QueryRunner类的query,update方法时,会自动帮你关闭通过DataSource得到的
池化链接,把此链接放置到连接池中。这样你就不需要利用DbUtils类的close方法来手动关闭链接了。

5 批处理

批处理的含义可以理解为多次调用QueryRunner的update方法,作用类似下面的伪代码所示:

List<String> names = new List<String>(){"aaaa","bbbb","cccc"};
for(String uname: names){
    queryRunner.update("insert emp(username) values(?)",uname);
}

上面的写法,会导致多次的数据库往返行为,性能并不好,所以才会有批处理,而DbUtils只是对
JDBC的批处理进行一个简单的封装,示例代码如下:

 QueryRunner queryRunner = new QueryRunner(ConnectionUtil.getDataSource());
        Object[][] emps = {{"dddd",4000},{"eeee",5000},{"fff",6000}};
        queryRunner.batch("INSERT into emp(username,salary) values(?,? );",emps);

上面的代码会导致往数据库里面插入3条记录,第一条记录username值为dddd,salary值为4000

批处理主要用在CUD操作上

6 事务处理

DbUtils这个类有commitAndClose,rollback,rollbackAndClose这样的方法,这点只是对我们
处理事务进行了一点点的简化,并没有太大的用处,所以这里就不讲了,只不过如果你非要用DbUtils来处理事务
记得实例化QueryRunner时要采用其默认构造函数以便由你自己处理Connection对象,最后再可以利用DbUtils
类的事务相关方法来提交事务、回滚事务。

7 扩展点

除了使用DbUtils默认提供的功能,你当然也可以对其进行一定的扩展,在这里只是简单的列出一些。

  1. 重写QueryRunner的wrap方法,以便对ResultSet进行修饰处理,使用代码可以参看StringTrimmedResultSet类
  2. 重写BeanProcessor的mapColumnsToProperties方法,修改映射规则
  3. 重写BeanProcessor的populateBean(ResultSet rs,T bean)方法
  4. 列数据处理,重写BeanProcessor的processColumn方法
  5. 列数据处理扩展,采用了SPI的技术,可以参考库里面columns包下面的一些类来扩展

参考资料

相关文章

网友评论

本文标题:DbUtils使用与源码分析

本文链接:https://www.haomeiwen.com/subject/ghhtpxtx.html