☰
Java连接MySQL实现增删改查:JDBC从零到实战全解析
2026/10/10 2:32:00 网站建设 项目流程

带新人带得多了,你会发现一个特别有意思的现象:很多人简历上写着熟练使用Java,可一旦让他在本地写一段连接数据库的增删改查,往往要在电脑前犹豫很久。不是能力不行,而是没把基础链条真正理顺。Java连接数据库实现增删改查,这个组合几乎是所有业务系统的起点,虽然看着简单,里面却串着JDBC、SQL、驱动、连接管理、异常处理、资源释放一大堆琐碎细节。当年我刚开始学的时候,最苦恼的其实不是语法,而是搞不明白错误到底出在哪一层。如果你也想从零起步,把这套功能完整拿下来,这篇文章就把整条链路拆开揉碎讲清楚:从环境搭建开始,建立一张用户表,再写一个DAO类,用PreparedStatement逐步完成新增、删除、修改、查询,最后聊聊那些只有亲手踩过才记得住的坑。键盘就在手边,跟着做一遍,比看十篇教程都有效。

1. 先搞清楚“增删改查”到底在做什么

1.1 标题里的四个动作,对应的核心逻辑

“增删改;查”这四个字,落到系统里就是四种最基础的数据操作:增加对应INSERT,删除对应DELETE,修改对应UPDATE,查询对应SELECT。在业务里,它们被统称为CRUD。很多新手以为这几个操作只要会写SQL就够了,写完后却发现Java代码根本跑不通。原因在于,CRUD的真正难点从来不在SQL本身,而在于几个容易被忽略的衔接点:Java怎么发起连接、用哪个对象执行SQL、参数怎么安全地传进去、返回的结果集怎么读,以及用完的连接和流到底关没关干净。这每一个点,串起来的其实就是JDBC的完整工作流。

在后面的实现里,我会用一个比较典型的“用户管理”场景来演示:有一张user_info表,里面存用户编号、用户名、密码、邮箱和创建时间。然后用Java写一个访问数据库的类,把这张表的增删改查都做一遍。选择这个场景是因为它足够典型,你以后写订单模块、写商品模块、写任何带列表页的后台功能,底层骨架都是同一套。

1.2 为什么第一个实战项目要绕开框架,直接学JDBC

现在很多新手一上来就学MyBatis,甚至直接上Spring Boot加JPA,问原因,他们说现在没人用原生JDBC了。这话对一半。框架确实把数据库操作简化了,但MyBatis也好,JPA也罢,底层都是把JDBC包装了一层。你如果连Statement和PreparedStatement的区别都不清楚,连ResultSet都没手动遍历过,那遇到MyBatis的#{}参数字符时,你就不知道它到底帮你做了什么,动态SQL拼接出问题的时候,排查起来更是一头雾水。

JDBC是Java连接数据库的官方标准接口,负责把Java程序和数据库之间的通道建起来。先练好JDBC,本质上是给自己打地基。地基稳了,后面看框架源码才不会像是看天书。这篇博文不会让你一辈子写原生JDBC,只是希望你在正式进入框架世界之前,先把这层窗户纸捅破。

1.3 适合什么人读,怎么读效率最高

如果你是刚学完Java语法,还不清楚类和接口怎么组织,这篇文章适合你;如果你已经在用框架,但遇到连接泄漏、SQL注入这些概念时总觉得含糊,这篇也适合你。如果你只是想找一个能直接复制的代码模板,那我把完整代码都放出来了,照着抄也不丢人。

我建议的读法是先花十分钟浏览全文,知道大概有几块内容,然后回到第二章,动手把环境准备起来,照着第四章的代码一行一行敲。别复制粘贴,哪怕敲得慢也要敲。原因很简单:CRUD的代码量不大,但手一懒,脑子就会自动默认自己会了,等面试官让你现场写的时候,往往就卡壳了。

2. 动手前的准备:环境、驱动和一张能承载增删改查的表

2.1 开发环境三件套:JDK、数据库、数据库驱动

既然标题写了“零基础入门”,那环境这关就不能跳过。先说JDK,版本选8以上都可以,我建议直接上17或者21这种长期支持版本,老项目用8也不用慌,后面代码基本兼容。数据库方面,这篇以主流的MySQL为例,你用MariaDB或者PostgreSQL也没问题,只是驱动类名和连接字符串略有差异。

