杰瑞科技汇

Java JDBC如何连接数据库?

Java JDBC 完整教程

目录

  1. 什么是 JDBC?
  2. JDBC 核心组件
  3. 环境准备
  4. JDBC 编程步骤(核心流程)
  5. 完整代码示例
  6. 处理 ResultSet
  7. 使用 PreparedStatement 防止 SQL 注入
  8. 事务管理
  9. 数据库连接池
  10. JDBC 4.0+ 自动加载驱动
  11. 最佳实践与总结
  12. 进阶学习

什么是 JDBC?

JDBC (Java Database Connectivity) 是 Java 语言中用来规范客户端程序如何访问数据库的应用程序接口,它提供了查询和更新数据库中数据的方法。

Java JDBC如何连接数据库?-图1
(图片来源网络,侵删)

JDBC Java 官方提供的一套规范(一套接口和类),用于让 Java 程序能够连接并操作各种不同的数据库(如 MySQL, Oracle, SQL Server, PostgreSQL 等)。

核心思想: “一次编写,到处运行”,Java 程序员只需要学习 JDBC 这一套统一的 API,就可以通过不同的 数据库驱动(Driver) 来操作不同的数据库,数据库驱动是由各个数据库厂商提供的,实现了 JDBC 接口的类库。


JDBC 核心组件

在开始编码前,需要了解几个核心的 JDBC 组件:

组件 接口/类 描述
Driver java.sql.Driver 数据库驱动接口,每个数据库厂商(如 MySQL)都会提供一个实现此接口的驱动类。
DriverManager java.sql.DriverManager 管理数据库驱动程序,用于建立和管理数据库连接。
Connection java.sql.Connection 代表与数据库的连接会话,通过它来创建 Statement 对象。
Statement java.sql.Statement 用于执行静态 SQL 语句并返回它所生成结果的对象。
PreparedStatement java.sql.PreparedStatement Statement 的子接口,用于执行预编译的 SQL 语句,能有效防止 SQL 注入,是推荐使用的 Statement。
CallableStatement java.sql.CallableStatement 用来调用数据库存储过程。
ResultSet java.sql.ResultSet 代表 SQL 查询的结果集,它是一个指向数据行的光标。
SQLException java.sql.SQLException 处理与数据库访问相关的错误或警告。

环境准备

在开始之前,你需要:

Java JDBC如何连接数据库?-图2
(图片来源网络,侵删)
  1. JDK 安装:确保你的系统已安装 Java 开发工具包。

  2. 数据库:安装一个数据库,MySQL。

  3. JDBC 驱动:下载对应数据库的 JDBC 驱动 JAR 包。

    如何添加驱动?

    Java JDBC如何连接数据库?-图3
    (图片来源网络,侵删)
    • IDE (如 IntelliJ IDEA / Eclipse): 将下载的 JAR 文件添加到项目的库(Library)或依赖(Dependency)中。
    • Maven 项目: 在 pom.xml 文件中添加依赖。
      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>8.0.28</version> <!-- 使用你下载的版本号 -->
      </dependency>

JDBC 编程步骤(核心流程)

使用 JDBC 操作数据库的标准流程如下:

  1. 加载并注册驱动:告诉 JVM 使用哪个数据库的驱动。
  2. 获取数据库连接:使用 DriverManager 获取一个 Connection 对象。
  3. 创建 Statement 对象:通过 Connection 对象创建 StatementPreparedStatement 对象,用于执行 SQL。
  4. 执行 SQL 语句
    • 对于查询(SELECT),使用 executeQuery() 方法,返回一个 ResultSet
    • 对于更新(INSERT, UPDATE, DELETE),使用 executeUpdate() 方法,返回受影响的行数。
  5. 处理结果集:如果是查询操作,遍历 ResultSet 获取数据。
  6. 关闭资源非常重要! 按照创建的相反顺序关闭 ResultSet, Statement, Connection,以释放数据库资源。

