DbUtils的使用

DbUtils 是一种用于对数据库进行操作的工具,底层实现了对 JDBC 的简单封装,使用起来非常简单,是到目前为止我最喜欢的操作数据库的工具了,特别是在查询时可以很简单的就实现数据的封装,这点比起之前使用 JDBC 来封装数据就简单的多了,而且 DbUtils 执行语句时仍然执行的是 SQL 语句,因此性能也能够得到保证,下面具体介绍吧。

1.核心类

使用 DbUtils 工具类的时候,最主要的类就是 QueryRunner 这个类了,首先可以看一下它的两个构造方法。

public QueryRunner()    无参构造
public QueryRunner(DataSource ds)    需要传入数据源的有参构造

下面介绍几个这个类中几个重要的进行 CRUD 操作的方法。

public int update(String sql, Object... params)        
public int update(Connection conn, String sql, Object... params)

上面的 update 方法可以执行增加、修改以及删除相关的 SQL 语句,其中第一个方法中的第一个参数是要执行的 SQL 语句,将参数传入方法时是传入的字符串类型,第二个参数便是要执行的 SQL 语句中可能需要的参数了,因为 SQl 语句所需要的参数个数不一定,因此上面写的参数是可变类型的,第二个方法中的第一个参数很明显就是一个连接对象了,关于这个连接对象的作用等一下再进行说明。

public <T> T query(String sql, ResultSetHandler<T> rsh, Object... params)
public <T> T query(Connection conn, String sql, ResultSetHandler<T> rsh, Object... params)

上面的 query 就是用来执行查询语句的了,第一个方法中第一个参数同样是要执行的 SQL 语句,第二个参数是一个结果集处理对象,DbUtils 之所以会具有这么强的数据封装能力,很大程度上就是因为这个对象的关系,等一下会详细的介绍这个对象,第三个参数则是 SQL 语句可能需要的参数了,至于第二个方法中的连接对象则是和 update 方法中的一样了。

现在这里总的来说一下,为什么构造函数中有的需要传入数据源,而有的不需要,以及在 updatequery 方法中为什么有的需要传入连接对象,而有的不需要?这里的原因就是有时候需要进行事务管理,在不需要事务管理的时候,我们通常使用需要数据源的构造方法来获取 QueryRunner 这个类的实例,然后在执行诸如 updatequery 方法的时候就使用不需要传入连接对象的方法就好了,而在需要进行事务管理的时候,我们必须保证所有的操作都是使用同一个连接对象完成的,因此在构建 QueryRunner 对象实例的时候则使用空参构造函数,但是在执行 update 或者 query 这些方法的时候,就需要使用需要传入连接对象的方法了,而且还必须保证同一个事务操作中传入的连接对象是同一个。

上面说到了事务管理,使用 DbUtils 进行事务管理的话则和另一个类有关系了,那就是 DbUtils 这个类了,这个类中定义了许多与事务操作相关的方法,比如事务的提交以及回滚。

public static void commitAndCloseQuietly(Connection conn)
public static void rollbackAndCloseQuietly(Connection conn)

使用上面的这两个提交以及回滚的方法就可以进行基本的事务管理的操作了。

2.增删改查操作

下面使用 DbUtils 来对数据库进行基本的增删改查操作,通过代码就可以看出使用这个工具相比之前直接使用 JDBC 来进行相同的操作的话,可以把代码写的多么简洁。

2.1 增加数据

代码如下:

public static void addRecord() throws SQLException {
    QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
    queryRunner.update("insert into account values(null,?,?);", "ddd", 10000);
}

上面方法中的第一句代码便是获得 QueryRunner 类的实例对象,因为使用的有参构造,所以需要传入一个数据源对象,这里的 JDBCUtils2 类是之前使用 c3p0 连接池时抽象出来的一个工具类,getDataSource 方法当然就是获取数据源对象了,第二句代码就是传入需要执行的 SQL 语句以及 SQL 语句需要的参数就可以了。

2.2 删除数据

代码如下:

public static void deleteRecord() throws SQLException {
    QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
    queryRunner.update("delete from account where id = ?", 1);
}

这里删除数据的代码和上面增加数据的代码十分相似了,只是传入的 SQL 语句不同,然后需要的实际参数也根据实际情况而不同。

2.3 修改数据

