是否使用 PDO: : ATTR_EMULATE_PREPARES?

以下是我到目前为止读到的关于 PDO::ATTR_EMULATE_PREPARES的内容:

  1. PDO 的准备模拟对性能更有利,因为 MySQL 的本机准备绕过了查询缓存
  2. MySQL 的本机准备对于安全性(防止 SQL 注入)更好。
  3. MySQL 的本机准备对于错误报告 更好。

我不知道这些说法是否正确。在选择 MySQL 接口时,我最关心的是防止 SQL 注入。第二个问题是性能。

我的应用程序目前使用过程化的 MySQLi (没有准备语句) ,并且相当多地使用了查询缓存。它很少在单个请求中重用事先准备好的语句。我开始将命名参数和准备语句的安全性迁移到 PDO。

我用的是 MySQL 5.1.61PHP 5.3.2

我是否应该启用 PDO::ATTR_EMULATE_PREPARES?是否有办法同时保证查询缓存的性能和准备语句的安全性?

71898 次浏览

在运行5.1时,我将关闭模拟准备功能,这意味着 PDO 将利用本机准备语句功能。

PDO _ MYSQL 将利用 MySQL 4.1及更高版本中提供的本机准备语句支持。如果您使用的是 mysql 客户机库的旧版本,PDO 将为您模拟它们。

Http://php.net/manual/en/ref.pdo-mysql.php

我放弃了 MySQLi,转而使用 PDO,以获得准备好的命名语句和更好的 API。

然而,为了平衡起见,PDO 的执行速度比 MySQLi 慢得可以忽略不计,但是要记住这一点。当我做出这个选择的时候,我就知道这一点,并且决定一个更好的 API 和使用行业标准比使用一个速度快得可以忽略不计的库把你绑定到一个特定的引擎更重要。FWIW 我认为 PHP 团队也在积极地看待 PDO 而不是 MySQLi 的未来。

回答你的问题:

  1. MySQL > = 5.1.17(或者对于 PREPAREEXECUTE语句 > = 5.1.21) 可以在查询缓存中使用准备好的语句。因此您的 MySQL + PHP 版本可以在查询缓存中使用准备好的语句。但是,请仔细注意 MySQL 文档中缓存查询结果的注意事项。有许多类型的查询不能被缓存,或者即使它们被缓存了也是无用的。根据我的经验,查询缓存通常不是一个非常大的胜利。查询和模式需要特殊的结构来最大限度地利用缓存。无论如何,从长远来看,应用程序级别的缓存最终都是必要的。

  2. 本地人的准备对安全没有任何影响。伪准备语句仍将转义查询参数值,它只是在带字符串的 PDO 库中完成,而不是在使用二进制协议的 MySQL 服务器上完成。换句话说,无论您的 EMULATE_PREPARES设置如何,相同的 PDO 代码都同样容易(或不容易)受到注入攻击。唯一的区别是参数替换发生在哪里——对于 EMULATE_PREPARES,它发生在 PDO 库中; 对于没有 EMULATE_PREPARES的情况,它发生在 MySQL 服务器上。

  3. 如果没有 EMULATE_PREPARES,你可能会在准备时而不是执行时得到语法错误; 如果没有 EMULATE_PREPARES,你只会在执行时得到语法错误,因为 PDO 在执行时才会给 MySQL 一个查询。请注意,这将影响您将要编写的代码!特别是如果你使用 PDO::ERRMODE_EXCEPTION

另一个考虑因素是:

  • prepare()(使用本机准备语句)有一个固定的成本,因此使用本机准备语句的 prepare();execute()可能比使用模拟准备语句发出纯文本查询慢一些。在许多数据库系统中,prepare()的查询计划也被缓存,并且可以与多个连接共享,但是我不认为 MySQL 可以做到这一点。因此,如果不为多个查询重用准备好的语句对象,则整体执行可能会较慢。

作为最后的建议 ,我认为对于旧版本的 MySQL + PHP,您应该模拟准备好的语句,但是对于最新版本,您应该关闭模拟。

在编写了一些使用 PDO 的应用程序之后,我做了一个 PDO 连接函数,其中有我认为是最好的设置。您可能应该使用这样的东西或调整到您的首选设置:

/**
* Return PDO handle for a MySQL connection using supplied settings
*
* Tries to do the right thing with different php and mysql versions.
*
* @param array $settings with keys: host, port, unix_socket, dbname, charset, user, pass. Some may be omitted or NULL.
* @return PDO
* @author Francis Avila
*/
function connect_PDO($settings)
{
$emulate_prepares_below_version = '5.1.17';


$dsndefaults = array_fill_keys(array('host', 'port', 'unix_socket', 'dbname', 'charset'), null);
$dsnarr = array_intersect_key($settings, $dsndefaults);
$dsnarr += $dsndefaults;


// connection options I like
$options = array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
);


