☰
JDBC实战指南:用Java代码轻松实现MySQL增删改查与事务
2026/10/22 7:58:15 网站建设 项目流程

很多人在学 MySQL 的时候,会陷入一个很尴尬的阶段:SELECT、INSERT、UPDATE、DELETE这些 SQL 语法早就背得滚瓜烂熟,在命令行或者 Navicat 里也敲得飞起,但一到真正的项目代码里,就卡住了。为什么卡住?因为业务系统里的 CRUD,从来不是让你把 SQL 语句复制粘贴到数据库工具里执行就完事了,而是要在应用代码里、通过编程语言去操作数据库。这个中间的桥梁,才是很多人没搞透的地方。

这篇文章就聚焦在这个核心痛点:用 Java 语言通过JDBC技术完成对 MySQL 的完整CRUD操作。我会从原理层面讲清楚为什么需要 JDBC、DriverManager、Connection、Statement这些概念到底扮演什么角色,然后直接上手敲代码,把增删改查、事务控制、批量操作这些真实项目里天天要用的东西全部过一遍,最后再聊几个我用这么久 JDBC 下来,觉得最值得警惕的坑。如果你已经会写基础 SQL,但对代码里操作数据库这件事还有点懵,这篇文章就是给你准备的。

1. 为什么纯 SQL 玩得转,一到代码 CRUD 就发懵

先聊点实际的体感。不少初学者有个错觉:MySQL 的 CRUD 很简单,无非就是四条语句嘛。确实,从语法角度来说,SELECT和UPDATE确实没什么难度。但是一旦你开始写代码,你会发现你需要面对一大堆从来没想过的问题。

第一个问题是连接管理。你在 Navicat 里查询数据,软件帮你把网络连接、认证、编码全处理好了,你只需要打开查询窗口写 SQL。但在程序代码里,没人帮你做这些事。你得自己创建连接、用完之后自己释放。连接释放不及时,数据库连接数会被耗尽,整个应用直接瘫痪。

第二个问题是参数的传递。你在数据库工具里写SELECT * FROM user WHERE id = 1,这很好办,id 是你手动填的。但程序里,这个 id 是用户在网页输入框里填的,是个动态值。你怎么把一个 Java 变量安全地拼到 SQL 字符串里,而不是拼出语法错误或者更严重的注入漏洞?

第三个问题是结果集的处理。你在数据库工具里看到查询结果是一张表,清清楚楚。但程序里,查询结果是一个ResultSet,是一个游标式的数据集合。你得手动迭代它,把每一行的数据取出来,存到一个 Java 对象里再交给上层使用。这一步的代码写起来繁琐,而且容易出各种类型转换错误。

第四个问题是编码与资源释放。数据库服务器返回的中文是 utf8 编码的字节流,你的 Java 程序是 Unicode 字符串,连接的字符集设置不对,就会出现乱码。同样的连接、语句、结果集,任何一个没有被正确关闭,最终都可能引发严重的系统问题。

所以你看,代码层面的 CRUD 和纯 SQL 操作,完全是两码事。SQL 本身只是一门语言,而用代码做 CRUD,是一门工程。这篇文章要做的,就是把工程里最基础也最核心的那条链路彻底打通。

2. JDBC 凭什么能成为 Java 连接 MySQL 的唯一标准

要说清楚 JDBC,得先明确两个核心概念。第一个是 JDBC 本身,第二个是数据库驱动。很多教程上来就让你引入一个 JAR 包,然后写代码,但你要是不理解这两个东西的关系,后面遇到问题就会一头雾水。

2.1 JDBC 不是工具,而是一套接口规范

JDBC(Java DataBase Connectivity)是 Java 官方定义的一套操作数据库的接口规范。注意"规范"这个词,它不是具体实现。它规定了 Java 程序访问数据库时必须要有的核心接口,比如Connection(连接)、Statement(语句)、ResultSet(结果集)。这些接口只定义了方法签名,但没有写实现逻辑。

