JDBC基本操作

JDBC 详解:Java 数据库连接的核心操作

JDBC(Java Database Connectivity)是 Java 访问数据库的标准接口,它定义了一套独立于特定数据库管理系统(DBMS)的 API,使 Java 程序能够通过统一的方式操作各种关系型数据库(如 MySQL、Oracle、SQL Server 等)。本文将详细讲解 JDBC 的核心概念、连接步骤及常用操作,帮助你掌握 Java 与数据库交互的基础。

JDBC 核心组件

JDBC 主要通过以下接口和类实现数据库交互:

组件 作用描述
Driver 数据库驱动接口,由各数据库厂商实现(如 MySQL 的 com.mysql.jdbc.Driver)。
DriverManager 驱动管理类,负责注册驱动、创建数据库连接。
Connection 数据库连接接口,代表与数据库的会话,用于创建执行 SQL 的对象。
Statement 执行静态 SQL 语句的接口,存在 SQL 注入风险。
PreparedStatement 预编译 SQL 语句的接口,可防止 SQL 注入,性能更优。
ResultSet 结果集接口,存储查询返回的数据,提供遍历和获取数据的方法。

JDBC 连接数据库的步骤

使用 JDBC 操作数据库的核心步骤可概括为:加载驱动 → 建立连接 → 执行 SQL → 处理结果 → 释放资源

准备工作

  • 引入数据库驱动:根据数据库类型添加对应的驱动 JAR 包(如 MySQL 驱动 mysql-connector-java)。

  • 创建数据库和表:以 MySQL 为例,创建测试表user:

    CREATE TABLE user (
        id INT PRIMARY KEY AUTO_INCREMENT,
        name VARCHAR(50) NOT NULL
    ) ENGINE = InnoDB DEFAULT CHARSET = utf8;

加载驱动与建立连接

(1)核心参数定义
// 数据库驱动类名(MySQL 8.0+ 需使用 com.mysql.cj.jdbc.Driver)
private static final String DRIVER = "com.mysql.jdbc.Driver";
// 数据库连接 URL(格式:jdbc:数据库类型://主机:端口/数据库名)
private static final String URL = "jdbc:mysql://localhost:3306/test?useSSL=false";
// 数据库用户名
private static final String USER = "root";
// 数据库密码
private static final String PASSWORD = "123456";
(2)加载驱动

通过 Class.forName(DRIVER) 加载驱动类,其原理是触发驱动类中的静态代码块,向 DriverManager 注册驱动实例:

// MySQL 驱动类的静态代码块(自动注册驱动)
static {
    try {
        DriverManager.registerDriver(new Driver());
    } catch (SQLException e) {
        throw new RuntimeException("驱动注册失败");
    }
}
(3)建立连接

通过 DriverManager.getConnection(URL, USER, PASSWORD) 获取 Connection 对象:

/**
 * 获取数据库连接
 */
public static Connection getConnection() {
    Connection conn = null;
    try {
        // 加载驱动(MySQL 5.1+ 可省略此步,DriverManager 会自动扫描并加载驱动)
        Class.forName(DRIVER);
        // 建立连接
        conn = DriverManager.getConnection(URL, USER, PASSWORD);
        System.out.println("数据库连接成功");
    } catch (ClassNotFoundException e) {
        throw new RuntimeException("驱动加载失败:" + e.getMessage());
    } catch (SQLException e) {
        throw new RuntimeException("连接建立失败:" + e.getMessage());
    }
    return conn;
}

注意:MySQL 5.1 之后的驱动可省略 Class.forName(DRIVER),因为 JDBC 4.0 引入了自动加载机制(通过 META-INF/services/java.sql.Driver 文件)。

释放资源

数据库连接、Statement、ResultSet 都是稀缺资源,使用后必须关闭,建议在 finally 块中处理:

/**
 * 关闭资源(Connection、PreparedStatement、ResultSet)
 */