代码如下:

public static void updateRecord() throws SQLException {
    QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
    queryRunner.update("update account set name = ? where id = ?", "eee", 1);
}

修改数据也和上面增加数据以及删除数据差不多,都只是需要传入的 SQL 语句不一样,而且在进行上面的三种操作的时候也都可以使用占位符 ? 来预先确定参数的位置,可以说操作起来和之前 JDBC 有相通之处,而且操作的步骤也简单得很。

2.4 查询数据

上面已经列举出了使用 DbUtils 对数据库进行增加、删除和修改的操作,下面就来演示一下使用 DbUtils 对数据库进行查询的操作,这里简单地分为查询一条记录以及查询多条记录。

查询一条记录

先列举代码:

public static void selectOneRecord() throws SQLException {
    QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
    Account account = queryRunner.query("select * from account where id = ?", new ResultSetHandler<Account>() {

        @Override
        public Account handle(ResultSet rs) throws SQLException {
            Account account = new Account();
            if(rs.next()) {
                account.setId(rs.getInt("id"));
                account.setName(rs.getString("name"));
                account.setMoney(rs.getDouble("money"));
            }
            return account;
        }
    }, 1);
    System.out.println(account);
}

似乎代码一下增加了很多,不过基本的结构仍然没有改变,上面执行 SQL 语句时是使用的 update() 方法,而现在这里是使用的 query() 方法,其中 query() 方法的第一个参数是要执行的 SQL 语句,第二个参数则是结果集处理对象,主要的作用则是将查询的结果封装成我们设计的类对象,以便于我们使用这个对象进行其它的编程操作,而该方法中的第三个参数则是 SQL 语句中可能需要的条件了,通过使用占位符 ? 动态地来传递参数。

这里需要重点说明一下的就是 ResultSetHandler 这个接口,上面的代码中我们使用一个实现了该接口的匿名内部类来完成对于查询结果的封装,其中尖括号中的类名就是我们想要封装的结果类型,其实仔细看代码的话,实现也是十分简单的,因为这个类实现了接口,所以就要重写接口中的 handle() 方法,看方法声明中的参数就可以看出这个方法需要传入的就是我们执行 SQL 语句得到的结果集,那方法体中的处理也就简单了,首先声明一个 Account 类的实例,然后判断得到的结果集是否有结果,有结果的话就使用结果集中的数据对我们刚声明的那个类实例进行封装,并在最后返回封装好的类实例对象,当然我们最后使用 QueryRunner 的对象调用 query() 方法返回的结果也就是这个封装好的对象了。

查询多条记录

代码如下:

public static void selectAllRecord() throws SQLException {
    QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
    List<Account> list = queryRunner.query("select * from account", new ResultSetHandler<List<Account>>() {

        @Override
        public List<Account> handle(ResultSet rs) throws SQLException {
            List<Account> list = new ArrayList<Account>();
            while(rs.next()) {
                Account account = new Account();
                account.setId(rs.getInt("id"));
                account.setName(rs.getString("name"));
                account.setMoney(rs.getDouble("money"));
                list.add(account);
            }
            return list;
        }
    });
    for (Account account : list) {
        System.out.println(account);
    }
}

查询多条记录和上面查询一条记录一样,同样也是使用的 query() 方法,传入的参数也同样是要执行的 SQL 语句、结果集处理对象以及 SQL 语句中可能需要的参数,所以需要重点说明的还是 ResultSetHandler 这个接口的匿名内部类实现对象,因为是查询的多条记录,因此会将多条记录封装成多个对象,然后再将这封装好的多个对象放入到一个 List 集合当中,所以使用 QueryRunner 类的对象调用 query() 方法返回的也就是这个 List 集合。

下面说明一下匿名内部类实现 ResultSetHandler 这个接口之后所要重写的 handle() 方法,因为将记录封装成对象的操作就是在这里面完成的,首先定义一个专门存放 Account 类对象的 ArrayList 集合,然后因为是查询的多条记录,所以需要对结果集循环遍历,依次对每一条记录进行封装操作,每次循环中都会在最开始声明一个 Account 的类对象,然后执行对记录的封装,最后就是将封装好的 Account 类对象放到最开始声明的 List 集合当中,方法最后将 List 集合返回,这样我们也就可以使用这个 List 集合去做其它的操作了。

