PreparedStatement详解

PreparedStatement 详解


一、PreparedStatement 概述

PreparedStatement 是 JDBC 中用于执行预编译 SQL 语句的接口,它继承自 Statement 接口,提供了更安全、更高效的 SQL 执行方式。

主要特点:

  1. 预编译:SQL 语句被预编译并存储在 PreparedStatement 对象中
  2. 参数化查询:使用占位符(?)代替直接拼接SQL值
  3. 防止SQL注入:自动处理特殊字符,提高安全性
  4. 性能优势:可重复使用,减少数据库编译开销

二、创建 PreparedStatement

// 基本创建方式
String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);

// 可指定结果集类型和并发模式
PreparedStatement pstmt = connection.prepareStatement(
    sql, 
    ResultSet.TYPE_SCROLL_INSENSITIVE,
    ResultSet.CONCUR_READ_ONLY);

// 可指定是否返回自动生成的主键
PreparedStatement pstmt = connection.prepareStatement(
    sql, 
    Statement.RETURN_GENERATED_KEYS);

三、参数设置方法

PreparedStatement 提供了一系列 setXxx() 方法来设置参数:

1. 基本数据类型

pstmt.setInt(int parameterIndex, int x);
pstmt.setLong(int parameterIndex, long x);
pstmt.setFloat(int parameterIndex, float x);
pstmt.setDouble(int parameterIndex, double x);
pstmt.setBoolean(int parameterIndex, boolean x);

2. 字符串和二进制数据

pstmt.setString(int parameterIndex, String x);
pstmt.setBytes(int parameterIndex, byte[] x);
pstmt.setBinaryStream(int parameterIndex, InputStream x);

3. 日期和时间

pstmt.setDate(int parameterIndex, Date x);
pstmt.setTime(int parameterIndex, Time x);
pstmt.setTimestamp(int parameterIndex, Timestamp x);

// 使用Calendar指定时区
pstmt.setDate(int parameterIndex, Date x, Calendar cal);

4. 其他类型

pstmt.setObject(int parameterIndex, Object x);
pstmt.setNull(int parameterIndex, int sqlType);

参数索引说明:

  • 参数索引从1开始,不是0
  • 每个?占位符必须设置值
  • 可以多次设置同一参数的值(会覆盖前值)

四、执行 PreparedStatement

1. 执行查询(返回ResultSet)

ResultSet rs = pstmt.executeQuery();

2. 执行更新(返回影响行数)

int rows = pstmt.executeUpdate();

3. 执行任意SQL(可能返回多个结果)

boolean hasResultSet = pstmt.execute();
if (hasResultSet) {
    ResultSet rs = pstmt.getResultSet();
}

五、批处理操作

PreparedStatement 特别适合批处理操作:

String sql = "INSERT INTO products (id, name, price) VALUES (?, ?, ?)";
PreparedStatement pstmt = connection.prepareStatement(sql);

for (Product product : productList) {
    pstmt.setInt(1, product.getId());
    pstmt.setString(2, product.getName());
    pstmt.setDouble(3, product.getPrice());
    pstmt.addBatch();  // 添加到批处理
    
    // 每100条执行一次,避免内存溢出
    if (i % 100 == 0) {
        pstmt.executeBatch();
    }
}

// 执行剩余的批处理
int[] result = pstmt.executeBatch();

// 清空批处理
pstmt.clearBatch();

六、获取自动生成的主键

String sql = "INSERT INTO users (name, age) VALUES (?, ?)";
PreparedStatement pstmt = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);

pstmt.setString(1, "张三");
pstmt.setInt(2, 25);
pstmt.executeUpdate();

// 获取生成的主键
ResultSet rs = pstmt.getGeneratedKeys();
if (rs.next()) {
    long id = rs.getLong(1);
    System.out.println("生成的主键ID: " + id);
}

七、PreparedStatement 高级特性

1. 流式处理大数据

// 设置大文本
Reader reader = new StringReader("很长的文本内容...");
pstmt.setCharacterStream(1, reader);

// 设置二进制流
InputStream is = new FileInputStream("image.jpg");
pstmt.setBinaryStream(2, is);

2. 可滚动/可更新的ResultSet

PreparedStatement pstmt = connection.prepareStatement(
    "SELECT * FROM employees WHERE dept = ?",
    ResultSet.TYPE_SCROLL_INSENSITIVE,
    ResultSet.CONCUR_UPDATABLE);

pstmt.setString(1, "IT");
ResultSet rs = pstmt.executeQuery();

// 更新结果集
rs.last();
rs.updateString("name", "New Name");
rs.updateRow();

3. 参数元数据

ParameterMetaData pmd = pstmt.getParameterMetaData();
int paramCount = pmd.getParameterCount();
for (int i = 1; i <= paramCount; i++) {
    String typeName = pmd.getParameterTypeName(i);
    System.out.println("参数" + i + "类型: " + typeName);
}

八、PreparedStatement 最佳实践

  1. 重用PreparedStatement:在循环外创建,循环内重复使用

    // 错误做法 - 每次循环都创建新的PreparedStatement
    for (User user : users) {
        PreparedStatement pstmt = conn.prepareStatement(...);
        // ...
    }
    
    // 正确做法 - 重用PreparedStatement
    PreparedStatement pstmt = conn.prepareStatement(...);
    for (User user : users) {
        pstmt.setString(1, user.getName());
        // ...
        pstmt.executeUpdate();
    }
    
  2. 总是关闭资源:使用try-with-resources

    try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
        pstmt.setInt(1, 101);
        try (ResultSet rs = pstmt.executeQuery()) {
            // 处理结果
        }
    }
    
  3. 合理设置fetchSize:大数据量查询时提高性能

    PreparedStatement pstmt = connection.prepareStatement(sql);
    pstmt.setFetchSize(1000);  // 每次从数据库获取1000条
    
  4. 设置查询超时:避免长时间运行的查询

    pstmt.setQueryTimeout(30);  // 30秒超时
    
  5. 处理NULL值

    pstmt.setNull(1, Types.VARCHAR);  // 明确设置NULL类型
    

九、与Statement的比较

特性 PreparedStatement Statement
SQL注入防护
性能 高(预编译) 低(每次编译)
参数设置 参数化(?) 字符串拼接
批处理 高效 效率较低
二进制数据 支持好 处理复杂
使用场景 重复执行相似SQL 一次性执行动态SQL

十、常见问题解决方案

1. 参数设置错误

问题:参数索引越界或类型不匹配

try {
    pstmt.setInt(1, 101);
} catch (SQLException e) {
    // 检查SQL中的?数量和设置是否匹配
}

2. 批处理部分失败

处理方式

try {
    int[] results = pstmt.executeBatch();
} catch (BatchUpdateException e) {
    int[] partialResults = e.getUpdateCounts();
    // 处理部分成功的情况
}

3. 大数据量处理

方案

// 设置适当的fetchSize
pstmt.setFetchSize(500);

// 使用流式处理
pstmt.setFetchSize(Integer.MIN_VALUE);

PreparedStatement 是JDBC中最重要、最常用的接口之一,正确使用它可以显著提高应用程序的安全性、性能和可靠性。在实际开发中,应该优先使用PreparedStatement而不是Statement,特别是在处理用户输入或需要重复执行相似SQL时。

你可能感兴趣的:(JDBC,java,数据库)