预编译最初的目的是提高代码的复用性,因为有很多只有参数值不同的 SQL(完全相同的 SQL 会从缓存里查),比如:
select * from user where id='1'
select * from user where id='2'
这些 SQL 的语法树相同,但每次都要进行重复的编译,很浪费时间。
而预编译可以将 SQL 语句模板化,值的位置用占位符替代,这样数据库就会事先编译好 SQL 语法结构,等真正调用的时候,再传入值执行,省掉了重复建立语法树的时间。
select * from user where id={占位符}
通过抓包来看,SQL 语句先被预编译(Prepare Statement),参数值先用占位符替代。等执行(Execute Statement)的时候,再传入参数。
用户传入的参数不参与语法树的构建,就改不了 SQL 的语法结构,也就避免了注入。
以 MyBatis(半自动化的持久层框架)为例,#{id}
这种格式传参,会先把 SQL 传给数据库进行预编译,等调用的时候,再用参数替换掉占位符,然后执行。
SELECT *
FROM user
WHERE id=#{id}
但有些 SQL 需要使用动态表名和列名,这种时候就不能使用预编译了,需要把 #{id}
换成 ${id}
,这样参数就会直接参与 SQL 编译,无法防止 SQL 注入,这时候就要手动过滤参数了。
提示:MyBatis 框架的预编译,是 JDBC 中的 PreparedStatement 类在起作用,它的对象包含了编译好的 SQL 语句。
PHP 中使用 MySQL 的预编译功能:
1)定义预编译的 SQL 语句,参数用占位符 ?
表示
$sql = "SELECT * FROM user WHERE id= ? ";
2)创建预处理对象
$mysqli_stmt = $mysqli->prepare($sql);
3)绑定参数
$mysqli_stmt->bind_param('i', $id);
4)绑定结果集
$mysqli_stmt->bind_result($username);
5)执行
$mysqli_stmt->execute();
预编译的局限性
预编译的机制是先编译,再传值,用户传递的参数无法改变 SQL 语法结构,从根本上解决了 SQL 注入的问题。
但并不是所有参数都可以使用预编译,比如动态表名和列名的场景,因为语义分析时,会解析语法树,检查表名和列名是否存在,所以表名和列名不能被占位符替代,也就没法使用预编译。
同理,排序场景的 ASC/DESC 也需要动态传参,不能使用预编译。