127

这是我到目前为止所读到的PDO::ATTR_EMULATE_PREPARES

  1. 由于 MySQL 的本机准备绕过查询缓存,因此 PDO 的准备模拟性能更好
  2. MySQL 的原生 prepare 更好的安全性(防止 SQL 注入)
  3. MySQL 的原生 prepare 更适合错误报告

我不知道这些陈述中的任何一个是否真实。在选择 MySQL 接口时,我最关心的是防止 SQL 注入。第二个问题是性能。

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

我正在使用MySQL 5.1.61PHP 5.3.2

我应该PDO::ATTR_EMULATE_PREPARES启用还是不启用?有没有办法兼顾查询缓存的性能和准备好的语句的安全性?

4

8 回答 8

115

回答您的疑虑:

  1. MySQL >= 5.1.17 (or >= 5.1.21 for the PREPAREand EXECUTEstatements)可以在查询缓存中使用准备好的语句。所以你的 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()a (使用本机预准备语句)有固定成本,因此使用本prepare();execute()机预准备语句的 a 可能比使用模拟预准备语句发出纯文本查询要慢一些。在许多数据库系统上,a 的查询计划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;
}
于 2012-05-04T19:48:39.450 回答
15

我很惊讶没有人提到关闭仿真的最大原因之一。启用仿真后,PDO 将所有整数和浮点数作为字符串返回。当您关闭仿真时,MySQL 中的整数和浮点数在 PHP 中变为整数和浮点数。

有关更多信息,请参阅此问题的公认答案:PHP + PDO + MySQL:如何将 MySQL 中的整数和数字列作为整数和数字在 PHP 中返回?.

于 2019-11-13T03:47:12.153 回答
10

PDO::ATTR_EMULATE_PREPARES当您的 PHP未pdo_mysql针对mysqlnd.

由于 oldlibmysql与某些功能不完全兼容,可能会导致奇怪的 bug,例如:

  1. 绑定时丢失 64 位整数的最高有效位PDO::PARAM_INT(0x12345678AB 在 64 位机器上将被裁剪为 0x345678AB)
  2. 无法进行简单的查询,例如LOCK TABLES(它抛出SQLSTATE[HY000]: General error: 2030 This command is not supported in the prepared statement protocol yet异常)
  3. 需要在下一个查询之前从结果中获取所有行或关闭游标( withmysqlnd或 emulated 会自动为您完成这项工作并且不会与 mysql 服务器不同步)

libmysql当迁移到用于pdo_mysql模块的其他服务器时,我在我的简单项目中发现了这些错误。也许还有更多的错误,我不知道。我还在新的 64 位 debian jessie 上进行了测试,所有列出的错误都在 I 时出现apt-get install php5-mysql,而在 I 时消失apt-get install php5-mysqlnd

WhenPDO::ATTR_EMULATE_PREPARES设置为 true(默认)- 这些错误无论如何都不会发生,因为 PDO 在此模式下根本不使用准备好的语句。因此,如果您使用pdo_mysql基于libmysql(“mysqlnd”子字符串未出现pdo_mysql在 phpinfo 部分的“客户端 API 版本”字段中) - 您不应该PDO::ATTR_EMULATE_PREPARES关闭。

于 2016-09-29T19:36:22.767 回答
9

我会在您运行 5.1 时关闭 emulate prepares,这意味着 PDO 将利用本机准备好的语句功能。

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

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

为了准备好命名语句和更好的 API,我放弃了 MySQLi 使用 PDO。

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

于 2012-05-04T09:06:32.937 回答
7

我建议启用真正的数据库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

于 2014-07-15T05:27:44.303 回答
6

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

这样做的主要原因是让数据库引擎进行准备而不是 PDO 是查询和实际数据是分开发送的,这增加了安全性。这意味着当参数被传递给查询时,尝试向其中注入 SQL 的尝试会被阻止,因为 MySQL 准备好的语句仅限于单个查询。这意味着当在参数中传递第二个查询时,真正的准备好的语句将失败。

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

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

于 2018-06-15T01:10:11.180 回答
3

作为记录

PDO::ATTR_EMULATE_PREPARES=true

它可能会产生令人讨厌的副作用。它可以将 int 值作为字符串返回。

PHP 7.4,带有 mysqlnd 的 pdo。

使用 PDO::ATTR_EMULATE_PREPARES=true 运行查询

列:id
类型:整
数值:1

使用 PDO::ATTR_EMULATE_PREPARES=false 运行查询

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

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

于 2020-07-27T14:55:11.890 回答
0

如果您有多个绑定参数,第一个肯定是正确的。我有一个带有 11 个参数的 SQL,并且没有模拟准备它需要 5 秒。启用模拟准备后,它下降到 0.25 秒。

尽管应该为 PHP 7.4.11 解决类似的问题https://bugs.php.net/bug.php?id=80027 ,但升级到 PHP 7.4.27 后问题仍然存在。

于 2021-12-27T18:53:24.950 回答