杰瑞科技汇

Java SQL IN参数化如何实现?

什么是参数化查询?

参数化查询是一种将 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 注入。


为什么必须使用参数化查询?

  1. 防止 SQL 注入:这是最重要、最核心的原因,它将数据和代码分离,确保用户输入不会被解释为 SQL 命令。
  2. 提高性能:对于需要重复执行的 SQL 语句(循环中插入多条数据),数据库可以缓存预编译的 SQL 语句模板,当后续执行时,数据库只需绑定新的参数即可,省去了每次都解析和编译 SQL 语句的开销。
  3. 代码更清晰、更易维护:将 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
);

核心步骤

  1. 加载驱动并获取连接
  2. 创建 PreparedStatement 对象,并传入带有 占位符的 SQL 模板。
  3. 设置参数:使用 setXxx() 方法(如 setString(), setInt())将 替换为实际的值,参数的索引从 1 开始。
  4. 执行 SQL:使用 executeQuery()(用于查询,返回 ResultSet)或 executeUpdate()(用于插入、更新、删除,返回受影响的行数)。
  5. 关闭资源非常重要! 按照 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 进行参数化查询。 它是编写安全、高效数据库应用程序的基础。

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