然后是数据库驱动。Java本身并不知道怎么跟MySQL通信,它通过一套JDBC接口去访问数据库,而真正实现这套接口的,就是数据库厂商发布的驱动JAR包。MySQL的驱动叫mysql-connector-j,PostgreSQL的驱动叫postgresql。驱动是Java和数据库之间的翻译官,没有它,程序连localhost上的数据库都连不上。

开发工具方面,你用IDEA或者Eclipse都行,社区版完全够用。新建一个普通的Java工程就可以,先不用引入Spring那些复杂的依赖,保持最简单,这样出问题了容易定位。

2.2 给工程引入驱动依赖的两种方式

给项目加驱动,根据你的工程类型,有两种常见做法。如果你用Maven管理依赖,那在pom.xml里加下面这一段就行:

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

这里要特别留意一下artifactId。比较老的文章里写的是mysql-connector-java,8.0之后官方把名字简化成了mysql-connector-j。如果照抄老文章的坐标,经常会出现依赖拉不下来或者版本对不上的问题。驱动版本尽量和数据库大版本匹配,比如MySQL 8.x配8.x驱动,没毛病。

如果你的工程没接Maven,那就手工下载对应版本的驱动JAR包,放到项目根目录下的lib文件夹里,然后选中JAR文件,右键选择“Add as Library”。这个操作其实就是把JAR包加入编译路径,IDE会自动识别里面的驱动类。新手阶段用这种方式反而对原理更透明,你能直观看到一个JAR文件是如何被工程引用的。

2.3 设计一张用户表,别忽视字段类型

在写Java代码之前,先把数据库的表建好。这里要强调,建表不是随便写几个字段就完事,字段类型设计直接影响后面Java代码怎么写。拿这张用户表举例:

CREATE TABLE user_info ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, email VARCHAR(100), created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

字段的选择都是有原因的。id用BIGINT而不用INT,是因为如果你的表将来数据量变大,INT的最大值只有二十多亿,看着很多,但对某些体系来说并不算宽裕。username和password用VARCHAR,长度给得够用就好。created_time设置DEFAULT CURRENT_TIMESTAMP,意思是插入数据时如果不显式传值,数据库就自动写当前时间,这样Java代码里少处理一个字段。

字符集必须用utf8mb4。很多老项目因为用了utf8,导致存emoji或者生僻字时报错,这问题排查起来特别隐蔽。表建好后,最好再插入两条测试数据,后面写查询代码时就能立刻看到效果。

INSERT INTO user_info(username, password, email) VALUES ('zhangsan', '123456', 'zhangsan@example.com'), ('lisi', 'abcdef', 'lisi@example.com');

然后你还需要一个单独的数据库,或者直接用现有的测试库。我后面写的连接字符串里会用到demo_db这个库名,建库语句就是CREATE DATABASE demo_db DEFAULT CHARSET utf8mb4;,非常简单。

3. 连接管理:写代码前必须搞懂的底层逻辑

3.1 连接字符串和URL参数逐个解释

JDBC连接数据库,靠的是一个类似网址的字符串。拿MySQL举例子,典型写法是这样的:

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

这段信息虽然只是一串字符,但新手最容易在这里翻车。简单拆一下:开头的jdbc:mysql是协议名,告诉驱动程序你要连的是MySQL;localhost是数据库所在机器的地址,本机就是localhost;3306是MySQL默认的端口;demo_db就是你要用的数据库名。

后面跟的参数更像是调试选项。useSSL=false表示本地开发不启用SSL证书校验,因为本地根本没证书,不关掉它,部分版本会一直报握手失败。serverTimezone=Asia/Shanghai很关键,MySQL 8的驱动对时区敏感,不设置时区,你执行查询时经常会抛出一个“Server time zone value”相关的异常。characterEncoding=utf8解决的就是后面会碰到的中文乱码问题。allowPublicKeyRetrieval=true是为解决MySQL 8使用caching_sha2_password认证时的一个公共密钥获取限制,连接报错时验证一遍就知道它的用处了。

3.2 把连接逻辑封装成一个工具类

