0Pricing
Java Academy · 课时

PreparedStatement

安全的参数化查询

PreparedStatement 是 CoddyKit 上的免费 Java Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Java Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Java Academy 课程共包含 4 节课。

为什么使用 PreparedStatement?

通过拼接用户输入来构建 SQL 既危险又缓慢。

PreparedStatement 可以同时解决这两个问题:

  • 安全性——值会与 SQL 分开传送,从而防止 SQL 注入。
  • 性能——数据库可以只解析并制定一次查询计划,然后重复使用该计划。

SQL 注入问题

请考虑使用字符串拼接来构建查询。如果用户将 '; DROP TABLE users; -- 作为姓名输入,拼接后的 SQL 就会将其作为命令执行。

这就是SQL 注入,是历史最久、危害最大的网络漏洞之一。PreparedStatement 从设计上杜绝了这种问题。

public class Main {
    public static void main(String[] args) {
        String userInput = "'; DROP TABLE users; --";
        // Dangerous: never do this
        String unsafe = "SELECT * FROM users WHERE name = '" + userInput + "'";
        System.out.println("Injected SQL becomes:");
        System.out.println(unsafe);
    }
}

占位符

在 PreparedStatement 中,为每个稍后要提供的值写入一个 ?。

然后使用类型化设置方法绑定值:

  • ps.setString(1, name)
  • ps.setInt(2, age)

参数索引从1 开始,与 ? 标记的顺序一致。

安全查询

使用 prepareStatement 创建语句,绑定参数,然后调用 executeQuery。

值始终不会接触 SQL 文本,因此无论用户输入什么,都不会发生注入。

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

public class Main {
    public static void main(String[] args) {
        String url = "jdbc:postgresql://localhost:5432/shop";
        String sql = "SELECT id FROM users WHERE name = ?";
        try (Connection conn = DriverManager.getConnection(url, "app", "secret");
             PreparedStatement ps = conn.prepareStatement(sql)) {
            ps.setString(1, "Ada");
            try (ResultSet rs = ps.executeQuery()) {
                while (rs.next()) System.out.println(rs.getInt("id"));
            }
        } catch (SQLException e) {
            System.out.println(e.getMessage());
        }
    }
}

绑定多个参数

每个 ? 都有各自的设置方法调用。顺序很重要:参数 1 对应第一个 ?,参数 2 对应第二个,以此类推。

请使用与列类型匹配的设置方法,例如 setInt、setString、setDouble 和 setBoolean 等。

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

public class Main {
    public static void main(String[] args) {
        String url = "jdbc:postgresql://localhost:5432/shop";
        String sql = "INSERT INTO users(name, age, active) VALUES (?, ?, ?)";
        try (Connection conn = DriverManager.getConnection(url, "app", "secret");
             PreparedStatement ps = conn.prepareStatement(sql)) {
            ps.setString(1, "Grace");
            ps.setInt(2, 42);
            ps.setBoolean(3, true);
            int rows = ps.executeUpdate();
            System.out.println("Inserted " + rows + " row");
        } catch (SQLException e) {
            System.out.println(e.getMessage());
        }
    }
}

处理 NULL

要绑定 SQL NULL,请使用 setNull(index, type),并传入一个 java.sql.Types 常量。

不要将 Java null 传给 setString 后寄希望于它正常工作——请明确指定:

  • ps.setNull(2, java.sql.Types.INTEGER)

获取生成的键

执行 INSERT 后,您通常需要获取自动生成的标识。

准备语句时传入 Statement.RETURN_GENERATED_KEYS,然后读取 ps.getGeneratedKeys()。

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

public class Main {
    public static void main(String[] args) {
        String url = "jdbc:postgresql://localhost:5432/shop";
        String sql = "INSERT INTO users(name) VALUES (?)";
        try (Connection conn = DriverManager.getConnection(url, "app", "secret");
             PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
            ps.setString(1, "Linus");
            ps.executeUpdate();
            try (ResultSet keys = ps.getGeneratedKeys()) {
                if (keys.next()) System.out.println("New id: " + keys.getLong(1));
            }
        } catch (SQLException e) {
            System.out.println(e.getMessage());
        }
    }
}

批量插入

逐条执行语句插入数千行数据会很慢。请使用批量处理:

  • 绑定参数,然后调用 ps.addBatch()。
  • 对每一行重复此操作。
  • 调用一次 ps.executeBatch()。

这样会将多行数据一起发送,从而大幅减少往返次数。

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

public class Main {
    public static void main(String[] args) {
        String url = "jdbc:postgresql://localhost:5432/shop";
        String sql = "INSERT INTO tags(label) VALUES (?)";
        String[] labels = {"java", "jdbc", "sql"};
        try (Connection conn = DriverManager.getConnection(url, "app", "secret");
             PreparedStatement ps = conn.prepareStatement(sql)) {
            for (String label : labels) {
                ps.setString(1, label);
                ps.addBatch();
            }
            int[] counts = ps.executeBatch();
            System.out.println("Batched " + counts.length + " inserts");
        } catch (SQLException e) {
            System.out.println(e.getMessage());
        }
    }
}

复用语句

一个 PreparedStatement 可以使用不同参数执行多次。

重新绑定占位符,然后再次调用 execute。数据库会复用缓存的执行计划,这就是预编译语句在重复查询中速度更快的原因。

切勿拼接

黄金规则是:任何会变化的值都必须是 ? 参数。

只有固定且由代码控制的标识符(例如列名白名单中的名称)才可以拼接——即使如此,也要根据允许列表进行验证。用户提供的数据始终要绑定,绝不能拼接。

关闭与复用的作用域

PreparedStatement 与其 Connection 绑定,使用完毕后应将其关闭。

在资源自动管理代码块中,请声明该语句,使其在连接之前关闭。如果您要在循环中多次复用语句,请在循环外只准备一次,并仅在循环内部重新绑定参数——每次迭代都重新准备会丢弃缓存的执行计划。

快速检查

为什么 PreparedStatement 能防止 SQL 注入?

回顾

您学会了编写安全且快速的查询:

  • 使用 ? 占位符和类型化设置器(索引从 1 开始)。
  • 使用 setString、setInt、setNull 等绑定值。
  • 使用 RETURN_GENERATED_KEYS 和 getGeneratedKeys 获取自动生成的键。
  • 使用 addBatch 和 executeBatch 批量执行多次写入。
  • 绝不要将用户输入拼接到 SQL 中。

常见问题解答

「PreparedStatement」课时是免费的吗?

是的 — 「PreparedStatement」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Java Academy 课程的其余内容,请升级到 CoddyKit PRO。 Java Academy 课程共包含 4 节课。

「PreparedStatement」这节课中我会学到什么?

安全的参数化查询 你通过在浏览器中直接运行的动手代码来练习 Java Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Java Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Java Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「PreparedStatement」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Java Academy 课中编写并运行代码吗?

能。每节 Java Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. JDBC 基础
  2. PreparedStatement
  3. 事务
  4. 使用 HikariCP 实现连接池
← 返回 Java Academy