【问题标题】:How to debug PDO database queries?如何调试 PDO 数据库查询?
【发布时间】:2011-01-25 13:17:16
【问题描述】:

在转向 PDO 之前,我通过连接字符串在 PHP 中创建了 SQL 查询。如果我遇到数据库语法错误,我可以回显最终的 SQL 查询字符串,自己在数据库上尝试,并对其进行调整,直到修复错误,然后将其放回代码中。

准备好的 PDO 语句更快、更好、更安全,但有一件事困扰着我:我从来没有看到发送到数据库的最终查询。当我在 Apache 日志或自定义日志文件中收到有关语法的错误时(我在 catch 块内记录错误),我看不到导致它们的查询。

有没有办法捕获 PDO 发送到数据库的完整 SQL 查询并将其记录到文件中?

【问题讨论】:

标签: php sql pdo


【解决方案1】:

你这样说:

我从来没有看到最终的查询,因为它是 发送到数据库

嗯,实际上,当使用准备好的语句时,没有“最终查询这样的东西:

  • 首先,一条语句被发送到数据库,并在那里准备
    • 数据库解析查询,并构建它的内部表示
  • 而且,当你绑定变量并执行语句时,只有变量被发送到数据库
    • 数据库将值“注入”到其内部的语句表示中


所以,回答你的问题:

有没有办法捕获完整的 PDO 向数据库发送的 SQL 查询 并将其记录到文件中?

否:因为任何地方都没有“完整的 SQL 查询”,所以无法捕获它。


出于调试目的,您可以做的最好的事情是通过将值注入语句的 SQL 字符串来“重新构造”一个​​“真实”的 SQL 查询。

在这种情况下,我通常会做的是:

  • 使用占位符回显与语句对应的 SQL 代码
  • 并在之后使用var_dump(或等效项),以显示参数的值
  • 这通常足以发现可能的错误,即使您没有任何可以执行的“真实”查询。

在调试方面这不是很好——但这就是准备好的语句的代价和它们带来的优势。

【讨论】:

  • 很好的解释 - 谢谢。显然我对它是如何工作的只有模糊的想法。我想当语句准备好时,生成的对象包含一个哈希或数字 ID,可以将其发送回数据库,并带有要插入的参数。
  • 不客气 :-) ;;;我不知道这是如何详细实现的,但我想是这样的——结果就是这样,反正;;;这是准备好的语句的好处之一:如果您必须多次执行相同的查询,它只会被发送到数据库并准备一次:对于每次执行,只会发送数据。
  • 更新:Aaron Patterson 在 Railsconf 2011 上提到他向 Rails 添加了更多准备好的语句,但在 PostgreSQL 中的好处比在 MySQL 中要大得多。他说这是因为 MySQL 在你执行准备好的查询之前实际上并没有创建查询计划。
【解决方案2】:

查看数据库日志

虽然 Pascal MARTIN 是正确的,PDO 不会一次将完整的查询发送到数据库,但 ryeguy 建议使用 DB 的日志记录功能实际上是允许的我来查看由数据库组装和执行的完整查询。

方法如下: (这些说明适用于 Windows 机器上的 MySQL - 您的情况可能会有所不同)

  • my.ini[mysqld] 部分下,添加log 命令,如log="C:\Program Files\MySQL\MySQL Server 5.1\data\mysql.log"
  • 重启 MySQL。
  • 它将开始记录该文件中的每个查询。

该文件会快速增长,因此请务必在完成测试后将其删除并关闭日志记录。

