Java高级篇之JDBC:连接数据库,完成查询、更新与事务

上一篇完成了HTTP请求的发送与响应读取。从这一篇开始进入数据库编程:程序怎样写入学生记录、查回数据,又怎样保证多条修改共同成功或共同撤销?

本篇使用Java21与H2 2.5.252,围绕学生表走完整个过程。H2采用嵌入式内存数据库,不需要另起服务,也不会访问已有业务数据库。

一、JDBC连接Java与数据库

JDBC 是Java访问关系数据库的标准接口。应用调用java.sql,具体数据库的驱动实现连接与SQL执行。更换数据库时需要调整驱动、连接地址和部分SQL,参数绑定与结果遍历仍遵循同一套接口。

角色 职责
DriverManager 根据JDBC地址取得连接
Connection 承载会话与事务
PreparedStatement 保存SQL模板并绑定参数
ResultSet 逐行读取查询结果

驱动JAR必须进入运行时classpath。现代驱动通常通过服务发现自动注册,不必把旧教程中的Class.forName复制进每个程序。缺少驱动时,先检查依赖是否进入运行环境。

二、用一张表确定数据规则

学生表包含id、name和score。id是主键,name和score不可为空,分数限制在0到100。数据库约束对所有写入入口生效,不能只依赖页面校验。

本例地址jdbc:h2:mem:lesson表示当前进程中的命名内存库。演示始终保持一个连接打开,最后关闭连接后数据结束生命周期。需要持久保存时应选择文件数据库或数据库服务。

从H2官方下载页取得h2-2.5.252.jar,与JdbcDemo.java放在同一目录,运行:

shell 复制代码
java --class-path h2-2.5.252.jar JdbcDemo.java

账号sa和空密码仅用于这个进程内临时库。业务数据库凭据由部署配置提供,不写入源码仓库。

三、SQL模板与参数值分开传入

插入模板是INSERT INTO student(id,name,score) VALUES(?,?,?),参数编号从1开始,setInt、setString按类型传值。姓名中的单引号作为字符串数据处理,不会被拼接成SQL语法。

占位符用于值,不用于表名或列名。动态排序字段应通过固定白名单映射,不能直接拼入用户输入。参数化也不代替权限检查:安全传入一个id,不表示当前用户有权修改该记录。

查询调用executeQuery得到ResultSet;增删改调用executeUpdate得到受影响行数。更新指定主键时,本例要求返回1;返回0说明目标不存在,应由业务决定后续处理。

四、完整示例:提交一次,再回滚一次

第一段事务插入两名学生、修改分数,然后提交。第二段先删除一人,再故意插入重复主键;发生冲突后回滚,检查前面的删除是否一起撤销。

java 复制代码
import java.sql.*;
public class JdbcDemo {
    static void rollback(Connection c, SQLException original) {
        try { c.rollback(); }
        catch (SQLException failure) { original.addSuppressed(failure); }
    }
    static void show(Connection c) throws SQLException {
        try (PreparedStatement p = c.prepareStatement("SELECT id, name, score FROM student ORDER BY id");
             ResultSet r = p.executeQuery()) {
            while (r.next()) System.out.printf("%d | %s | %d%n", r.getInt("id"), r.getString("name"), r.getInt("score"));
        }
    }
    public static void main(String[] args) throws SQLException {
        try (Connection c = DriverManager.getConnection("jdbc:h2:mem:lesson", "sa", "")) {
            try (Statement s = c.createStatement()) {
                s.executeUpdate("CREATE TABLE student(id INT PRIMARY KEY, name VARCHAR(40) NOT NULL, score INT NOT NULL CHECK(score BETWEEN 0 AND 100))");
            }
            c.setAutoCommit(false);
            try {
                try (PreparedStatement p = c.prepareStatement("INSERT INTO student(id,name,score) VALUES(?,?,?)")) {
                    p.setInt(1,1); p.setString(2,"小林"); p.setInt(3,80); p.executeUpdate();
                    p.setInt(1,2); p.setString(2,"小陈"); p.setInt(3,90); p.executeUpdate();
                }
                try (PreparedStatement p = c.prepareStatement("UPDATE student SET score=? WHERE id=?")) {
                    p.setInt(1,85); p.setInt(2,1);
                    if (p.executeUpdate()!=1) throw new SQLException("更新目标不存在");
                }
                c.commit();
            } catch (SQLException e) { rollback(c, e); throw e; }
            System.out.println("提交后:"); show(c); c.commit();
            try {
                try (PreparedStatement p = c.prepareStatement("DELETE FROM student WHERE id=?")) {
                    p.setInt(1,2); p.executeUpdate();
                }
                try (PreparedStatement p = c.prepareStatement("INSERT INTO student VALUES(?,?,?)")) {
                    p.setInt(1,1); p.setString(2,"重复编号"); p.setInt(3,60); p.executeUpdate();
                }
                c.commit();
                throw new AssertionError("应触发主键冲突");
            } catch (SQLException expected) {
                rollback(c, expected);
                if (!"23505".equals(expected.getSQLState()) || expected.getSuppressed().length > 0) throw expected;
                System.out.println("主键冲突,事务已回滚");
            }
            System.out.println("回滚后:"); show(c); c.commit();
        }
    }
}

