用JDBC操作数据库,最磨人的不是SQL本身,而是每次写那套“获取连接、创建Statement、遍历结果集、finally里关资源”的模板代码。DBUtils就是冲着这个痛点来的,它把JDBC的脏活累活都给封装好了,一行代码就能完成查询映射,增删改也是干净利落。与之搭配的DBCP连接池,解决的是另一层问题——数据库连接的创建和销毁太昂贵,池化之后能让应用在高并发下不被连接拖垮。这一期内容我不打算念API手册,而是按我在项目里实际用下来的经验,把QueryRunner核心类怎么用、查询和增删改怎么落地、DBCP连接池怎么配,拆开了揉碎了讲清楚。适合那些已经写过原生JDBC、想把数据访问层收拾得清爽一点的Java开发者。
1. 整体设计与解决思路
1.1 原生JDBC开发的真实痛点
先聊一个几乎每个人都经历过的场景。你要查一个用户列表,用原生JDBC写,大概长这样:
public List<User> findAll() { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; List<User> list = new ArrayList<>(); try { conn = DriverManager.getConnection(url, username, password); String sql = "SELECT id, name, email FROM user"; ps = conn.prepareStatement(sql); rs = ps.executeQuery(); while (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setName(rs.getString("name")); user.setEmail(rs.getString("email")); list.add(user); } return list; } catch (SQLException e) { throw new RuntimeException(e); } finally { try { if (rs != null) rs.close(); } catch (SQLException e) { } try { if (ps != null) ps.close(); } catch (SQLException e) { } try { if (conn != null) conn.close(); } catch (SQLException e) { } } }这段代码看着不多,但每一个查询方法都得复制一份。一个DAO层几十个方法,代码量就被大量重复的样板代码灌满了。而且最容易出问题的地方集中在finally块——忘了关闭ResultSet、PreparedStatement、Connection,轻则连接泄漏,重则把数据库连接池活活耗尽。反过来,真正核心的业务逻辑只有两三行,绝大部分精力都耗在了“伺候”JDBC API上。
另一个问题是结果集的映射。数据库是关系表,Java是对象,这两者之间没有任何自动转换机制,每一列都得手动set到实体类上。表字段多的时候,这个机械劳动非常容易写错、漏写。
1.2 为什么选择DBUtils + DBCP这套组合
市面上做数据库访问的工具不少,MyBatis、Hibernate、Spring JDBC Template等等,性能和学习成本差异很大。DBUtils走的是一条极轻量的路线:它不搞复杂的XML映射,不引入字节码增强,也不要求你学一套全新的查询语法,本质上就是“对JDBC做薄薄的一层封装”,API保留了JDBC的原汁原味,同时把重复代码消灭掉。
它的核心价值可以概括为三句话:
- 查询自动映射:通过ResultSetHandler把结果集转换为实体对象、List、Map、单一数值等。
- 统一管理资源:内部帮你处理Statement和ResultSet的关闭,降低资源泄漏的概率。
- 参数化SQL支持:基于PreparedStatement占位符,原生防SQL注入。
而DBCP连接池解决的问题独立于DBUtils。每条真实数据库连接从建立到销毁都是有成本的,TCP握手、认证、内存分配,高并发下如果每个请求都新建连接,数据库必然扛不住。连接池的思路是提前创建一批连接放在池子里,请求来了直接借用,用完还回去,连接就活活复用起来了。
这两个东西各管一层,组合在一起非常自然:DBCP负责把连接管好,DBUtils负责把数据库操作写爽,它们之间甚至不需要做任何特殊粘合,因为DBUtils天生支持传入DataSource。
2. DBUtils核心类QueryRunner深入拆解
2.1 QueryRunner对象的两种创建方式
QueryRunner是DBUtils最核心的类,即使整个工具包只认识一个类,也必须把它搞明白。创建方式有两大类,对应完全不同的使用风格。
第一种是空构造:
QueryRunner runner = new QueryRunner();这种方式创建的QueryRunner对象内部不持有任何连接或数据源,执行任何SQL都必须显式把Connection传进去,比如runner.update(conn, sql, params)、runner.query(conn, sql, handler, params)。这种模式下,连接的获取、关闭、事务提交全部由调用方自己管,适合需要手动控制事务的场景。
第二种是传入DataSource:
QueryRunner runner = new QueryRunner(dataSource);传入DataSource之后,执行SQL时就不需要传入Connection了,QueryRunner在内部自己从数据源获取连接,执行完再自动归还关闭。比如runner.query(sql, handler, params)或runner.update(sql, params)。这种风格最省心,日常增删改查基本选它。
实际项目里我是这样分场景的:简单查询和单表操作就走DataSource模式,代码最少;涉及多表写入需要事务的时候,改用空QueryRunner手动传Connection,事务边界就掌握在自己手里了。
2.2 核心方法族一览
QueryRunner的主要方法不多,但每个都很能打。用一个表格把它们的基本职责列出来:
| 方法 | 作用 | 返回值 |
|---|---|---|
| update | 执行增、删、改操作 | int(受影响行数) |
| query | 执行查询操作 | T(由ResultSetHandler决定) |
| batch | 批量执行增删改SQL | int[](每批受影响行数) |
| insert | 执行插入并返回生成的主键 | T(由ResultSetHandler决定) |
| insertBatch | 批量插入并返回主键 | T[] |
| execute | 执行任意SQL | boolean |
方法都有重载:带Connection参数和不带Connection参数两种形态。带Connection时,连接生命周期由你控制;不带Connection时,QueryRunner从构造传入的DataSource获取连接并在finally中关闭。
这里有一个容易混淆的点要记住:用new QueryRunner(dataSource)之后,你执行query/update根本不用写关闭连接的代码,QueryRunner内部会在finally块中自动把连接归还给连接池。我之前见过有同学在用了DataSouce模式之后,还在外面手动把从查询里拿到的Connection关掉,反而把连接池里的连接给关了,下一个请求再用就报错。
2.3 参数占位符与SQL注入防护
QueryRunner的SQL参数全部走PreparedStatement的?占位符机制。举个例子:
String sql = "UPDATE user SET name = ? WHERE id = ?"; int rows = runner.update(sql, "张三", 1001);方法内部会把参数按顺序绑定到占位符上。参数个数和占位符个数必须完全一致,否则运行时会抛SQLException:参数索引越界或者找不到参数。这个检查发生在数据库驱动执行阶段,所以不是编译期就能发现的错误,写完代码一定要跑一遍冒烟测试。
因为底层是PreparedStatement预编译,?里的值只作为参数传递,不会被拼接进SQL语句,所以不需要担心字符串拼接带来的SQL注入问题。但要注意一个常见误用:如果SQL里有表名、列名、排序字段需要动态拼接,这些地方不能使用占位符,只能白名单校验后手动拼接,这是几乎所有ORM框架都无法完美解决的通病。
3. 查询操作:ResultSetHandler全攻略
3.1 内置Handler速查表
查询操作除了SQL语句本身,另一个关键点就是结果集怎么处理。QueryRunner把这一层抽象成了ResultSetHandler接口,相当于一个“结果集转换器”。DBUtils自带了好几个实用处理器,绝对够日常用:
| 处理器类 | 用途 |
|---|---|
| BeanHandler | 把第一行记录封装为指定类型的JavaBean |
| BeanListHandler | 把所有记录封装为JavaBean的List |
| MapHandler | 把第一行记录封装为Map |
| MapListHandler | 把所有记录封装为Map的List |
| ScalarHandler | 把第一行第一列封装为指定类型,常用于统计、聚合 |
| ColumnListHandler | 把某一列的所有值封装为List |
| KeyedHandler | 返回一个Map,key为指定列的值,value为对应行的Map |
| ArrayHandler | 把第一行记录封装为Object数组 |
| ArrayListHandler | 把每行记录封装为Object数组,再放到List里 |
3.2 实体查询和结果集映射实战
最常见的查询场景就是返回实体对象列表,对应BeanListHandler:
QueryRunner runner = new QueryRunner(dataSource); String sql = "SELECT id, name, age, email FROM user WHERE age > ?"; List<User> users = runner.query(sql, new BeanListHandler<>(User.class), 18);这套代码里完全没有ResultSet和循环赋值的影子,一行query搞定映射。User类要求是一个标准的JavaBean:无参构造、私有属性、getter/setter。DBUtils通过反射创建对象并按列名调用setter赋值。
这里必须提醒一个细节:DBUtils的默认映射规则是列名与JavaBean属性名必须一致,它不会自动处理下划线转驼峰。比如数据库字段是user_name,JavaBean属性是userName,如果直接执行SELECT user_name FROM user,映射结果中userName就是null。两种解决办法:
第一种是查询时给列起别名:
SELECT user_name AS userName FROM user第二种是自定义RowProcessor。DBUtils提供了GenerousBeanProcessor,可以通过它开启下划线到驼峰的自动转换,这个我在后面自定义处理器部分细说。
再补充一个关于ScalarHandler的用法。之前做一个统计接口,要查符合条件的用户总数,按传统写法要先拿ResultSet再调rs.getInt(1),而ScalarHandler直接返回你想要的类型:
String sql = "SELECT COUNT(*) FROM user WHERE status = ?"; Long total = runner.query(sql, new ScalarHandler<>(), 1);注意count函数返回的通常是Long或BigInteger,接收类型别用int,最好直接声明为Long,避免类型转换异常。
3.3 自定义Handler与下划线转驼峰
当内置Handler满足不了需求时,就需要自己实现ResultSetHandler接口。这个接口只有一个方法:
public interface ResultSetHandler<T> { T handle(ResultSet rs) throws SQLException; }参数就是你拿到的原始ResultSet,返回任意类型。比如要把查询结果组装成一个树形结构、或者按照某个字段分组计算,直接在这个方法里写逻辑就行。
public class UserSummaryHandler implements ResultSetHandler<Map<String, Object>> { @Override public Map<String, Object> handle(ResultSet rs) throws SQLException { Map<String, Object> result = new HashMap<>(); int activeCount = 0, inactiveCount = 0; while (rs.next()) { int status = rs.getInt("status"); if (status == 1) activeCount++; else inactiveCount++; } result.put("activeCount", activeCount); result.put("inactiveCount", inactiveCount); return result; } }自定义Handler的收益在于可以把复杂的统计逻辑和DAO层彻底解耦,测试起来也方便,只要构造一个ResultSet就可以单测这个转换器。如果要做下划线转驼峰,推荐解决方案是注册一个自定义RowProcessor:
BeanProcessor beans = new GenerousBeanProcessor(); RowProcessor processor = new BasicRowProcessor(beans); QueryRunner runner = new QueryRunner(dataSource); String sql = "SELECT user_name, user_email FROM user WHERE id = ?"; User user = runner.query(sql, new BeanHandler<>(User.class, processor), 1);GenerousBeanProcessor会自动把user_name映射到userName,省去了每个查询都写别名的麻烦。
4. 增删改操作与批量处理
4.1 update方法执行增删改的落地写法
增删改统一走QueryRunner的update方法,返回一个int表示受影响行数,逻辑层可以用这个返回值判断操作是否真的影响了数据。
插入示例:
String sql = "INSERT INTO user(name, age, email) VALUES(?, ?, ?)"; int rows = runner.update(sql, "王小明", 25, "xmwang@example.com");更新示例:
String sql = "UPDATE user SET age = ? WHERE id = ?"; int rows = runner.update(sql, 26, 1001);删除示例:
String sql = "DELETE FROM user WHERE id = ?"; int rows = runner.update(sql, 1001);这三个例子看着简单,但有几个执行细节值得注意。update方法内部如果走的是DataSource模式,连接获取和关闭都由QueryRunner处理;如果你用的是new QueryRunner()且手动传了Connection,那么Connection不会在这里被关闭,需要你自己在finally里关,否则连接就会泄漏。
另外,插入操作如果想把数据库自动生成的主键拿回来,普通的update拿不到主键,要改用insert或者insertBatch,它们会配合ResultSetHandler返回主键。比如:
String sql = "INSERT INTO user(name, age) VALUES(?, ?)"; Long id = runner.insert(sql, new ScalarHandler<>(), "小明", 25);这个方法底层走的是Statement.RETURN_GENERATED_KEYS机制,Media、博客文章这些需要插入后立刻拿到主键用来写关联表的场景特别实用。
4.2 batch批量操作提升效率
批量操作在批量导入、初始化数据这类场景下特别能提效。写一个简单的例子:一次性插入500条用户记录。
String sql = "INSERT INTO user(name, age) VALUES(?, ?)"; Object[][] params = new Object[users.size()][2]; for (int i = 0; i < users.size(); i++) { params[i][0] = users.get(i).getName(); params[i][1] = users.get(i).getAge(); } int[] result = runner.batch(sql, params);params是一个二维数组,第一维是批次序号,第二维是每个SQL占位符的参数值。batch方法内部会用同一个PreparedStatement对象连续执行这些参数组,相比一遍遍地单独执行update,省去了反复预编译SQL和网络往返的开销。
执行完后返回一个int数组,每个元素是对应一批参数的受影响行数。检查这个数组非常有用,比如理想情况下500个元素全为1,如果某一行数据有问题,对应的值就不是1,方便定位哪条插入失败。
批量操作还有一个容易被忽略的细节:它默认是自动提交的,每一批参数执行完就立即提交。如果希望在批量插入过程中有一行失败就整体回滚,需要手动走事务模式,先setAutoCommit(false),batch执行完后统一commit,catch块里rollback。
4.3 事务控制:QueryRunner和Connection的配合
事务控制是DBUtils使用中最容易踩坑的区域。记住一个核心原则:当Connection由你自己传入时,事务完全由你控制;当连接由DataSource提供时,每次操作独立提交,不存在跨操作事务。
多表写入场景的经典写法:
Connection conn = null; try { conn = dataSource.getConnection(); conn.setAutoCommit(false); QueryRunner runner = new QueryRunner(); runner.update(conn, "UPDATE account SET balance = balance - ? WHERE id = ?", 500, 1); runner.update(conn, "UPDATE account SET balance = balance + ? WHERE id = ?", 500, 2); conn.commit(); } catch (SQLException e) { if (conn != null) { conn.rollback(); } throw e; } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } }这里用的是new QueryRunner(),每次update手动传入同一个conn,这才能保证两次更新在同一个数据库事务里。如果改成new QueryRunner(dataSource)再分别调两次update,连接会被QueryRunner自行获取和关闭,相当于两个独立自动提交操作,一旦第二句失败,第一句无法回滚。
还有一个我在项目里踩过的坑:事务回滚之后,把连接还给连接池之前,一定要把autoCommit恢复成true。因为很多连接池在归还连接时不会自动重置autoCommit状态,这条连接下次被借出时仍然是false,会导致后续所有本应自动提交的SQL全部被攒着不提交,直到莫名等来一个Commit。
5. DBCP连接池实战配置
5.1 数据库连接池解决了什么问题
直连数据库的性能瓶颈不在SQL执行,而在“连接”本身。每次新建连接都要走TCP握手、驱动初始化、服务端认证、会话上下文创建,这个流程在局域网环境下少说几毫秒,在跨网络环境下可能达到几十甚至上百毫秒。高并发接口如果大量线程同时建立新连接,数据库端的线程栈和内存开销会倍增,而连接在大量线程快速建连断连时还会出现TIME_WAIT状态堆积。
连接池的思路是从一开始就按配置创建好一批连接放在池子里,请求来了从池中借出一条,执行完再还回去,循环使用。这样连接的平均建立成本被摊薄到几乎为零,数据库负载也平稳得多。
一个形象的类比是餐厅的服务员配置:与其每来一桌客人都临时招一个新服务员,不如固定养一帮服务员,最多同时接待那么多桌,客人多了就在门口排队等,这样人力成本可控,服务质量也稳定。DBCP的maxTotal就相当于服务员的上限,maxWaitMillis就是客人排队等待的最长忍耐时间。
5.2 DBCP核心参数与完整配置示例
DBCP是Apache Commons开源组件,目前主流使用的是DBCP2。使用前引入Maven依赖:
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-dbcp2</artifactId> <version>2.9.0</version> </dependency>DBCP2内部依赖commons-pool2,这个依赖会传递引入,不需要手动再添加。核心配置代码如下:
BasicDataSource dataSource = new BasicDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/my_db?useUnicode=true&characterEncoding=utf8mb4"); dataSource.setUsername("root"); dataSource.setPassword("yourpassword"); dataSource.setInitialSize(5); dataSource.setMaxTotal(20); dataSource.setMaxIdle(10); dataSource.setMinIdle(3); dataSource.setMaxWaitMillis(3000); dataSource.setTestOnBorrow(true); dataSource.setTestWhileIdle(true); dataSource.setValidationQuery("SELECT 1"); dataSource.setTimeBetweenEvictionRunsMillis(60000); dataSource.setMinEvictableIdleTimeMillis(1800000);这些参数每个都对应一个具体的性能调优点:
| 参数 | 含义 | 建议 |
|---|---|---|
| initialSize | 启动时创建的初始连接数 | 5~10,避免冷启动间隙性慢请求 |
| maxTotal | 连接池中最大连接总数 | 根据QPS和数据库规格定,别拍脑袋设几百 |
| maxIdle | 池中最多保持空闲连接数 | 略等于initialSize,防止闲置连接占资源 |
| minIdle | 池中至少保持的空闲连接数 | 3~5,防止突发QPS时临时建连 |
| maxWaitMillis | 借不到连接时的最大等待时间 | 公司内部系统3000ms足够,超过就别等了 |
| testOnBorrow | 借出连接前是否测试连接有效性 | 生产环境建议true,代价是一个验证SQL |
| validationQuery | 连接有效性的探活SQL | MySQL用 SELECT 1,Oracle用 SELECT 1 FROM DUAL |
| timeBetweenEvictionRunsMillis | 池维护线程多久跑一次淘汰/清理 | 默认-1不跑,配置一个正值让空闲连接被定期清理 |
配置连接池最重要的思维是:参数不是越大越好。maxTotal设得过高,意味着数据库需要维护大量不活跃连接,反而加重MySQL端负担。不管单机能撑多大,先看业务并发的实际需求,再留20%到30%富余量就够用了。
5.3 从DriverManager切换到DBCP的迁移成本
很多最初用DriverManager开发的项目,迁移到DBCP其实非常平滑,因为数据访问层只管要一个Connection,连接池提供了和DriverManager.getConnection()几乎一致的接口。对比一下两种方式的差异:
原生方式:
Class.forName("com.mysql.cj.jdbc.Driver"); Connection conn = DriverManager.getConnection(url, username, password);连接池方式:
BasicDataSource ds = new BasicDataSource(); ds.setDriverClassName("com.mysql.cj.jdbc.Driver"); ds.setUrl(url); ds.setUsername(username); ds.setPassword(password); Connection conn = ds.getConnection();拿到Connection之后的所有代码都不需要改。只要把获取连接的入口从DriverManager换成DataSource,中间层代码保持原样。配合DBUtils时,可以直接把dataSource传给QueryRunner构造器,连手动getConnection的代码都可以省掉。
6. 组合落地:DBUtils + DBCP写一个完整数据访问层
6.1 封装连接池工具类
项目中第一步是做一个容器级别的DataSource单例,避免每个类里都new一个BasicDataSource。用静态方法实现一个工具类:
public final class JdbcUtils { private static final BasicDataSource DATA_SOURCE = new BasicDataSource(); static { DATA_SOURCE.setDriverClassName("com.mysql.cj.jdbc.Driver"); DATA_SOURCE.setUrl("jdbc:mysql://localhost:3306/my_db?useUnicode=true&characterEncoding=utf8"); DATA_SOURCE.setUsername("root"); DATA_SOURCE.setPassword("yourpassword"); DATA_SOURCE.setInitialSize(5); DATA_SOURCE.setMaxTotal(20); DATA_SOURCE.setMaxIdle(10); DATA_SOURCE.setMinIdle(3); DATA_SOURCE.setMaxWaitMillis(3000); } public static DataSource getDataSource() { return DATA_SOURCE; } public static Connection getConnection() throws SQLException { return DATA_SOURCE.getConnection(); } private JdbcUtils() { } }封装为单例的核心原因是BasicDataSource本身内部维护连接池,如果每次调用都new一个,池子就失去意义了,连接会被重复创建和销毁,退化成原生连接。所以JdbcUtils.getDataSource()必须返回同一个实例。
更规范的做法是把连接参数放进properties文件,程序启动时读取,这样换环境只需要改配置文件,不用重新编译。但是核心思路不变:全局只维护一个DataSource单例。
6.2 DAO层代码示例
有了工具类之后,DAO层写起来就会非常放松。一个完整的UserDao如下:
public class UserDao { private final QueryRunner runner = new QueryRunner(JdbcUtils.getDataSource()); public List<User> findUsersByAge(int age) { String sql = "SELECT id, name, age, email FROM user WHERE age >= ?"; return runner.query(sql, new BeanListHandler<>(User.class), age); } public User findUserById(long id) { String sql = "SELECT id, name, age, email FROM user WHERE id = ?"; return runner.query(sql, new BeanHandler<>(User.class), id); } public long insertUser(User user) { String sql = "INSERT INTO user(name, age, email) VALUES(?, ?, ?)"; return runner.insert(sql, new ScalarHandler<>(), user.getName(), user.getAge(), user.getEmail()); } public int updateUserAge(long id, int age) { String sql = "UPDATE user SET age = ? WHERE id = ?"; return runner.update(sql, age, id); } public int deleteUser(long id) { String sql = "DELETE FROM user WHERE id = ?"; return runner.update(sql, id); } }这段代码的简洁程度和原生JDBC相比是质的差别,每个方法都是一两行的核心逻辑,没有资源释放的烦恼。
6.3 一个必须分清的用户误操作:谁在管理连接生命周期
用了new QueryRunner(JdbcUtils.getDataSource())之后,很多人反而困惑:那我的Connection到底在哪里关闭?
答案是QueryRunner内部帮你关了。看它的执行流程:从数据源取出连接,执行SQL,处理ResultSet,finally块里把连接关闭归还给连接池。整个过程对业务代码完全透明。所以当你使用DataSource构造QueryRunner时,千万不要再在业务代码里手动去拿JdbcUtils.getConnection()来执行,那样多拿了一条连接,最后没关就泄漏了。
反过来,如果用new QueryRunner()并手动传入Connection,那么连接的关闭责任完全在调用方。区分这两者的边界,是DBUtils使用中最重要的基本功之一。
7. 常见问题与排查实录
7.1 连接池耗尽:Timeout waiting for idle object
这是DBCP下最常见的运行时异常,错误信息类似Caused by: java.util.NoSuchElementException: Timeout waiting for idle object。它意味着连接池里所有连接都处于“借出”状态,新请求在maxWaitMillis时间内也没等到认为空闲的连接,直接被拒绝。
排查步骤我一般按这个顺序来:
第一步,确认代码里是否有未关闭的Connection。这个用连接池的removeAbandonedOnBorrow可以自动回收一部分被遗弃的连接,但治标不治本,还得人工review所有手动获取连接的地方。
第二步,看是不是并发峰值超过了maxTotal。用数据库端show processlist看连接数,如果长期接近maxTotal上限,说明参数配置不够,可以把maxTotal适当调大,同时评估一下慢SQL数量。
第三步,排查是不是有长事务占着连接不归还。一条连接一旦进入事务且不commit,它就一直被占用,并发一高池子就被打满。这种情况需要的不是扩连接,而是优化事务粒度。
7.2 BeanHandler映射结果全是null
查询语句执行成功,但返回的实体对象属性全是null。这种问题90%以上是列名和JavaBean属性名对不上。比如数据库列是user_name,JavaBean属性却是userName,默认的BasicRowProcessor不会自动做下划线到驼峰的转换。
处理方案有两种。第一种是在SQL里给列起别名:
SELECT user_name AS userName FROM user第二种是使用GenerousBeanProcessor注册RowProcessor,前面已经给过代码。还要确认JavaBean的setter确实是public的,DBUtils依赖setter进行属性赋值,如果属性名匹配但setter写成了private,也会导致null。
还有一个小坑是实体属性用了基本类型int/long,而数据库该字段允许NULL。映射时如果读到NULL,尝试给int赋值会抛异常;用了Integer/Long则不会。所以实体类属性最好全部用包装类型。
7.3 MySQL驱动版本与连接参数问题
很多老项目从DBCP1迁移到DBCP2之后,连接突然报Cannot create PoolableConnectionFactory或者Communications link failure。一个高概率原因是驱动类名写错了。MySQL 8.x的驱动类名是com.mysql.cj.jdbc.Driver,MySQL 5.x是com.mysql.jdbc.Driver,两者不能混用。
另一个常见问题是时区参数缺失:The server time zone value '�й���ʱ��' is unrecognized。加连接参数解决:
jdbc:mysql://localhost:3306/my_db?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true这个问题在MySQL 8.0和JDBC驱动版本不一致时特别容易遇到,配置里加上serverTimezone基本就能解决。
7.4 批量操作效率反而不升反降
有人用batch插入1000条数据,发现执行时间比循环update还长,原因通常是batch数组能过大,每一次batch调用都拿着一个巨大的二维数组,数据库端内存和日志开销都很大。建议把批量数据分片执行,每批200到500条,分批提交,这样执行效率和稳定性都会好很多。
从项目实践角度来说,DBUtils和DBCP这对组合非常适合中小型应用和中间件SDK的开发,它们比传统JDBC省心得多,又不会像重ORM那样引入复杂的缓存与代理机制,出了问题一眼能看穿。我个人在多个内部管理系统的数据访问层都采用了这套方案,历经多次版本迭代,使用体验一直很稳。如果你也正在为DAO层代码的臃肿发愁,不妨先用一个简单的表把QueryRunner跑起来感受一下,它带来的简化是那种“效率突然高了一截”的爽快感。之后如果业务规模变大、查询场景变得复杂,再在这个基础上向更重的框架演化,也是顺理成章的事情。