1. 概述

本文将深入对比 JDBC 中的 StatementPreparedStatement 两个接口的核心差异。我们不会涉及 CallableStatement——它是用于调用数据库存储过程的接口,不在本文讨论范围内。

如果你在项目中还在随手用 Statement 拼接 SQL,那这篇文章可能会帮你避开几个生产环境的“惊吓”。

2. JDBC 接口简介

StatementPreparedStatement 都可用于执行 SQL 查询,表面看方法相似,实则在功能、安全性和性能上差异巨大:

  • Statement:用于执行纯字符串形式的 SQL 查询
  • PreparedStatement:用于执行带参数占位符的预编译 SQL 查询

为了演示方便,我们引入 H2 数据库的 JDBC 依赖:

<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <version>2.1.214</version>
</dependency>

接下来,我们使用一个简单的实体类贯穿全文:

public class PersonEntity {
    private int id;
    private String name;

    // standard setters and getters
}

3. Statement:简单但危险

Statement 看似简单直接,但在实际开发中是个“踩坑重灾区”。主要有以下几个问题:

❌ 1. 代码可读性差

拼接 SQL 字符串会让代码迅速变得难以维护:

public void insert(PersonEntity personEntity) {
    String query = "INSERT INTO persons(id, name) VALUES(" + personEntity.getId() + ", '"
      + personEntity.getName() + "')";

    Statement statement = connection.createStatement();
    statement.executeUpdate(query);
}

这种字符串拼接方式不仅丑,还容易出错——比如忘了加引号、括号不匹配等。

❌ 2. 存在 SQL 注入风险

这是最致命的问题。来看两个典型例子:

dao.update(new PersonEntity(1, "hacker' --"));
dao.insert(new PersonEntity(1, "O'Brien"));
  • 第一行:hacker' -- 会被拼成 'hacker' --'-- 后的内容在 SQL 中是注释,可能导致 UPDATE 不加条件地更新全表。
  • 第二行:O'Brien 中的单引号未转义,直接导致 SQL 语法错误。

攻击者完全可以构造恶意输入,绕过权限、删除数据甚至拖库。

❌ 3. 无法利用数据库预编译与缓存

每次执行,JDBC 都会把完整的 SQL 字符串(含具体值)发送给数据库。这意味着:

  • 数据库无法识别这是“同一个”SQL,每次都要重新解析、优化
  • 无法利用执行计划缓存(plan cache),性能损耗明显
  • 批量操作必须循环执行,效率低下

✅ 4. 适合 DDL 操作

虽然问题多,但 Statement 也有适用场景——执行 DDL 语句(如 CREATE, ALTER, DROP)时,通常不需要参数,用 Statement 反而更直接:

Statement stmt = connection.createStatement();
stmt.execute("CREATE TABLE IF NOT EXISTS persons (id INT PRIMARY KEY, name VARCHAR(50))");

4. PreparedStatement:安全高效的首选

PreparedStatementStatement 的增强版,专为解决上述问题而生。

✅ 1. 代码清晰,参数化设计

使用 ? 占位符,逻辑清晰,不易出错:

String query = "INSERT INTO persons(id, name) VALUES(?, ?)";
PreparedStatement pstmt = connection.prepareStatement(query);
pstmt.setInt(1, personEntity.getId());
pstmt.setString(2, personEntity.getName());
pstmt.executeUpdate();

一眼就能看出 SQL 结构和参数绑定位置,维护成本大幅降低。

✅ 2. 自动防止 SQL 注入

PreparedStatement 会自动对参数进行转义和类型处理,从根本上杜绝 SQL 注入。比如 O'Brien 会被安全地处理为字符串值,而不是 SQL 片段。

⚠️ 注意:只有使用 ? 占位符才是安全的。如果拼接表名、字段名等无法预编译的部分,仍需自行校验。

✅ 3. 支持预编译,提升性能

数据库收到 PreparedStatement 后:

  1. 解析 SQL 模板(不含具体值)
  2. 生成执行计划并缓存
  3. 后续相同模板的请求直接复用计划

这不仅减少了数据库 CPU 开销,还通过二进制协议减少网络传输数据量,通信更高效。

✅ 4. 支持批量操作(Batch)

批量插入/更新时性能优势明显:

public void insert(List<PersonEntity> personEntities) throws SQLException {
    String query = "INSERT INTO persons(id, name) VALUES( ?, ?)";
    PreparedStatement preparedStatement = connection.prepareStatement(query);
    for (PersonEntity personEntity: personEntities) {
        preparedStatement.setInt(1, personEntity.getId());
        preparedStatement.setString(2, personEntity.getName());
        preparedStatement.addBatch();
    }
    preparedStatement.executeBatch();
}

✅ 一次预编译,多次参数绑定,最后批量提交,效率远高于逐条执行。

✅ 5. 支持大对象和复杂类型

  • 存储文件?用 setBlob() / getBlob() 处理 BLOB
  • 存储长文本?用 CLOB
  • 传列表参数?可将 java.sql.Array 映射为 SQL 数组

这些特性让 PreparedStatement 更适合处理复杂业务场景。

✅ 6. 提供元数据支持

可通过 getMetaData() 获取结果集的结构信息,便于动态处理结果,比如字段名、类型等。

5. 总结

对比项 Statement PreparedStatement
SQL 拼接 ❌ 字符串拼接,易错 ✅ 参数化,清晰安全
SQL 注入 ❌ 高风险 ✅ 自动转义,安全
性能 ❌ 无缓存,重复解析 ✅ 预编译 + 执行计划缓存
批量操作 ❌ 需循环执行 ✅ 原生支持 batch
适用场景 DDL(CREATE/ALTER/DROP) DML(INSERT/UPDATE/DELETE)

结论:

  • 执行 DDL 或一次性管理脚本,可用 Statement
  • 所有涉及用户输入或高频执行的 DML 操作,必须使用 PreparedStatement

示例代码已托管至 GitHub:https://github.com/yourname/jdbc-examples


原始标题:Difference Between Statement and PreparedStatement