【讨论】:

  • 只是一个注释——我必须避开 my.ini 中的斜线。所以,我的条目看起来像 log="C:\\temp\\MySQL\\mysql.log"。
  • 可能工作取决于PDO::ATTR_EMULATE_PREPARES的设置。有关更多信息,请参阅此答案:stackoverflow.com/questions/10658865/#answer-10658929
  • 因此我讨厌 PDO。
  • @webbiedave - 哦,哇!您的链接答案意味着我的答案仅在 PDO 无法以最佳方式工作时才有效,而是发送整个查询以向后兼容旧版本的 MySQL 或旧驱动程序。很有趣。
  • 在 MySQL 5.5+ 中,您需要 general_log 而不是 log。见dev.mysql.com/doc/refman/5.5/en/query-log.html
【解决方案3】:

您可能想要做的是在语句句柄上使用debugDumpParams()。您可以在将值绑定到准备好的查询后随时运行它(无需execute() 语句)。

它不会为您构建准备好的语句,但会显示您的参数。

【讨论】:

  • 唯一的问题是它输出调试而不是在内部存储它而不“回显”它。我不能这样记录。
  • 您可以使用输出缓冲 (ob_start()...) 来存储输出并记录它。
  • bugs.php.net/bug.php?id=52384 已在 7.1 中修复,您可以看到值 :) 有点晚了,但它是 php
【解决方案4】:

当然可以使用这种模式调试{{ PDO::ATTR_ERRMODE }} 只需在查询前添加新行,即可显示调试行。

$db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING );
$db->query('SELECT *******');  

【讨论】:

  • 你不会在使用准备好的语句时调用->query吗?
  • 谢谢,这对我帮助很大! :)
  • 非常感谢!
【解决方案5】:

一个旧帖子,但也许有人会觉得这很有用;

function pdo_sql_debug($sql,$placeholders){
    foreach($placeholders as $k => $v){
        $sql = preg_replace('/:'.$k.'/',"'".$v."'",$sql);
    }
    return $sql;
}

【讨论】:

  • 对于同样可以处理数字参数的类似函数,请参阅my answer(感谢 php.net 上的评论者)。
【解决方案6】:

这是一个查看有效 SQL 的函数,改编自“Mark”php.net 的评论:

function sql_debug($sql_string, array $params = null) {
    if (!empty($params)) {
        $indexed = $params == array_values($params);
        foreach($params as $k=>$v) {
            if (is_object($v)) {
                if ($v instanceof \DateTime) $v = $v->format('Y-m-d H:i:s');
                else continue;
            }
            elseif (is_string($v)) $v="'$v'";
            elseif ($v === null) $v='NULL';
            elseif (is_array($v)) $v = implode(',', $v);

            if ($indexed) {
                $sql_string = preg_replace('/\?/', $v, $sql_string, 1);
            }
            else {
                if ($k[0] != ':') $k = ':'.$k; //add leading colon if it was left out
                $sql_string = str_replace($k,$v,$sql_string);
            }
        }
    }
    return $sql_string;
}

