在 Java 中,PreparedStatement
和普通Statement
都是用于执行 SQL 语句的接口,但它们在功能、性能、安全性等方面有显著区别,主要差异如下:
一、区别
1. SQL 语句预编译
-
普通 Statement :每次执行 SQL 时,数据库都需要对 SQL 语句进行编译(词法分析、语法分析等),然后执行。如果多次执行结构相同的 SQL(仅参数不同),数据库会重复编译,效率较低。
-
PreparedStatement :SQL 语句在创建时就会被预编译并存储在数据库中,后续执行时只需传入参数即可,避免了重复编译,提升了多次执行相同结构 SQL 的效率。
2. 参数传递方式
-
普通 Statement:通过字符串拼接的方式传入参数,例如:
javaString sql = "SELECT * FROM user WHERE id = " + userId;
这种方式容易因参数格式错误(如字符串未加引号)导致 SQL 语法错误。
-
PreparedStatement :使用占位符
?
代替参数,通过setXxx(index, value)
方法设置参数,例如:javaString sql = "SELECT * FROM user WHERE id = ?"; preparedStatement.setInt(1, userId); // 索引从1开始
无需手动处理参数格式(如字符串自动加引号),更简洁、不易出错。
3. SQL 注入防护
-
普通 Statement :由于通过字符串拼接参数,若参数包含恶意 SQL 片段(如
' OR '1'='1
),可能导致SQL 注入攻击。例如:java// 恶意参数可能使SQL变为:SELECT * FROM user WHERE name = '' OR '1'='1' String sql = "SELECT * FROM user WHERE name = '" + userName + "'";
-
PreparedStatement :预编译时已确定 SQL 结构,参数会被数据库视为纯数据而非 SQL 指令,从根本上避免了 SQL 注入,安全性更高。
4. 适用场景
-
普通 Statement :适合执行一次性、结构不固定的 SQL 语句(如动态生成的复杂 SQL,且参数安全可控)。
-
PreparedStatement :适合执行多次重复、结构固定的 SQL(如增删改查中参数变化的场景),尤其是涉及用户输入参数时,优先使用以保证安全。
二、PreparedStatement预编译
PreparedStatement
的预编译机制涉及 Java JDBC 规范和数据库驱动的底层实现,不同数据库(如 MySQL、Oracle)的驱动源码会有差异,但核心逻辑一致:在创建PreparedStatement
时将 SQL 发送到数据库进行预编译,执行时仅传递参数。
以下从MySQL 驱动源码(com.mysql.cj.jdbc.ClientPreparedStatement
)示例方面解析其核心流程:
1. 创建 PreparedStatement 时触发预编译
当调用connection.prepareStatement(sql)
时,驱动会执行以下步骤(简化逻辑):
java
// com.mysql.cj.jdbc.ConnectionImpl#prepareStatement
public PreparedStatement prepareStatement(String sql) throws SQLException {
// 1. 校验SQL,处理数据库方言(如MySQL的特殊语法)
// 2. 创建ClientPreparedStatement实例,传入SQL
ClientPreparedStatement pStmt = new ClientPreparedStatement(this, sql);
// 3. 发送SQL到数据库进行预编译(核心步骤)
pStmt.initialize(sql);
return pStmt;
}
2. 预编译核心:向数据库发送 "预处理" 指令
initialize
方法中,驱动会通过网络向 MySQL 服务器发送COM_STMT_PREPARE
命令(MySQL 协议规定),触发数据库端的编译:
java
// com.mysql.cj.jdbc.ClientPreparedStatement#initialize
private void initialize(String sql) throws SQLException {
// 1. 生成预编译请求包(包含SQL语句)
Buffer sendPacket = this.session.getPacketFactory().getBuffer();
sendPacket.writeInt(0); // 包长度占位符
sendPacket.writeByte((byte) 0x16); // COM_STMT_PREPARE命令标识
sendPacket.writeStringNoNull(sql); // 写入SQL语句
// 2. 发送请求到MySQL服务器
this.session.sendCommand(sendPacket, false, 0);
// 3. 接收数据库返回的预编译结果(包含语句ID、参数元信息等)
Buffer resultPacket = this.session.readPacket();
this.statementId = resultPacket.readInt4(); // 数据库生成的语句ID(后续执行用)
this.parameterCount = resultPacket.readUnsignedShort(); // 参数个数
// ... 解析其他元信息(如结果集列信息)
}
关键 :数据库会为预编译的 SQL 生成一个唯一的statementId
,后续执行时只需发送statementId
和参数,无需重复传递完整 SQL。
3. 执行时仅传递参数
调用execute()
时,驱动会发送COM_STMT_EXECUTE
命令,携带statementId
和参数值:
java
// com.mysql.cj.jdbc.ClientPreparedStatement#execute
public boolean execute() throws SQLException {
// 1. 校验参数是否完整(与预编译时的参数个数匹配)
checkAllParametersSet();
// 2. 生成执行请求包(包含statementId和参数)
Buffer sendPacket = this.session.getPacketFactory().getBuffer();
sendPacket.writeInt(0); // 包长度占位符
sendPacket.writeByte((byte) 0x17); // COM_STMT_EXECUTE命令标识
sendPacket.writeInt4(this.statementId); // 预编译时的语句ID
// ... 写入参数值(根据参数类型编码,如字符串、数字等)
// 3. 发送请求到数据库,执行预编译好的SQL
this.session.sendCommand(sendPacket, false, 0);
// 4. 处理执行结果
return handleResultSets();
}
三、预编译的本质
- 数据库端行为 :预编译是数据库对 SQL 进行词法分析、语法分析、生成执行计划的过程,结果缓存到数据库中(通过
statementId
索引)。 - 驱动职责:负责按照数据库协议(如 MySQL 的 COM 命令)传递 SQL 和参数,确保预编译和执行的通信正确。
四、特殊说明
-
预编译缓存 :部分数据库(如 MySQL)会缓存预编译语句,但默认缓存大小有限(可通过
max_prepared_stmt_count
调整)。 -
本地预编译开关 :MySQL 驱动可通过
useServerPrepStmts=true
强制使用服务器端预编译(默认可能本地模拟以提升性能)。
如果觉得还不错的话,关注、 分享、 在看, 原创不易,且看且珍惜~
