JDBC 从入门到实战:Java 操作数据库的核心技术详解

JDBC(Java Database Connectivity)是 Java 访问关系型数据库的标准 API,它定义了一套统一的接口,让 Java 程序能够独立于具体的数据库产品(如 MySQL、Oracle、PostgreSQL)进行数据操作。无论是简单的增删改查,还是复杂的事务处理,JDBC 都是 Java 后端开发的必备技能。本文将从“核心概念→环境搭建→基础操作→实战案例→高级特性”五个维度,带大家系统掌握 JDBC,所有示例基于 MySQL 数据库,可直接复制运行。

一、JDBC 核心概念:先搞懂“是什么”和“为什么”

1. 什么是 JDBC?

  • 技术定义:JDBC 是 Sun 公司定义的一套用于访问数据库的接口(java.sqljavax.sql 包下),数据库厂商(如 MySQL、Oracle)提供接口的实现类(即 JDBC 驱动),Java 程序通过调用接口方法,间接操作数据库。
  • 核心作用:屏蔽不同数据库的底层差异,实现“一次编码,多库兼容”(理论上,更换数据库只需替换驱动和连接信息,无需修改业务代码)。
  • 核心组件:JDBC 接口主要包括 5 个核心类/接口,协同完成数据库操作:
    组件作用
    DriverManager管理 JDBC 驱动,负责创建数据库连接
    Connection代表数据库连接,是所有操作的基础
    Statement执行静态 SQL 语句(存在 SQL 注入风险)
    PreparedStatement执行预编译 SQL 语句(推荐,防 SQL 注入)
    ResultSet存储 SQL 查询的结果集

2. JDBC 工作原理

JDBC 采用“接口-实现”的设计模式,工作流程如下:

  1. 加载数据库驱动(JDBC 4.0+ 无需手动加载,自动识别);
  2. 通过 DriverManager 获取数据库连接(Connection);
  3. 通过 Connection 创建 StatementPreparedStatement 对象;
  4. 执行 SQL 语句,查询操作返回 ResultSet 结果集;
  5. 处理结果集(查询)或获取影响行数(增删改);
  6. 关闭资源(ResultSetStatementConnection),避免内存泄漏。

3. JDBC 与 ORM 框架的关系

很多开发者会混淆 JDBC 和 MyBatis、Hibernate 等 ORM 框架,核心关系如下:

  • JDBC:底层 API,操作数据库的“基础工具”,需要手动编写 SQL 和处理结果集,代码繁琐但灵活;
  • ORM 框架:基于 JDBC 封装的高级工具(如 MyBatis、Hibernate),通过注解或 XML 映射数据,简化 SQL 编写和结果集映射,提升开发效率。

类比:JDBC 是“手动挡汽车”,需要手动控制离合、换挡;ORM 框架是“自动挡汽车”,自动处理底层细节,更易操作,但底层仍依赖 JDBC。

二、JDBC 开发环境搭建

要使用 JDBC 操作 MySQL,需准备三个核心组件:JDK、MySQL 数据库、JDBC 驱动(MySQL Connector/J)。

