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 中加入:

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 获取数据库连接

java 复制代码
String url = "jdbc:mysql://localhost:3306/library_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8";
String user = "root";
String password = "your_password";

Connection conn = DriverManager.getConnection(url, user, password);

连接 URL 参数说明

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

3.2 一个完整的查询示例

java 复制代码
String sql = "SELECT id, name, email FROM readers";

// 推荐使用 try-with-resources 自动释放资源
try (Connection conn = DriverManager.getConnection(url, user, password);
     Statement stmt = conn.createStatement();
     ResultSet rs = stmt.executeQuery(sql)) {

    while (rs.next()) {
        int id = rs.getInt("id");
        String name = rs.getString("name");
        String email = rs.getString("email");
        System.out.printf("%d: %s (%s)%n", id, name, email);
    }

} catch (SQLException e) {
    e.printStackTrace();
}

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


4. Statement 与 SQL 注入风险

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

4.1 注入演示

假设登录逻辑这样写:

java 复制代码
String username = request.getParameter("username");
String password = request.getParameter("password");

String sql = "SELECT * FROM users WHERE username='" + username + "' AND password='" + password + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);

如果用户输入:

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

拼接后的 SQL 变为:

sql 复制代码
SELECT * FROM users WHERE username='' OR '1'='1' AND password='' OR '1'='1'

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

4.2 防范之道:PreparedStatement

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

改用 PreparedStatement 的登录查询:

java 复制代码
String sql = "SELECT * FROM users WHERE username=? AND password=?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setString(1, username);
    ps.setString(2, password);
    try (ResultSet rs = ps.executeQuery()) {
        if (rs.next()) {
            // 登录成功
        }
    }
}

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

额外优点

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

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

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

5.1 插入数据

java 复制代码
String insertSql = "INSERT INTO readers (name, email, phone) VALUES (?, ?, ?)";
try (Connection conn = DriverManager.getConnection(url, user, password);
     PreparedStatement ps = conn.prepareStatement(insertSql)) {

    ps.setString(1, "新读者");
    ps.setString(2, "new@example.com");
    ps.setString(3, "13800000000");

    int rows = ps.executeUpdate();
    System.out.println("插入了 " + rows + " 行");
}

5.2 更新与删除

java 复制代码
// 更新
String updateSql = "UPDATE readers SET email=? WHERE id=?";
try (PreparedStatement ps = conn.prepareStatement(updateSql)) {
    ps.setString(1, "updated@example.com");
    ps.setInt(2, 1);
    int rows = ps.executeUpdate();
    System.out.println("更新了 " + rows + " 行");
}

// 删除
String deleteSql = "DELETE FROM readers WHERE id=?";
try (PreparedStatement ps = conn.prepareStatement(deleteSql)) {
    ps.setInt(1, 10);
    int rows = ps.executeUpdate();
    System.out.println("删除了 " + rows + " 行");
}

6. 查询与处理 ResultSet

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

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

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

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

java 复制代码
String querySql = "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 (Connection conn = DriverManager.getConnection(url, user, password);
     PreparedStatement ps = conn.prepareStatement(querySql)) {

    ps.setInt(1, 1); // 读者ID=1

    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            String title = rs.getString("title");
            Date borrowDate = rs.getDate("borrow_date");
            Date dueDate = rs.getDate("due_date");
            Date returnDate = 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 块中)

示例:银行转账(模拟)

