☕ 第十一章 JDBC
📝 一句话概括

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() 聚合
📝 名词解释:JavaBean

就是「只有私有属性 + getter/setter + 空构造」的标准类——咱们第三章封装(3.3 封装性)写出规矩、一路用到现在的那种类。Student 就是标准的 JavaBean。

ScalarHandler 的返回类型要稍微留心:COUNT(*) 这类聚合值,返回的是 Long(要用 long 接就自动拆箱),别拿 int 的思路硬接大表计数。

四、管家凭什么会拼对象?——反射正式上岗

BeanListHandler 凭什么知道把 name 列的值装进 setName()?

  1. 拿到结果集的列名(name、age...)
  2. 拼出 set + 首字母大写 + 括号 → setName(...)
  3. 按方法名找到方法并调用——这不就是 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 和「我要什么形状」。这行代码的舒适度,就是工具类的含金量。

🧪 三个 30 秒小实验

实验 1:属性名对不上会怎样? 把 Student 的 name 改名成 xingming 再查 → 不报错,但 getName() 是 null。记住:默默丢列,不吭声。

实验 2:用错 Handler 会怎样? 查全表却传 new BeanHandler<>() → 不报错,但 list 里只有第一行那一条记录。形状选错,数据悄悄缩水。

实验 3:update 跑 SELECT 会怎样? qr.update("SELECT * FROM student") → 抛 SQLException,和 11.2 的 Statement 串门同一个下场——两路分工是 JDBC 合同层定的,管家也改不了。

六、常见误区

  1. 以为 DbUtils 能防 SQL 注入——不能。SQL 拼接的安全问题属于 PreparedStatement 的战场,DbUtils 只是省样板代码,该用占位符还得用。
  2. 忘了导两个 jar——commons-dbutils 是管家本体,Druid(或别的池子)是租车行,两个都要 Add as Library。
  3. 属性名和列名对不上还怪管家——对不上不报错、字段是 null,排查第一嫌疑。
  4. 拿 int 接 ScalarHandler 的 COUNT 结果——它给的是 Long,赋给 int 编译都过不了(自动拆箱也不会从 Long 拆到 int)。
  5. 以为还得手动关连接——构造时传了 DataSource,连接的借还管家全包;只有手动传 Connection 的用法才需要自己关。

七、自测题

❓ Q1:DbUtils 的两大助手分别叫什么、各管什么?

QueryRunner(执行者:接 SQL 和参数去跑)和 ResultSetHandler(装配工:决定结果集拼成什么形状)。

❓ Q2:`new QueryRunner(ds)` 传连接池进去,省了哪些事?

连接的借、还、关全由管家接管——不用再写 getConnection、不用 try-with-resources 套娃、不用手动 close(11.4 的「close 是还车」由管家代劳)。

❓ Q3:查全表、按 id 查一个人、统计人数,各用哪个 Handler?

查全表 → BeanListHandler(List<Student>);查一个人 → BeanHandler(一个对象);统计人数 → ScalarHandler(一个 Long)。

❓ Q4:查出来所有 String 字段都是 null,但没报任何错,最可能的原因?

JavaBean 属性名和列名对不上——装配靠反射拿列名调 setXxx,对不上的列默默丢掉。改属性名(或加列别名)即可。

❓ Q5(毕业题):不看书默写「用 DbUtils 查全表返回 `List` 并遍历打印」,再补一行统计总人数。

要点:① 构造 QueryRunner(ds);② qr.query(sql, new BeanListHandler<>(Student.class));③ 增强 for / forEach 遍历;④ qr.query("SELECT COUNT(*) FROM student", new ScalarHandler<>()) 用 Long 接;⑤ 想起来属性名 = 列名。全对就毕业。

下一步学什么

相关笔记