楼层: 首页/ 软件技术/ JavaEE / Jakarta EE/ JDBC:Java 连接数据库的官方通道
04

JDBC:Java 连接数据库的官方通道

JDBC · PreparedStatement · Transactions · Batch

网页上的数据哪来的?存在 MySQL 这类数据库里。JDBC(Java Database Connectivity)是 Java 定的一套"连数据库"的标准接口——不管 MySQL、PostgreSQL、Oracle,只要装了对应驱动,Java 代码写法几乎一样。下面五步走是 JDBC 的固定流程,背下来。

JDBC 第一步:加 MySQL 驱动依赖(Maven)

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.0.33</version>   <!-- 以 Maven 中央仓库最新稳定版为准 -->
</dependency>

装完驱动,三件套就齐了:JDK 写代码、Tomcat 跑代码、MySQL 存数据。下面正式开始读代码。

JDBC 五步走:注册驱动 → 连接 → 语句 → 结果集 → 关闭

import java.sql.*;

public class JdbcDemo {
    public static void main(String[] args) {
        // try-with-resources 自动关 Connection / PreparedStatement / ResultSet
        String url = "jdbc:mysql://localhost:3306/testdb?useSSL=false&characterEncoding=utf8";
        String sql = "SELECT id, name, score FROM students WHERE name = ?";

        // 用 PreparedStatement:? 是占位符,参数用 setXxx 绑定,防 SQL 注入
        try (Connection conn = DriverManager.getConnection(url, "root", "你的密码");
             PreparedStatement ps = conn.prepareStatement(sql)) {

            ps.setString(1, "小明");          // 第一个 ? 填"小明"

            try (ResultSet rs = ps.executeQuery()) {
                while (rs.next()) {            // 一行行往下走
                    int id = rs.getInt("id");
                    String name = rs.getString("name");
                    double score = rs.getDouble("score");
                    System.out.println(id + " " + name + " " + score);
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

论为什么必须用 PreparedStatement

Statement 是把 SQL 字符串拼好直接执行。用户在登录框输入 ' OR '1'='1,拼出来的 SQL 就变成 SELECT * FROM users WHERE name='' OR '1'='1'——恒真,直接登录成功,这就是著名的 SQL 注入。

PreparedStatement 先把 SQL 模板发给数据库预编译,参数用 ? 占位、单独传,数据库自动转义——你输入的引号永远只是"一个字符",不可能改变 SQL 结构。写 JDBC,永远用 PreparedStatement,永远不拼字符串 SQL。

事务:要么全成,要么全不成

转账:A 扣 100,B 加 100。如果 A 扣完、B 还没加,数据库崩了——钱凭空消失。事务(Transaction)就是把这两步打包,要么都成功,失败就一起回滚。

JDBC 事务模板

Connection conn = /* 拿到连接 */;
try {
    conn.setAutoCommit(false);    // 关掉自动提交,手动控制事务
    // ... 执行 A 扣款、B 加款两条 SQL ...
    conn.commit();              // 都成功,一起提交
} catch (SQLException e) {
    conn.rollback();             // 任何一步出错,整体回滚,假装啥都没发生
    throw e;
} finally {
    conn.setAutoCommit(true);
}

三种语句对象,选哪个

语句对象用途
Statement静态 SQL,无参。已被 PreparedStatement 基本淘汰,别用。
PreparedStatement预编译 + ? 占位符,防注入、可复用,日常首选。
CallableStatement调用数据库存储过程,项目里不常见,知道有它即可。

批量插入:别一条一条插,1000 条就慢给你看

批量导入 1 万条数据,循环 1 万次 executeUpdate,每次都和数据库往返一次,能跑到天亮。批处理(addBatch/executeBatch)把一批 SQL 打包一次性发过去,速度差几十倍。

JDBC 批处理模板

try (Connection conn = ds.getConnection();
     PreparedStatement ps = conn.prepareStatement(
             "INSERT INTO students(id, name) VALUES(?, ?)")) {

    conn.setAutoCommit(false);          // 批处理配合事务,更快更安全
    for (int i = 0; i < 10000; i++) {
        ps.setInt(1, i);
        ps.setString(2, "学生" + i);
        ps.addBatch();                 // 攒进批次,不立刻发
        if (i % 1000 == 0) {
            ps.executeBatch();          // 每攒 1000 条发一次
        }
    }
    ps.executeBatch();                  // 把剩下的尾巴发完
    conn.commit();
} catch (SQLException e) {
    // 出错 conn.rollback(),见上一节
}
方法用在哪
executeQuery()SELECT 查询,返回 ResultSet。
executeUpdate()INSERT / UPDATE / DELETE,返回影响行数。
executeBatch()批量执行一组更新,返回每条影响行数的数组。

连接、语句、结果集:谁先关

JDBC 三个资源是"套娃":Connection 开 PreparedStatement,再开 ResultSet。正确关闭顺序是从里到外:先关 ResultSet,再关 Statement,最后关 Connection。但你根本不用记——try-with-resources 按声明的反序自动关,顺序永远正确。手写 finally 的年代已经过去了,别给自己加戏。

完整 DAO 层案例:建表 SQL + HikariCP + PreparedStatement + 事务 + CRUD

-- 第一步:MySQL 里建表
CREATE TABLE students (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(50) NOT NULL,
  score INT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

// 第二步:StudentDao.java —— 完整 DAO
import java.sql.*;
import java.util.*;
import com.zaxxer.hikari.HikariDataSource;

public class StudentDao {
    private final HikariDataSource ds;

    public StudentDao(HikariDataSource ds) { this.ds = ds; }

    // 查全部
    public List<Map<String, Object>> findAll() throws SQLException {
        String sql = "SELECT id, name, score FROM students ORDER BY id";
        try (Connection conn = ds.getConnection();
             PreparedStatement ps = conn.prepareStatement(sql);
             ResultSet rs = ps.executeQuery()) {
            List<Map<String, Object>> list = new ArrayList<>();
            while (rs.next()) {
                Map<String, Object> row = new HashMap<>();
                row.put("id", rs.getInt("id"));
                row.put("name", rs.getString("name"));
                row.put("score", rs.getInt("score"));
                list.add(row);
            }
            return list;
        }
    }

    // 插入(带返回自增主键)
    public int insert(String name, int score) throws SQLException {
        String sql = "INSERT INTO students(name, score) VALUES(?, ?)";
        try (Connection conn = ds.getConnection();
             PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
            ps.setString(1, name);
            ps.setInt(2, score);
            ps.executeUpdate();
            // 拿回自增 id
            try (ResultSet keys = ps.getGeneratedKeys()) {
                return keys.next() ? keys.getInt(1) : -1;
            }
        }
    }

    // 事务:同时改两个学生的分数,要么都成要么都回滚
    public void transferScore(int fromId, int toId, int delta) throws SQLException {
        Connection conn = ds.getConnection();
        try {
            conn.setAutoCommit(false);   // 手动事务
            try (PreparedStatement ps1 = conn.prepareStatement(
                    "UPDATE students SET score = score - ? WHERE id = ?")) {
                ps1.setInt(1, delta); ps1.setInt(2, fromId);
                ps1.executeUpdate();
            }
            try (PreparedStatement ps2 = conn.prepareStatement(
                    "UPDATE students SET score = score + ? WHERE id = ?")) {
                ps2.setInt(1, delta); ps2.setInt(2, toId);
                ps2.executeUpdate();
            }
            conn.commit();              // 都成功才提交
        } catch (SQLException e) {
            conn.rollback();             // 出错全回滚
            throw e;
        } finally {
            conn.setAutoCommit(true);
            conn.close();                // close 是还回池子
        }
    }
}

// 调用示例:
// dao.insert("小明", 92);          → 返回 1(自增 id)
// dao.findAll();                   → [{id=1, name=小明, score=92}]
// dao.transferScore(1, 2, 10);     → 1 号扣 10 分,2 号加 10 分,要么都成要么都不成
ResultSet 用完不关 = 连接泄漏

JDBC 里 Connection、Statement、ResultSet 都是昂贵资源,用完不关,连接池很快被占满,整个网站卡死。养成习惯:一律 try-with-resources,让编译器替你关。手写 finally close 十个里有九个漏一半。

章末面试 · JDBC(5 题)

1.(概念题)为什么必须用 PreparedStatement 而不是 Statement?

查看答案

答案:防 SQL 注入。Statement 拼字符串,用户输入 ' OR '1'='1 能把 SQL 变恒真。PreparedStatement 用 ? 占位符,参数单独传,数据库当数据处理,注入不了。

2.(概念题)JDBC 事务怎么控制?默认是什么?

查看答案

答案:默认自动提交(每条 SQL 立刻 commit)。要手动事务就 conn.setAutoCommit(false),都成功调 commit(),出错调 rollback()。

3.(选择题)JDBC 事务隔离级别有哪几个?默认是?

查看答案

答案:读未提交、读已提交、可重复读、串行化。MySQL InnoDB 默认可重复读(REPEATABLE READ),Oracle 默认读已提交。

4.(概念题)ResultSet、Statement、Connection 谁先关?

查看答案

答案:从里到外:ResultSet → Statement → Connection。但用 try-with-resources 自动按声明反序关,顺序永远正确,不用记。

5.(排错题)插入中文到数据库变成问号?

查看答案

答案:JDBC URL 没带 characterEncoding=utf8,或库表不是 utf8mb4。三处对齐:JDBC URL、库、表。