public static void close(Connection conn, PreparedStatement pstmt, ResultSet rs) {
    try {
        if (rs != null) rs.close();    // 先关闭结果集
        if (pstmt != null) pstmt.close(); // 再关闭语句对象
        if (conn != null) conn.close();   // 最后关闭连接
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

执行 SQL 语句:Statement 与 PreparedStatement

Statement:执行静态 SQL(不推荐)

Statement 用于执行静态 SQL 语句(SQL 字符串固定),但存在 SQL 注入风险,且每次执行都需重新编译 SQL,性能较差。

示例:插入数据
/**
 * 使用 Statement 插入数据(存在 SQL 注入风险)
 */
public static void insertWithStatement(String name) {
    Connection conn = null;
    Statement stmt = null;
    try {
        conn = getConnection();
        stmt = conn.createStatement();
        // 拼接 SQL(危险!若 name 包含恶意代码,如 "张三'; DROP TABLE user; --",将导致数据泄露或破坏)
        String sql = "INSERT INTO user (name) VALUES ('" + name + "')";
        int rows = stmt.executeUpdate(sql); // 执行 INSERT/UPDATE/DELETE,返回受影响行数
        System.out.println("插入成功,影响行数:" + rows);
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        close(conn, stmt, null); // ResultSet 为 null
    }
}

PreparedStatement:预编译 SQL(推荐)

PreparedStatementStatement 的子接口,支持 带占位符(?)的 SQL,执行前会预编译 SQL 模板,后续只需填充参数,可:

  • 防止 SQL 注入(参数自动转义);
  • 重复执行时无需重新编译,性能更优。
示例:插入、更新、删除数据
(1)插入数据(含自增主键获取)
/**
 * 使用 PreparedStatement 插入数据,并获取自增主键
 */
public static int insertWithPreparedStatement(String name) {
    Connection conn = null;
    PreparedStatement pstmt = null;
    ResultSet rs = null;
    int generatedId = -1; // 存储自增主键
    try {
        conn = getConnection();
        // SQL 模板(? 为占位符,索引从 1 开始)
        String sql = "INSERT INTO user (name) VALUES (?)";
        // 第二个参数指定返回自增主键
        pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
        // 设置参数(1 表示第一个占位符,String 类型)
        pstmt.setString(1, name);
        // 执行插入,返回受影响行数
        int rows = pstmt.executeUpdate();
        System.out.println("插入成功,影响行数:" + rows);
        
        // 获取自增主键
        rs = pstmt.getGeneratedKeys();
        if (rs.next()) {
            generatedId = rs.getInt(1); // 主键列索引为 1
        }
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        close(conn, pstmt, rs);
    }
    return generatedId;
}
(2)更新数据
/**
 * 使用 PreparedStatement 更新数据
 */
public static void updateWithPreparedStatement(int id, String newName) {
    Connection conn = null;
    PreparedStatement pstmt = null;
    try {
        conn = getConnection();
        String sql = "UPDATE user SET name = ? WHERE id = ?";
        pstmt = conn.prepareStatement(sql);
        pstmt.setString(1, newName); // 第一个占位符:新名称
        pstmt.setInt(2, id);         // 第二个占位符:ID
        int rows = pstmt.executeUpdate();
        System.out.println("更新成功,影响行数:" + rows);
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        close(conn, pstmt, null);
    }
}
(3)删除数据
/**
 * 使用 PreparedStatement 删除数据
 */
public static void deleteWithPreparedStatement(int id) {
    Connection conn = null;
    PreparedStatement pstmt = null;
    try {
        conn = getConnection();
        String sql = "DELETE FROM user WHERE id = ?";
        pstmt = conn.prepareStatement(sql);
        pstmt.setInt(1, id);
        int rows = pstmt.executeUpdate();
        System.out.println("删除成功,影响行数:" + rows);
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        close(conn, pstmt, null);
    }
}

查询数据:处理 ResultSet

executeQuery() 用于执行 SELECT 语句,返回 ResultSet 结果集,需通过 next() 方法遍历数据行,通过列名或索引获取字段值。

示例:查询所有用户
/**
 * 查询所有用户并打印结果
 */
public static void queryAllUsers() {
    Connection conn = null;
    PreparedStatement pstmt = null;
    ResultSet rs = null;
    try {
        conn = getConnection();
        String sql = "SELECT id, name FROM user";
        pstmt = conn.prepareStatement(sql);
        rs = pstmt.executeQuery(); // 执行查询,返回结果集
        
        // 遍历结果集(rs.next() 移动到下一行,初始在第一行之前)
        while (rs.next()) {
            int id = rs.getInt("id"); // 通过列名获取
            String name = rs.getString(2); // 通过列索引获取(索引从 1 开始)
            System.out.println("ID: " + id + ", Name: " + name);
        }
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        close(conn, pstmt, rs); // 务必关闭结果集
    }
}

批量操作:高效处理大量数据

当需要插入、更新多条记录时,使用批量操作可大幅提升性能(减少网络交互次数)。

示例:批量插入数据

/**
 * 批量插入用户数据
 */
public static void batchInsertUsers(List<String> names) {
    Connection conn = null;
    PreparedStatement pstmt = null;
    try {
        conn = getConnection();
        // 关闭自动提交(开启事务)
        conn.setAutoCommit(false);
        String sql = "INSERT INTO user (name) VALUES (?)";
        pstmt = conn.prepareStatement(sql);
        
        // 添加批量任务
        for (String name : names) {
            pstmt.setString(1, name);
            pstmt.addBatch(); // 将 SQL 加入批处理队列
        }
        
        // 执行批量操作(返回每个 SQL 影响的行数数组)
        int[] rows = pstmt.executeBatch();
        System.out.println("批量插入完成,总影响行数:" + Arrays.stream(rows).sum());
        
        // 提交事务
        conn.commit();
    } catch (SQLException e) {
        // 事务回滚
        if (conn != null) {
            try {
                conn.rollback();
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        }
        e.printStackTrace();
    } finally {
        // 恢复自动提交
        if (conn != null) {
            try {
                conn.setAutoCommit(true);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        close(conn, pstmt, null);
    }
}

优化点

  • 关闭自动提交(conn.setAutoCommit(false)),批量执行后手动提交,减少事务开销。
  • 批量大小适中(如每次 1000 条),避免内存溢出。

JDBC 最佳实践

  1. 优先使用 PreparedStatement:防止 SQL 注入,提升重复执行性能。
  2. 及时释放资源:在 finally 块中关闭 ConnectionPreparedStatementResultSet,避免资源泄露。
  3. 使用连接池:生产环境中,通过连接池(如 HikariCP、C3P0)管理连接,避免频繁创建和关闭连接的开销。
  4. 处理事务:对批量操作或多步 SQL,使用事务(commit()/rollback())保证数据一致性。
  5. 避免硬编码:数据库参数(URL、用户名、密码)应配置在外部文件(如 properties)中,便于维护