1. 环境准备

  • JDK:推荐 JDK 8(兼容性最好);
  • MySQL:推荐 MySQL 8.0(需提前创建数据库和表,后续示例用);
  • JDBC 驱动:MySQL 官方提供的驱动包(mysql-connector-java),获取方式:
    • 方式 1:Maven 依赖(推荐,自动管理版本):
      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>8.0.33</version> <!-- 与 MySQL 版本匹配,8.0+ 对应此版本 -->
      </dependency>
      
    • 方式 2:手动下载:从 MySQL 官网(https://dev.mysql.com/downloads/connector/j/)下载 JAR 包,添加到项目 lib 目录并添加依赖。

2. 准备测试环境

提前创建测试数据库和表,后续所有示例基于此结构:

-- 1. 创建数据库
CREATE DATABASE IF NOT EXISTS jdbc_demo 
CHARACTER SET utf8mb4 
COLLATE utf8mb4_general_ci;

-- 2. 切换数据库
USE jdbc_demo;

-- 3. 创建学生表
CREATE TABLE IF NOT EXISTS student (
    id INT PRIMARY KEY AUTO_INCREMENT, -- 自增主键
    name VARCHAR(50) NOT NULL, -- 姓名(非空)
    age INT CHECK (age > 0 AND age < 100), -- 年龄范围
    gender ENUM('男', '女', '其他') DEFAULT '男', -- 性别
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP -- 创建时间
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 4. 插入测试数据
INSERT INTO student (name, age, gender)
VALUES ('张三', 20, '男'), ('李四', 19, '女'), ('王五', 21, '男');

三、JDBC 基础操作:CRUD 核心实现

JDBC 操作数据库的核心是“CRUD”(新增 Create、查询 Read、修改 Update、删除 Delete),以下是完整实现示例,包含代码注释和注意事项。

1. 核心常量定义(避免硬编码)

首先定义数据库连接信息常量,方便后续维护:

public class JDBCConstants {
    // 数据库连接 URL(MySQL 8.0+ 需指定 serverTimezone)
    public static final String URL = "jdbc:mysql://localhost:3306/jdbc_demo?serverTimezone=UTC&useSSL=false";
    public static final String USERNAME = "root"; // 数据库用户名(替换为自己的)
    public static final String PASSWORD = "123456"; // 数据库密码(替换为自己的)
}

2. 新增数据(INSERT)

使用 PreparedStatement 执行预编译 SQL,避免 SQL 注入:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class JDBCInsertDemo {
    public static void main(String[] args) {
        // 声明资源(Connection、PreparedStatement 需关闭,用 try-with-resources 自动关闭)
        try (
            // 1. 获取数据库连接
            Connection conn = DriverManager.getConnection(JDBCConstants.URL, JDBCConstants.USERNAME, JDBCConstants.PASSWORD);
            // 2. 创建 PreparedStatement,预编译 SQL(? 是占位符,后续赋值)
            PreparedStatement pstmt = conn.prepareStatement("INSERT INTO student (name, age, gender) VALUES (?, ?, ?)")
        ) {
            // 3. 给占位符赋值(索引从 1 开始,而非 0)
            pstmt.setString(1, "赵六"); // 第一个 ? 赋值为字符串
            pstmt.setInt(2, 18); // 第二个 ? 赋值为整数
            pstmt.setString(3, "女"); // 第三个 ? 赋值为字符串

            // 4. 执行 SQL(INSERT/UPDATE/DELETE 用 executeUpdate(),返回影响行数)
            int affectedRows = pstmt.executeUpdate();
            System.out.println("新增成功,影响行数:" + affectedRows);

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

3. 查询数据(SELECT)

查询操作需处理 ResultSet 结果集,提取查询数据:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class JDBCSelectDemo {
    public static void main(String[] args) {
        try (
            Connection conn = DriverManager.getConnection(JDBCConstants.URL, JDBCConstants.USERNAME, JDBCConstants.PASSWORD);
            // 预编译查询 SQL
            PreparedStatement pstmt = conn.prepareStatement("SELECT id, name, age, gender, create_time FROM student WHERE age > ?");
        ) {
            // 给占位符赋值:查询年龄大于 19 的学生
            pstmt.setInt(1, 19);

            // 执行查询(SELECT 用 executeQuery(),返回 ResultSet 结果集)
            try (ResultSet rs = pstmt.executeQuery()) {
                // 遍历结果集(rs.next() 移动到下一行,返回 true 表示有数据)
                while (rs.next()) {
                    // 通过列名或索引获取数据(推荐列名,更稳定)
                    int id = rs.getInt("id");
                    String name = rs.getString("name");
                    int age = rs.getInt("age");
                    String gender = rs.getString("gender");
                    String createTime = rs.getString("create_time");

                    // 打印结果
                    System.out.printf("ID:%d,姓名:%s,年龄:%d,性别:%s,创建时间:%s%n",
                            id, name, age, gender, createTime);
                }
            }

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

4. 修改数据(UPDATE)

与新增操作类似,使用 executeUpdate() 执行更新 SQL:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class JDBCUpdateDemo {
    public static void main(String[] args) {
        try (
            Connection conn = DriverManager.getConnection(JDBCConstants.URL, JDBCConstants.USERNAME, JDBCConstants.PASSWORD);
            // 预编译更新 SQL:修改姓名为“张三”的年龄
            PreparedStatement pstmt = conn.prepareStatement("UPDATE student SET age = ? WHERE name = ?")
        ) {
            // 赋值占位符(注意:占位符顺序与 SQL 中一致)
            pstmt.setInt(1, 22); // 第一个 ?:新年龄
            pstmt.setString(2, "张三"); // 第二个 ?:查询条件(姓名)

            int affectedRows = pstmt.executeUpdate();
            System.out.println("修改成功,影响行数:" + affectedRows);

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

5. 删除数据(DELETE)

注意:删除操作需谨慎,建议添加明确的 WHERE 条件:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class JDBCDeleteDemo {
    public static void main(String[] args) {
        try (
            Connection conn = DriverManager.getConnection(JDBCConstants.URL, JDBCConstants.USERNAME, JDBCConstants.PASSWORD);
            // 预编译删除 SQL:删除姓名为“赵六”的学生
            PreparedStatement pstmt = conn.prepareStatement("DELETE FROM student WHERE name = ?")
        ) {
            pstmt.setString(1, "赵六");

            int affectedRows = pstmt.executeUpdate();
            System.out.println("删除成功,影响行数:" + affectedRows);

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

6. 核心注意事项

  • 资源关闭ConnectionPreparedStatementResultSet 都是稀缺资源,必须关闭。推荐使用 try-with-resources 语法(JDK 7+ 支持),自动关闭资源,无需手动写 close()
  • SQL 注入风险:严禁使用 Statement 执行动态 SQL(如拼接用户输入的参数),必须使用 PreparedStatement 预编译 SQL,通过占位符赋值;
  • 占位符索引PreparedStatement 的占位符 ? 索引从 1 开始,而非 0,避免索引越界;
  • 连接信息配置:避免硬编码 URL、用户名、密码,建议放在配置文件(如 jdbc.properties)中,后续实战会优化。

四、JDBC 实战优化:工具类封装 + 配置文件

前面的示例存在代码冗余(如重复获取连接、关闭资源),实际开发中会封装 JDBC 工具类,同时将配置信息写入配置文件,提升代码复用性和可维护性。

1. 步骤 1:创建配置文件(jdbc.properties)

src/main/resources 目录下创建配置文件,存储数据库连接信息:

# jdbc.properties
jdbc.url=jdbc:mysql://localhost:3306/jdbc_demo?serverTimezone=UTC&useSSL=false
jdbc.username=root
jdbc.password=123456
jdbc.driver=com.mysql.cj.jdbc.Driver # MySQL 8.0+ 驱动类名(可选,JDBC 4.0+ 自动加载)

2. 步骤 2:封装 JDBC 工具类(JDBCUtils.java)

封装连接获取、资源关闭等通用操作:

import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

public class JDBCUtils {
    // 加载配置文件
    private static final Properties props = new Properties();

    // 静态代码块:加载配置文件(只执行一次)
    static {
        try (
            // 读取配置文件(类路径下的资源)
            InputStream in = JDBCUtils.class.getClassLoader().getResourceAsStream("jdbc.properties")
        ) {
            props.load(in); // 加载配置到 Properties 对象
            // 加载驱动(MySQL 8.0+ 可省略,JDBC 4.0+ 自动识别)
            Class.forName(props.getProperty("jdbc.driver"));
        } catch (IOException | ClassNotFoundException e) {
            throw new RuntimeException("JDBC 配置文件加载失败!", e);
        }
    }

    // 1. 获取数据库连接
    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(
            props.getProperty("jdbc.url"),
            props.getProperty("jdbc.username"),
            props.getProperty("jdbc.password")
        );
    }

    // 2. 关闭资源(查询操作:Connection + Statement + ResultSet)
    public static void close(Connection conn, Statement stmt, ResultSet rs) {
        // 关闭 ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        // 关闭 Statement(PreparedStatement 是其子类)
        if (stmt != null) {
            try {
                stmt.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        // 关闭 Connection
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    // 3. 重载:关闭资源(增删改操作:Connection + Statement,无 ResultSet)
    public static void close(Connection conn, Statement stmt) {
        close(conn, stmt, null);
    }
}

3. 步骤 3:使用工具类简化 CRUD 操作

以查询操作为例,简化后的代码:

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class JDBCUtilsDemo {
    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            // 1. 获取连接(通过工具类)
            conn = JDBCUtils.getConnection();
            // 2. 创建 PreparedStatement
            pstmt = conn.prepareStatement("SELECT * FROM student");
            // 3. 执行查询
            rs = pstmt.executeQuery();
            // 4. 处理结果集
            while (rs.next()) {
                System.out.printf("ID:%d,姓名:%s,年龄:%d%n",
                        rs.getInt("id"), rs.getString("name"), rs.getInt("age"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 5. 关闭资源(通过工具类)
            JDBCUtils.close(conn, pstmt, rs);
        }
    }
}

五、JDBC 高级特性:事务、批量操作与存储过程

1. 事务管理(ACID 特性)

JDBC 事务默认自动提交(执行一条 SQL 自动提交一次),实际开发中需手动控制事务(如转账操作:扣款和到账必须同时成功或失败)。

事务核心 API
  • conn.setAutoCommit(false):关闭自动提交,开启手动事务;
  • conn.commit():提交事务(所有操作成功后执行);
  • conn.rollback():回滚事务(出现异常时执行,撤销所有操作);
  • conn.setSavepoint():设置保存点(回滚到指定节点,而非全部)。
实战示例:模拟转账操作
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

// 假设存在账户表 account:id(主键)、username(用户名)、balance(余额)
public class JDBC事务Demo {
    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement pstmt1 = null; // 扣款操作
        PreparedStatement pstmt2 = null; // 到账操作

        try {
            // 1. 获取连接
            conn = JDBCUtils.getConnection();
            // 2. 关闭自动提交,开启事务
            conn.setAutoCommit(false);

            // 3. 执行扣款(用户 A 减少 100 元)
            String sql1 = "UPDATE account SET balance = balance - 100 WHERE username = 'A'";
            pstmt1 = conn.prepareStatement(sql1);
            pstmt1.executeUpdate();

            // 模拟异常(测试回滚,实际开发中无需写)
            // int i = 1 / 0;

            // 4. 执行到账(用户 B 增加 100 元)
            String sql2 = "UPDATE account SET balance = balance + 100 WHERE username = 'B'";
            pstmt2 = conn.prepareStatement(sql2);
            pstmt2.executeUpdate();

            // 5. 所有操作成功,提交事务
            conn.commit();
            System.out.println("转账成功!");

        } catch (Exception e) {
            // 6. 出现异常,回滚事务
            try {
                if (conn != null) {
                    conn.rollback();
                    System.out.println("转账失败,事务回滚!");
                }
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
            e.printStackTrace();
        } finally {
            // 7. 关闭资源
            JDBCUtils.close(conn, pstmt1);
            JDBCUtils.close(null, pstmt2); // conn 已在上面关闭,此处传 null
        }
    }
}

2. 批量操作(Batch)

当需要执行大量相同结构的 SQL 语句(如批量插入 1000 条数据)时,使用批量操作可大幅提升性能(减少数据库连接次数)。

批量操作核心 API
  • pstmt.addBatch():将 SQL 语句添加到批处理队列;
  • pstmt.executeBatch():执行批处理队列中的所有 SQL;
  • pstmt.clearBatch():清空批处理队列。
实战示例:批量插入 1000 条学生数据
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class JDBCBatchDemo {
    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement pstmt = null;

        try {
            conn = JDBCUtils.getConnection();
            // 关闭自动提交(批量操作推荐关闭,提升性能)
            conn.setAutoCommit(false);

            String sql = "INSERT INTO student (name, age, gender) VALUES (?, ?, ?)";
            pstmt = conn.prepareStatement(sql);

            // 批量添加 1000 条数据
            for (int i = 1; i <= 1000; i++) {
                pstmt.setString(1, "学生" + i);
                pstmt.setInt(2, 18 + (i % 5)); // 年龄 18-22 随机
                pstmt.setString(3, i % 2 == 0 ? "女" : "男"); // 性别交替

                pstmt.addBatch(); // 添加到批处理队列

                // 每 200 条执行一次(避免队列过大)
                if (i % 200 == 0) {
                    pstmt.executeBatch(); // 执行批处理
                    pstmt.clearBatch(); // 清空队列
                }
            }

            // 执行剩余的 SQL
            pstmt.executeBatch();
            conn.commit(); // 提交事务
            System.out.println("批量插入 1000 条数据成功!");

        } catch (SQLException e) {
            try {
                if (conn != null) {
                    conn.rollback();
                }
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
            e.printStackTrace();
        } finally {
            JDBCUtils.close(conn, pstmt);
        }
    }
}

3. 调用存储过程

存储过程是预编译存储在数据库中的 SQL 语句集合,JDBC 可通过 CallableStatement 调用存储过程。

步骤 1:创建 MySQL 存储过程
-- 创建存储过程:根据姓名查询学生年龄
DELIMITER // -- 修改语句结束符为 //(避免与存储过程中的 ; 冲突)
CREATE PROCEDURE getStudentAge(IN studentName VARCHAR(50), OUT studentAge INT)
BEGIN
    SELECT age INTO studentAge FROM student WHERE name = studentName;
END //
DELIMITER ; -- 恢复语句结束符为 ;
步骤 2:JDBC 调用存储过程
import java.sql.Connection;
import java.sql.CallableStatement;
import java.sql.SQLException;
import java.sql.Types;

public class JDBCStoredProcedureDemo {
    public static void main(String[] args) {
        Connection conn = null;
        CallableStatement cstmt = null;

        try {
            conn = JDBCUtils.getConnection();
            // 调用存储过程的 SQL 格式:{call 存储过程名(?, ?)}
            String sql = "{call getStudentAge(?, ?)}";
            cstmt = conn.prepareCall(sql);

            // 1. 设置输入参数(IN 类型:学生姓名)
            cstmt.setString(1, "张三");

            // 2. 注册输出参数(OUT 类型:学生年龄,指定类型为 INT)
            cstmt.registerOutParameter(2, Types.INTEGER);

            // 3. 执行存储过程
            cstmt.execute();

            // 4. 获取输出参数结果
            int age = cstmt.getInt(2);
            System.out.println("张三的年龄:" + (age == 0 ? "未找到该学生" : age));

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(conn, cstmt);
        }
    }
}

六、JDBC 常见问题与避坑指南

1. 连接数据库失败(Communications link failure)

  • 常见原因
    1. MySQL 服务未启动;
    2. URL 错误(如端口号错误、数据库名拼写错误);
    3. MySQL 8.0+ 未指定 serverTimezone(需添加 serverTimezone=UTC);
    4. 防火墙拦截 3306 端口。
  • 解决方案
    • 启动 MySQL 服务(net start mysql 命令);
    • 检查 URL 格式:jdbc:mysql://localhost:3306/数据库名?serverTimezone=UTC&useSSL=false
    • 关闭防火墙或开放 3306 端口。

2. 中文乱码问题

  • 原因:数据库/表/字段字符集不是 utf8mb4,或 JDBC 连接未指定编码;
  • 解决方案
    • 创建数据库时指定字符集(CHARACTER SET utf8mb4);
    • URL 中添加编码参数:useUnicode=true&characterEncoding=utf8
    • 确保 Java 程序编码为 UTF-8(IDE 中设置)。

3. SQL 注入风险

  • 问题场景:使用 Statement 拼接 SQL 语句(如 String sql = "SELECT * FROM user WHERE username = '" + username + "'"),若用户输入 ' OR '1'='1,会导致查询所有用户数据;
  • 解决方案
    • 严禁使用 Statement,必须使用 PreparedStatement 预编译 SQL;
    • 对用户输入的参数进行过滤(如过滤特殊字符 ';)。

4. 资源泄漏(Connection 未关闭)

  • 问题:未关闭 Connection 会导致数据库连接池耗尽,后续无法获取连接;
  • 解决方案
    • 强制使用 try-with-resources 语法,自动关闭资源;
    • 若使用传统 try-catch-finally,必须在 finally 中关闭资源(先关 ResultSet,再关 Statement,最后关 Connection)。

5. 批量操作性能不佳

  • 原因:未关闭自动提交(默认每次 addBatch() 都提交一次);
  • 解决方案
    • 批量操作前执行 conn.setAutoCommit(false)
    • 分批执行 executeBatch()(如每 200 条执行一次),避免队列过大。

七、总结:JDBC 学习路径与发展

1. 学习路径

  • 基础阶段:掌握 JDBC 核心组件(ConnectionPreparedStatementResultSet),实现 CRUD 操作;
  • 进阶阶段:封装 JDBC 工具类、配置文件,理解事务、批量操作、存储过程;
  • 实战阶段:结合 Servlet/JSP 实现 Web 应用(如用户登录、商品管理);
  • 框架阶段:学习 MyBatis、Spring JDBC 等框架,理解框架如何简化 JDBC 开发。

2. JDBC 的核心价值

  • JDBC 是 Java 操作数据库的基础,所有 ORM 框架(MyBatis、Hibernate)、数据访问框架(Spring JDBC)都基于 JDBC 实现;
  • 掌握 JDBC 原理,能更好地理解框架的底层逻辑,排查框架使用中的问题;
  • 小型项目或性能要求高的场景,JDBC 仍是最优选择(无框架 overhead)。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