为什么官方要这么设计?因为数据库有那么多家,MySQL、Oracle、PostgreSQL,每个数据库的通信协议都不一样。如果每个开发商都搞一套自己的 API,那开发者换数据库就得重写所有代码。JDBC 的存在,让 Java 和数据库之间有了一个标准的适配层。

2.2 数据库驱动才是真正干活的

你引入的那个 MySQL Connector/J JAR 包,就是 MySQL 官方对 JDBC 接口的具体实现,我们称它为"驱动"。你的代码里写的DriverManager.getConnection(url, username, password),本质上是在调用接口,但真正干活的是驱动里实现的那一套复杂逻辑:网络协议握手、认证、SQL 语句的字节流转发、结果集的解析。

为了加深理解,可以把这个流程类比成 USB 接口标准:USB 是 JDBC,你的 U 盘厂商根据 USB 规范做出来的内部电路和固件就是驱动。你只需要把 U 盘插到 USB 口上就能用,完全不用关心它里面是哪种存储芯片。在代码里,你只需要调整连接字符串和引入对应的驱动 JAR 包,就能在 MySQL 和别的数据库之间无缝切换,这就是接口规范最大的价值。

2.3 连接参数里每个字段的用意

既然要理解,干脆把连接字符串彻底拆开看一遍:

jdbc:mysql://localhost:3306/my_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8

前面这部分jdbc:mysql://是 JDBC URL 的固定前缀,告诉驱动你要使用 JDBC 协议连接 MySQL 数据库。localhost:3306是数据库服务器的地址和端口,3306 是 MySQL 的默认端口。my_db是你要操作的数据库名。

后面的?后面跟着的是一系列连接参数,这几个参数几乎每个项目都会用到。

  • useSSL=false:本地开发环境一般没有配置 SSL 证书,所以直接关闭 SSL 加密连接。如果不加这个参数,新版驱动默认认为你希望开启 SSL,在连接时可能会报错或警告,白白增加不必要的握手开销。

  • serverTimezone=Asia/Shanghai:这个参数很重要。MySQL 驱动在获取时间类型字段时,会涉及时区转换。如果不指定服务器时区,驱动可能会默认使用驱动自身的时区,导致你读到的时间和你本地时间差了几个小时。尤其是在使用高版本驱动时,时区问题几乎是必踩的坑。

  • characterEncoding=utf8:这个参数告诉 MySQL 驱动,客户端使用 utf8 编码进行通信。不指定的话,很容易出现中文乱码,特别是通过命令行或者老版本的工具往数据库里插入中文数据的时候。

驱动类版本不同,URL 的参数兼容性也有一些差异,但上面这三个组合基本覆盖了绝大多数常见场景,也是我平时新建项目时的固定起点。

3. 直接从零搭建一个 JDBC CRUD 类

理论讲得再多,都不如直接开写。就在这一章,我直接把一个基本的、完整的 JDBC CRUD 类写出来,配合每一步的说明,你可以直接照着敲,敲完就能用。

3.1 环境准备与 Jar 包引入

先确认你的环境:JDK 8 或者更高版本,一个 MySQL 5.7 或者 8.0 的实例,IDE 用 IDEA 或者 Eclipse 都行。我假设你已经建好了一个数据库,里面有一张user表,字段至少有这几个:

CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `name` varchar(50) NOT NULL COMMENT '用户名', `age` int DEFAULT NULL COMMENT '年龄', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

驱动 JAR 包的引入方式,如果用的是 Maven 项目,就直接在pom.xml里加依赖:

<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>

如果你用的是 5.x 版本的 MySQL,可以选5.1.49这个兼容性不错的版本。不过我个人建议,只要不是历史遗留老项目,都用 8.x 新驱动,它对连接参数的支持更完善,而且在 8.x 驱动下,默认的驱动类名注册方式也更简洁。