上面使用 query() 方法查询一条以及多条记录,在将查询结果封装成对象的时候我们都是自己去实现的 ResultSetHandler 接口,然后再在实现的匿名内部类中去重写接口中的 handle() 方法,这样就可以完成对数据的封装,而 DbUtils 之所以具有这么灵活而且强大的功能,很重要的一点就在于在将查询结果封装为对象的时候,DbUtils 本身提供了许多 ResultSetHandler 接口的实现类。

3.ResultSetHandler的实现类

ResultSetHandler 接口的实现类一共有如下这些:

AbstractKeyedHandler
AbstractListHandler
ArrayHandler
ArrayListHandler
BeanHandler
BeanListHandler
ColumnListHandler
KeyedHandler
MapHandler
MapListHandler
ScalarHandler

最开始的两个是抽象类,这里就不再做说明了,主要说明一下其它的九个实现类。这些实现类有一个相似的特点,就是除了 KeyedHandler 这个类之外,类名中带有 List 的类最后封装得到的一定是多个值或者对象,而不带 List 的类则最后封装得到的一般是一个值或者对象。

3.1 ArrayHandler

这个实现类是将查询得到的一条记录封装成一个数组对象,即将这条记录中的各个值放到数组当中。

代码如下:

public static void ArrayHandlerDemo() throws SQLException {
    QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
    Object[] object = queryRunner.query("select * from account where id = ?", new ArrayHandler(), 1);
    System.out.println(Arrays.toString(object));
}

3.2 ArrayListHandler

很明显可以看出,ArrayListHandler 这个实现类是和上面的 ArrayHandler 相对应的,之前的 ArrayHandler 是将查询得到的一条记录封装成一个数组对象,则这里的 ArrayListHandler 是将查询得到的多条记录分别封装到多个数组当中,最后再将这些数组放入到一个 List 集合当中。

代码如下:

public static void ArrayListHandlerDemo() throws SQLException {
    QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
    List<Object[]>  objs = queryRunner.query("select * from account", new ArrayListHandler());
    for (Object[] obj : objs) {
        System.out.println(Arrays.toString(obj));
    }
}

3.3 BeanHandler

这个就真的是比较常用的了,从名字就可以看出,是将查询得到的一条记录封装成一个 Bean 对象,而这个 Bean 对象的类型则是创建 BeanHandler 类对象时指定的泛型类型。这个在做单表查询时十分常见,通过指定需要得到记录的主键,就可以得到唯一的一条记录。

代码如下:

public static void BeanHandlerDemo() throws SQLException {
    QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
    Account account = queryRunner.query("select * from account where id = ?", new BeanHandler<Account>(Account.class), 1);
    System.out.println(account);
}

3.4 BeanListHandler

这个显然就是和上面的 BeanHandler 相对应的了,上面的 BeanHandler 是将查询得到的一条记录封装成一个 Bean 对象,而 BeanListHandler 则是将查询得到的多条记录分别封装成多个 Bean 对象,然后再将这些 Bean 对象放入到一个 List 集合当中。这个实现类同样在单表查询时使用的非常多,比如需要查询一张数据表中所有记录的时候。

代码如下:

public static void BeanListHandlerDemo() throws SQLException {
    QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
    List<Account> list = queryRunner.query("select * from account", new BeanListHandler<Account>(Account.class));
    for (Account account : list) {
        System.out.println(account);
    }
}

3.5 ColumnListHandler

这个实现类则和上面的几个实现类不同,这个实现类主要是针对某一列的数据进行封装的,它的作用就是将查询得到的多条记录中某列的值全部放到一个 List 集合当中,当然要封装的列是由我们来进行指定的。

代码如下:

public static void ColumnListHandlerDemo() throws SQLException {
    QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
    List<Object> list = queryRunner.query("select * from account", new ColumnListHandler("name"));
    for (Object object : list) {
        System.out.println(object);
    }
}

3.6 MapHandler

这个同样也是十分重要的,这个实现类的作用是将查询得到的一条记录封装到一个 Map 集合当中,集合的结构就是查询得到记录的列名作为 Map 集合的 key,而查询得到记录的数据则作为 Map 集合中对应列的值。

代码如下:

