news 2026/6/23 7:01:17

JDBC 编程:用 Java 连接 MySQL

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JDBC 编程:用 Java 连接 MySQL

在前面的文章中,我们一直在 MySQL 命令行或图形化工具中直接编写 SQL。然而,实际的应用系统中,数据库总是躲在服务端程序的背后——用户点击按钮,后端代码去执行 SQL,再把结果返回给前端。对于 Java 开发者来说,连接和操作数据库的标准方式就是JDBC(Java Database Connectivity)

本文将系统讲解 JDBC 编程的核心知识,内容包括:

  • JDBC 的定位与原理
  • 从加载驱动到获取连接的完整步骤
  • StatementPreparedStatement的使用与对比(重点:防 SQL 注入)
  • 执行 DML(增删改)与 DQL(查询)操作
  • 处理ResultSet结果集
  • 在 JDBC 中控制事务
  • 实战:用 Java 实现一个带事务的“图书借阅”功能

读完本文后,你将能够独立编写 Java 代码来操作 MySQL 数据库,并知道如何写出安全、高效的数据访问层。


1. 什么是 JDBC?

JDBC 是 Java 定义的一套接口java.sqljavax.sql包),它规定了 Java 程序应该如何与数据库通信。数据库厂商(如 Oracle、MySQL)则提供这些接口的实现类,也就是所谓的“数据库驱动”。

这种设计的最大好处是:我们的 Java 代码只需要面向 JDBC 接口编程,切换数据库时只需更换驱动 jar 包和连接 URL,业务代码几乎无需改动。


2. 环境准备:添加 MySQL 驱动

我们使用 Maven 来管理依赖。在pom.xml中加入:

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

如果不用 Maven,也可以从 MySQL Connector/J 下载页 手动下载 jar 并加入 classpath。

注意:驱动类名从 8.0 起由com.mysql.jdbc.Driver改为com.mysql.cj.jdbc.Driver,不过由于 SPI 自动注册机制,我们通常不需要手动执行Class.forName()了。


3. JDBC 编程六步走

无论多复杂的数据库操作,JDBC 编程的核心流程都可以归纳为以下六个步骤:

  1. 加载驱动(可选,现代 JDBC 自动完成)
  2. 获取连接DriverManager.getConnection()
  3. 创建 Statement / PreparedStatement 对象
  4. 执行 SQLexecuteUpdate()executeQuery()
  5. 处理结果集ResultSet
  6. 释放资源ConnectionStatementResultSet—— 顺序关闭)

我们先从一个最简单的查询示例开始,再逐步深入。

3.1 获取数据库连接

Stringurl="jdbc:mysql://localhost:3306/library_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8";Stringuser="root";Stringpassword="your_password";Connectionconn=DriverManager.getConnection(url,user,password);

连接 URL 参数说明

  • useSSL=false:开发环境可关闭 SSL,生产环境务必配置证书。
  • serverTimezone=UTC:指定时区,防止时间字段偏移。MySQL 8.0 驱动要求明确设置。
  • characterEncoding=utf8:告诉驱动使用 UTF-8 通信。

3.2 一个完整的查询示例

Stringsql="SELECT id, name, email FROM readers";// 推荐使用 try-with-resources 自动释放资源try(Connectionconn=DriverManager.getConnection(url,user,password);Statementstmt=conn.createStatement();ResultSetrs=stmt.executeQuery(sql)){while(rs.next()){intid=rs.getInt("id");Stringname=rs.getString("name");Stringemail=rs.getString("email");System.out.printf("%d: %s (%s)%n",id,name,email);}}catch(SQLExceptione){e.printStackTrace();}

try-with-resources语法能确保ConnectionStatementResultSet在代码块结束后自动关闭,省去繁琐的finally手动关闭。


4. Statement 与 SQL 注入风险

上面的例子使用了Statement,它把 SQL 字符串原封不动地发送给数据库。如果 SQL 中拼接了用户输入,就会产生致命的安全漏洞——SQL 注入

4.1 注入演示

假设登录逻辑这样写:

Stringusername=request.getParameter("username");Stringpassword=request.getParameter("password");Stringsql="SELECT * FROM users WHERE username='"+username+"' AND password='"+password+"'";Statementstmt=conn.createStatement();ResultSetrs=stmt.executeQuery(sql);

如果用户输入:

  • username =' OR '1'='1
  • password =' OR '1'='1