每个增删改查方法都需要连接数据库,如果每个方法都重复写一遍DriverManager.getConnection,代码会变得啰嗦,而且改一次连接参数要动很多地方。所以行业惯例是做一个工具类,统一管理连接的创建。

package com.demo.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public final class JdbcUtil { private static final String URL = "jdbc:mysql://localhost:3306/demo_db" + "?useSSL=false" + "&serverTimezone=Asia/Shanghai" + "&characterEncoding=utf8" + "&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASSWORD = "你自己改"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("找不到JDBC驱动,请检查依赖是否引入"); } } private JdbcUtil() { } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }

这里有个细节值得说一下。Class.forName("com.mysql.cj.jdbc.Driver")是驱动类加载的经典写法,目的是把驱动类注册到DriverManager里。高版本的JDBC驱动其实支持SPI机制,也就是驱动包自己会在META-INF里声明驱动类,不写这行也能自动加载。但新手阶段保留这行没有坏处,它能让你提前感知到“驱动加载”这一步的存在,以后看到莫名其妙的ClassNotFoundException时,思路会更清楚。

真正获取连接用的是DriverManager.getConnection(URL, USER, PASSWORD)。这个方法是JDBC的标准入口,它拿着你给的地址和账户密码去和数据库握手,握手成功就返回一个Connection对象。注意这个对象不是普通的Java对象,它背后是一条真实存在的数据库网络连接,操作完必须关闭,否则连接数会被耗尽。这个坑后面专门用一节说。

3.3 生产环境必须使用连接池,为什么

既然提到连接很宝贵,那就得说说生产环境和学习的区别。学习时你可以每次都DriverManager.getConnection,用完就关。但生产环境面对的是大量并发请求,数据库连接的建立费时费力,频繁创建销毁对数据库服务器压力很大。业界通用的做法是使用连接池。

连接池的原理就像共享单车:预先准备一批现成的连接放在池子里,谁要用就取一辆,用完还回去,而不是直接销毁。主流的连接池有HikariCP、Druid、Tomcat JDBC Pool等。如果是学习阶段,你只需要知道有这一层东西存在,等真正做项目时,用Spring Boot默认的HikariCP就够了。

4. 增删改查的实现:从实体类到完整调用

4.1 实体类和DAO分层,提前养成好习惯

虽然这是一个入门项目,但从一开始就保持清晰的分层,能避免以后项目变大时代码乱成一锅粥。我建议至少分成三层:实体类放在entity包,访问数据库的类放在dao包,程序入口放在main包。

实体类是数据库中一行数据的Java映射。user_info表对应一个User类:

package com.demo.entity; import java.time.LocalDateTime; public class User { private Long id; private String username; private String password; private String email; private LocalDateTime createdTime; public User() { } public User(Long id, String username, String password, String email, LocalDateTime createdTime) { this.id = id; this.username = username; this.password = password; this.email = email; this.createdTime = createdTime; } // getter和setter方法,IDE可以自动生成,这里不逐一列出 }

DAO是Data Access Object,也叫数据访问对象。它的作用是把对数据库的各种操作封装起来,让上层业务代码不直接接触JDBC那些烦琐接口。接口可以先定义好,方便以后换实现:

package com.demo.dao; import com.demo.entity.User; import java.util.List; public interface UserDao { List<User> findAll(); User findById(Long id); int insert(User user); int update(User user); int deleteById(Long id); }

接口定义好之后,真正干活的UserDaoImpl类就要逐个实现这些方法了。下面几节挨个讲。

4.2 查询操作:读取ResultSet并映射成Java对象

查询是所有操作里最能体现JDBC特性的一个,因为涉及ResultSet结果集的读取。先看查询全部用户的方法:

package com.demo.dao.impl; import com.demo.dao.UserDao; import com.demo.entity.User; import com.demo.util.JdbcUtil; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; public class UserDaoImpl implements UserDao { @Override public List<User> findAll() { String sql = "select id, username, password, email, created_time from user_info"; List<User> users = new ArrayList<>(); try (Connection conn = JdbcUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { User user = new User(); user.setId(rs.getLong("id")); user.setUsername(rs.getString("username")); user.setPassword(rs.getString("password")); user.setEmail(rs.getString("email")); Timestamp ts = rs.getTimestamp("created_time"); if (ts != null) { user.setCreatedTime(ts.toLocalDateTime()); } users.add(user); } } catch (SQLException e) { throw new RuntimeException("查询用户列表失败", e); } return users; } }

这里有几个新手容易懵的点。第一,ResultSet并不是一次把全部数据都装进内存,它维护着一个游标,一开始游标指向第一行数据之前。每次调用rs.next(),游标就往下一行移动一次;如果当前没有更多数据,就返回false。所以while循环所有行,就是用游标一行行划过去。第二,取字段值用getLong、getString之类的方法,参数可以写列的序号也可以写列名,我建议写列名,代码可读性高,不容易因为SELECT语句里字段顺序调整而出错。

我注意到,有人觉得DAO方法里抛出RuntimeException是非主流做法。其实这是刻意为之。数据库异常是典型的运行时异常,如果在DAO层强行让上层去处理,调用方代码会被try-catch淹没。把底层异常包成RuntimeException抛上去,由全局统一异常处理来兜底,是实际项目里更常见的姿势。

再来看按ID查询单个用户。区别只是SQL多了一个where条件,并且要给条件参数赋值:

@Override public User findById(Long id) { String sql = "select id, username, password, email, created_time from user_info where id = ?"; try (Connection conn = JdbcUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setLong(1, id); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User user = new User(); user.setId(rs.getLong("id")); user.setUsername(rs.getString("username")); user.setPassword(rs.getString("password")); user.setEmail(rs.getString("email")); Timestamp ts = rs.getTimestamp("created_time"); if (ts != null) { user.setCreatedTime(ts.toLocalDateTime()); } return user; } } } catch (SQLException e) { throw new RuntimeException("查询用户失败", e); } return null; }