// connection charset handling for old php versions
if ($dsnarr['charset'] and version_compare(PHP_VERSION, '5.3.6', '<')) {
$options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES '.$dsnarr['charset'];
}
$dsnpairs = array();
foreach ($dsnarr as $k => $v) {
if ($v===null) continue;
$dsnpairs[] = "{$k}={$v}";
}


$dsn = 'mysql:'.implode(';', $dsnpairs);
$dbh = new PDO($dsn, $settings['user'], $settings['pass'], $options);


// Set prepared statement emulation depending on server version
$serverversion = $dbh->getAttribute(PDO::ATTR_SERVER_VERSION);
$emulate_prepares = (version_compare($serverversion, $emulate_prepares_below_version, '<'));
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, $emulate_prepares);


return $dbh;
}

我建议启用实际的数据库 PREPARE调用,因为模拟并不能捕捉所有的内容。例如,它将准备 INSERT;

var_dump($dbh->prepare('INSERT;'));
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
var_dump($dbh->prepare('INSERT;'));

输出

object(PDOStatement)#2 (1) {
["queryString"]=>
string(7) "INSERT;"
}
bool(false)

我很乐意为实际工作的代码承担性能损失。

FWIW

PHP 版本: PHP 5.4.9-4ubuntu2.4(cli)

MySQL 版本: 5.5.34-0ubuntu0

当您的 PHP pdo_mysql没有根据 mysqlnd编译时,要注意禁用 PDO::ATTR_EMULATE_PREPARES(打开本机准备)。

因为旧的 libmysql与某些函数不完全兼容,它可能导致奇怪的 bug,例如:

  1. 当绑定为 PDO::PARAM_INT(0x12345678AB 将在64位机器上裁剪为0x345678AB)时,64位整数的最大有效位丢失
  2. 无法执行像 LOCK TABLES这样的简单查询(它抛出 SQLSTATE[HY000]: General error: 2030 This command is not supported in the prepared statement protocol yet异常)
  3. 需要在下次查询之前从 result 或 close 游标中获取所有行(使用 mysqlnd或模拟准备,它会自动为您完成这项工作,并且不会与 mysql 服务器失去同步)

这些 bug 是我在我的简单项目中发现的,当迁移到其他使用 libmysql作为 pdo_mysql模块的服务器时。也许还有更多的虫子,我不知道。此外,我测试了新鲜的64位 debian jessie,所有列出的错误发生时,我 apt-get install php5-mysql,并消失时,我 apt-get install php5-mysqlnd

PDO::ATTR_EMULATE_PREPARES被设置为 true (作为默认值)时——这些错误不会发生,因为 PDO 在这种模式下根本不使用准备好的语句。因此,如果您使用基于 libmysqlpdo_mysql(“ mysqlnd”子字符串没有出现在 phpinfo 中 pdo_mysql部分的“ Client API version”字段中)-您不应该关闭 PDO::ATTR_EMULATE_PREPARES

为什么将仿真切换为“假”?

这样做的主要原因是让数据库引擎执行 是发送查询和实际数据 这意味着当参数 传递给查询时,将阻止向其中注入 SQL 的尝试, 因为 MySQL 准备语句仅限于一个查询 意味着真正的准备语句在传递第二个 参数中的查询。

反对使用数据库引擎进行准备的主要论据是 PDO 是到服务器的两次旅行——一次是为了准备,另一次是为了准备 传递参数-但我认为增加的安全性是 另外,至少在 MySQL 的情况下,查询缓存没有 从5.1版本开始就是一个问题。

Https://tech.michaelseiler.net/2016/07/04/dont-emulate-prepared-statements-pdo-mysql/

我很惊讶没有人提到关闭仿真的最大原因之一。打开模拟之后,PDO 返回所有的整数和浮点数作为 绳子。当您关闭仿真时,MySQL 中的整数和浮点数在 PHP 中变成整数和浮点数。

有关更多信息,请参见此问题的公认答案: PHP + PDO + MySQL: 如何在 PHP 中以整数和数字的形式返回 MySQL 中的整数和数字列?

郑重声明

ATTR _ EMULATE _ PREPARES = true

它可能产生一个讨厌的副作用,它可能以字符串的形式返回 int 值。

PHP 7.4,pdo 和 mysqlnd。

使用 PDO: : ATTR _ EMULATE _ PREPARES = true 运行查询

列: id
类型: 整数
价值: 1

使用 PDO: : ATTR _ EMULATE _ PREPARES = false 运行查询

列: id
类型: 字符串
价值: “1”

在任何情况下,无论配置如何,十进制值总是返回一个字符串:-(

如果有多个绑定参数,则第一个参数肯定为 true。我有一个11个参数的 SQL 和 WITHOUT 模拟准备它需要5秒钟。在启用模拟器之后,准备时间降到了0.25秒。

虽然类似的问题 https://bugs.php.net/bug.php?id=80027应该在 PHP 7.4.11中得到解决,但是在升级到 PHP 7.4.27之后,这个问题仍然存在。