什么是参数化查询?
参数化查询是一种将 SQL 语句的“结构”和“数据”分开处理的技术,你首先定义一个 SQL 模板,其中用占位符(在 Java 中是 )来代替实际的值,然后将这些值作为参数传递给数据库。
举个简单的例子:
不安全的字符串拼接(SQL 注入高危方式):
String username = "admin"; String password = "' OR '1'='1"; // 恶意输入 // 危险!直接拼接字符串 String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
password 是恶意输入如 "' OR '1'='1",那么最终的 SQL 会变成:
SELECT * FROM users WHERE username = 'admin' AND password = '' OR '1'='1'
这个 WHERE 条件永远为真,攻击者就可以绕过密码验证登录系统。
安全的参数化查询:
String username = "admin"; String password = "' OR '1'='1"; // 恶意输入 // 安全!使用 ? 作为占位符 String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
无论 password 的值是什么,它都会被当作一个“字符串值”来处理,而不会成为 SQL 语句的一部分,从而彻底杜绝 SQL 注入。
为什么必须使用参数化查询?
- 防止 SQL 注入:这是最重要、最核心的原因,它将数据和代码分离,确保用户输入不会被解释为 SQL 命令。
- 提高性能:对于需要重复执行的 SQL 语句(循环中插入多条数据),数据库可以缓存预编译的 SQL 语句模板,当后续执行时,数据库只需绑定新的参数即可,省去了每次都解析和编译 SQL 语句的开销。
- 代码更清晰、更易维护:将 SQL 逻辑与数据值分开,使得代码更易读,也避免了复杂的字符串拼接和转义问题。
如何在 Java 中使用 PreparedStatement?
下面通过一个完整的例子,展示如何使用 PreparedStatement 进行查询、插入、更新和删除操作。
准备工作
假设我们有一个 users 表:
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL
);
核心步骤
- 加载驱动并获取连接。
- 创建
PreparedStatement对象,并传入带有 占位符的 SQL 模板。 - 设置参数:使用
setXxx()方法(如setString(),setInt())将 替换为实际的值,参数的索引从 1 开始。 - 执行 SQL:使用
executeQuery()(用于查询,返回ResultSet)或executeUpdate()(用于插入、更新、删除,返回受影响的行数)。 - 关闭资源:非常重要! 按照
ResultSet->PreparedStatement->Connection的顺序关闭,以避免资源泄漏。
完整代码示例
这里使用 JDBC 和 try-with-resources 语句,它可以自动关闭实现了 AutoCloseable 接口(如 Connection, PreparedStatement, ResultSet)的资源,是现代 Java 推荐的最佳实践。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class PreparedStatementExample {
// 数据库连接信息 (请根据你的实际情况修改)
private static final String DB_URL = "jdbc:mysql://localhost:3306/your_database";
private static final String USER = "your_username";
private static final String PASS = "your_password";
public static void main(String[] args) {
// --- 1. 查询示例 ---
findUserByUsername("testuser");
// --- 2. 插入示例 ---
insertUser("newuser", "newuser@example.com");
// --- 3. 更新示例 ---
updateUserEmail("newuser", "updated@example.com");
// --- 4. 删除示例 ---
deleteUser("newuser");
}
/**
* 根据用户名查询用户
* @param username 要查询的用户名
*/
public static void findUserByUsername(String username) {
String sql = "SELECT id, username, email FROM users WHERE username = ?";
// try-with-resources 自动关闭 Connection, PreparedStatement, ResultSet
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
// 设置参数 (索引从 1 开始)
pstmt.setString(1, username);
// 执行查询
try (ResultSet rs = pstmt.executeQuery()) {
System.out.println("--- 查询用户: " + username + " ---");
if (rs.next()) {
// 从结果集中获取数据
int id = rs.getInt("id");
String foundUsername = rs.getString("username");
String email = rs.getString("email");
System.out.println("ID: " + id + ", 用户名: " + foundUsername + ", 邮箱: " + email);
} else {
System.out.println("未找到用户: " + username);
}
}
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 插入一个新用户
* @param username 用户名
* @param email 邮箱
*/
public static void insertUser(String username, String email) {
String sql = "INSERT INTO users (username, email) VALUES (?, ?)";
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
// 设置参数
pstmt.setString(1, username);
pstmt.setString(2, email);
// 执行更新
int affectedRows = pstmt.executeUpdate();
System.out.println("\n--- 插入用户: " + username + " ---");
System.out.println("受影响的行数: " + affectedRows);
} catch (SQLException e) {
// 如果用户名已存在,会抛出异常 (UNIQUE constraint violation)
System.err.println("插入用户失败: " + e.getMessage());
}
}
/**
* 更新用户的邮箱
* @param username 要更新的用户名
* @param newEmail 新的邮箱
*/
public static void updateUserEmail(String username, String newEmail) {
String sql = "UPDATE users SET email = ? WHERE username = ?";
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
// 设置参数 (注意顺序要与 SQL 中的 ? 顺序一致)
pstmt.setString(1, newEmail); // 第一个 ? 是新邮箱
pstmt.setString(2, username); // 第二个 ? 是用户名
int affectedRows = pstmt.executeUpdate();
System.out.println("\n--- 更新用户邮箱: " + username + " ---");
System.out.println("受影响的行数: " + affectedRows);
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 删除一个用户
* @param username 要删除的用户名
*/
public static void deleteUser(String username) {
String sql = "DELETE FROM users WHERE username = ?";
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, username);
int affectedRows = pstmt.executeUpdate();
System.out.println("\n--- 删除用户: " + username + " ---");
System.out.println("受影响的行数: " + affectedRows);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
PreparedStatement 的常用方法
| 方法 | 描述 |
|---|---|
setString(int index, String x) |
设置指定位置的参数为 String 类型。 |
setInt(int index, int x) |
设置指定位置的参数为 int 类型。 |
setLong(int index, long x) |
设置指定位置的参数为 long 类型。 |
setDouble(int index, double x) |
设置指定位置的参数为 double 类型。 |
setBoolean(int index, boolean x) |
设置指定位置的参数为 boolean 类型。 |
setDate(int index, Date x) |
设置指定位置的参数为 Date 类型。 |
setTimestamp(int index, Timestamp x) |
设置指定位置的参数为 Timestamp 类型。 |
setObject(int index, Object x) |
一个通用的方法,可以设置任意类型的 Java 对象。 |
executeQuery() |
执行查询,返回一个 ResultSet 对象。 |
executeUpdate() |
执行 INSERT, UPDATE, DELETE 等操作,返回受影响的行数。 |
addBatch() |
将当前的一组参数添加到批处理中。 |
executeBatch() |
执行批处理中的所有 SQL 语句。 |
批处理操作
当需要执行大量相同的 SQL 语句(插入 1000 条数据)时,使用批处理可以极大地提高性能。
public static void batchInsertUsers() {
String sql = "INSERT INTO users (username, email) VALUES (?, ?)";
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
// 关闭自动提交,手动控制事务
conn.setAutoCommit(false);
// 添加多个 SQL 语句到批处理中
for (int i = 1; i <= 1000; i++) {
pstmt.setString(1, "batchuser" + i);
pstmt.setString(2, "batch" + i + "@example.com");
pstmt.addBatch(); // 添加到批处理
// 每执行 100 条提交一次,以避免内存占用过大
if (i % 100 == 0) {
pstmt.executeBatch();
conn.commit(); // 提交事务
}
}
// 执行剩余的批处理
pstmt.executeBatch();
conn.commit(); // 提交最后的事务
System.out.println("批处理插入完成。");
} catch (SQLException e) {
e.printStackTrace();
// 发生异常时回滚事务
try {
if (conn != null) {
conn.rollback();
}
} catch (SQLException ex) {
ex.printStackTrace();
}
}
}
| 特性 | Statement (字符串拼接) |
PreparedStatement (参数化查询) |
|---|---|---|
| 安全性 | 低,极易发生 SQL 注入 | 高,有效防止 SQL 注入 |
| 性能 | 每次都需编译 SQL | 预编译后可复用,性能更高 |
| 易用性 | 字符串拼接繁琐,易出错 | 代码清晰,参数设置简单 |
| 适用场景 | 极少数情况,如动态 SQL 表名 | 所有涉及用户输入的 SQL 操作 |
在 Java 中进行数据库操作时,始终、优先、必须使用 PreparedStatement 进行参数化查询。 它是编写安全、高效数据库应用程序的基础。