SQL里的问号是占位符,真正传入的值通过ps.setLong(1, id)设置。这个设计是PreparedStatement的精髓,后面专门讲。

4.3 新增操作:PreparedStatement的参数绑定

新增方法比查询多一层参数绑定的理解。看代码:

@Override public int insert(User user) { String sql = "insert into user_info(username, password, email) values (?, ?, ?)"; try (Connection conn = JdbcUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, user.getUsername()); ps.setString(2, user.getPassword()); ps.setString(3, user.getEmail()); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("新增用户失败", e); } }

先说一个原则:执行INSERT、DELETE、UPDATE这些会改变数据的语句,用executeUpdate();执行SELECT用executeQuery()。executeUpdate()的返回值是影响的行数,比如插入成功就返回1,你可以用这个数值判断操作是否生效。

再说为什么必须坚持占位符加setXxx,而不是用字符串拼接。假如你用这种写法:

String sql = "insert into user_info(username, password, email) values ('" + user.getUsername() + "', ...)";

一旦用户名里含有一个单引号,SQL就会语法错乱。更可怕的是有心人传一段' or '1'='1这种内容,可能会直接改变SQL语义,这就是臭名昭著的SQL注入漏洞。PreparedStatement的意义在于把SQL结构和参数值分离开:先让数据库对SQL结构做预编译,参数再按类型安全地绑定进去。你的输入只是数据,永远不可能再变成SQL命令的一部分。

如果插入的表有自增主键,有时业务需要立刻拿到新生成的ID,比如前端新增完用户就要跳转到详情页。这时可以在创建PreparedStatement时加一个参数:

PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);

执行完executeUpdate()后,再取生成的键值:

try (ResultSet keys = ps.getGeneratedKeys()) { if (keys.next()) { user.setId(keys.getLong(1)); } }

这个功能在框架调用链里非常常见,但很多入门文章不提,我顺手讲一下,你用得上。

4.4 修改与删除:注意条件别漏掉

修改用户,逻辑上分两种:全字段覆盖更新,或者按需更新。全字段更新最简单,直接写一条UPDATE语句,把所有可变字段都SET一遍,用id定位要改的行:

@Override public int update(User user) { String sql = "update user_info set username = ?, password = ?, email = ? where id = ?"; try (Connection conn = JdbcUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, user.getUsername()); ps.setString(2, user.getPassword()); ps.setString(3, user.getEmail()); ps.setLong(4, user.getId()); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("更新用户失败", e); } }

