Apache Commons DbUtils 快速入门与集成示例
从零跑通 Apache Commons DbUtils 1.8.1 的完整 CRUD:建表、插入、查询映射到 Bean 与 Map、更新、删除,并给出绑定数据源、事务回滚、批量执行与取回自增主键的集成示例,代码与回显全部实测。
Apache Commons DbUtils 快速入门与集成示例
目标:用一篇文档从零跑通 Apache Commons DbUtils 1.8.1 的完整增删改查,并给出绑定数据源、事务回滚、批量执行、取回自增主键四类集成示例。所有代码与回显已在 Ubuntu 22.04(JDK 17 + Maven,连接 H2 内嵌数据库)实测通过。
一、环境信息
本章交代实测环境与依赖坐标,让文中每个示例都可以独立复现。Commons DbUtils 1.8.1 编译目标是 Java 8,本文用 JDK 17 运行,并用零安装的 H2 内嵌数据库提供真实 JDBC 连接。
cat /etc/os-release | grep -E "^(NAME|VERSION)="
java -version
mvn -v | grep -E "Apache Maven|Java version"
实测回显:
NAME="Ubuntu"
VERSION="22.04.5 LTS (Jammy Jellyfish)"
openjdk version "17.0.20.1" 2026-08-18
OpenJDK Runtime Environment (build 17.0.20.1+1-1-22.04-Ubuntu)
Apache Maven 3.6.3
Java version: 17.0.20.1, vendor: Ubuntu, runtime: /usr/lib/jvm/java-17-openjdk-amd64
文中统一使用的坐标为:
commons-dbutils:commons-dbutils:1.8.1 (Apache Commons DbUtils 本库)
com.h2database:h2:2.5.252 (H2 内嵌数据库驱动)
💡 技术拓展:在 DbUtils 之上「连接从哪来」决定了代码写法,常见三种获取方式对比,本文两种都用到(第三章用
DriverManager,第四、五章用DataSource):
| 获取方式 | 载体 | 连接归属 | 适用场景 |
|---|---|---|---|
DriverManager 直连 | JDBC 驱动 | 调用方自建自关 | 快速验证、临时脚本(本文第三章) |
DataSource | javax.sql.DataSource 实现 | 每次 getConnection 取、用后关 | 与数据源解耦,便于替换实现(本文第四、五章) |
| 连接池 | DBCP / HikariCP 等 | 池统一取还 | 生产环境,连接复用 |
DbUtils 只做「执行 SQL + 结果集转换」,不接管连接池与事务,因此上面三种连接来源它都兼容。
二、前置条件与工程骨架
本章搭建一个可运行示例的 Java 工程,后续所有示例类都放进这个工程。创建目录与 pom.xml:
mkdir -p /opt/dbutils-demo/src/main/java/net/quzuo/demo
cd /opt/dbutils-demo
该命令无输出。pom.xml 内容:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>net.quzuo.demo</groupId>
<artifactId>dbutils-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.8.1</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.5.252</version>
</dependency>
</dependencies>
</project>
逐项说明:maven.compiler.source/target 指定按 Java 17 语法编译;project.build.sourceEncoding 强制源码按 UTF-8 读取;依赖里 commons-dbutils 是本库、h2 是验证用的内嵌数据库驱动。确认依赖解析结果:
mvn dependency:tree | grep -E "dbutils|h2"
实测回显:
[INFO] +- commons-dbutils:commons-dbutils:jar:1.8.1:compile
[INFO] \- com.h2database:h2:jar:2.5.252:compile
依赖树只有本库与 H2 驱动两个 compile 节点。示例中要映射的实体,写入 src/main/java/net/quzuo/demo/Product.java:
package net.quzuo.demo;
import java.math.BigDecimal;
public class Product {
private int id;
private String name;
private BigDecimal price;
private int stock;
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public BigDecimal getPrice() { return price; }
public void setPrice(BigDecimal price) { this.price = price; }
public int getStock() { return stock; }
public void setStock(int stock) { this.stock = stock; }
@Override
public String toString() {
return "Product{id=" + id + ", name='" + name + "', price=" + price + ", stock=" + stock + "}";
}
}
该 Bean 有 id/name/price/stock 四个属性,属性名与后面建表语句的列名一一对应(大小写不敏感匹配)。
三、快速入门:完整 CRUD 走一遍
本章用一个程序把增(INSERT)、查(SELECT)、改(UPDATE)、删(DELETE)全部走通,展示 DbUtils 最常用的写法。写入 src/main/java/net/quzuo/demo/QuickStart.java:
package net.quzuo.demo;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.MapListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.List;
import java.util.Map;
public class QuickStart {
public static void main(String[] args) throws Exception {
Connection conn = DriverManager.getConnection("jdbc:h2:mem:quick;DB_CLOSE_DELAY=-1");
QueryRunner runner = new QueryRunner();
// 1. 建表
runner.update(conn, "create table product("
+ "id int auto_increment primary key, name varchar(32), price decimal(10,2), stock int)");
System.out.println("1) 建表完成");
// 2. 插入(逐条)
runner.update(conn, "insert into product(name, price, stock) values(?, ?, ?)", "键盘", new BigDecimal("199.00"), 50);
runner.update(conn, "insert into product(name, price, stock) values(?, ?, ?)", "鼠标", new BigDecimal("89.50"), 120);
System.out.println("2) after insert count = "
+ runner.query(conn, "select count(*) from product", new ScalarHandler<>()));
// 3. 查询映射到 Bean
List<Product> products = runner.query(conn,
"select id, name, price, stock from product order by id",
new BeanListHandler<>(Product.class));
System.out.println("3) BeanListHandler 映射: " + products);
// 4. 查询映射到 Map
List<Map<String, Object>> maps = runner.query(conn,
"select id, name, stock from product order by id",
new MapListHandler());
System.out.println("4) MapListHandler 映射: " + maps);
// 5. 更新(价格 +30,库存 -5)
int updated = runner.update(conn,
"update product set price = price + ?, stock = stock - ? where name = ?",
new BigDecimal("30.00"), 5, "键盘");
System.out.println("5) 受影响行数 = " + updated);
// 6. 验证更新
Product keyboard = runner.query(conn,
"select id, name, price, stock from product where name = ?",
new BeanListHandler<>(Product.class), "键盘").get(0);
System.out.println("6) 更新后键盘: " + keyboard);
// 7. 删除
int deleted = runner.update(conn, "delete from product where name = ?", "鼠标");
System.out.println("7) 删除行数 = " + deleted);
// 8. 验证删除后总数
System.out.println("8) after delete count = "
+ runner.query(conn, "select count(*) from product", new ScalarHandler<>()));
conn.close();
}
}
3.1 程序结构与关键写法
程序只用到一个执行入口 QueryRunner 与三个处理器:
new QueryRunner():不绑数据源,update/query全部显式传入conn(连接由本程序创建与关闭)。runner.update(conn, sql, 可变参数):用于建表、插入、更新、删除,返回受影响行数(DDL 返回 0)。new BeanListHandler<>(Product.class):把结果行映射成Product对象列表,供「取实体」用。new MapListHandler():把结果行映射成Map列表,适合只取部分列、不建实体的场合。new ScalarHandler<>():取单值,用于select count(*)这类聚合。
参数化写法使用 ? 占位符,参数按顺序跟在 SQL 之后(如 runner.update(conn, sql, "键盘", new BigDecimal("199.00"), 50)),由 DbUtils 内部用 PreparedStatement 填充,避免 SQL 拼接注入。
3.2 运行结果
编译运行:
mvn -q compile exec:java -Dexec.mainClass=net.quzuo.demo.QuickStart
实测回显:
1) 建表完成
2) after insert count = 2
3) BeanListHandler 映射: [Product{id=1, name='键盘', price=199.00, stock=50}, Product{id=2, name='鼠标', price=89.50, stock=120}]
4) MapListHandler 映射: [{ID=1, NAME=键盘, STOCK=50}, {ID=2, NAME=鼠标, STOCK=120}]
5) 受影响行数 = 1
6) 更新后键盘: Product{id=1, name='键盘', price=229.00, stock=45}
7) 删除行数 = 1
8) after delete count = 1
逐步解读:
- 第 2 行
after insert count = 2:两条insert各成功写入一行,用ScalarHandler取回的计数为 2。 - 第 3 行:
BeanListHandler把每行映射为Product,中文值、BigDecimal价格、整型库存都正确落到属性上。 - 第 4 行:
MapListHandler只查id, name, stock三列,key 是数据库返回的大写列名(H2 把未加引号的标识符折叠为大写)。 - 第 5、6 行:一次
update同时改价格与库存,返回受影响行数 1;再查回来确认price=229.00(199.00+30)、stock=45(50-5)。 - 第 7、8 行:删除返回受影响行数 1,删除后总数为 1,增删改查闭环成立。
至此,DbUtils 的完整 CRUD 已跑通。
四、集成示例一:绑定数据源
本章演示第二种连接来源——把 DataSource 交给 QueryRunner,之后执行 SQL 就不必再手动创建和关闭连接。写入 src/main/java/net/quzuo/demo/DataSourceDemo.java:
package net.quzuo.demo;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import org.h2.jdbcx.JdbcDataSource;
import java.math.BigDecimal;
import java.util.List;
public class DataSourceDemo {
public static void main(String[] args) throws Exception {
JdbcDataSource ds = new JdbcDataSource();
ds.setURL("jdbc:h2:mem:dsdemo;DB_CLOSE_DELAY=-1");
ds.setUser("sa");
ds.setPassword("");
QueryRunner runner = new QueryRunner(ds);
System.out.println("是否绑定同一数据源: " + (runner.getDataSource() == ds));
runner.update("create table product("
+ "id int auto_increment primary key, name varchar(32), price decimal(10,2), stock int)");
runner.update("insert into product(name, price, stock) values(?, ?, ?)",
"键盘", new BigDecimal("199.00"), 50);
runner.update("insert into product(name, price, stock) values(?, ?, ?)",
"鼠标", new BigDecimal("89.50"), 120);
List<Product> products = runner.query("select id, name, price, stock from product order by id",
new BeanListHandler<>(Product.class));
System.out.println("查询结果: " + products);
System.out.println("总记录数: " + runner.query("select count(*) from product", new ScalarHandler<>()));
}
}
写法要点:new QueryRunner(ds) 绑定数据源后,所有 update/query 都省略了 Connection 参数——DbUtils 会在每次调用时从数据源取连接、用完立即归还并关闭,因此循环里反复执行 SQL 不会泄漏连接。编译运行:
mvn -q compile exec:java -Dexec.mainClass=net.quzuo.demo.DataSourceDemo
实测回显:
是否绑定同一数据源: true
查询结果: [Product{id=1, name='键盘', price=199.00, stock=50}, Product{id=2, name='鼠标', price=89.50, stock=120}]
总记录数: 2
第一行证明 getDataSource() 返回的正是构造时传入的实例;后两行说明省去 Connection 后查询与聚合照常工作。生产环境把这里的 JdbcDataSource 换成连接池的数据源即可,代码其余部分不必改动。
五、集成示例二:事务、批量与取回主键
本章覆盖三类高频集成需求:手动事务、批量执行、取回自增主键。它们都构建在数据源之上。
5.1 事务控制与回滚
QueryRunner 本身不开启事务,事务需要调用方在 Connection 上控制。写入 src/main/java/net/quzuo/demo/TransactionDemo.java:
package net.quzuo.demo;
import org.apache.commons.dbutils.DbUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import org.h2.jdbcx.JdbcDataSource;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.SQLException;
public class TransactionDemo {
public static void main(String[] args) throws Exception {
JdbcDataSource ds = new JdbcDataSource();
ds.setURL("jdbc:h2:mem:tx;DB_CLOSE_DELAY=-1");
ds.setUser("sa");
ds.setPassword("");
QueryRunner runner = new QueryRunner(ds);
runner.update("create table product("
+ "id int auto_increment primary key, name varchar(32), price decimal(10,2), stock int)");
runner.update("insert into product(name, price, stock) values(?, ?, ?)",
"键盘", new BigDecimal("199.00"), 50);
Connection conn = ds.getConnection();
try {
conn.setAutoCommit(false);
runner.update(conn, "update product set stock = stock - ? where name = ?", 5, "键盘");
Product after = runner.query(conn, "select id, name, price, stock from product where name = ?",
new BeanHandler<>(Product.class), "键盘");
System.out.println("事务内读取: " + after);
DbUtils.rollbackAndClose(conn);
System.out.println("已回滚");
} catch (SQLException e) {
DbUtils.rollbackAndClose(conn);
}
Product reverted = runner.query("select id, name, price, stock from product where name = ?",
new BeanHandler<>(Product.class), "键盘");
System.out.println("回滚后再读: " + reverted);
}
}
要点:conn.setAutoCommit(false) 开启事务;同一连接上连续执行 SQL;用 DbUtils.rollbackAndClose(conn) 回滚并关闭连接(DbUtils 的工具类 DbUtils 提供 commitAndClose/rollbackAndClose/closeQuietly 等资源管理方法)。编译运行:
mvn -q compile exec:java -Dexec.mainClass=net.quzuo.demo.TransactionDemo
实测回显:
事务内读取: Product{id=1, name='键盘', price=199.00, stock=45}
已回滚
回滚后再读: Product{id=1, name='键盘', price=199.00, stock=50}
事务内读到库存已被减到 45,回滚后再读恢复到 50,说明未提交的修改被正确撤销。
5.2 批量执行
batch 用同一段 SQL 跑多组参数,返回每条语句的受影响行数数组。写入 src/main/java/net/quzuo/demo/BatchDemo.java:
package net.quzuo.demo;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import org.h2.jdbcx.JdbcDataSource;
import java.math.BigDecimal;
import java.util.Arrays;
public class BatchDemo {
public static void main(String[] args) throws Exception {
JdbcDataSource ds = new JdbcDataSource();
ds.setURL("jdbc:h2:mem:batch;DB_CLOSE_DELAY=-1");
ds.setUser("sa");
ds.setPassword("");
QueryRunner runner = new QueryRunner(ds);
runner.update("create table product("
+ "id int auto_increment primary key, name varchar(32), price decimal(10,2), stock int)");
int[] affected = runner.batch("insert into product(name, price, stock) values(?, ?, ?)",
new Object[][]{
{"键盘", new BigDecimal("199.00"), 50},
{"鼠标", new BigDecimal("89.50"), 120},
{"显示器", new BigDecimal("1299.00"), 20}
});
System.out.println("batch 每行受影响数: " + Arrays.toString(affected));
System.out.println("总记录数: " + runner.query("select count(*) from product", new ScalarHandler<>()));
}
}
要点:batch 的第二个参数是二维数组,每个一维数组是一条语句的一组参数,顺序与外层数组一致。编译运行:
mvn -q compile exec:java -Dexec.mainClass=net.quzuo.demo.BatchDemo
实测回显:
batch 每行受影响数: [1, 1, 1]
总记录数: 3
返回数组 [1, 1, 1] 表示三条 INSERT 各成功插入一行,总数 3 印证批量生效。
5.3 取回自增主键
insert 在写入后用处理器承接数据库回传的生成键。写入 src/main/java/net/quzuo/demo/KeyDemo.java:
package net.quzuo.demo;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import org.h2.jdbcx.JdbcDataSource;
import java.math.BigDecimal;
public class KeyDemo {
public static void main(String[] args) throws Exception {
JdbcDataSource ds = new JdbcDataSource();
ds.setURL("jdbc:h2:mem:keys;DB_CLOSE_DELAY=-1");
ds.setUser("sa");
ds.setPassword("");
QueryRunner runner = new QueryRunner(ds);
runner.update("create table product("
+ "id int auto_increment primary key, name varchar(32), price decimal(10,2), stock int)");
Object key1 = runner.insert("insert into product(name, price, stock) values(?, ?, ?)",
new ScalarHandler<>(), "键盘", new BigDecimal("199.00"), 50);
Object key2 = runner.insert("insert into product(name, price, stock) values(?, ?, ?)",
new ScalarHandler<>(), "鼠标", new BigDecimal("89.50"), 120);
System.out.println("第一条自增主键: " + key1);
System.out.println("第二条自增主键: " + key2);
}
}
要点:insert(sql, handler, 可变参数) 与 update 写法一致,差别在于它返回 handler 转换后的结果——ScalarHandler 直接拿到自增主键。编译运行:
mvn -q compile exec:java -Dexec.mainClass=net.quzuo.demo.KeyDemo
实测回显:
第一条自增主键: 1
第二条自增主键: 2
两次插入分别取回主键 1 与 2,可用于插入后立即得到新记录 ID。
⚠️ 注意:若把
insert的处理器换成BeanHandler<>(Product.class),返回的Product只有主键列被填充,其余属性仍是默认值。实测回显为Product{id=1, name='null', price=null, stock=0}——因为数据库回传的生成键结果集只含主键列,BeanHandler只能映射这一列。要拿完整行,需按主键再查一次,或直接用ScalarHandler取主键。
六、常见问题
Q1:Runner.update 返回值是 0,是没执行成功吗?
不是。update 返回受影响行数:建表等 DDL 语句本就返回 0,update/delete 命中若干行则返回对应行数(本文实测改库存返回 1、删除返回 1)。判断成败要看是否有异常抛出与后续查询结果,而非单看该返回值。
Q2:用 QueryRunner()(不绑数据源)调用 query(sql, handler) 报错怎么办?
报错信息是「QueryRunner requires a DataSource to be invoked in this way, or a Connection should be passed in」。要么改成 QueryRunner(dataSource)(见第四章),要么在每个方法里显式传入 Connection(见第三章)。
Q3:MapListHandler 返回的 Map 里 key 为什么是大写的?
H2 默认把未加引号的标识符折叠为大写,所以返回列名是 ID/NAME 形态。这是数据库的标识符规则,不是 DbUtils 的处理;读取时按实际返回 key 访问即可。
Q4:插入后想拿到新记录的自增主键,该怎么写?
用 runner.insert(sql, new ScalarHandler<>(), 可变参数),返回即主键值(本文 5.3 实测得到 1、2)。不要用 BeanHandler 承接,它只会映射生成键结果集的列。
Q5:事务里怎么保证出错时回滚并释放连接?
用 DbUtils.rollbackAndClose(conn),它同时完成回滚与关闭;正常提交则用 DbUtils.commitAndClose(conn)。这两个工具方法来自 org.apache.commons.dbutils.DbUtils,能避免手动 rollback+close 的遗漏。
Q6:BeanHandler 查出来 createdAt 是 null,数据库列是 created_at,怎么回事?
默认 BeanProcessor 不做下划线转驼峰,created_at 无法映射到 createdAt。改处理器为 new BeanListHandler<>(User.class, new BasicRowProcessor(new GenerousBeanProcessor())) 即可正确映射。
七、参考链接
- Apache Commons DbUtils 官方主页:https://commons.apache.org/proper/commons-dbutils/
QueryRunnerAPI 文档:https://commons.apache.org/proper/commons-dbutils/apidocs/org/apache/commons/dbutils/QueryRunner.htmlDbUtils工具类 API 文档:https://commons.apache.org/proper/commons-dbutils/apidocs/org/apache/commons/dbutils/DbUtils.htmlhandlers包(各 ResultSetHandler 实现)API 文档:https://commons.apache.org/proper/commons-dbutils/apidocs/org/apache/commons/dbutils/handlers/package-summary.html- Maven 中央仓库 1.8.1 目录:https://repo1.maven.org/maven2/commons-dbutils/commons-dbutils/1.8.1/
- H2 Database 主页:https://h2database.com/
- H2 生成列与自增语法文档:https://h2database.com/html/features.html