本次实际运行结果:

text 复制代码
提交后:
1 | 小林 | 85
2 | 小陈 | 90
主键冲突,事务已回滚
回滚后:
1 | 小林 | 85
2 | 小陈 | 90

最后小陈仍然存在。回滚撤销了本事务尚未提交的DELETE,第一段已经提交的85分保持不变。

第二段检查H2主键冲突的SQLState为23505,其他数据库故障继续抛出。rollback辅助方法把回滚失败附加到原异常上,保留最初的错误;只有回滚成功且冲突类型符合预期,程序才继续检查数据。

五、事务边界为什么放在Connection上

新连接默认自动提交。多条语句组成一个工作单元时,先setAutoCommit(false),成功后commit,失败后rollback。参与同一事务的语句必须使用同一个连接,分别打开两个连接不会自动形成一笔事务。

建表放在演示事务之前,避免把不同数据库的DDL提交行为混入增删改。业务事务要尽量短,开启事务后等待用户输入或耗时网络调用,会延长连接与锁的占用。

原子性不等于所有并发问题都自动消失。读后修改仍要结合隔离级别、约束或条件更新。例如扣库存可以让UPDATE同时要求库存足够,再检查受影响行数。

六、结果读取与资源关闭

ResultSet初始游标在第一行之前,next返回true才能读取当前行。零条记录时循环不进入。查询显式列出字段,用列名读取,让字段映射清楚可见。

SQL的NULL与Java基本类型不同。允许NULL的整数列使用getInt读取后,要结合wasNull区分NULL和0,也可以使用驱动支持的类型化getObject。本文score是NOT NULL,直接读int即可。

try-with-resources按声明的反序关闭:先结果集,再语句,最后连接。关闭前明确提交或回滚,不把close当成事务策略。使用连接池时,close通常表示归还连接,应用仍要结束事务并释放查询资源。

七、定位错误与组织代码

连接失败检查地址、驱动、账号和网络;SQL错误检查语法、表结构和参数类型;数据冲突检查主键、唯一键与外键。SQLException的SQLState、厂商错误码和异常链可用于定位,日志避免泄露密码与敏感数据。

不要捕获异常后返回空列表冒充"查询成功但没有数据"。执行失败与零条结果应分别表达。

扩展程序时,可以把SQL访问提取成独立方法,事务仍由一项完整业务的边界统一管理。理解这层分工后,再看ORM的数据映射与持久化管理就更容易。

🧠 思维导图