完整代码示例

假设我们有一个 users 表,结构如下:

CREATE DATABASE mydb;
USE mydb;
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL UNIQUE,
    age INT
);
INSERT INTO users (name, email, age) VALUES
('Alice', 'alice@example.com', 28),
('Bob', 'bob@example.com', 32),
('Charlie', 'charlie@example.com', 24);

示例 1:查询数据

import java.sql.*;
public class JdbcSelectExample {
    // 数据库连接信息
    private static final String URL = "jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC";
    private static final String USER = "root"; // 你的数据库用户名
    private static final String PASSWORD = "password"; // 你的数据库密码
    public static void main(String[] args) {
        // 1. 定义资源变量
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        try {
            // 2. 加载并注册驱动 (JDBC 4.0后可省略,但显式写出更清晰)
            Class.forName("com.mysql.cj.jdbc.Driver");
            // 3. 获取数据库连接
            conn = DriverManager.getConnection(URL, USER, PASSWORD);
            System.out.println("数据库连接成功!");
            // 4. 创建 Statement 对象
            stmt = conn.createStatement();
            // 5. 执行 SQL 查询
            String sql = "SELECT id, name, email, age FROM users";
            rs = stmt.executeQuery(sql);
            // 6. 处理结果集
            System.out.println("ID\tName\tEmail\t\tAge");
            System.out.println("----------------------------------------");
            while (rs.next()) {
                // 通过列名或列索引获取数据
                int id = rs.getInt("id");
                String name = rs.getString("name");
                String email = rs.getString("email");
                int age = rs.getInt("age");
                System.out.printf("%d\t%s\t%s\t%d\n", id, name, email, age);
            }
        } catch (ClassNotFoundException e) {
            System.err.println("找不到 MySQL 驱动类!");
            e.printStackTrace();
        } catch (SQLException e) {
            System.err.println("数据库操作出错!");
            e.printStackTrace();
        } finally {
            // 7. 关闭资源 (非常重要!)
            try {
                if (rs != null) rs.close();
                if (stmt != null) stmt.close();
                if (conn != null) conn.close();
                System.out.println("资源已关闭。");
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

示例 2:插入数据

import java.sql.*;
public class JdbcInsertExample {
    private static final String URL = "jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC";
    private static final String USER = "root";
    private static final String PASSWORD = "password";
    public static void main(String[] args) {
        String sql = "INSERT INTO users (name, email, age) VALUES (?, ?, ?)"; // 使用问号作为占位符
        Connection conn = null;
        PreparedStatement pstmt = null;
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
            conn = DriverManager.getConnection(URL, USER, PASSWORD);
            // 创建 PreparedStatement 对象
            pstmt = conn.prepareStatement(sql);
            // 设置参数 (索引从 1 开始)
            pstmt.setString(1, "David");
            pstmt.setString(2, "david@example.com");
            pstmt.setInt(3, 35);
            // 执行更新
            int affectedRows = pstmt.executeUpdate();
            System.out.println("成功插入 " + affectedRows + " 行数据。");
        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                if (pstmt != null) pstmt.close();
                if (conn != null) conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

处理 ResultSet

ResultSet 是一个指向数据行的光标,默认情况下它位于第一行之前。

方法 描述
next() 将光标移动到下一行,如果存在下一行,则返回 true,否则返回 false,通常用于 while 循环。
previous() 将光标移动到上一行。
absolute(int row) 将光标移动到指定的行号。
beforeFirst() / afterLast() 将光标移动到第一行之前 / 最后一行之后。
isFirst() / isLast() 判断光标是否在第一行 / 最后一行。
getXXX(int columnIndex) 通过列的索引(从 1 开始)获取值。XXX 是数据类型,如 getInt(), getString(), getDate()
getXXX(String columnName) 通过列的名称获取值。推荐使用此方法,因为代码更具可读性,且在 SQL 查询顺序改变时不易出错。

使用 PreparedStatement 防止 SQL 注入

Statement 直接拼接 SQL 字符串,非常容易受到 SQL 注入 攻击。

什么是 SQL 注入? 用户输入恶意 SQL 代码,改变了原有的 SQL 逻辑,从而执行非预期的操作。

PreparedStatement 的优势:

  1. 防止 SQL 注入:它会对输入参数进行转义处理,确保用户输入的数据不会被当作 SQL 代码执行。
  2. 性能更高:对于需要多次执行的 SQL 语句(只是参数不同),数据库可以预编译 SQL 语句,提高执行效率。

对比示例:

// 危险的 Statement 用法
String userInput = "admin' OR '1'='1";
String sql = "SELECT * FROM users WHERE name = '" + userInput + "'"; // SQL 拼接
// 最终执行的 SQL 可能是: SELECT * FROM users WHERE name = 'admin' OR '1'='1'
// 这会返回所有用户,导致严重安全问题!
// 安全的 PreparedStatement 用法
String sql = "SELECT * FROM users WHERE name = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, userInput); // 参数会被安全地转义
// 无论 userInput 是什么,它都只会被当作一个字符串值来处理。

只要有参数,就 必须使用 PreparedStatement


事务管理

事务是一组操作的集合,它们要么全部成功,要么全部失败,事务具有 ACID 特性(原子性、一致性、隔离性、持久性)。

在 JDBC 中,默认情况下,每个 SQL 语句都是一个独立的事务。

如何手动管理事务?

  1. 开启事务conn.setAutoCommit(false);
  2. 提交事务conn.commit(); (如果所有操作都成功)
  3. 回滚事务conn.rollback(); (如果某个操作失败)

示例代码:

Connection conn = null;
try {
    conn = DriverManager.getConnection(URL, USER, PASSWORD);
    // 开启事务
    conn.setAutoCommit(false);
    // 操作1: 转账
    pstmt1 = conn.prepareStatement("UPDATE accounts SET balance = balance - 100 WHERE user_id = 1");
    pstmt1.executeUpdate();
    // 模拟一个错误
    // int i = 1 / 0; // 如果取消这行注释,事务将回滚
    // 操作2: 收款
    pstmt2 = conn.prepareStatement("UPDATE accounts SET balance = balance + 100 WHERE user_id = 2");
    pstmt2.executeUpdate();
    // 提交事务
    conn.commit();
    System.out.println("事务提交成功!");
} catch (Exception e) {
    // 发生异常,回滚事务
    try {
        if (conn != null) {
            conn.rollback();
            System.out.println("事务已回滚!");
        }
    } catch (SQLException ex) {
        ex.printStackTrace();
    }
    e.printStackTrace();
} finally {
    // ... 关闭资源
    // 记得恢复自动提交模式
    if (conn != null) {
        try {
            conn.setAutoCommit(true);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

数据库连接池

直接使用 DriverManager 获取连接的方式效率低下,因为创建和销毁连接是一个耗时耗资源的操作。

连接池 是一个管理数据库连接的缓冲池,它预先创建一定数量的连接,当应用程序需要连接时,从池中获取一个,用完后归还给池,而不是销毁,这极大地提高了性能。

常用的连接池:

  • HikariCP: 目前性能最好的连接池,是 Spring Boot 2.x 的默认连接池。
  • Druid: 阿里巴巴开源的连接池,功能强大,带有监控界面。
  • C3P0: 一个老牌的连接池实现。

使用 HikariCP 的示例:

  1. 添加 Maven 依赖:

    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
        <version>5.0.1</version>
    </dependency>
  2. 代码示例:

    import com.zaxxer.hikari.HikariConfig;
    import com.zaxxer.hikari.HikariDataSource;
    import java.sql.Connection;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    public class JdbcPoolExample {
        private static HikariDataSource dataSource;
        static {
            HikariConfig config = new HikariConfig();
            config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC");
            config.setUsername("root");
            config.setPassword("password");
            config.setDriverClassName("com.mysql.cj.jdbc.Driver");
            // 连接池配置
            config.setMaximumPoolSize(10); // 最大连接数
            config.setMinimumIdle(5);       // 最小空闲连接数
            config.setConnectionTimeout(30000); // 连接超时时间 (毫秒)
            dataSource = new HikariDataSource(config);
        }
        public static void main(String[] args) {
            Connection conn = null;
            PreparedStatement pstmt = null;
            ResultSet rs = null;
            try {
                // 从连接池获取连接
                conn = dataSource.getConnection();
                System.out.println("从连接池获取连接成功!");
                String sql = "SELECT name FROM users WHERE id = ?";
                pstmt = conn.prepareStatement(sql);
                pstmt.setInt(1, 1);
                rs = pstmt.executeQuery();
                if (rs.next()) {
                    System.out.println("用户名: " + rs.getString("name"));
                }
            } catch (SQLException e) {
                e.printStackTrace();
            } finally {
                // 关闭连接,实际上是将连接归还给连接池
                try {
                    if (rs != null) rs.close();
                    if (pstmt != null) pstmt.close();
                    if (conn != null) conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

JDBC 4.0+ 自动加载驱动

从 JDBC 4.0 (Java 6) 开始,不再需要手动调用 Class.forName("com.mysql.cj.jdbc.Driver")

只要你的 JDBC 驱动 JAR 文件在项目的类路径(Classpath)中,JVM 启动时会自动加载驱动,在现代 Java 项目中,这行代码通常是 可以省略 的。

显式写出这行代码没有坏处,它能让代码的意图更清晰,并且兼容非常老的 Java 环境。


最佳实践与总结

  1. 永远使用 PreparedStatement:除非你 100% 确定你的 SQL 语句是固定的、没有用户输入拼接的,否则请始终使用 PreparedStatement 来防止 SQL 注入。

  2. 总是关闭资源:使用 try-with-resources 语句可以简化资源关闭的代码,避免忘记关闭导致的资源泄漏。

    // try-with-resources 示例 (JDBC 4.0+)
    try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
         PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM users");
         ResultSet rs = pstmt.executeQuery()) {
        // 在这里处理结果...
        // conn, pstmt, rs 会在 try 块结束时自动关闭
    } catch (SQLException e) {
        e.printStackTrace();
    }
  3. 使用连接池:在生产环境中,绝对不要 直接使用 DriverManager 获取连接,务必使用 HikariCP 等高性能连接池。

  4. 善用事务:对于多个关联的数据库操作,一定要使用事务来保证数据的一致性。

  5. 配置外部化:不要将数据库 URL、用户名、密码等硬编码在代码中,应该放在配置文件(如 application.properties)中。

  6. 考虑使用 ORM 框架:对于复杂的业务逻辑,直接写 SQL 会变得非常繁琐,可以考虑使用 MyBatis 或 JPA (Hibernate) 等 ORM 框架,它们能将对象和数据库表进行映射,让你用面向对象的方式操作数据库。


进阶学习

当你掌握了 JDBC 的基础后,可以继续学习以下内容:

  • 批处理 (addBatch(), executeBatch()):用于一次性执行多条相似的 SQL 语句,提高批量操作的效率。
  • 可滚动和可更新的 ResultSet:通过创建 StatementConnection 时设置参数,可以创建一个可以前后滚动、甚至可以直接修改数据的 ResultSet
  • 数据库元数据 (DatabaseMetaData):获取数据库本身的信息,如数据库版本、支持的 SQL 语法、表名、列名等。
  • 参数化元数据 (ParameterMetaData):获取 PreparedStatement 中参数的元数据。
  • RowSetResultSet 的一个扩展,它是一个 JavaBean,可以更方便地在不同层之间传递数据,并且支持离线操作。

希望这份详细的教程能帮助你全面掌握 Java JDBC!

分享:
扫描分享到社交APP
上一篇
下一篇