如何调试 PDO 数据库查询?

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

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

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

158276 次浏览

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

  • 使用 DB 的日志记录函数(但即使这样,它通常也会显示为两个独立的语句(即“ not final”) ,至少在 Postgres 中是这样)
  • 输出 SQL 查询和 参数,拼凑起来 你自己

你这么说:

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

实际上,在使用预处理语句时,根本就不存在“ < em > final query ”这样的东西:

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


所以,回答你的问题:

有没有办法捕捉到 由 PDO 发送到数据库的 SQL 查询 然后记录到一个文件里?

没有: 因为在任何地方都没有“ 完成 SQL 查询”,所以没有办法捕捉它。


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

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

  • 使用占位符回显与语句对应的 SQL 代码
  • 然后使用 var_dump (或同等物)来显示参数的值
  • 这通常足以看到一个可能的错误,即使您没有任何可以执行的“实际”查询。

当涉及到调试时,这并不是很好——但这就是准备语句的代价和它们带来的好处。

查看数据库日志

虽然 Pascal MARTIN是正确的,PDO 不会立即向数据库发送完整的查询,但是 Ryeguy建议使用 DB 的日志功能实际上允许我看到数据库组装和执行的完整查询。

方法如下: (这些说明是针对 Windows 机器上的 MySQL 的——你的经验可能会有所不同)

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

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

您可能需要在语句句柄上使用 DumpParams ()。您可以在将值绑定到准备好的查询之后的任何时候运行它(不需要 execute()语句)。

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

一个旧的职位,但也许有人会发现这有用;

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

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

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

例如,您有这个 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;

搜索互联网,我发现这是一个可以接受的解决方案。使用不同的类来代替 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);
}


}

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

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

(源代码链接)

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

这里有一个函数可以看到什么是有效的 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;
}

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

Pdo-debug

找到项目的 GitHub 在这里,查看 博客文章在这里解释。添加一行到您的 poser.json 中,然后您可以像这样使用它:

echo debugPDO($sql, $parameters);

$SQL 是原始 SQL 语句,$properties 是参数的数组: 关键是无名氏(“ : user _ id”)或未命名参数的数目(“ ?”)值是。.价值。

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

非常感谢来自 StackOverflow 线程 从 PDO 获取原始 SQL 查询字符串大个子麦克,它们基本上编写了这个脚本背后的整个主要函数。大点!

我对于为了调试目的而捕获 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);
}
}

如果任何人有任何更好的想法,如何获得相关信息到我的错误处理程序比设置表模型作为一个全局变量,我会很高兴听到它和编辑我的代码。

这个代码对我很有用:

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

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

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

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

[mysqld]中加入:

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

下面是我用来返回带有“已解析”参数的 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);

这个函数不添加引号的查询,但为我的工作。

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

记录所有查询并跟踪 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 查询。

跟踪日志以查看您的查询

输入此 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

只要您继续跟踪日志,您的应用程序做出的任何新查询都会自动弹出到 view 。要离开机尾,按 cmd/ctrl c

笔记

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

信用与感谢

在 Ubuntu 上大声呼喊 上面是 Nathan Long 的回答来解决这个问题。也到 Dikirill为他的评论内森的职位,导致我到这个解决方案。

爱你,堆满了!

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

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

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

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

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