如果ATTR_EMULATE_PREPARES为false,则准备好的插入不会执行任何操作

时间:2015-09-08 07:01:24

标签: php mysql pdo

因此,经过与PDO的大量斗争,我已经在这里缩小了问题的根源。当我将属性ATTR_EMULATE_PREPARES设置为false时,我的插入查询运行时没有错误,但不会在数据库表中添加条目。

但是当我通过PhpMyAdmin运行查询或将ATTR_EMULATE_PREPARES设置为true时,它会成功执行。

这让我有点沮丧,因为没有充分的理由说明它为什么不起作用。

这是我通过PhpMyAdmin直接执行的查询。

INSERT INTO account (guid, displayname, password_hash, password_salt, email, superuser) VALUES (UNHEX('fcfd7f2355f211e5acfd2174e316c493'), 'bob', 'test', 'test', 'test', 1);

以下是相关的代码部分。

$db = null;
try
{
    $db = new PDO($pdo_connectionstring, $pdo_username, $pdo_password);
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
}
catch (PDOException $ex)
{
    die('[FATAL ERROR] Could not connect to the Database: '.$ex->getMessage());
}

try
{
    $stmt = $db->prepare("INSERT INTO `account` (`guid`, `displayname`, `password_hash`, `password_salt`, `email`, `superuser`) VALUES (UNHEX(:guid), :displayname, :passwordhash, :passwordsalt, :email, :superuser);");

    $stmt->bindValue(':guid', $guid, PDO::PARAM_STR);
    $stmt->bindValue(':displayname', $displayname, PDO::PARAM_STR);
    $stmt->bindValue(':passwordhash', $hash, PDO::PARAM_STR);
    $stmt->bindValue(':passwordsalt', $salt, PDO::PARAM_STR);
    $stmt->bindValue(':email', $email, PDO::PARAM_STR);
    $stmt->bindValue(':superuser', $superuser, PDO::PARAM_BOOL);

    $stmt->execute();
}
catch (Exception $ex)
{
    $jsonresult['generalerror'] = 'Failed to create user. Please contact your GM: ' . $ex->getMessage();
}

编辑:以下是我的数据库架构的SQLFiddle,以及相关系统和组件的版本信息

  • Debian 6.0.10在1and1&#39的共享主机上运行
  • PHP 5.4.44
  • MySQL服务器5.5.44-0 + deb7u1-log

编辑:感谢@RyanVincent搞清楚这一点。第一部分是我的数据库列superuser被定义为tinyint(1),尽管它是一种常见的布尔存储数据类型,但在绑定值时需要PDO::PARAM_INT。第二部分是当PDO驱动程序运行时PDO::ATTR_EMULATE_PREPARES设置为false,如果数据库返回错误消息,则不会抛出错误或异常,仅在本地PDO驱动程序时遇到问题。 PDOStatement::execute()返回一个布尔值,表明查询是否成功。如果PDOStatement::errorCode()返回false,则由开发人员手动检查PDOStatement::errorInfo()execute()。这在事务期间尤为重要,因为如果其中一个语句失败,则回滚事务很重要。 PDO的一个恼人的怪癖是如果你设置了错误的数据类型I.E. PDO::PARAM_BOOL代替PDO::PARAM_INT,就像我一样,返回的errorInfo()几乎是空的,无论如何都会让你摸不着头脑。

ATTR_EMULATE_PREPARES设置为false时,

TL:DR ,使用其他错误捕获功能,如果某些功能无效,请检查并仔细检查您的数据类型。

1 个答案:

答案 0 :(得分:3)

问题是,当emulate_prepares用作false时,查询将失败,并且PDO不会引发任何异常。

TL;DR :转到:测试设置和解释

TL;DR :goto:测试和结果

TL;DR :转到:结论

PDO准备的查询处理概述

mysql驱动程序:

  • 了解named placeholders,它只使用?来标记bind variables的位置。
  • 如果成功,则每个命令都会返回有用的信息false,如果它无法完成请求。

结果是,当您PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION时,PDO有责任将其转换为Exception