拼接后的 SQL 变为:

SELECT*FROMusersWHEREusername=''OR'1'='1'ANDpassword=''OR'1'='1'

条件'1'='1'恒成立,结果返回所有用户,登录被绕过。

4.2 防范之道:PreparedStatement

PreparedStatement使用占位符?来替代直接拼接字符串,数据库会对 SQL 模板进行预编译,参数只是作为数据填充,永远不会被当作 SQL 代码执行。

改用PreparedStatement的登录查询:

Stringsql="SELECT * FROM users WHERE username=? AND password=?";try(PreparedStatementps=conn.prepareStatement(sql)){ps.setString(1,username);ps.setString(2,password);try(ResultSetrs=ps.executeQuery()){if(rs.next()){// 登录成功}}}

此时即使用户输入' OR '1'='1,也会被当作普通的字符串值,不再构成注入威胁。永远不要用Statement拼接用户输入,始终使用PreparedStatement

额外优点

  • 预编译:同一模板 SQL 多次执行时性能更好(MySQL 8.0 默认开启服务端预编译)。
  • 代码可读性高:不再需要手动拼单引号和转义。

5. 执行增删改操作(DML)

PreparedStatementexecuteUpdate()方法用于执行INSERTUPDATEDELETE语句,返回受影响的行数

5.1 插入数据

StringinsertSql="INSERT INTO readers (name, email, phone) VALUES (?, ?, ?)";try(Connectionconn=DriverManager.getConnection(url,user,password);PreparedStatementps=conn.prepareStatement(insertSql)){ps.setString(1,"新读者");ps.setString(2,"new@example.com");ps.setString(3,"13800000000");introws=ps.executeUpdate();System.out.println("插入了 "+rows+" 行");}

5.2 更新与删除

// 更新StringupdateSql="UPDATE readers SET email=? WHERE id=?";try(PreparedStatementps=conn.prepareStatement(updateSql)){ps.setString(1,"updated@example.com");ps.setInt(2,1);introws=ps.executeUpdate();System.out.println("更新了 "+rows+" 行");}// 删除StringdeleteSql="DELETE FROM readers WHERE id=?";try(PreparedStatementps=conn.prepareStatement(deleteSql)){ps.setInt(1,10);introws=ps.executeUpdate();System.out.println("删除了 "+rows+" 行");}

6. 查询与处理 ResultSet

executeQuery()返回ResultSet对象,它代表一个二维表,内部有一个游标初始指向第一行之前。调用next()方法可逐行向后移动,并读取各列数据。

常用取值方法(根据列类型选择):

  • getInt(columnIndex)getInt("columnName")
  • getString(…)
  • getDate(…)getTimestamp(…)
  • getDouble(…)

示例:查询指定读者的借阅记录并打印

StringquerySql="SELECT b.title, br.borrow_date, br.due_date, br.return_date "+"FROM borrow_records br "+"JOIN books b ON br.book_id = b.id "+"WHERE br.reader_id = ?";try(Connectionconn=DriverManager.getConnection(url,user,password);PreparedStatementps=conn.prepareStatement(querySql)){ps.setInt(1,1);// 读者ID=1try(ResultSetrs=ps.executeQuery()){while(rs.next()){Stringtitle=rs.getString("title");DateborrowDate=rs.getDate("borrow_date");DatedueDate=rs.getDate("due_date");DatereturnDate=rs.getDate("return_date");System.out.printf("书名:%s,借出:%s,应还:%s,归还:%s%n",title,borrowDate,dueDate,returnDate!=null?returnDate:"未还");}}}

注意getDate()返回java.sql.Date,只包含日期部分。如果要获取精确时间,可以用getTimestamp()


7. JDBC 中的事务控制

默认情况下,Connection处于自动提交模式,即每执行一条 SQL 就立即提交。对于需要原子性的多个操作,我们必须关闭自动提交,手动控制事务边界。

关键方法

  • conn.setAutoCommit(false);—— 关闭自动提交,开启事务
  • conn.commit();—— 提交事务
  • conn.rollback();—— 回滚事务(通常在 catch 块中)

示例:银行转账(模拟)

Connectionconn=null;try{conn=DriverManager.getConnection(url,user,password);conn.setAutoCommit(false);// 开启事务StringdebitSql="UPDATE accounts SET balance = balance - ? WHERE id = ?";StringcreditSql="UPDATE accounts SET balance = balance + ? WHERE id = ?";try(PreparedStatementdebitPs=conn.prepareStatement(debitSql);PreparedStatementcreditPs=conn.prepareStatement(creditSql)){debitPs.setDouble(1,100.0);debitPs.setInt(2,1);debitPs.executeUpdate();creditPs.setDouble(1,100.0);creditPs.setInt(2,2);creditPs.executeUpdate();conn.commit();// 全部成功,提交System.out.println("转账成功");}catch(SQLExceptione){conn.rollback();// 任何一步失败,回滚所有System.out.println("转账失败,已回滚");e.printStackTrace();}}finally{if(conn!=null){conn.setAutoCommit(true);// 恢复默认conn.close();}}

强烈建议:始终在finally中将autoCommit重置为true,否则归还到连接池后可能引发奇怪的行为。


8. 实战:Java 实现图书借阅功能

现在,让我们把事务和PreparedStatement运用到图书管理系统中。借阅一本书需要两个动作:

  1. borrow_records表插入一条借阅记录。
  2. 将对应图书的stock减 1。

这两个操作必须在一个事务中完成,否则可能出现“记录了借阅但库存没减”或“库存减了但没记录”的数据不一致。

8.1 数据库表准备

假设你的library_db数据库中已存在以下表(参考第一阶段实战):

  • books(id, title, author, stock, ...)
  • borrow_records(id, reader_id, book_id, borrow_date, due_date, return_date)

如果还没有,请先使用之前的建表语句创建。

8.2 Java 代码实现

importjava.sql.*;importjava.time.LocalDate;publicclassLibraryService{privatestaticfinalStringURL="jdbc:mysql://localhost:3306/library_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8";privatestaticfinalStringUSER="root";privatestaticfinalStringPASSWORD="your_password";/** * 借阅图书 * * @param readerId 读者ID * @param bookId 图书ID * @param borrowDuration 借阅天数(用于计算应还日期) * @return 是否借阅成功 */publicbooleanborrowBook(intreaderId,intbookId,intborrowDuration){StringcheckStockSql="SELECT stock FROM books WHERE id = ?";StringinsertBorrowSql="INSERT INTO borrow_records (reader_id, book_id, borrow_date, due_date) "+"VALUES (?, ?, CURRENT_DATE, DATE_ADD(CURRENT_DATE, INTERVAL ? DAY))";StringupdateStockSql="UPDATE books SET stock = stock - 1 WHERE id = ? AND stock > 0";Connectionconn=null;try{conn=DriverManager.getConnection(URL,USER,PASSWORD);conn.setAutoCommit(false);// 开启事务// 1. 检查库存intstock;try(PreparedStatementcheckPs=conn.prepareStatement(checkStockSql)){checkPs.setInt(1,bookId);try(ResultSetrs=checkPs.executeQuery()){if(!rs.next()){thrownewRuntimeException("图书不存在");}stock=rs.getInt("stock");}}if(stock<=0){thrownewRuntimeException("库存不足,无法借阅");}// 2. 插入借阅记录try(PreparedStatementinsertPs=conn.prepareStatement(insertBorrowSql)){insertPs.setInt(1,readerId);insertPs.setInt(2,bookId);insertPs.setInt(3,borrowDuration);introws=insertPs.executeUpdate();if(rows!=1){thrownewRuntimeException("插入借阅记录失败");}}// 3. 更新库存try(PreparedStatementupdatePs=conn.prepareStatement(updateStockSql)){updatePs.setInt(1,bookId);introws=updatePs.executeUpdate();if(rows!=1){thrownewRuntimeException("更新库存失败,可能库存已为0");}}conn.commit();// 全部成功,提交事务System.out.println("借阅成功!读者"+readerId+" 借了图书"+bookId);returntrue;}catch(Exceptione){if(conn!=null){try{conn.rollback();// 出现异常,回滚System.out.println("借阅失败,事务已回滚:"+e.getMessage());}catch(SQLExceptionex){ex.printStackTrace();}}returnfalse;}finally{if(conn!=null){try{conn.setAutoCommit(true);// 恢复自动提交conn.close();}catch(SQLExceptione){e.printStackTrace();}}}}publicstaticvoidmain(String[]args){LibraryServiceservice=newLibraryService();// 测试:读者ID=1 借阅图书ID=3,借阅期14天booleansuccess=service.borrowBook(1,3,14);System.out.println("借阅结果:"+(success?"成功":"失败"));}}

关键点解读

  • 使用stock > 0作为更新条件,并检查受影响行数,防止并发超借(初版依然存在并发问题,后续将用悲观锁/乐观锁优化,这里先掌握基本事务)。
  • 库存检查和更新放同一事务中,保证原子性。
  • 所有资源都用try-with-resourcesfinally确保释放。
  • 异常时rollback,成功后commit

9. 小结

本文从零搭建了 Java 连接 MySQL 的完整知识体系:

  • JDBC 六步走:加载驱动 → 获取连接 → 创建 Statement/PreparedStatement → 执行 SQL → 处理结果集 → 释放资源。
  • 安全性:绝不要用Statement拼接用户输入,使用PreparedStatement能防止 SQL 注入,并提升性能。
  • DML 与查询executeUpdate()处理增删改,executeQuery()返回ResultSet
  • 事务控制setAutoCommit(false)开始事务,commit()提交,rollback()回滚。务必在连接归还前恢复自动提交。
  • 实战:结合图书管理系统,实现了一个带事务控制的借阅功能,涵盖库存检查、记录插入、库存更新三个步骤。

JDBC 是 Java 数据库开发的基石,无论后续使用的框架是 MyBatis 还是 JPA,底层都是通过 JDBC 与数据库通信。深入理解它,能帮助你写出更高效、更安全的持久层代码。

思考题

  1. 上面的借阅方法在并发情况下可能出现什么问题?(提示:两个线程同时借走最后一本书)
  2. PreparedStatement的预编译在 MySQL 8.0 中默认是服务端还是客户端?如何配置?
  3. 如果用try-with-resources管理Connection,还需要手动rollback吗?为什么?

参考资料

  • MySQL Connector/J Developer Guide
  • JDBC™ 4.3 Specification

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/11 13:12:43

互信息链式法则与变分推断在机器学习中的应用

1. 互信息链式法则的数学本质与信息流分解互信息&#xff08;Mutual Information&#xff09;作为信息论的核心概念&#xff0c;量化了两个随机变量之间的统计依赖性。当涉及三个及以上变量时&#xff0c;链式法则揭示了信息传递的层次结构。让我们从基础定义出发&#xff0c;逐…

作者头像 李华
网站建设 2026/6/23 7:01:16

3D高斯泼溅与社交感知结合的虚拟头像生成技术

1. 项目概述在虚拟现实和数字人交互领域&#xff0c;高保真对话头像生成一直是个技术难点。传统方法往往只关注说话者的语音驱动&#xff0c;而忽略了对话中至关重要的社交互动维度。RSATalker的创新之处在于&#xff0c;它首次将社交关系建模引入3D高斯泼溅技术框架&#xff0…

作者头像 李华
网站建设 2026/6/8 21:38:30

非交换几何在热力学修正中的理论与应用

1. 非交换几何与热力学修正的理论基础非交换几何作为现代理论物理的重要研究方向&#xff0c;其核心思想源于对传统时空连续性的挑战。在经典物理框架中&#xff0c;我们默认空间坐标满足交换关系[x_i, x_j]0&#xff0c;但在普朗克尺度&#xff08;~10^-35米&#xff09;下&am…

作者头像 李华
网站建设 2026/6/8 21:37:32

AKStream:跨平台流媒体管理接口平台的架构创新与实战应用

AKStream&#xff1a;跨平台流媒体管理接口平台的架构创新与实战应用 【免费下载链接】AKStream AKStream是一套全平台(Linux,MacOS,Windows)、全架构(X86_64,Arm...)、全功能的流媒体管理控制接口平台。集成GB28181,RTSP,RTMP,HTTP等设备推拉流控制、PTZ控制、音视频文件录制管…

作者头像 李华
网站建设 2026/6/8 21:33:51

GPT-4参数量与激活率真相:1.8万亿不是显存需求,2%不是固定开关

1. 这句话到底在说什么&#xff1f;先别急着转发&#xff0c;我们来拆开看看“GPT-4 Has 1.8 Trillion Parameters. It Uses 2% of Them Per Token.”——这句话过去两年在技术社区、自媒体和AI科普帖里反复刷屏&#xff0c;常被当作“大模型黑科技”的标志性论断&#xff1a;万…

作者头像 李华