这里最容易犯的错是漏掉where条件。一旦漏掉,UPDATE会对整张表生效,所有用户的密码都会被改成同一个值。所以凡是写UPDATE和DELETE,第一件事就是确认where条件写没写、参数传没传。

按需更新要复杂一些,因为字段可能有为空的情况,比如只改密码,其他字段不传。这时如果还用上面那条SQL,会把其他字段覆盖成空。处理办法是动态拼接SQL:先判断哪些字段不为空,再把对应碎片拼进去。这其实就是MyBatis动态SQL的雏形。学习阶段可以先把全字段更新写熟练,然后再去想动态拼接的问题,循序渐进。

删除就直观多了:

@Override public int deleteById(Long id) { String sql = "delete from user_info where id = ?"; try (Connection conn = JdbcUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setLong(1, id); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("删除用户失败", e); } }

我顺便给一句忠告:真正的企业项目里,物理删除一般要慎用。用户删了可能还会想恢复,订单删了审计对不上账。所以很多系统会用逻辑删除,也就是加一个is_deleted字段,删除操作其实是执行UPDATE把is_deleted改成1,查询时统一带条件过滤掉这些记录。入门时掌握物理删除没问题,但脑子里要有这层意识。

4.5 事务:一次操作需要多条SQL时,必须依赖它

上面的增删改查都是单条SQL,数据库自动提交,不会出大问题。但真实业务往往是多条SQL组合。比如转账场景:张三扣钱,李四加钱,这两步必须同时成功或同时失败。如果第一步执行成功而第二步抛异常,钱就凭空消失了。

JDBC里控制这个行为的开关是setAutoCommit。默认情况下,每条SQL执行完就会被自动提交,但你可以关闭自动提交,手动控制提交时机:

Connection conn = null; try { conn = JdbcUtil.getConnection(); conn.setAutoCommit(false); try (PreparedStatement ps1 = conn.prepareStatement("update account set money = money - 500 where id = 1")) { ps1.executeUpdate(); } try (PreparedStatement ps2 = conn.prepareStatement("update account set money = money + 500 where id = 2")) { ps2.executeUpdate(); } conn.commit(); } catch (Exception e) { try { if (conn != null) { conn.rollback(); } } catch (SQLException ex) { ex.printStackTrace(); } throw new RuntimeException("转账失败", e); } finally { try { if (conn != null) { conn.close(); } } catch (SQLException e) { e.printStackTrace(); } }

注意,同一个事务里的多条SQL必须使用同一个Connection对象,因为事务是以连接为单位的。如果你在每条SQL里都重新getConnection(),那就等于开了好几个独立事务,根本没法一起提交或回滚。这是一个很容易忽略细节的领域。

4.6 把增删改查串起来,写一个控制台演示入口

代码都写齐了,最后需要一个入口把功能跑通,也让新手直观看到CRUD的执行效果。写一个简单的Main类:

