DbUtils 是别人替你写好的 JDBC 工具类:QueryRunner 管执行,ResultSetHandler 管「结果集拼成什么形状」——while 循环、getXxx 抠值、手工拼对象这些重复劳动,从此一行代码全包。
上一节 11.4 数据库连接池 里留了个钩子:JDBCUtils 工具类——把租车行和五步套路封装起来,以后写查询只剩核心逻辑。这节的 DbUtils 就是别人写好的成品工具类(Apache Commons 出品),比咱自己手搓的更周全。
一、先看看手写 JDBC 有多啰嗦
回顾 11.2 Demo12 查全表的姿势:
List<Student> list = new ArrayList<>();
ResultSet rs = stmt.executeQuery(sql);
while (rs.next()) { // 1. 一行行挪光标
int id = rs.getInt("id"); // 2. 一列列抠值
String name = rs.getString("name");
Student s = new Student(); // 3. 手工 new 对象
s.setId(id); // 4. 手工装字段
s.setName(name);
list.add(s); // 5. 装进集合
}
问题来了:这个套路对任何表都长一个样——换张表,无非是列名、类型、对象类不同,循环结构原封不动。重复劳动写十遍,错一处(比如漏个 next())还要当场翻车。
「重复的套路 + 只变一点」= 该抽工具了。而「结果集怎么拼成对象」这件事只差一张说明书——把说明书当参数传进去,套路就能写死。这就是 DbUtils 的设计。
二、DbUtils 两大助手
DbUtils 全家就围绕两个人干活:
| 助手 | 角色 | 一句话职责 |
|---|---|---|
| QueryRunner | 执行者 | 接 SQL 和参数,去数据库跑一趟 |
| ResultSetHandler | 装配工 | 决定查回来的结果集拼成什么形状 |
// 构造时直接把 11.4 的租车行(连接池)交给它
QueryRunner qr = new QueryRunner(dataSource);
构造方法传 DataSource——连接的借、还、关全由管家接管,你从此不再碰 Connection,也不再写 try-with-resources 套娃。11.4 的池子直接无缝接上。
两大方法(对应 11.2 Statement 的两路)
int rows = qr.update(sql, 参数...); // 增删改 → 返回受影响行数
T result = qr.query(sql, handler, 参数...); // 查询 → 返回 handler 指定的形状
update / query 的分工和 executeUpdate / executeQuery 一模一样(11.2 的账),只是名字更短、参数更省。
三、三大 Handler:一个查询一个形状
查询结果就三种常见形状,官方各给了一个现成 Handler:
| Handler | 结果集长相 | 返回什么 | 典型场景 |
|---|---|---|---|
| BeanHandler | 一条记录 | 一个 JavaBean 对象 | 按主键查一个人 |
| BeanListHandler | 多行记录 | List<JavaBean> |
查全表 |
| ScalarHandler | 单个值 | Long 等包装类型 |
COUNT(*)、MAX() 聚合 |
就是「只有私有属性 + getter/setter + 空构造」的标准类——咱们第三章封装(3.3 封装性)写出规矩、一路用到现在的那种类。Student 就是标准的 JavaBean。
ScalarHandler 的返回类型要稍微留心:COUNT(*) 这类聚合值,返回的是 Long(要用 long 接就自动拆箱),别拿 int 的思路硬接大表计数。
四、管家凭什么会拼对象?——反射正式上岗
BeanListHandler 凭什么知道把 name 列的值装进 setName()?
- 拿到结果集的列名(
name、age...) - 拼出
set+ 首字母大写 + 括号 →setName(...) - 按方法名找到方法并调用——这不就是 9.1 反射概述 干的事吗!运行时按名字拆类、调方法
所以有一条保命规矩:
JavaBean 的属性名(name)要和表的列名(name)一致(或按驼峰对应),管家才拼得进去。对不上的列默默丢掉、不报错——查出来字段是 null,九成是这个原因。
这一下把第九章的伏笔全收了:9.1 说反射是「Spring / MyBatis 这类框架的命根子」——DbUtils 虽小,已经是微型框架的现场:你不写装配代码,只贴个「说明书参数」,框架替你干。
五、动手:Demo15 用 DbUtils 查全表
准备动作(和 11.3 导驱动一样):把 commons-dbutils-1.8.jar 和 Druid 的 jar 一起丢进 libs,各自 Add as Library。
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import java.util.List;
public class Demo15 {
public static void main(String[] args) throws Exception {
// 1. 借 11.4 的租车行(Druid 数据源,配置同 Demo14)
DataSource ds = DruidFactory.getDataSource();
QueryRunner qr = new QueryRunner(ds);
// 2. 查全表:一行拿到 List<Student>
List<Student> list = qr.query(
"SELECT id, name, age FROM student",
new BeanListHandler<>(Student.class));
// 3. 遍历——已经是普通 Java 对象,和数据库没半点关系了
list.forEach(s -> System.out.println(s.getName() + " " + s.getAge()));
// 4. 统计人数:ScalarHandler 出场
Long count = qr.query("SELECT COUNT(*) FROM student",
new ScalarHandler<>());
System.out.println("共 " + count + " 人");
}
}
对比 Demo12 的手写查询:循环没了、抠值没了、拼对象没了、关资源也没了——只剩 SQL 和「我要什么形状」。这行代码的舒适度,就是工具类的含金量。
实验 1:属性名对不上会怎样?
把 Student 的 name 改名成 xingming 再查 → 不报错,但 getName() 是 null。记住:默默丢列,不吭声。
实验 2:用错 Handler 会怎样?
查全表却传 new BeanHandler<>() → 不报错,但 list 里只有第一行那一条记录。形状选错,数据悄悄缩水。
实验 3:update 跑 SELECT 会怎样?
qr.update("SELECT * FROM student") → 抛 SQLException,和 11.2 的 Statement 串门同一个下场——两路分工是 JDBC 合同层定的,管家也改不了。
六、常见误区
- 以为 DbUtils 能防 SQL 注入——不能。SQL 拼接的安全问题属于
PreparedStatement的战场,DbUtils 只是省样板代码,该用占位符还得用。 - 忘了导两个 jar——
commons-dbutils是管家本体,Druid(或别的池子)是租车行,两个都要 Add as Library。 - 属性名和列名对不上还怪管家——对不上不报错、字段是
null,排查第一嫌疑。 - 拿
int接ScalarHandler的COUNT结果——它给的是Long,赋给int编译都过不了(自动拆箱也不会从 Long 拆到 int)。 - 以为还得手动关连接——构造时传了
DataSource,连接的借还管家全包;只有手动传Connection的用法才需要自己关。
七、自测题
QueryRunner(执行者:接 SQL 和参数去跑)和 ResultSetHandler(装配工:决定结果集拼成什么形状)。
连接的借、还、关全由管家接管——不用再写 getConnection、不用 try-with-resources 套娃、不用手动 close(11.4 的「close 是还车」由管家代劳)。
查全表 → BeanListHandler(List<Student>);查一个人 → BeanHandler(一个对象);统计人数 → ScalarHandler(一个 Long)。
JavaBean 属性名和列名对不上——装配靠反射拿列名调 setXxx,对不上的列默默丢掉。改属性名(或加列别名)即可。
要点:① 构造 QueryRunner(ds);② qr.query(sql, new BeanListHandler<>(Student.class));③ 增强 for / forEach 遍历;④ qr.query("SELECT COUNT(*) FROM student", new ScalarHandler<>()) 用 Long 接;⑤ 想起来属性名 = 列名。全对就毕业。
下一步学什么
- ☑ 11.6 本章小结 ✅ 已完成——点此复习:五节串线 + 四段模板 + 五大名坑 + 毕业大题「员工管理小系统核心层」。
相关笔记
- 第十一章 JDBC/11.1 什么是JDBC:总合同——DbUtils 也是站在接口上干活的
- 第十一章 JDBC/11.2 JDBC的常用API:update/query 两路分工的出处
- 第十一章 JDBC/11.3 JDBC编程:手拼 SQL 的难受与导 jar 姿势
- 第十一章 JDBC/11.4 数据库连接池:构造参数里的那家租车行
- 第九章 反射机制和注解/9.1 反射概述:装配对象的底层引擎
- 第三章 面向对象(上)/3.3 封装性:JavaBean 的出身