JDBC 编程:用 Java 连接 MySQL

JDBC 编程:用 Java 连接 MySQL 在前面的文章中我们一直在 MySQL 命令行或图形化工具中直接编写 SQL。然而实际的应用系统中数据库总是躲在服务端程序的背后——用户点击按钮后端代码去执行 SQL再把结果返回给前端。对于 Java 开发者来说连接和操作数据库的标准方式就是JDBCJava Database Connectivity。本文将系统讲解 JDBC 编程的核心知识内容包括JDBC 的定位与原理从加载驱动到获取连接的完整步骤Statement与PreparedStatement的使用与对比重点防 SQL 注入执行 DML增删改与 DQL查询操作处理ResultSet结果集在 JDBC 中控制事务实战用 Java 实现一个带事务的“图书借阅”功能读完本文后你将能够独立编写 Java 代码来操作 MySQL 数据库并知道如何写出安全、高效的数据访问层。1. 什么是 JDBCJDBC 是 Java 定义的一套接口java.sql和javax.sql包它规定了 Java 程序应该如何与数据库通信。数据库厂商如 Oracle、MySQL则提供这些接口的实现类也就是所谓的“数据库驱动”。这种设计的最大好处是我们的 Java 代码只需要面向 JDBC 接口编程切换数据库时只需更换驱动 jar 包和连接 URL业务代码几乎无需改动。2. 环境准备添加 MySQL 驱动我们使用 Maven 来管理依赖。在pom.xml中加入dependencygroupIdcom.mysql/groupIdartifactIdmysql-connector-j/artifactIdversion8.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 编程的核心流程都可以归纳为以下六个步骤加载驱动可选现代 JDBC 自动完成获取连接DriverManager.getConnection()创建 Statement / PreparedStatement 对象执行 SQLexecuteUpdate()或executeQuery()处理结果集ResultSet释放资源Connection、Statement、ResultSet—— 顺序关闭我们先从一个最简单的查询示例开始再逐步深入。3.1 获取数据库连接Stringurljdbc:mysql://localhost:3306/library_db?useSSLfalseserverTimezoneUTCcharacterEncodingutf8;Stringuserroot;Stringpasswordyour_password;ConnectionconnDriverManager.getConnection(url,user,password);连接 URL 参数说明useSSLfalse开发环境可关闭 SSL生产环境务必配置证书。serverTimezoneUTC指定时区防止时间字段偏移。MySQL 8.0 驱动要求明确设置。characterEncodingutf8告诉驱动使用 UTF-8 通信。3.2 一个完整的查询示例StringsqlSELECT id, name, email FROM readers;// 推荐使用 try-with-resources 自动释放资源try(ConnectionconnDriverManager.getConnection(url,user,password);Statementstmtconn.createStatement();ResultSetrsstmt.executeQuery(sql)){while(rs.next()){intidrs.getInt(id);Stringnamers.getString(name);Stringemailrs.getString(email);System.out.printf(%d: %s (%s)%n,id,name,email);}}catch(SQLExceptione){e.printStackTrace();}try-with-resources语法能确保Connection、Statement、ResultSet在代码块结束后自动关闭省去繁琐的finally手动关闭。4. Statement 与 SQL 注入风险上面的例子使用了Statement它把 SQL 字符串原封不动地发送给数据库。如果 SQL 中拼接了用户输入就会产生致命的安全漏洞——SQL 注入。4.1 注入演示假设登录逻辑这样写Stringusernamerequest.getParameter(username);Stringpasswordrequest.getParameter(password);StringsqlSELECT * FROM users WHERE usernameusername AND passwordpassword;Statementstmtconn.createStatement();ResultSetrsstmt.executeQuery(sql);如果用户输入username OR 11password OR 11拼接后的 SQL 变为SELECT*FROMusersWHEREusernameOR11ANDpasswordOR11条件11恒成立结果返回所有用户登录被绕过。4.2 防范之道PreparedStatementPreparedStatement使用占位符?来替代直接拼接字符串数据库会对 SQL 模板进行预编译参数只是作为数据填充永远不会被当作 SQL 代码执行。改用PreparedStatement的登录查询StringsqlSELECT * FROM users WHERE username? AND password?;try(PreparedStatementpsconn.prepareStatement(sql)){ps.setString(1,username);ps.setString(2,password);try(ResultSetrsps.executeQuery()){if(rs.next()){// 登录成功}}}此时即使用户输入 OR 11也会被当作普通的字符串值不再构成注入威胁。永远不要用Statement拼接用户输入始终使用PreparedStatement。额外优点预编译同一模板 SQL 多次执行时性能更好MySQL 8.0 默认开启服务端预编译。代码可读性高不再需要手动拼单引号和转义。5. 执行增删改操作DMLPreparedStatement的executeUpdate()方法用于执行INSERT、UPDATE、DELETE语句返回受影响的行数。5.1 插入数据StringinsertSqlINSERT INTO readers (name, email, phone) VALUES (?, ?, ?);try(ConnectionconnDriverManager.getConnection(url,user,password);PreparedStatementpsconn.prepareStatement(insertSql)){ps.setString(1,新读者);ps.setString(2,newexample.com);ps.setString(3,13800000000);introwsps.executeUpdate();System.out.println(插入了 rows 行);}5.2 更新与删除// 更新StringupdateSqlUPDATE readers SET email? WHERE id?;try(PreparedStatementpsconn.prepareStatement(updateSql)){ps.setString(1,updatedexample.com);ps.setInt(2,1);introwsps.executeUpdate();System.out.println(更新了 rows 行);}// 删除StringdeleteSqlDELETE FROM readers WHERE id?;try(PreparedStatementpsconn.prepareStatement(deleteSql)){ps.setInt(1,10);introwsps.executeUpdate();System.out.println(删除了 rows 行);}6. 查询与处理 ResultSetexecuteQuery()返回ResultSet对象它代表一个二维表内部有一个游标初始指向第一行之前。调用next()方法可逐行向后移动并读取各列数据。常用取值方法根据列类型选择getInt(columnIndex)或getInt(columnName)getString(…)getDate(…)、getTimestamp(…)getDouble(…)等示例查询指定读者的借阅记录并打印StringquerySqlSELECT 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(ConnectionconnDriverManager.getConnection(url,user,password);PreparedStatementpsconn.prepareStatement(querySql)){ps.setInt(1,1);// 读者ID1try(ResultSetrsps.executeQuery()){while(rs.next()){Stringtitlers.getString(title);DateborrowDaters.getDate(borrow_date);DatedueDaters.getDate(due_date);DatereturnDaters.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 块中示例银行转账模拟Connectionconnnull;try{connDriverManager.getConnection(url,user,password);conn.setAutoCommit(false);// 开启事务StringdebitSqlUPDATE accounts SET balance balance - ? WHERE id ?;StringcreditSqlUPDATE accounts SET balance balance ? WHERE id ?;try(PreparedStatementdebitPsconn.prepareStatement(debitSql);PreparedStatementcreditPsconn.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运用到图书管理系统中。借阅一本书需要两个动作向borrow_records表插入一条借阅记录。将对应图书的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{privatestaticfinalStringURLjdbc:mysql://localhost:3306/library_db?useSSLfalseserverTimezoneUTCcharacterEncodingutf8;privatestaticfinalStringUSERroot;privatestaticfinalStringPASSWORDyour_password;/** * 借阅图书 * * param readerId 读者ID * param bookId 图书ID * param borrowDuration 借阅天数用于计算应还日期 * return 是否借阅成功 */publicbooleanborrowBook(intreaderId,intbookId,intborrowDuration){StringcheckStockSqlSELECT stock FROM books WHERE id ?;StringinsertBorrowSqlINSERT INTO borrow_records (reader_id, book_id, borrow_date, due_date) VALUES (?, ?, CURRENT_DATE, DATE_ADD(CURRENT_DATE, INTERVAL ? DAY));StringupdateStockSqlUPDATE books SET stock stock - 1 WHERE id ? AND stock 0;Connectionconnnull;try{connDriverManager.getConnection(URL,USER,PASSWORD);conn.setAutoCommit(false);// 开启事务// 1. 检查库存intstock;try(PreparedStatementcheckPsconn.prepareStatement(checkStockSql)){checkPs.setInt(1,bookId);try(ResultSetrscheckPs.executeQuery()){if(!rs.next()){thrownewRuntimeException(图书不存在);}stockrs.getInt(stock);}}if(stock0){thrownewRuntimeException(库存不足无法借阅);}// 2. 插入借阅记录try(PreparedStatementinsertPsconn.prepareStatement(insertBorrowSql)){insertPs.setInt(1,readerId);insertPs.setInt(2,bookId);insertPs.setInt(3,borrowDuration);introwsinsertPs.executeUpdate();if(rows!1){thrownewRuntimeException(插入借阅记录失败);}}// 3. 更新库存try(PreparedStatementupdatePsconn.prepareStatement(updateStockSql)){updatePs.setInt(1,bookId);introwsupdatePs.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){LibraryServiceservicenewLibraryService();// 测试读者ID1 借阅图书ID3借阅期14天booleansuccessservice.borrowBook(1,3,14);System.out.println(借阅结果(success?成功:失败));}}关键点解读使用stock 0作为更新条件并检查受影响行数防止并发超借初版依然存在并发问题后续将用悲观锁/乐观锁优化这里先掌握基本事务。库存检查和更新放同一事务中保证原子性。所有资源都用try-with-resources或finally确保释放。异常时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 与数据库通信。深入理解它能帮助你写出更高效、更安全的持久层代码。思考题上面的借阅方法在并发情况下可能出现什么问题提示两个线程同时借走最后一本书PreparedStatement的预编译在 MySQL 8.0 中默认是服务端还是客户端如何配置如果用try-with-resources管理Connection还需要手动rollback吗为什么参考资料MySQL Connector/J Developer GuideJDBC™ 4.3 Specification