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 反馈 — 无需本地设置。
此课程中的所有课时
- JDBC 基础
- PreparedStatement
- 事务
- 使用 HikariCP 实现连接池