【问题标题】:PDO MySQL: Use PDO::ATTR_EMULATE_PREPARES or not?PDO MySQL:是否使用 PDO::ATTR_EMULATE_PREPARES?
【发布时间】:2012-04-24 04:59:34
【问题描述】:

这是我目前所读到的关于 PDO::ATTR_EMULATE_PREPARES 的内容:

  1. PDO's prepare emulation is better for performance since MySQL's native prepare bypasses the query cache
  2. MySQL's native prepare is better for security (preventing SQL Injection)
  3. MySQL's native prepare is better for error reporting

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

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

我正在使用MySQL 5.1.61PHP 5.3.2

我是否应该启用PDO::ATTR_EMULATE_PREPARES?有没有办法兼顾查询缓存的性能和预处理语句的安全性?

【问题讨论】:

  • 诚实吗?继续使用 MySQLi。如果它已经在使用准备好的语句,那么 PDO 基本上是一个毫无意义的抽象层。 编辑:PDO 对于您不确定将哪个数据库进入后端的新领域应用程序非常有用。
  • 对不起,我的问题之前不清楚。我已经编辑过了。该应用程序目前不使用 MySQLi 中的预准备语句;只是 mysqli_run_query()。根据我的阅读,MySQLi 准备好的语句也绕过了查询缓存。

标签: php mysql pdo


【解决方案1】:

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

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

【讨论】:

【解决方案2】:

记录在案

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”

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

【讨论】:

  • 十进制值总是返回字符串是唯一正确的方法
  • 从 MySQL 的角度来看是的,但在 PHP 方面是错误的。 Java 和 C# 都将 Decimal 视为数值。
  • 不,不是。这对于整个计算机科学来说都是正确的。如果您认为这是错误的,那么您需要另一种任意精度的类型
  • @YourCommonSense 想深入了解您为什么这么认为?
  • @YourCommonSense 浮点数不能准确表示 0.2,而小数可以。但是,这是一个 PHP 问题,而不是整个计算机科学问题。许多语言(和数据库)具有可以准确表示数字(例如 0.2)的内在数据类型。 PHP 可以,但基础语言中没有内在数据类型。但是要说十进制值总是作为字符串返回是唯一正确的方法是meh。那是假设您希望 0.2 被准确表示,而不是在“2”之前排序的“12”。 “2”也不等于“2.0”。
【解决方案3】:

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

有关详细信息,请参阅此问题的公认答案:PHP + PDO + MySQL: how do I return integer and numeric columns from MySQL as integers and numerics in PHP?

【讨论】:

  • 这在 PHP 版本中是正确的 8.1。但是,从 8.1 开始,在正确返回整数和浮点数方面,模拟准备将与本机准备兼容。请参阅PHP 8.1 Upgrade Guide 了解更多信息。
【解决方案4】:

为什么要将模拟切换为“假”?

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

反对使用数据库引擎进行准备 vs 的主要论点 PDO 是对服务器的两次访问——一次用于准备,另一次 让参数传递——但我认为增加的安全性是 值得。此外,至少在 MySQL 的情况下,查询缓存还没有 自 5.1 版以来一直存在问题。

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

【讨论】:

  • Query caching is gone 不管怎样:查询缓存自 MySQL 5.7.20 起已弃用,并在 MySQL 8.0 中删除。
【解决方案5】:

当您的 PHP pdo_mysql 未针对 mysqlnd 编译时,请注意禁用 PDO::ATTR_EMULATE_PREPARES(打开本机准备)。

由于旧的libmysql 不完全兼容某些功能,可能会导致奇怪的错误,例如:

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

当迁移到使用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 部分的“客户端 API 版本”字段中) - 您不应该关闭 PDO::ATTR_EMULATE_PREPARES

【讨论】:

  • 这种担忧在 2019 年仍然有效吗?!
【解决方案6】:

回答您的疑虑:

  1. MySQL >= 5.1.17(或者对于 PREPAREEXECUTE 语句 >= 5.1.21)can use prepared statements in the query cache。所以你的 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;
}

【讨论】:

  • Re #2:MySQL 作为参数接收的值(对于本地准备好的语句)肯定不会被解析为 SQL?因此,注入的风险必须低于使用 PDO 的准备模拟,其中任何转义缺陷(例如,mysql_real_escape_string 的多字节字符的历史问题)仍然会留下注入攻击的余地?
  • @eggyal,您正在假设准备好的语句是如何实现的。 PDO 可能在其模拟准备转义中存在错误,但 MySQL 也可能存在错误。 AFAIK,模拟准备没有发现任何问题,这可能导致参数文字未经转义。
  • 很棒的答案,但我有一个问题:如果关闭 EMULATION,执行会不会变慢? PHP 必须将准备好的语句发送到 MySQL 进行验证,然后才发送参数。因此,如果您使用准备好的语句 5 次,PHP 将与 MySQL 对话 6 次(而不是 5 次)。这不会让它变慢吗?此外,我认为 PDO 在验证过程中出现错误的可能性更大,而不是 MySQL...
  • 请注意this answer 在后台使用mysql_real_escape_string 重新准备语句仿真中的要点以及可能出现的后续漏洞(在非常特殊的边缘情况下)。
  • +1 好答案!但是为了记录,如果您使用本机准备,即使在 MySQL 服务器端,参数也永远不会转义或组合到 SQL 查询中。当您执行并提供参数时,查询已被解析并转换为 MySQL 中的内部数据结构。阅读 MySQL 优化器工程师的这篇博客,解释这个过程:guilhembichot.blogspot.com/2014/05/… 我并不是说这意味着原生准备更好,因为我们相信 PDO 代码可以正确地进行转义(我这样做)。
【解决方案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

【讨论】:

  • 这是一个有趣的观点。我猜想仿真将服务器端解析推迟到执行阶段。虽然这没什么大不了的(错误的 SQL 最终会失败),但让prepare 完成它应该做的工作会更干净。 (此外,我一直认为客户端参数解析器必然有自己的错误。)
  • IDK 如果你有兴趣,但here's a little writeup 我注意到 PDO 的一些其他虚假行为导致我一开始就陷入了这个兔子洞。似乎缺乏对多个查询的处理。
  • 我刚刚在 GitHub 上查看了一些迁移库...你知道吗,this one 与我的博文几乎完全相同。
【解决方案8】:

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

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

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

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

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

【讨论】:

  • 感谢您提供的信息。无法使用查询缓存对您的性能有何影响,或者您以前是否使用过?
  • 我不能说作为框架我在多个级别上使用缓存。不过,您始终可以显式使用 SELECT SQL_CACHE
  • 甚至不知道有一个 SELECT SQL_CACHE 选项。但是,这似乎仍然行不通。来自文档:“查询结果被缓存如果它是可缓存的...”dev.mysql.com/doc/refman/5.1/en/query-cache-in-select.html
  • 是的。这取决于查询的性质,而不是平台细节。
  • 我读到的意思是“查询结果被缓存除非有其他东西阻止它被缓存”,从我之前读到的内容来看,它包括准备好的语句。但是,感谢 Francis Avila 的回答,我知道我的 MySQL 版本不再适用。
猜你喜欢
  • 2013-11-13
  • 2012-11-04
  • 1970-01-01
  • 1970-01-01
  • 2013-04-22
  • 2019-02-24
相关资源
最近更新 更多