如果没有用 Maven 而是手动导入 JAR 包,直接去 MySQL 官网下载对应版本的 Connector/J,然后把它放入项目的 lib 目录并加入 Build Path 即可。注意一个细节:驱动 JAR 包的版本和 MySQL 服务端版本并不是必须完全一致的。比如 MySQL 8.0 服务器完全可以用 5.1.49 驱动连接,只是有些新特性用不了而已。反过来,MySQL 5.7 服务器用 8.x 驱动也没问题。

3.2 标准的连接获取与关闭代码

在写具体 CRUD 之前,我先提供一个最基础的工具类,用来获取连接和关闭资源。这个类虽然简单,但是后面每一个方法都要用到它。

import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class JdbcUtil { private static final String URL = "jdbc:mysql://localhost:3306/my_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USERNAME = "root"; private static final String PASSWORD = "your_password"; static { try { // 注册驱动。JDBC 4.0 之后这句可以省略,但保留更安全,尤其在低版本环境 Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 注意关闭顺序:先 ResultSet,再 Statement,最后 Connection if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (stmt != null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }

这里有一个非常容易被忽略的细节:关闭顺序。ResultSet依赖Statement,Statement依赖Connection。如果先把Connection关了,它底层的资源会级联释放,很多时候你感觉不到关闭ResultSet和Statement的必要性。但当你用连接池的时候,连接归还给连接池并不是真正的物理关闭,如果不显式关闭Statement和ResultSet,这哥俩占用的数据库端游标资源就一直得不到释放,时间长了会报"游标数超限"之类的错误。所以老老实实按逆序关闭,养成习惯。

3.3 新增数据:从 Statement 到 PreparedStatement 的进化

直接上新增数据的代码:

public int insertUser(String name, Integer age, String email) { Connection conn = null; PreparedStatement ps = null; try { conn = JdbcUtil.getConnection(); String sql = "INSERT INTO user(name, age, email) VALUES(?, ?, ?)"; ps = conn.prepareStatement(sql); ps.setString(1, name); ps.setInt(2, age); ps.setString(3, email); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return 0; } finally { JdbcUtil.close(conn, ps, null); } }

看到?占位符了吧,这就是PreparedStatement和Statement最大的区别。SQL 骨架先预编译好,具体参数再用setString、setInt等方法逐个填充。好处是双重的:

  • 安全性:?占位符传参,驱动会把这些值当作纯粹的数据处理,不会再被拼接成 SQL 语句片段。这能从根上杜绝 SQL 注入,因为你压根就不应该考虑手动去转义单引号。

  • 性能:SQL 骨架相同的情况下,PreparedStatement可以复用数据库端的预编译结果,省去每次执行时重复的语法解析开销。对于循环插入或者频繁执行的语句,提升是实打实的。

顺便强调一句:能用PreparedStatement就用PreparedStatement,永远不要用Statement拼字符串 SQL。这句话你在任何项目里看到有同事违反,可以直接让他过来读这篇文章。

3.4 查询数据:ResultSet 游标处理的完整拆解

查询是 CRUD 里最繁琐的环节,因为你要自己处理结果集。直接看代码:

public List<User> findAll() { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; List<User> list = new ArrayList<>(); try { conn = JdbcUtil.getConnection(); String sql = "SELECT id, name, age, email, create_time FROM user"; ps = conn.prepareStatement(sql); rs = ps.executeQuery(); while (rs.next()) { User u = new User(); u.setId(rs.getLong("id")); u.setName(rs.getString("name")); u.setAge(rs.getInt("age")); u.setEmail(rs.getString("email")); u.setCreateTime(rs.getTimestamp("create_time")); list.add(u); } } catch (SQLException e) { e.printStackTrace(); } finally { JdbcUtil.close(conn, ps, rs); } return list; }

这段代码的核心动作是rs.next()。ResultSet内部像一个游标,一开始指向第一行之前的位置。调用一次next(),游标向后移动一行;如果有数据,就返回true,这时你可以用rs.getXxx("列名")来取值。循环到next()返回false,说明数据取完了。

取值的时候有两个小细节值得注意。第一,列名和你的实体类属性名不一定一样,需要做一次手动映射,当你不知道表里有哪些字段的时候会很痛。第二,用getInt取值时,如果数据库里该字段是NULL,Java 侧得到的会是 0 而不是null;getString得到null倒是没问题。所以设计表结构的时候,建议把非空约束做足,避免在代码里处理莫名其妙的默认值。

3.5 更新与删除:你只需要关注返回的行数

更新和删除的逻辑几乎一模一样,都是执行一条带占位符的 SQL,然后通过executeUpdate获取受影响的行数。我把两个方法合并在一起讲:

public int updateUser(Long id, String name) { Connection conn = null; PreparedStatement ps = null; try { conn = JdbcUtil.getConnection(); String sql = "UPDATE user SET name = ? WHERE id = ?"; ps = conn.prepareStatement(sql); ps.setString(1, name); ps.setLong(2, id); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return 0; } finally { JdbcUtil.close(conn, ps, null); } } public int deleteUser(Long id) { Connection conn = null; PreparedStatement ps = null; try { conn = JdbcUtil.getConnection(); String sql = "DELETE FROM user WHERE id = ?"; ps = conn.prepareStatement(sql); ps.setLong(1, id); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return 0; } finally { JdbcUtil.close(conn, ps, null); } }

executeUpdate返回的是受影响行数,比如我删除一条不存在的数据,它返回 0,方便上层判断操作是否真正生效。在实际项目里,这个返回值往往直接被业务层拿来判断是否抛出异常或者提示操作失败。

到这里,一个最基础的、完整的 JDBC CRUD 你已经能跑通了。但你如果仔细想想,会发现这套代码在实际生产中依然有一堆问题:每执行一个操作都要获取连接、关闭连接,数据库连接的开销其实非常大;多条语句的业务逻辑没有事务保护,中间崩了数据就残缺了;一次循环插入几千条数据,每条都提交,慢得一塌糊涂。接下来我把这三个最突出的痛点,一个一个解决掉。

4. 事务与批量处理:从"能用"到"扛得住"

数据库连接的建立是一个非常昂贵的操作,包括 TCP 握手、认证、服务端分配会话资源,整个过程可能耗时几十甚至上百毫秒。如果每执行一条 SQL 就新建一个连接并关闭,性能根本无法接受。真实的项目里几乎都用连接池来管理连接,但因为篇幅限制,我不在这里展开连接池的配置,而是聚焦在事务控制与批量处理这两个你一定会在代码里用到的关键点。

4.1 事务控制的本质:把 AUTOCOMMIT 关掉

MySQL 默认是自动提交模式,也就是每条INSERT、UPDATE、DELETE语句一旦执行成功,就会立即持久化,无法回滚。这个机制在大部分简单场景没问题,比如注册一个用户,就一条INSERT,无所谓。但在业务场景里,一个功能经常涉及多张表的修改。比如创建一个订单,你需要往订单表插入一条记录、往订单明细表插入多条记录、还要扣减库存。如果第二条语句执行失败,第一条已经提交了,数据就变成残缺的。这时候你需要的就是本地事务。

在 JDBC 里,事务控制的代码模式永远是老三步:

public void createOrder(Order order) { Connection conn = null; PreparedStatement ps1 = null; PreparedStatement ps2 = null; try { conn = JdbcUtil.getConnection(); // 1. 关闭自动提交 conn.setAutoCommit(false); // 2. 执行多条 SQL(订单表 + 明细表) String sql1 = "INSERT INTO orders(order_no, user_id, amount) VALUES(?, ?, ?)"; ps1 = conn.prepareStatement(sql1); // 设置参数... ps1.executeUpdate(); String sql2 = "INSERT INTO order_detail(order_id, product_id, num) VALUES(?, ?, ?)"; ps2 = conn.prepareStatement(sql2); // 设置参数... ps2.executeUpdate(); // 3. 全部成功才提交 conn.commit(); } catch (SQLException e) { // 任何一步出错,回滚到事务开始之前的状态 try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); } finally { // 注意:使用完要把 autoCommit 恢复为 true,方便连接被连接池复用 try { conn.setAutoCommit(true); } catch (SQLException e) { e.printStackTrace(); } JdbcUtil.close(conn, ps1, null); JdbcUtil.close(null, ps2, null); } }

几个容易踩坑的细节:

第一,setAutoCommit(false)必须在执行任何 SQL 之前调用,一旦执行了语句再看回滚,那已经提交的部分是回不来的。

第二,finally里要把自动提交恢复为默认的 true。尤其在用了连接池之后,连接是被复用的,如果你把一个自动提交已经关掉的连接归还给连接池,下一次别人从这个连接池拿到连接时会发现事务还开着,继续执行事务外的语句,天知道会发生什么。

第三,一个Connection对象同时只能带一个事务。如果你在一个事务里要执行很多条语句,它们会自动属于同一个事务,直到commit()或rollback()为止。

4.2 批处理:告别一条条执行的龟速

批量操作是初学者最容易忽视的性能优化点。比如你要往某张表里插入 1000 条初始化数据,用最朴素的写法调用insertUser()1000 次,每次从建连接到 SQL 执行再到关闭连接,总耗时几十秒很常见。如果你用PreparedStatement的批处理,这个时间能缩短到原来的十分之一甚至更多。

public void batchInsertUsers(List<User> userList) { Connection conn = null; PreparedStatement ps = null; String sql = "INSERT INTO user(name, age, email) VALUES(?, ?, ?)"; try { conn = JdbcUtil.getConnection(); conn.setAutoCommit(false); ps = conn.prepareStatement(sql); for (User u : userList) { ps.setString(1, u.getName()); ps.setInt(2, u.getAge()); ps.setString(3, u.getEmail()); // 把参数加入批处理队列 ps.addBatch(); // 每 500 条执行一次,避免一次性堆积太多 SQL if (userList.indexOf(u) % 500 == 499) { ps.executeBatch(); conn.commit(); } } // 处理剩余的不足 500 条的部分 ps.executeBatch(); conn.commit(); } catch (SQLException e) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); } finally { try { conn.setAutoCommit(true); } catch (SQLException e) { e.printStackTrace(); } JdbcUtil.close(conn, ps, null); } }

这里的核心动作是addBatch()+executeBatch()。每调用一次addBatch(),驱动就把当前的 SQL 和参数序列化到客户端的缓冲区里,等到executeBatch()调用时一次性发给数据库服务端执行。网络往返次数从 N 次降到 N/批大小次,性能提升非常明显。

需要特别提醒的是批处理执行时机。你不能一口气加完十万条再一次性执行,那样客户端内存很容易爆炸,而且数据库端一次性处理太多也容易出问题。所以通常的做法是分批提交,每批几百条到一千条左右,并根据实际情况调整。上述代码我用了indexOf(u) % 500来判断是否凑满 500 条,实际写代码时你直接用循环变量控制即可,这里只是为了展示批处理的基本结构。

有朋友可能会问:executeBatch()返回的是什么?它返回一个int[]数组,每个元素代表对应 SQL 执行后影响的行数。如果某一条失败,MySQL 默认的行为取决于连接的属性设置,不是特别容易处理。所以批量操作一般要配合事务使用,一旦某一步出错,整个批处理回滚,保住数据一致性,这是最稳妥的方案。

5. 长期使用 JDBC 后,我总结的关键避坑经验

写了这么多年 JDBC,踩过的坑数量绝对不少。这里挑几个最有代表性的问题,它们几乎困扰过每一个从纯 SQL 过渡到代码操作数据库的新手。我把问题、现象和解决思路一次性总结清楚。

5.1 列名大小写与下划线风格的映射陷阱

MySQL 的表字段在 Windows 上可能不区分大小写,在 Linux 上区分,而 Java 的实体类属性命名习惯又是驼峰风格。比如数据库字段create_time,你 Java 实体类里叫createTime,rs.getTimestamp("create_time")没问题,但如果你偷懒写成rs.getTimestamp("createTime"),MySQL 在 Linux 上可能直接报"列名不存在"。这个问题的标准解法是:查询 SQL 里给列名起别名,把下划线风格的列名映射成驼峰风格,例如SELECT create_time AS createTime。然后你在代码里用rs.getTimestamp("createTime")就能和实体类属性完全对齐。别看这个细节小,排查列名报错的代价远大于写别名的时间。

5.2 连接关闭得慢,应用卡顿就在一瞬间

这是最典型的隐形杀手。很多初学者知道要关闭资源,但习惯性地只关Connection,认为Statement和ResultSet会跟着被关掉。在直接使用DriverManager的情况下,这个想法勉强能凑合,因为连接物理关闭时确实会级联释放。但一旦你接入了连接池,比如 HikariCP、Druid,Connection的物理连接不会真正销毁,而是被归还到连接池里。这时候如果你没有关闭Statement和ResultSet,它们持有的游标和预编译资源就一直悬挂着。日积月累,数据库端的PreparedStatements缓存和游标数会疯涨,应用最终会因为连接超时或者游标耗尽而卡死。所以务必养成"先关子资源、再关父资源"的固定顺序。

5.3 时区问题:日期时间类型不是读出来就完事

DATETIME和TIMESTAMP是 MySQL 里最常踩坑的数据类型。JDBC 驱动从数据库读取时间值时,会涉及一个时区转换。如果你的连接字符串没有指定serverTimezone,驱动会使用 JVM 所在机器的默认时区。假设你的服务器跑在某个时区,而连接串没指定Asia/Shanghai,你会发现读出来的时间总是差好几个小时,而且找遍 MySQL 配置也查不出原因——问题根本不在数据库配置,而在驱动与连接串的参数设置。反过来,你插入一个java.util.Date时,也应该确认它的时区语义,否则会插入一个相差若干小时的时间。业界通用的做法就是,从一开始就显式指定serverTimezone=Asia/Shanghai,并且在连接串里加上useLegacyDatetimeCode=false以保证驱动严格使用普通连接时区解析逻辑,避免一些老驱动的时间转换怪癖。

5.4 自增主键回填:插入数据后到底拿到什么 id 了

插入一条记录后,常常需要立刻拿到这条记录的自增主键 ID,去生成关联数据。很多人的第一反应是再查一次:SELECT MAX(id) FROM user。这个写法在多线程并发场景下必出问题,因为不同线程插入的记录 ID 可能会互相干扰,你拿到的很可能不是你的那条记录的 ID。正确的做法是在prepareStatement时声明你要回填主键:

String sql = "INSERT INTO user(name, age, email) VALUES(?, ?, ?)"; ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); ps.setString(1, name); ps.setInt(2, age); ps.setString(3, email); ps.executeUpdate(); // 从驱动结果里获取自动生成的主键 ResultSet rs = ps.getGeneratedKeys(); if (rs.next()) { long generatedId = rs.getLong(1); }

一句话解释:prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)告诉驱动,执行插入后把数据库生成的自增主键返回给我。返回的ResultSet中第一列就是生成的主键值。这个方法既准确又高效,也是 JDBC 规范里原生支持的标准用法。用熟这个写法之后,你会彻底告别"插入后再查询"这种又慢又危险的老思路。

5.5 大数据量查询别一次全加载到内存

如果你的业务在做报表统计,一条SELECT可能返回几十万行。用刚才那种while (rs.next())的方式,驱动默认会把所有查询结果通过网络拉取到客户端内存中。大结果集会造成 JVM 内存暴涨,系统频繁 GC。针对这个问题,MySQL 驱动有两个思路:一个是setFetchSize(Integer.MIN_VALUE)启用流式读取,逐行从服务器拉取;另一个是分页查询,从 SQL 层面用LIMIT控制每页大小。对于初学者,我强烈建议先考虑分页查询,因为流式读取对连接占用的时间更长,处理不好反而拖累整个连接池。只有当你确实需要一次性遍历全量数据且无法改造成分页逻辑时,再考虑流式方案。

6. JDBC 之上的演进:为什么实际项目里很少直接写这些代码

搞清楚 JDBC CRUD 之后,你可能会产生一个疑问:既然直接用 JDBC 这么繁琐,那为什么我还要学它?原因很简单,因为所有 Java 世界的数据库框架,底层都只是对 JDBC 做了一层封装。

MyBatis 的SqlSession、Mapper 接口,Spring 的JdbcTemplate,甚至 Hibernate 的Session,本质上都是在你前面看到的这些Connection、PreparedStatement、ResultSet之上套了一层更友好的 API。用它们开发效率确实高很多,但一旦遇到框架解决不了的性能问题或者奇特的 SQL 场景,最终的排查路径永远会回到 JDBC 层面的机制上。所以我把 JDBC 的基础打牢,学框架就会轻松很多,碰到问题也能卡位在正确的排查方向上。

同时,JDBC 这门技术本身也在演进。你目前看到的DriverManager.getConnection是入门版,而在 Spring Boot 的自动配置里,实际使用的是DataSource(数据源)体系,由连接池提供连接,JDBC API 的Connection接口定义则完全没变。理解了 JDBC 的接口规范,你就等于拿到了理解整个数据库连接体系的金钥匙。

还有一点有意思的地方,最近这几年,Java 官方的 JDBC 驱动也做了一些小改动,比如可选的驱动自动加载,就是靠META-INF/services/java.sql.Driver那个文件实现的。甚至在新驱动的实现里,DriverManager都不需要手动Class.forName注册了,getConnection时会自动发现类路径下的驱动。

但不管它怎么演进,经典的五个步骤依然是核心:注册驱动(或自动注册)、获取连接、创建语句、执行 SQL、处理结果与释放资源。抓住这条主线,你之后看任何 ORM 框架的源码,都会觉得似曾相识。

7. 写在最后的几个实操建议

这一大段代码和原理讲下来,如果你真的从头到尾敲了一遍,应该已经对 MySQL 的 CRUD 从"SQL 层面"到"代码层面"的转化有了一个完整的体感。最后再分享几个我平时写代码时很重视的小习惯,算是抛砖引玉。

第一个建议是写好JdbcUtil这个工具类之后,不要急着扩展,先去跑一段真实数据的增删改查。确认连接、释放、参数绑定这几个基础动作完全没毛病,再往里面加连接池、加 ORM 框架。基础不牢,后面框架一接手反而会让你连错误的根源都找不准。

第二个建议是善用日志。JDBC 原生代码里,在关键方法执行前后打印 SQL 和参数会大大加快排查速度。等以后用了 MyBatis 这类框架,它的 SQL 日志功能很强大,但原理都是一样的,本质上就是把预编译后的 SQL 骨架和参数值打印出来。

第三个建议是别过度设计。学 JDBC 的人最容易犯的毛病是写一堆BaseDao、BaseService抽象层,觉得这样设计很优雅。但如果你只是学到 JDBC 这个阶段,完全没有必要。你的目标是把 JDBC 这条链路摸透,而不是用它去支撑一个巨型业务系统。先把代码写清楚、写安全,比什么都重要。

最后是给自己留个进阶路线图:JDBC 熟练之后,优先去啃连接池,尤其是 HikariCP;然后简单用一用 Spring 的JdbcTemplate感受一下封装思路;接着再进入 MyBatis 或者 MyBatis-Plus 的世界。你会发现,原来你在 JDBC 里学到的每一个细节,都没有白学。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询