java 复制代码
Connection conn = null;
try {
    conn = DriverManager.getConnection(url, user, password);
    conn.setAutoCommit(false);   // 开启事务

    String debitSql = "UPDATE accounts SET balance = balance - ? WHERE id = ?";
    String creditSql = "UPDATE accounts SET balance = balance + ? WHERE id = ?";

    try (PreparedStatement debitPs = conn.prepareStatement(debitSql);
         PreparedStatement creditPs = 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 (SQLException e) {
        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 代码实现

java 复制代码
import java.sql.*;
import java.time.LocalDate;

public class LibraryService {

    private static final String URL = "jdbc:mysql://localhost:3306/library_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8";
    private static final String USER = "root";
    private static final String PASSWORD = "your_password";

    /**
     * 借阅图书
     *
     * @param readerId 读者ID
     * @param bookId   图书ID
     * @param borrowDuration 借阅天数(用于计算应还日期)
     * @return 是否借阅成功
     */
    public boolean borrowBook(int readerId, int bookId, int borrowDuration) {
        String checkStockSql = "SELECT stock FROM books WHERE id = ?";
        String insertBorrowSql = "INSERT INTO borrow_records (reader_id, book_id, borrow_date, due_date) " +
                                 "VALUES (?, ?, CURRENT_DATE, DATE_ADD(CURRENT_DATE, INTERVAL ? DAY))";
        String updateStockSql = "UPDATE books SET stock = stock - 1 WHERE id = ? AND stock > 0";

        Connection conn = null;
        try {
            conn = DriverManager.getConnection(URL, USER, PASSWORD);
            conn.setAutoCommit(false);   // 开启事务

            // 1. 检查库存
            int stock;
            try (PreparedStatement checkPs = conn.prepareStatement(checkStockSql)) {
                checkPs.setInt(1, bookId);
                try (ResultSet rs = checkPs.executeQuery()) {
                    if (!rs.next()) {
                        throw new RuntimeException("图书不存在");
                    }
                    stock = rs.getInt("stock");
                }
            }
            if (stock <= 0) {
                throw new RuntimeException("库存不足,无法借阅");
            }

            // 2. 插入借阅记录
            try (PreparedStatement insertPs = conn.prepareStatement(insertBorrowSql)) {
                insertPs.setInt(1, readerId);
                insertPs.setInt(2, bookId);
                insertPs.setInt(3, borrowDuration);
                int rows = insertPs.executeUpdate();
                if (rows != 1) {
                    throw new RuntimeException("插入借阅记录失败");
                }
            }

            // 3. 更新库存
            try (PreparedStatement updatePs = conn.prepareStatement(updateStockSql)) {
                updatePs.setInt(1, bookId);
                int rows = updatePs.executeUpdate();
                if (rows != 1) {
                    throw new RuntimeException("更新库存失败,可能库存已为0");
                }
            }

            conn.commit();   // 全部成功,提交事务
            System.out.println("借阅成功!读者" + readerId + " 借了图书" + bookId);
            return true;

        } catch (Exception e) {
            if (conn != null) {
                try {
                    conn.rollback();  // 出现异常,回滚
                    System.out.println("借阅失败,事务已回滚:" + e.getMessage());
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
            return false;
        } finally {
            if (conn != null) {
                try {
                    conn.setAutoCommit(true); // 恢复自动提交
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        LibraryService service = new LibraryService();
        // 测试:读者ID=1 借阅图书ID=3,借阅期14天
        boolean success = 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 吗?为什么?

参考资料


相关推荐
孫治AllenSun几秒前
【JVM】四大引用类型分析
java·开发语言·jvm
看-是灰机1 分钟前
使用go语言实现对接
linux·开发语言·后端·docker·语言模型·golang·飞书
于慨2 分钟前
安装java
java
夕除3 分钟前
sign 是什么
java·前端
秋风渡.13 分钟前
MySQL数据库操作
数据库·mysql
Penina101828 分钟前
代码如何导入idea
java·intellij idea
.Hypocritical.35 分钟前
Maven笔记——2
java·笔记·maven
乐橙开放平台41 分钟前
养殖 SaaS 笔记:乐橙 IoT 物模型管环境,视频 OpenAPI 管回看
数据库·笔记·物联网·mysql·音视频
huohuopro44 分钟前
手写 Tomcat 步骤教程
java·网络
snow@li1 小时前
Java:Lombok 完整讲解
java