public static void MapHandlerDemo() throws SQLException {
    QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
    Map<String, Object> map = queryRunner.query("select * from account where id = ?", new MapHandler(), 1);
    for (String key : map.keySet()) {
        System.out.println(key+" "+map.get(key));
    }
}

3.7 MapListHandler

很明显,这个实现类是和上面的 MapHandler 相对应的,上面的 MapHandler 是将查询得到的一条记录封装成一个 Map 集合,则这个则是将查询得到的多条记录分别封装到多个 Map 集合当中,最后再将这多个 Map 集合放到一个 List 集合当中。

代码如下:

public static void MapListHandlerDemo() throws SQLException {
    QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
    List<Map<String, Object>> list = queryRunner.query("select * from account", new MapListHandler());
    for (Map<String, Object> map : list) {
        System.out.println(map);
    }
}

这里需要重点强调一下的就是,因为我们最后封装每一条记录得到的都是一个 Map 集合,如果我们配合使用另一个工具 BeanUtils,那我们也是可以将得到的 Map 集合转化成我们想要的 Bean 对象的,主要就是其中 BeanUtils 类的 public static void populate(Object bean, Map properties) 方法,可是我们前面不是已经有了像 BeanHandlerBeanListHandler 这样的实现类了吗?为什么还需要先得到 Map 集合对象,然后再封装成 Bean 对象,答案就是上面的两个实现类都是针对于单表进行操作的,在对多表进行操作时就没有办法了,而在多表查询时,我们仍然可以得到每一条查询结果的 Map 集合对象,如果查询结果是两张表连接查询得到的结果,那我们就可以使用这个 Map 集合来完成对两个不同类型的类对象的属性赋值,为什么可以呢?这是因为上面说的工具中的 BeanUtils 类方法 populate() 是根据属性名称来完成属性赋值的,如果 Map 集合当中有和我们当前想封装对象不符的属性,那么在属性赋值的时候是直接跳过的,而如果缺少我们想封装对象中相应的属性,那么这个属性就会直接被赋值为 null,因此可以使用那个 Map 集合来对不同的类对象进行属性赋值。不过也是因为这个原因,所以需要注意的一点就是,如果需要使用一个 Map 集合对象来对多个不同的类对象赋值的话,那么在设计类的时候就需要注意最好不要将这些类的属性名称设置为一样,否则在进行属性封装的时候就会有一定的困难,虽然也是可以完成封装的。

3.8 KeyedHandler

这个实现类的话最后的封装结果也和 Map 集合有关系,不过得到结果的结果就要比上面的 MapListHandler 要复杂一点了,首先 KeyedHandler 同样也是用于封装多条记录的,而最后多条记录的封装结果都是放在一个 Map 集合当中,这个 Map 集合的 key 是什么呢?它的 key 就是由我们指定的某一列的值,比如如果指定的是 name 的话,那每一条记录的 name 属性值就是这个 Map 集合的 key 了,那这个 Map 集合的 value 呢?这个 Map 集合中的 value 就是每一条查询结果封装得到的 Map 集合,这就是使用 KeyedHandler 实现类封装数据得到结果的结构。

代码如下:

public static void KeyedHandlerDemo() throws SQLException {
    QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
    Map<Object, Map<String, Object>> list = queryRunner.query("select * from account", new KeyedHandler("name"));
    for (Object key : list.keySet()) {
        System.out.println(key+" "+list.get(key));
    }
}

3.9 ScalarHandler

这个封装类的结果和上面所有的实现类都不同,而且也是最简单的,因为它返回的结果就是一个值,具体的应用也应该是十分清楚的,比如要查询一张表中所有的记录条数,那么使用前面的实现类就不好使了,而使用这个实现类就可以很简单的完成功能。

代码如下:

public static void ScalarHandlerDemo() throws SQLException {
    QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getDataSource());
    Object value = queryRunner.query("select count(*) from account", new ScalarHandler());
    System.out.println(value);
}

4.总结

使用 DbUtils 这个工具,从代码上就可以看出是十分简单的,并且它也具有十分灵活的封装数据功能,我觉得掌握使用这个工具的重点在于理解 ResultSetHandler 的实现类得到封装结果的内部结构,这个工具用多了之后真的会上瘾,觉得怎么可以设计的这么简洁,真是越用越喜欢。

坚持原创技术分享,您的支持将鼓励我继续创作!