package com.demo; import com.demo.dao.UserDao; import com.demo.dao.impl.UserDaoImpl; import com.demo.entity.User; public class Main { public static void main(String[] args) { UserDao dao = new UserDaoImpl(); // 1. 新增 User newUser = new User(); newUser.setUsername("wangwu"); newUser.setPassword("111222"); newUser.setEmail("wangwu@example.com"); int insertRows = dao.insert(newUser); System.out.println("新增影响行数:" + insertRows); // 2. 查询全部 System.out.println("--- 当前用户列表 ---"); for (User user : dao.findAll()) { System.out.println(user.getId() + " " + user.getUsername() + " " + user.getEmail()); } // 3. 修改 newUser.setPassword("888888"); int updateRows = dao.update(newUser); System.out.println("修改影响行数:" + updateRows); // 4. 按ID查询 User target = dao.findById(newUser.getId()); System.out.println("按ID查询用户名:" + target.getUsername()); // 5. 删除 int deleteRows = dao.deleteById(newUser.getId()); System.out.println("删除影响行数:" + deleteRows); } }

这个主流程几乎可以照搬到任何入门项目里。新增一条记录,查出来核对一下,修改后再看影响行数,最后删掉收尾。整个链路跑通,你对JDBC的感观认知就建立起来了。

5. 那些必须亲手踩过的坑:排查方法实录

5.1 高频故障速查表

我把自己和带的新人遇到过的典型问题汇总成了表格。遇到报错时,先对照这张表能省下大量搜索时间。

现象主要原因处理方法
ClassNotFoundException: com.mysql.cj.jdbc.Driver驱动JAR没有引入,或者类名写错用Maven重新导入依赖;检查类名,5.x驱动用com.mysql.jdbc.Driver,8.x驱动用com.mysql.cj.jdbc.Driver
Communications link failure数据库服务没有启动,端口号不对,或者地址写错确认数据库进程存在,检查3306端口是否监听,确认localhost不是远程地址
Server time zone value 'XXX' is unrecognizedMySQL 8驱动校验时区失败在URL末尾加serverTimezone=Asia/Shanghai
中文数据存进去变成问号数据库表字符集、连接字符集不一致表使用utf8mb4,URL加characterEncoding=utf8,连接串别丢了该参数
Too many connections连接对象没关闭,或连接池连接被耗尽检查所有Connection是否都在finally或try-with-resources中关闭
No operations allowed after connection closed用了已关闭的Connection,或者连接被连接池回收检查代码里是否手动调用过close,别在异步线程里复用同一个连接

5.2 最典型的隐蔽坑:SQL注入和字符串拼接

这个坑值得重复强调,因为我见过太多新手在入门时就养成了坏习惯。用字符串拼接SQL虽然“看着简单”,但它有两个问题。第一个是语法层面的引号问题,第二个是安全层面的注入问题。

举一个极端的例子:登录时本来应该验证用户名和密码,如果代码写成字符串拼接,攻击者在用户名输入框里填admin' --,后面的密码条件就变成了注释的一部分,登录逻辑直接被绕过。PreparedStatement的占位符机制可以完全杜绝这种问题,因为传入的字符串永远只是参数值,不会被解析成SQL结构。

如果你接手了老项目,在代码里看到Statement字符串拼接,不要犹豫,尽快替换成PreparedStatement。这不是风格偏好问题,是底线问题。

5.3 资源泄漏:看不见的连接杀手

Connection、PreparedStatement、ResultSet这三个对象都持有底层资源,用完必须释放。很多人只记得关闭Connection,以为连接关了其他也跟着关了,这是误解。规范的写法就是这篇代码一直在用的try-with-resources,它保证无论程序是正常结束还是抛异常,括号里的资源都会自动关闭。

特别注意一个场景:你在一个事务里手动创建了Connection,如果异常发生,必须把回滚和关闭都处理好。我上面写事务代码时已经演示了标准写法:catch块里rollback,finally块里close。哪怕你事务代码里的SQL写得再漂亮,连接不关,高并发下一晚就能把连接池拖垮。

写到这里我想起一个真实案例:一次线上系统频繁卡死,查遍应用日志发现每秒创建了上百个连接,最后定位到一个没有关闭PreparedStatement的查询方法。问题不大,影响巨大。类可以在这个方法被调用了几万次后发现连接数爆炸。这种教训,懂的人自然懂。

5.4 下一步怎么走:从JDBC平滑过渡到框架

原生JDBC写完后,你会发现代码里有很多重复模板:每次都要获取连接、创建PreparedStatement、遍历ResultSet、关闭资源。这些重复正是框架存在的理由。当有一天你开始觉得这些模板代码烦人,那就是该接触Spring的JdbcTemplate、或者MyBatis的时候了。

JdbcTemplate把连接获取、资源释放这些模板代码都内部处理了,你只需要写SQL和参数映射。MyBatis则更进一步,把SQL和Java方法解耦,还提供了动态SQL能力。当你回头看框架代码时,你能清晰说出PreparedStatement在哪里被创建、ResultSet在哪里被解析,你对框架的理解就会比别人深一个层次。

我个人的建议是,先用这篇文章的代码跑通JDBC,然后去读Spring JdbcTemplate的源码片段,重点看它如何封装连接和ResultSet。这个阅读过程会比你想象的顺畅,因为底层你已经完全清楚了。数据库操作这条路,往后越走越宽,但起点永远是这个看着不起眼的增删改查。把这一步踩实,后面学什么框架都不慌。

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

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

立即咨询