PHP中PDO数据访问对象处理LIMIT子句的一处BUG

近期的项目中,数据访问层类使用了PDO作为底层,PDO相比mysql_*系列函数具有更高的性能和安全性。在使用时却发现了PDO对象处理带有LIMIT子句的SQL语句时,不能正确处理LIMIT子句中的占位符。例如:

$sql = "SELECT * FROM user LIMIT ?, ?";
$stmt = $pdo->prepare($sql);
$stmt->execute(array(0, 20));
.... 

在这段代码中,按照PDO文档的说法,两个问号的位置将会被0, 20这两个参数填充,并且类型将会使用和参数类型相匹配的PDO数据类型。但是执行这条语句却返回一个空集合。

var_dump($stmt->fetchAll());
// 输出 empty 

然而将问号的位置直接填写数字,是可以正常取得20条记录的。

在谷歌上搜索发现,有其他人也遇到了同样的BUG:

http://stackoverflow.com/questions/5508993/pdo-limit-and-offset

http://stackoverflow.com/questions/2269840/php-pdo-bindvalue-in-limit

http://bbs.phpchina.com/thread-149339-1-1.html

关于这一Bug,可以在PHP官方的Bug Report平台找到:

https://bugs.php.net/bug.php?id=44639

解决的方法是,使用bindValue方法绑定值到SQL语句,并且指定第三个参数为INT类型:

$sql = "SELECT * FROM user LIMIT ?, ?";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(1, 0, PDO::PARAM_INT);
$stmt->bindValue(2, 20, PDO::PARAM_INT);
$stmt->execute();

这样再次执行,发现20条记录被正确地查询出来。

不过这样不算完美解决,我们通常会对PDO对象进行一些封装才使用,比如构建一次查出来SQL语句并返回结果集数组的方法,传参是不能用bindValue的。于是我写了一个用于自动选择类型的函数:

function select_datatype($val) {
if (is_bool($val)) {
return PDO::PARAM_BOOL;
} elseif (is_int($val)) {
return PDO::PARAM_INT;
} elseif {is_null($val)) {
return PDO::PARAM_NULL;
} else {
return PDO::PARAM_STR;
}
}

这样将$stmt->execute(array(0, 20));类似的语句,转换为循环遍历使用bindValue()绑定值,并使用select_datatype()选择类型即可解决这一问题。

其实早在2008年,就有人向PHP官方报告了这个Bug,但是至今仍未被Fix。如果之后有时间,研究研究能不能做个针对这个问题的补丁。