Statement和PreparedStatement区别

在 Java 中,PreparedStatement和普通Statement都是用于执行 SQL 语句的接口,但它们在功能、性能、安全性等方面有显著区别,主要差异如下:

一、区别

1. SQL 语句预编译
  • 普通 Statement :每次执行 SQL 时,数据库都需要对 SQL 语句进行编译(词法分析、语法分析等),然后执行。如果多次执行结构相同的 SQL(仅参数不同),数据库会重复编译,效率较低。

  • PreparedStatement :SQL 语句在创建时就会被预编译并存储在数据库中,后续执行时只需传入参数即可,避免了重复编译,提升了多次执行相同结构 SQL 的效率。

2. 参数传递方式
  • 普通 Statement:通过字符串拼接的方式传入参数,例如:

    java 复制代码
    String sql = "SELECT * FROM user WHERE id = " + userId;

    这种方式容易因参数格式错误(如字符串未加引号)导致 SQL 语法错误。

  • PreparedStatement :使用占位符? 代替参数,通过setXxx(index, value)方法设置参数,例如:

    java 复制代码
    String 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();
}

三、预编译的本质

  1. 数据库端行为 :预编译是数据库对 SQL 进行词法分析、语法分析、生成执行计划的过程,结果缓存到数据库中(通过statementId索引)。
  2. 驱动职责:负责按照数据库协议(如 MySQL 的 COM 命令)传递 SQL 和参数,确保预编译和执行的通信正确。

四、特殊说明

  • 预编译缓存 :部分数据库(如 MySQL)会缓存预编译语句,但默认缓存大小有限(可通过max_prepared_stmt_count调整)。

  • 本地预编译开关 :MySQL 驱动可通过useServerPrepStmts=true强制使用服务器端预编译(默认可能本地模拟以提升性能)。


如果觉得还不错的话,关注、 分享、 在看, 原创不易,且看且珍惜~

相关推荐
m0_736927045 小时前
想抓PostgreSQL里的慢SQL?pg_stat_statements基础黑匣子和pg_stat_monitor时间窗,谁能帮你更准揪出性能小偷?
java·数据库·sql·postgresql
lang201509285 小时前
MySQL 8.0.29 及以上版本中 SSL/TLS 会话复用(Session Reuse)
数据库·mysql
望获linux5 小时前
【实时Linux实战系列】使用 u-trace 或 a-trace 进行用户态应用剖析
java·linux·前端·网络·数据库·elasticsearch·操作系统
清和与九6 小时前
binLog、redoLog和undoLog的区别
数据库·oracle
望获linux6 小时前
【实时Linux实战系列】FPGA 与实时 Linux 的协同设计
大数据·linux·服务器·网络·数据库·fpga开发·操作系统
总有刁民想爱朕ha6 小时前
Python自动化从入门到实战(24)如何高效的备份mysql数据库,数据备份datadir目录直接复制可行吗?一篇给小白的完全指南
数据库·python·自动化·mysql数据库备份
朝九晚五ฺ7 小时前
【Redis学习】持久化机制(RDB/AOF)
数据库·redis·学习
虾说羊7 小时前
sql中连接方式
数据库·sql
liweiweili1267 小时前
Django中处理多数据库场景
数据库·python·django