1. 概述
本文将深入对比 JDBC 中的 Statement
和 PreparedStatement
两个接口的核心差异。我们不会涉及 CallableStatement
——它是用于调用数据库存储过程的接口,不在本文讨论范围内。
如果你在项目中还在随手用 Statement
拼接 SQL,那这篇文章可能会帮你避开几个生产环境的“惊吓”。
2. JDBC 接口简介
Statement
和 PreparedStatement
都可用于执行 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:安全高效的首选
PreparedStatement
是 Statement
的增强版,专为解决上述问题而生。
✅ 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
后:
- 解析 SQL 模板(不含具体值)
- 生成执行计划并缓存
- 后续相同模板的请求直接复用计划
这不仅减少了数据库 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