如何调试PDO数据库查询?


140

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

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

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


4
记录在一个文件/var/log/mysql/*。PDO绑定的参数不会导致语法错误,因此您所需要做的只是准备好的SQL查询。
Xeoncross 2011年

1
请参阅stackoverflow.com/questions/210564/…中的代码(不在接受的答案中)。并不是说已经发布了一些更新。
Mawg说恢复Monica 2014年

1
通过作曲家简单的单行:github.com/panique/pdo-debug
SLIQ

2
Xeoncross的回答对我有所帮助。这是一篇文章,说明如何打开此功能。在许多服务器安装中,默认情况下是关闭的。 pontikis.net/blog/何时及何时启用mysql
mrbinky3000 2014年

2
试试var_dump($pdo_instance->debugDumpParams())
Daniel Petrovaliev 2015年

Answers:


99

你这样说:

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

好吧,实际上,在使用准备好的语句时,没有“ 最终查询 ”之类的东西

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


因此,回答您的问题:

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

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


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

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

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

就调试而言,这并不好-但这是准备好的语句的价格及其带来的优势。


1
很好的解释-谢谢。显然,我对此工作原理只有模糊的想法。我想,当准备语句,结果对象包含哈希或数字ID可与参数被发送回数据库插件。
内森龙

别客气 :-) ;;; 我不知道这是如何实现的,但是我想是这样的-无论如何,结果就是这样。这是使用预处理语句的好处之一:如果必须多次执行相同的查询,则该查询将仅发送到数据库并进行一次准备:对于每次执行,仅发送数据。
Pascal MARTIN'3

1
更新:亚伦·帕特森(Aaron Patterson)在Railsconf 2011上提到,他在Rails中添加了更多准备好的语句,但是PostgreSQL的好处比MySQL大得多。他说这是因为MySQL直到您执行准备好的查询后才真正创建查询计划。
内森·朗

85

在数据库日志中查找

尽管Pascal MARTIN认为PDO不会一次将完整的查询全部发送到数据库是正确的,但是ryeguy关于使用DB的日志记录功能的建议实际上使我可以看到完整的查询是由数据库组装和执行的。

方法如下:(这些说明适用于Windows计算机上的MySQL-您的工作量可能会有所不同)

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

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


1
只是一个注释-我不得不在my.ini中转义斜线。因此,我的输入看起来像log =“ C:\\ temp \\ MySQL \\ mysql.log”。
吉姆(Jim)

4
可能取决于的设置PDO::ATTR_EMULATE_PREPARES。请参阅此答案以了解更多信息:stackoverflow.com/questions/10658865/#answer-10658929
webbiedave'5

23
因此,我讨厌PDO。
Salman 2013年

1
@webbiedave-哦,哇!您的链接答案暗示我的答案仅在PDO不能以最佳方式工作时才起作用,而是发送整个查询以实现与旧版本的MySQL或旧驱动程序的向后兼容性。有趣。
内森·朗

13
在MySQL 5.5+中,您需要general_log而不是log。参见dev.mysql.com/doc/refman/5.5/en/query-log.html
Adrian Macneil 2013年

17

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

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

->query使用准备好的语句时您不会打电话吗?
EoghanM

17

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

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


2
唯一的问题是,它输出调试而不是在内部存储调试而不会“回显”它。我不能这样记录。
里卡多·马丁斯

3
您可以使用输出缓冲(ob_start()...)来存储输出并记录它。
Cranio

bugs.php.net/bug.php?id=52384在7.1中修复,您可以看到这些值:)有点晚了,但它是php
Sander Visser

12

旧文章,但也许​​有人会觉得有用。

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

1
对于也可以处理数字参数的类似功能,请参阅我的答案(感谢php.net上的注释器)。
马特·布朗

9

这是一个函数,用于查看有效的SQL,该函数通过php.net上的 “ Mark”添加注释:

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;
}

为什么“标记”在$ k中使用冒号str_replace(":$k" ....?关联索引已经在$ params数组中具有它。
艾伦(Alan)

好的问题...这可能会解释它:stackoverflow.com/questions/9778887/…。我个人使用此功能调试Doctrine查询,并且我认为Doctrine使用编号而不是命名参数,因此我没有注意到此问题。我更新了该函数,以便现在无论有无冒号都可以使用。
马特·布朗

请注意,此解决方案将替换:name_long:name。至少:name要比以前更重要:name_long。MySQL准备好的语句可以正确处理此问题,因此请不要让它造成混淆。
Zim84 '18

8

不可以。客户端不准备PDO查询。PDO只是将SQL查询和参数发送到数据库服务器。该数据库是什么呢取代(的?“S)。您有两种选择:

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

我从没想过要检查数据库的日志。我在MySQL目录中闲逛,看不到任何日志文件,但是也许日志记录是我必须在某个地方打开的选项。
内森·朗

是的,您必须打开它。我不知道具体细节,但是默认情况下它不会记录每个查询。
ryeguy 2010年

5

除了检查错误日志外,几乎没有任何关于错误显示的内容,但是有一个非常有用的功能:

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

源链接

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


2
这是错误的方式。PDO足够聪明,可以使此代码无用。只要告诉它对错误抛出异常即可。PHP将完成其余的工作,比此有限的功能要好得多。另外,不要将所有错误直接打印到浏览器中。有更好的方法。
您的常识

3
那是官方文档,当然没有人会在生产中打印该错误,这又是官方网站(php.net)的示例,请参见代码示例下面的链接。可以肯定的是,更好的方法是在PDO实例中使用其他参数$ db-> setAttribute(PDO :: ATTR_ERRMODE,PDO :: ERRMODE_EXCEPTION),但不幸的是,您无法访问该代码
Zippp 2013年

4

例如,您有以下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;

1
为我工作。您在第二个代码示例中有误:));应该为);(仅一个圆括号)。
Jasom Dotnet

2

搜索互联网时,我发现这是可以接受的解决方案。使用不同的类代替PDO,并且通过魔术函数调用来调用PDO函数。我不确定这会造成严重的性能问题。但是,直到将有意义的日志记录功能添加到PDO之前,它都可以使用。

因此,根据该线程,您可以为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); 
}

}

2

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

并添加到[mysqld]

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

1

这是我制作的函数,用于返回带有“已解决”参数的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);

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


0

我为调试目的而捕获PDO豁免的解决方案的问题在于,它仅捕获了PDO豁免(duh),却没有捕获被注册为php错误的语法错误(我不确定为什么会这样,但是“为什么”与解决方案无关。我所有的PDO调用都来自一个表模型类,我将其扩展为与所有表的所有交互...在尝试调试代码时,这很复杂,因为错误会在我的execute调用所在的位置注册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);
    }
}

如果有人比将表模型设置为全局变量更好地了解与错误处理程序相关的信息,我将很高兴听到并编辑我的代码。


0

这段代码对我很有用:

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

不要忘记用您的名字替换$ data和$ query


0

我使用此类调试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);
    }
}

0

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

pdo调试

此处找到该项目的GitHub主页,在此处查看说明其博客文章。一行添加到composer.json中,然后您可以像这样使用它:

echo debugPDO($sql, $parameters);

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

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

非常感谢bigwebguy麦克从StackOverflow的线程获取原始的SQL查询字符串从PDO写基本上整个主函数这个脚本后面。大起来!


0

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

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. 您可能还需要重新启动Web服务器。(我不记得我使用的顺序)。对于我的安装,这是nginx的:sudo service nginx restart

干得好!你们都准备好了 现在,您要做的就是拖尾日志文件,以便您可以实时查看应用程序进行的PDO查询。

尾日志查看您的查询

输入此cmd tail -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)在上面的回答大声呼喊,以供inspo在Ubuntu上弄清楚。也要dikirill对Nathan的帖子的评论,这使我了这个解决方案。

爱你stackoverflow!


0

在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因为此日志文件将快速增长并且可能非常庞大。


并非所有人都使用mysql。
令人恐惧的分号
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.