#mermaid-svg-AzjwtKvR9iFZ5fmC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AzjwtKvR9iFZ5fmC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AzjwtKvR9iFZ5fmC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AzjwtKvR9iFZ5fmC .error-icon{fill:#552222;}#mermaid-svg-AzjwtKvR9iFZ5fmC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AzjwtKvR9iFZ5fmC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AzjwtKvR9iFZ5fmC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AzjwtKvR9iFZ5fmC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AzjwtKvR9iFZ5fmC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AzjwtKvR9iFZ5fmC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AzjwtKvR9iFZ5fmC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AzjwtKvR9iFZ5fmC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AzjwtKvR9iFZ5fmC .marker.cross{stroke:#333333;}#mermaid-svg-AzjwtKvR9iFZ5fmC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AzjwtKvR9iFZ5fmC p{margin:0;}#mermaid-svg-AzjwtKvR9iFZ5fmC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AzjwtKvR9iFZ5fmC .cluster-label text{fill:#333;}#mermaid-svg-AzjwtKvR9iFZ5fmC .cluster-label span{color:#333;}#mermaid-svg-AzjwtKvR9iFZ5fmC .cluster-label span p{background-color:transparent;}#mermaid-svg-AzjwtKvR9iFZ5fmC .label text,#mermaid-svg-AzjwtKvR9iFZ5fmC span{fill:#333;color:#333;}#mermaid-svg-AzjwtKvR9iFZ5fmC .node rect,#mermaid-svg-AzjwtKvR9iFZ5fmC .node circle,#mermaid-svg-AzjwtKvR9iFZ5fmC .node ellipse,#mermaid-svg-AzjwtKvR9iFZ5fmC .node polygon,#mermaid-svg-AzjwtKvR9iFZ5fmC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AzjwtKvR9iFZ5fmC .rough-node .label text,#mermaid-svg-AzjwtKvR9iFZ5fmC .node .label text,#mermaid-svg-AzjwtKvR9iFZ5fmC .image-shape .label,#mermaid-svg-AzjwtKvR9iFZ5fmC .icon-shape .label{text-anchor:middle;}#mermaid-svg-AzjwtKvR9iFZ5fmC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AzjwtKvR9iFZ5fmC .rough-node .label,#mermaid-svg-AzjwtKvR9iFZ5fmC .node .label,#mermaid-svg-AzjwtKvR9iFZ5fmC .image-shape .label,#mermaid-svg-AzjwtKvR9iFZ5fmC .icon-shape .label{text-align:center;}#mermaid-svg-AzjwtKvR9iFZ5fmC .node.clickable{cursor:pointer;}#mermaid-svg-AzjwtKvR9iFZ5fmC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AzjwtKvR9iFZ5fmC .arrowheadPath{fill:#333333;}#mermaid-svg-AzjwtKvR9iFZ5fmC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AzjwtKvR9iFZ5fmC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AzjwtKvR9iFZ5fmC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AzjwtKvR9iFZ5fmC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AzjwtKvR9iFZ5fmC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AzjwtKvR9iFZ5fmC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AzjwtKvR9iFZ5fmC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AzjwtKvR9iFZ5fmC .cluster text{fill:#333;}#mermaid-svg-AzjwtKvR9iFZ5fmC .cluster span{color:#333;}#mermaid-svg-AzjwtKvR9iFZ5fmC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AzjwtKvR9iFZ5fmC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AzjwtKvR9iFZ5fmC rect.text{fill:none;stroke-width:0;}#mermaid-svg-AzjwtKvR9iFZ5fmC .icon-shape,#mermaid-svg-AzjwtKvR9iFZ5fmC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AzjwtKvR9iFZ5fmC .icon-shape p,#mermaid-svg-AzjwtKvR9iFZ5fmC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AzjwtKvR9iFZ5fmC .icon-shape .label rect,#mermaid-svg-AzjwtKvR9iFZ5fmC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AzjwtKvR9iFZ5fmC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AzjwtKvR9iFZ5fmC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AzjwtKvR9iFZ5fmC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} JDBC数据库访问
连接
驱动与JDBC地址
Connection会话
SQL执行
模板与参数绑定
查询与更新行数
事务
commit提交
rollback撤销
结果与资源
next遍历与类型映射
关闭资源与异常定位

总结要点

连接、语句和结果各司其职。驱动建立数据库通道,PreparedStatement绑定值,ResultSet逐行读取结果。

多条修改围绕事务组织。明确提交与回滚位置,检查受影响行数,让失败路径保持数据一致。

资源与错误也是接口设计的一部分。及时释放资源,区分零条结果和执行失败。

下一篇学习ORM,以Hibernate理解对象、表与持久化操作之间的映射。

👉 如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊

参考资料:Connection、PreparedStatement、H2使用说明。

相关推荐
*长铗归来*1 小时前
MySQL新学知识(三)MySQL 事件(EVENT)
数据库·mysql
bksczm1 小时前
Redis数据结构之string
java·redis·mybatis
hzxxxz1 小时前
PHP引擎两个高危漏洞复盘:计数顺序与旧地址缺陷的机理与处置清单
开发语言·php
cc_瀚海知行1 小时前
资料检索 1|直接放入全文、按需读文件和先检索,有什么区别?
java·人工智能·ai
马立杰1 小时前
PHP 未授权 RCE 应急响应:CVE-2026-109618 CVE-2026-109619 排查与加固实战
android·开发语言·php
布莱克6051 小时前
C++ 内存泄漏全解析:常见场景、成因与规避实践
开发语言·c++·内存泄露
Warolitbos1 小时前
【数据库】SQL进阶15题,窗口函数/索引/事务一页速查
数据库·sql·学习·mysql·面试
BioRunYiXue1 小时前
使用docker分析高通量测序数据
java·数据库·人工智能·spring cloud·docker·tomcat·html
jimy11 小时前
lvalue、xvalue、prvalue(一):三者的分类
开发语言·c++