通过PDO执行查询时预计会发生什么情况:

  • prepare:使用PDOnamed placeholders发送查询。
  • PDO将其格式化为'?'并将其发送给mysql驱动程序。
  • mysql驱动程序向它发送实际的mysql服务器,该服务器对其进行处理,并返回说明它如何返回mysql驱动程序的信息
  • mysql驱动程序将结果发送回PDO
  • PDO检查并根据需要将其转换为有用的结果或error codesexceptions

可以看出,由于混乱发生,所以有比我们想要的更多的地方。

为了调试正在发生的事情,我想确切地知道mysql server收到的内容和返回的内容。

为此,mysql server提供了general logging for queries,请参阅:5.2.3 The General Query Log

我已提供实用程序来控制general log的开启和关闭,以便写入log files并写入mysql.general_log表。

经过测试的代码:IMysqlGeneralLogging: Control the MySQL General Logging Facilityclass

调试和解释实际的O / P代码:

我提供了PHPUnit测试脚本来运行O / P代码并检查。我正在使用它作为执行和报告'工具,而不是测试任何东西。

这真正解释了我发现并展示它的内容。花费了相当多的时间和相当多的“试验和错误”。重点在于错误'。 ; - )

测试设置和说明

/**
 * Test Data for all tests...
 */
protected $guid             = '420D4B65565311E5958000FFD7CBE75F';
protected $displayname     = 'Example User 42';
protected $hash             = '$2y$17$12345678901234567890121234567890123456789012345678942';
protected $salt             = '$2y$17$1234567890123456789042$';
protected $email            = 'example42@example.com';
protected $superuser       =  true;

/**
 * Expected Results
 * Must be static so they are not reset with each test
 */
protected static $rcExpected = array(
    'prepare'       => true,
    'execute'       => true,
    'exception'     => false,
    'exceptionMsg'  => '',
);

/**
 * Actual results stored in here. 
 */
protected static $rcActual = array(
    'prepare'       => false,
    'execute'       => false,
    'exception'     => false,
    'exceptionMsg'  => '',
);

使用的代码已更改为将结果存储在上述数组中。

    // these change for each test...
    $this->userPDO->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $this->userPDO->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);

    try
    {
        self::$rcActual['exception'] = false;

        $stmt = $this->userPDO->prepare(
                   "INSERT INTO `account`
                            (`guid`,       `displayname`, `password_hash`, `password_salt`, `email`, `superuser`)
                    VALUES
                            (UNHEX(:guid), :displayname,  :passwordhash,   :passwordsalt,   :email,  :superuser);");


        self::$rcActual['prepare'] = $stmt !== false; // record result

        $stmt->bindValue(':guid',         $this->guid,         PDO::PARAM_STR);
        $stmt->bindValue(':displayname',  $this->displayname, PDO::PARAM_STR);
        $stmt->bindValue(':passwordhash', $this->hash,         PDO::PARAM_STR);
        $stmt->bindValue(':passwordsalt', $this->salt,         PDO::PARAM_STR);
        $stmt->bindValue(':email',        $this->email,        PDO::PARAM_STR);
        $stmt->bindValue(':superuser',    $this->superuser,   PDO::PARAM_BOOL);

        self::$rcActual['execute'] = $stmt->execute(); // record result

        $this->assertTrue(self::$rcActual['execute']);
        $this->assertFalse(self::$rcActual['exception']);
    }
    catch (\Exception $e) {
        self::$rcActual['exception'] = true;
        self::$rcActual['exeptionMsg'] = $e->getCode() .' : '. $e->getMessage();
        $this->assertTrue(self::$rcActual['exception']);
    }

启动mysql general logging到跟踪文件的代码:

    $testFilename = $this->logging->newTraceFilename('Test_01_EmulatesOn_ExceptionOn'));
    $this->logging->setLogFile($testFilename);
    $this->logging->startLoggingToFile(); // start logging...

并停止记录到跟踪文件:

    $this->logging->stopLogging();

测试和结果:

测试系列1 - 标准PDO(模拟开启):

    $this->userPDO->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $this->userPDO->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);

测试1:一切都好......

测试代码:Q32451215Test 01 EmulatesOn_ExpectsAllOk

结果:

预期/实际:

prepare   : Expected:  true, Actual: true
execute   : Expected:  true, Actual: true
exception : Expected:  false, Actual: false
exceptionMsg : Expected:  '', Actual: ''