【讨论】:

  • 为什么str_replace(":$k" ....中的“Mark”在$k之前使用冒号?关联索引已经在 $params 数组中有它。
  • 好问题...这可以解释它:stackoverflow.com/questions/9778887/…。我个人使用这个函数来调试 Doctrine 查询,我认为 Doctrine 使用编号而不是命名参数,所以我没有注意到这个问题。我更新了函数,现在它可以在有或没有前导冒号的情况下工作。
  • 请注意,此解决方案将 :name_long 替换为 :name。至少如果:name 出现在:name_long 之前。 MySQLprepared statements 可以正确处理这个问题,所以不要让你感到困惑。
【解决方案7】:

没有。 PDO 查询不是在客户端准备的。 PDO 只是将 SQL 查询和参数发送到数据库服务器。 数据库 是替换(? 的)。你有两个选择:

  • 使用数据库的日志记录功能(但即便如此,它通常至少在 Postgres 中显示为两个单独的语句(即“非最终”)
  • 输出 SQL 查询和 参数并将其拼凑在一起 你自己

【讨论】:

  • 我从没想过要检查数据库的日志。我在 MySQL 目录中四处寻找,没有看到任何日志文件,但也许日志记录是我必须在某处打开的选项。
  • 是的,你必须打开它。我不知道具体细节,但默认情况下它不会记录每个查询。
【解决方案8】:

除了检查错误日志外,几乎没有提及错误显示, 但有一个相当有用的功能:

<?php
/* Provoke an error -- bogus SQL syntax */
$stmt = $dbh->prepare('bogus sql');
if (!$stmt) {
    echo "\PDO::errorInfo():\n";
    print_r($dbh->errorInfo());
}
?>

(source link)

很明显,这段代码可以修改为异常消息 或任何其他类型的错误处理

【讨论】:

  • 这是错误的方式。 PDO 足够聪明,可以让这段代码毫无用处。只需告诉它在错误时抛出异常。 PHP 将完成剩下的工作,比这个有限的功能更好。另外,,学习不要将所有错误都直接打印到浏览器中。还有更好的方法。
  • 这是官方文档,当然没有人会在生产中打印该错误,这也是来自官方网站(php.net)的示例,请参见代码示例下方的链接。当然更好的是在 PDO 实例化中使用额外的参数 $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) 但不幸的是你无法访问该代码
【解决方案9】:

例如你有这个 pdo 语句:

$query="insert into tblTest (field1, field2, field3)
values (:val1, :val2, :val3)";
$res=$db->prepare($query);
$res->execute(array(
  ':val1'=>$val1,
  ':val2'=>$val2,
  ':val3'=>$val3,
));

现在您可以通过定义这样的数组来获取执行的查询:

$assoc=array(
  ':val1'=>$val1,
  ':val2'=>$val2,
  ':val3'=>$val3,
);
$exQuery=str_replace(array_keys($assoc), array_values($assoc), $query);
echo $exQuery;

【讨论】:

  • 为我工作。您在第二个代码示例中有错误:)); 应该是 );(只有一个圆括号)。
【解决方案10】:

搜索互联网我发现这是一个可以接受的解决方案。使用不同的类而不是 PDO,并且 PDO 函数通过魔术函数调用来调用。我不确定这会造成严重的性能问题。但在向 PDO 添加合理的日志记录功能之前,它可以一直使用。

因此,根据thread,您可以为您的 PDO 连接编写一个包装器,该包装器可以记录并在遇到错误时引发异常。

这是一个简单的例子:

class LoggedPDOSTatement extends PDOStatement    {

function execute ($array)    {
    parent::execute ($array);
    $errors = parent::errorInfo();
    if ($errors[0] != '00000'):
        throw new Exception ($errors[2]);
    endif;
  }

}

因此您可以使用该类而不是 PDOStatement:

$this->db->setAttribute (PDO::ATTR_STATEMENT_CLASS, array ('LoggedPDOStatement', array()));

这里提到了一个 PDO 装饰器实现:

class LoggedPDOStatement    {

function __construct ($stmt)    {
    $this->stmt = $stmt;
}

function execute ($params = null)    {
    $result = $this->stmt->execute ($params); 
    if ($this->stmt->errorCode() != PDO::ERR_NONE):
        $errors = $this->stmt->errorInfo();
        $this->paint ($errors[2]);
    endif;
    return $result;
}

function bindValue ($key, $value)    {
    $this->values[$key] = $value;    
    return $this->stmt->bindValue ($key, $value);
}

function paint ($message = false)    {
    echo '<pre>';
    echo '<table cellpadding="5px">';
    echo '<tr><td colspan="2">Message: ' . $message . '</td></tr>';
    echo '<tr><td colspan="2">Query: ' . $this->stmt->queryString . '</td></tr>';
    if (count ($this->values) > 0):
    foreach ($this->values as $key => $value):
    echo '<tr><th align="left" style="background-color: #ccc;">' . $key . '</th><td>' . $value . '</td></tr>';
    endforeach;
    endif;
    echo '</table>';
    echo '</pre>';
}

function __call ($method, $params)    {
    return call_user_func_array (array ($this->stmt, $method), $params); 
}

}