跟踪日志:

150914 14:15:30   569 Query INSERT INTO `account`
                            (`guid`,       `displayname`, `password_hash`, `password_salt`, `email`, `superuser`)
                    VALUES
                            (UNHEX('420D4B65565311E5958000FFD7CBE75F'), 'Example User 42',  '$2y$17$12345678901234567890121234567890123456789012345678942',   '$2y$17$1234567890123456789042$',   'example42@example.com',  1)

注意:

  • 发送到服务器的单独的prepareexecute语句。有趣?

测试系列2 - PDO(模拟OFF):

测试2A:与测试1相同的设置,除了模拟OFF

测试代码:Q32451215Test 02A EmulatesOff__WithSuperUserAsBooleanParamType

未更改的绑定语句:

 $stmt->bindValue(':superuser',    $this->superuser,   PDO::PARAM_BOOL);

结果:

预期/实际:

prepare   : Expected:  true, Actual: true
execute   : Expected:  true, Actual: false
exception : Expected:  false, Actual: false
exceptionMsg : Expected:  '', Actual: ''
errorCode : '00000', ErrorMsg: array (
  0 => '00000',
  1 => NULL,
  2 => NULL,
)

跟踪日志:

150915 11:37:12   693 Prepare   INSERT INTO `account`
                            (`guid`,       `displayname`, `password_hash`, `password_salt`, `email`, `superuser`)
                    VALUES
                            (UNHEX(?), ?,  ?,   ?,   ?,  ?)

注意:

这些是unexpected; - /

  • 跟踪日志仅包含prepare statement
  • 没有execute命令发送到服务器。
  • PDO execute语句返回false但未提出exception
  • 没有返回异常的errorCodeerrorInfo消息。

这表示PDOmysql driver不喜欢' PDO :: PARAM_BOOL`或提供给它的实际值。

  • 检查此SQL statement失败的唯一方法是检查单个prepareexecute语句返回的结果!另外,你不知道为什么失败了!

这使得调试有趣; - /

测试2B:除了PDO::PARAM_INT

之外,与测试2A相同的设置

测试代码:Q32451215Test 02B EmulatesOff__WithSuperUserAsIntegerParamType

这是对代码的唯一更改:

  $stmt->bindValue(':superuser',    $this->superuser,   PDO::PARAM_INT);

结果:

预期/实际:

prepare   : Expected:  true, Actual: true
execute   : Expected:  true, Actual: true
exception : Expected:  false, Actual: false
exceptionMsg : Expected:  '', Actual: ''
errorCode : '00000', ErrorMsg: ''

跟踪日志:

150915 12:06:07   709 Prepare   INSERT INTO `account`
                            (`guid`,       `displayname`, `password_hash`, `password_salt`, `email`, `superuser`)
                    VALUES
                            (UNHEX(?), ?,  ?,   ?,   ?,  ?)

      709 Execute   INSERT INTO `account`
                            (`guid`,       `displayname`, `password_hash`, `password_salt`, `email`, `superuser`)
                    VALUES
                            (UNHEX('420D4B65565311E5958000FFD7CBE75F'), 'Example User 42',  '$2y$17$12345678901234567890121234567890123456789012345678942',   '$2y$17$1234567890123456789042$',   'example42@example.com',  1)

注意:

这些完全符合预期:

  • 跟踪日志包含prepareexecute语句。
  • PDO execute语句返回true
  • 没有返回异常的errorCodeerrorInfo消息。

奇怪的是,只有binding paramater被更改了。

结论

标准PDO:Emulates_Prepares ON

PDO::ATTR_ERRMODE ==> PDO::ERRMODE_EXCEPTION
  • 错误总会引发异常。

  • 绑定参数将尝试以合理的方式解释,而不会导致失败。

  • 正常使用此模式。

标准PDO:Emulates_Prepares OFF

PDO::ATTR_ERRMODE ==> PDO::ERRMODE_EXCEPTION
  • 不会一直提出异常

  • 绑定参数将完全按照请求进行解释。

  • 这可能导致从false声明返回PDO execute
  • 这是查询失败的唯一线索。

  • 始终检查PDO preparePDO execute返回 false