【讨论】:

    【解决方案11】:

    要在 WAMP 中登录 MySQL,您需要编辑 my.ini(例如,在 wamp\bin\mysql\mysql5.6.17\my.ini 下)

    并添加到[mysqld]:

    general_log = 1
    general_log_file="c:\\tmp\\mysql.log"
    

    【讨论】:

      【解决方案12】:

      这是我创建的一个函数,用于返回带有“已解析”参数的 SQL 查询。

      function paramToString($query, $parameters) {
          if(!empty($parameters)) {
              foreach($parameters as $key => $value) {
                  preg_match('/(\?(?!=))/i', $query, $match, PREG_OFFSET_CAPTURE);
                  $query = substr_replace($query, $value, $match[0][1], 1);
              }
          }
          return $query;
          $query = "SELECT email FROM table WHERE id = ? AND username = ?";
          $values = [1, 'Super'];
      
          echo paramToString($query, $values);
      

      假设你是这样执行的

      $values = array(1, 'SomeUsername');
      $smth->execute($values);
      

      此功能不会在查询中添加引号,但可以为我完成这项工作。

      【讨论】:

      • 我添加了这个:if (is_string($value)) { $value = "'".$value."'"; }
      【解决方案13】:

      如何在 Ubuntu 中调试 PDO mysql 数据库查询

      TL;DR 记录所有查询并跟踪 mysql 日志。

      这些说明适用于我安装的 Ubuntu 14.04。发出命令lsb_release -a 以获取您的版本。您的安装可能会有所不同。

      开启登录mysql

      1. 转到您的开发服务器 cmd 行
      2. 更改目录cd /etc/mysql。您应该会看到一个名为my.cnf 的文件。这就是我们要更改的文件。
      3. 通过输入 cat my.cnf | grep general_log 来验证您是否在正确的位置。这会为您过滤 my.cnf 文件。您应该会看到两个条目:#general_log_file = /var/log/mysql/mysql.log && #general_log = 1
      4. 取消注释这两行并通过您选择的编辑器保存。
      5. 重启mysql:sudo service mysql restart
      6. 您可能还需要重新启动您的网络服务器。 (我不记得我使用的顺序)。对于我的安装,那是 nginx:sudo service nginx restart

      干得好!你都准备好了。现在您所要做的就是跟踪日志文件,这样您就可以实时查看您的应用发出的 PDO 查询。

      跟踪日志以查看您的查询

      输入这个cmdtail -f /var/log/mysql/mysql.log

      您的输出将如下所示:

      73 Connect  xyz@localhost on your_db
      73 Query    SET NAMES utf8mb4
      74 Connect  xyz@localhost on your_db
      75 Connect  xyz@localhost on your_db
      74 Quit 
      75 Prepare  SELECT email FROM customer WHERE email=? LIMIT ?
      75 Execute  SELECT email FROM customer WHERE email='a@b.co' LIMIT 5
      75 Close stmt   
      75 Quit 
      73 Quit 
      

      只要您继续跟踪日志,您的应用进行的任何新查询都会自动弹出。要退出尾部,请点击cmd/ctrl c

      注意事项

      1. 小心:此日志文件可能会变得很大。我只在我的开发服务器上运行它。
      2. 日志文件太大?截断它。这意味着文件保留,但内容被删除。 truncate --size 0 mysql.log
      3. 很酷,日志文件列出了 mysql 连接。我知道其中之一来自我正在转换的遗留 mysqli 代码。第三个来自我的新 PDO 连接。但是,不确定第二个来自哪里。如果您知道快速找到它的方法,请告诉我。

      感谢和感谢

      Nathan Long’s answer above 大喊大叫,让inspo 在Ubuntu 上解决这个问题。还要感谢 dikirill 他对 Nathan 帖子的评论,这使我找到了这个解决方案。

      爱你stackoverflow!

      【讨论】:

        【解决方案14】:

        为调试目的捕获 PDO 豁免的解决方案的问题是它只捕获了 PDO 豁免 (duh),但没有捕获注册为 php 错误的语法错误(我不确定这是为什么,但“为什么”与解决方案无关)。我所有的 PDO 调用都来自一个表模型类,我扩展了我与所有表的所有交互......当我尝试调试代码时这很复杂,因为错误会注册我的执行调用所在的 php 代码行打电话,但没有告诉我电话是从哪里打来的。我使用以下代码解决了这个问题:

        /**
         * Executes a line of sql with PDO.
         * 
         * @param string $sql
         * @param array $params
         */
        class TableModel{
            var $_db; //PDO connection
            var $_query; //PDO query
        
            function execute($sql, $params) { 
                //we're saving this as a global, so it's available to the error handler
                global $_tm;
                //setting these so they're available to the error handler as well
                $this->_sql = $sql;
                $this->_paramArray = $params;            
        
                $this->_db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
                $this->_query = $this->_db->prepare($sql);
        
                try {
                    //set a custom error handler for pdo to catch any php errors
                    set_error_handler('pdoErrorHandler');
        
                    //save the table model object to make it available to the pdoErrorHandler
                    $_tm = $this;
                    $this->_query->execute($params);
        
                    //now we restore the normal error handler
                    restore_error_handler();
                } catch (Exception $ex) {
                    pdoErrorHandler();
                    return false;
                }            
            }
        }
        

        因此,上面的代码捕获了 PDO 异常和 php 语法错误,并以相同的方式处理它们。我的错误处理程序如下所示:

        function pdoErrorHandler() {
            //get all the stuff that we set in the table model
            global $_tm;
            $sql = $_tm->_sql;
            $params = $_tm->_params;
            $query = $tm->_query;
        
            $message = 'PDO error: ' . $sql . ' (' . implode(', ', $params) . ") \n";
        
            //get trace info, so we can know where the sql call originated from
            ob_start();
            debug_backtrace(); //I have a custom method here that parses debug backtrace, but this will work as well
            $trace = ob_get_clean();
        
            //log the error in a civilized manner
            error_log($message);
        
            if(admin(){
                //print error to screen based on your environment, logged in credentials, etc.
                print_r($message);
            }
        }
        

        如果有人对如何向我的错误处理程序获取相关信息而不是将表模型设置为全局变量有更好的想法,我会很高兴听到并编辑我的代码。

        【讨论】:

          【解决方案15】:

          这段代码对我很有用:

          echo str_replace(array_keys($data), array_values($data), $query->queryString);
          

          别忘了用你的名字替换 $data 和 $query

          【讨论】:

            【解决方案16】:

            我使用这个类来调试 PDO(使用Log4PHP

            <?php
            
            /**
             * Extends PDO and logs all queries that are executed and how long
             * they take, including queries issued via prepared statements
             */
            class LoggedPDO extends PDO
            {
            
                public static $log = array();
            
                public function __construct($dsn, $username = null, $password = null, $options = null)
                {
                    parent::__construct($dsn, $username, $password, $options);
                }
            
                public function query($query)
                {
                    $result = parent::query($query);
                    return $result;
                }
            
                /**
                 * @return LoggedPDOStatement
                 */
                public function prepare($statement, $options = NULL)
                {
                    if (!$options) {
                        $options = array();
                    }
                    return new \LoggedPDOStatement(parent::prepare($statement, $options));
                }
            }
            
            /**
             * PDOStatement decorator that logs when a PDOStatement is
             * executed, and the time it took to run
             * @see LoggedPDO
             */
            class LoggedPDOStatement
            {
            
                /**
                 * The PDOStatement we decorate
                 */
                private $statement;
                protected $_debugValues = null;
            
                public function __construct(PDOStatement $statement)
                {
                    $this->statement = $statement;
                }
            
                public function getLogger()
                {
                    return \Logger::getLogger('PDO sql');
                }
            
                /**
                 * When execute is called record the time it takes and
                 * then log the query
                 * @return PDO result set
                 */
                public function execute(array $params = array())
                {
                    $start = microtime(true);
                    if (empty($params)) {
                        $result = $this->statement->execute();
                    } else {
                        foreach ($params as $key => $value) {
                            $this->_debugValues[$key] = $value;
                        }
                        $result = $this->statement->execute($params);
                    }
            
                    $this->getLogger()->debug($this->_debugQuery());
            
                    $time = microtime(true) - $start;
                    $ar = (int) $this->statement->rowCount();
                    $this->getLogger()->debug('Affected rows: ' . $ar . ' Query took: ' . round($time * 1000, 3) . ' ms');
                    return $result;
                }
            
                public function bindValue($parameter, $value, $data_type = false)
                {
                    $this->_debugValues[$parameter] = $value;
                    return $this->statement->bindValue($parameter, $value, $data_type);
                }
            
                public function _debugQuery($replaced = true)
                {
                    $q = $this->statement->queryString;
            
                    if (!$replaced) {
                        return $q;
                    }
            
                    return preg_replace_callback('/:([0-9a-z_]+)/i', array($this, '_debugReplace'), $q);
                }
            
                protected function _debugReplace($m)
                {
                    $v = $this->_debugValues[$m[0]];
            
                    if ($v === null) {
                        return "NULL";
                    }
                    if (!is_numeric($v)) {
                        $v = str_replace("'", "''", $v);
                    }
            
                    return "'" . $v . "'";
                }
            
                /**
                 * Other than execute pass all other calls to the PDOStatement object
                 * @param string $function_name
                 * @param array $parameters arguments
                 */
                public function __call($function_name, $parameters)
                {
                    return call_user_func_array(array($this->statement, $function_name), $parameters);
                }
            }
            

            【讨论】:

              【解决方案17】:

              我已经为此创建了一个现代的 Composer 加载项目/存储库:

              pdo 调试

              找到项目的GitHub home here,查看blog post explaining it here。在你的 composer.json 中添加一行,然后你可以像这样使用它:

              echo debugPDO($sql, $parameters);
              

              $sql 是原始 SQL 语句,$parameters 是参数数组:键是占位符名称(“:user_id”)或未命名参数的编号(“?”),值是..嗯,价值。

              背后的逻辑:此脚本将简单地对参数进行分级并将它们替换为提供的 SQL 字符串。超级简单,但对 99% 的用例都超级有效。注意:这只是一个基本的模拟,不是真正的 PDO 调试(因为这是不可能的,因为 PHP 将原始 SQL 和参数单独发送到 MySQL 服务器)。

              非常感谢来自 StackOverflow 线程 Getting raw SQL query string from PDObigwebguyMike,他们基本上编写了这个脚本背后的整个 main 函数。加油!

              【讨论】:

                【解决方案18】:

                在 Debian NGINX 环境中,我做了以下操作。

                转到/etc/mysql/mysql.conf.d 编辑mysqld.cnf 如果你找到log-error = /var/log/mysql/error.log 在下面添加以下两行。

                general_log_file        = /var/log/mysql/mysql.log
                general_log             = 1
                

                要查看日志,请转到 /var/log/mysqltail -f mysql.log

                如果您在生产环境中,请记住在完成调试后将这些行注释掉删除mysql.log,因为此日志文件会快速增长并且可能很大。

                【讨论】:

                  猜你喜欢
                  • 2012-05-18
                  • 2012-06-17
                  • 1970-01-01
                  • 2018-04-06
                  • 2016-09-03
                  • 1970-01-01
                  • 2014-11-06
                  • 2012-06-22
                  相关资源
                  最近更新 更多