从PDO预处理语句中获取原始SQL查询字符串

Wil*_*lco 125 php mysql sql pdo pdostatement

有没有办法在预准备语句上调用PDOStatement :: execute()时获取原始SQL字符串?出于调试目的,这将非常有用.

Bil*_*win 107

我假设你的意思是你想要最终的SQL查询,并将参数值插入其中.我知道这对调试很有用,但它不是预处理语句的工作方式.参数不与客户端的预准备语句组合,因此PDO永远不应该访问与其参数组合的查询字符串.

执行prepare()时,SQL语句将发送到数据库服务器,执行execute()时会单独发送参数.MySQL的通用查询日志确实显示了在执行()后插值的最终SQL.以下是我的常规查询日志的摘录.我从mysql CLI运行查询,而不是从PDO运行,但原理是一样的.

081016 16:51:28 2 Query       prepare s1 from 'select * from foo where i = ?'
                2 Prepare     [2] select * from foo where i = ?
081016 16:51:39 2 Query       set @a =1
081016 16:51:47 2 Query       execute s1 using @a
                2 Execute     [2] select * from foo where i = 1
Run Code Online (Sandbox Code Playgroud)

如果设置PDO属性PDO :: ATTR_EMULATE_PREPARES,也可以获得所需的内容.在此模式下,PDO将参数插入到SQL查询中,并在执行()时发送整个查询. 这不是真正准备好的查询. 您将通过在execute()之前将变量插入SQL字符串来规避准备好的查询的好处.


来自@afilina的评论:

不,文本SQL查询在执行期间与参数组合.所以PDO没有什么可以告诉你的.

在内部,如果使用PDO :: ATTR_EMULATE_PREPARES,PDO会在执行准备和执行之前复制SQL查询并将参数值插入其中.但是PDO不公开这个修改过的SQL查询.

PDOStatement对象具有$ queryString属性,但这仅在PDOStatement的构造函数中设置,并且在使用参数重写查询时不会更新.

PDO要求他们公开重写的查询是一个合理的功能请求.但即使这样也不会给你"完整"的查询,除非你使用PDO :: ATTR_EMULATE_PREPARES.

这就是为什么我展示使用MySQL服务器的通用查询日志的上述解决方法,因为在这种情况下,甚至在服务器上重写带有参数占位符的准备好的查询,并将参数值重新填充到查询字符串中.但这仅在日志记录期间完成,而不是在查询执行期间完成.

  • 当PDO :: ATTR_EMULATE_PREPARES设置为TRUE时,如何获得孔查询? (9认同)
  • 哇,不赞成投票吗?请不要开枪。我只是在描述它是如何工作的。 (3认同)
  • @Yasen Zhelev:如果PDO正在模拟准备,那么它会在准备查询之前将参数值插入到查询中.所以MySQL永远不会看到带参数占位符的查询版本.MySQL仅记录完整查询. (2认同)
  • @Bill:'参数不与客户端的预备语句相结合' - 等待 - 但它们是否在服务器端合并?或者mysql如何将值插入数据库? (2认同)

big*_*guy 106

/**
 * Replaces any parameter placeholders in a query with the value of that
 * parameter. Useful for debugging. Assumes anonymous parameters from 
 * $params are are in the same order as specified in $query
 *
 * @param string $query The sql query with parameter placeholders
 * @param array $params The array of substitution parameters
 * @return string The interpolated query
 */
public static function interpolateQuery($query, $params) {
    $keys = array();

    # build a regular expression for each parameter
    foreach ($params as $key => $value) {
        if (is_string($key)) {
            $keys[] = '/:'.$key.'/';
        } else {
            $keys[] = '/[?]/';
        }
    }

    $query = preg_replace($keys, $params, $query, 1, $count);

    #trigger_error('replaced '.$count.' keys');

    return $query;
}
Run Code Online (Sandbox Code Playgroud)

  • 为什么不使用`strtr()`:更快,更简单,相同的结果.`strtr($ query,$ params);` (6认同)
  • 这是一个好的开始,但是如果 $param 的值本身包含一个问号(“?”),它就会失败。 (2认同)

小智 30

我修改了方法以包括处理像WHERE IN(?)这样的语句的数组输出.

更新:刚添加检查NULL值和重复的$ params,因此不会修改实际的$ param值.

伟大的工作bigwebguy,谢谢!

/**
 * Replaces any parameter placeholders in a query with the value of that
 * parameter. Useful for debugging. Assumes anonymous parameters from 
 * $params are are in the same order as specified in $query
 *
 * @param string $query The sql query with parameter placeholders
 * @param array $params The array of substitution parameters
 * @return string The interpolated query
 */
public function interpolateQuery($query, $params) {
    $keys = array();
    $values = $params;

    # build a regular expression for each parameter
    foreach ($params as $key => $value) {
        if (is_string($key)) {
            $keys[] = '/:'.$key.'/';
        } else {
            $keys[] = '/[?]/';
        }

        if (is_string($value))
            $values[$key] = "'" . $value . "'";

        if (is_array($value))
            $values[$key] = "'" . implode("','", $value) . "'";

        if (is_null($value))
            $values[$key] = 'NULL';
    }

    $query = preg_replace($keys, $values, $query);

    return $query;
}
Run Code Online (Sandbox Code Playgroud)

  • 我认为你必须做`$ values = $ params;`而不是`$ values = array()`. (2认同)

Gla*_*bot 8

PDOStatement有一个公共属性$ queryString.它应该是你想要的.

我刚刚注意到PDOStatement有一个未记录的方法debugDumpParams(),你可能也想看一下.


Chr*_* Go 8

Mike为代码添加了更多内容 - 遍历值以添加单引号

/**
 * Replaces any parameter placeholders in a query with the value of that
 * parameter. Useful for debugging. Assumes anonymous parameters from 
 * $params are are in the same order as specified in $query
 *
 * @param string $query The sql query with parameter placeholders
 * @param array $params The array of substitution parameters
 * @return string The interpolated query
 */
public function interpolateQuery($query, $params) {
    $keys = array();
    $values = $params;

    # build a regular expression for each parameter
    foreach ($params as $key => $value) {
        if (is_string($key)) {
            $keys[] = '/:'.$key.'/';
        } else {
            $keys[] = '/[?]/';
        }

        if (is_array($value))
            $values[$key] = implode(',', $value);

        if (is_null($value))
            $values[$key] = 'NULL';
    }
    // Walk the array to see if we can add single-quotes to strings
    array_walk($values, create_function('&$v, $k', 'if (!is_numeric($v) && $v!="NULL") $v = "\'".$v."\'";'));

    $query = preg_replace($keys, $values, $query, 1, $count);

    return $query;
}
Run Code Online (Sandbox Code Playgroud)

  • 我们在哪里可以看到? (2认同)

Jim*_*ane 8

可能有点晚了,但现在有了 PDOStatement::debugDumpParams

直接在输出上转储预准备语句包含的信息.它将提供正在使用的SQL查询,使用的参数数量(参数),参数列表及其名称,类型(参数类型)作为整数,它们的键名称或位置以及查询中的位置(如果这样PDO驱动程序支持,否则,它将为-1).

你可以在官方的php文档上找到更多

例:

<?php
/* Execute a prepared statement by binding PHP variables */
$calories = 150;
$colour = 'red';
$sth = $dbh->prepare('SELECT name, colour, calories
    FROM fruit
    WHERE calories < :calories AND colour = :colour');
$sth->bindParam(':calories', $calories, PDO::PARAM_INT);
$sth->bindValue(':colour', $colour, PDO::PARAM_STR, 12);
$sth->execute();

$sth->debugDumpParams();

?>
Run Code Online (Sandbox Code Playgroud)

  • 注意 [PHP Bug #52384:PDOStatement::debugDumpParams 不会发出绑定参数值。](https://bugs.php.net/bug.php?id=52384) (2认同)

Jac*_*chi 6

一种解决方案是自愿在查询中放入错误并打印错误消息:

//Connection to the database
$co = new PDO('mysql:dbname=myDB;host=localhost','root','');
//We allow to print the errors whenever there is one
$co->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

//We create our prepared statement
$stmt = $co->prepare("ELECT * FROM Person WHERE age=:age"); //I removed the 'S' of 'SELECT'
$stmt->bindValue(':age','18',PDO::PARAM_STR);
try {
    $stmt->execute();
} catch (PDOException $e) {
    echo $e->getMessage();
}
Run Code Online (Sandbox Code Playgroud)

标准输出:

SQLSTATE [42000]:语法错误或访问冲突:[...] 在第1行的'ELECT * FROM Person WHERE age = 18'附近

重要的是要注意,它仅打印查询的前80个字符。


Noa*_*eck 5

我花了很多时间研究这种情况以满足我自己的需要。这个和其他几个 SO 线程对我帮助很大,所以我想分享我的想法。

虽然在排除故障时访问插入的查询字符串是一个显着的好处,但我们希望能够仅维护某些查询的日志(因此,为此目的使用数据库日志并不理想)。我们还希望能够在任何给定时间使用日志重新创建表的条件,因此,我们需要确保正确转义内插字符串。最后,我们希望将此功能扩展到我们的整个代码库,而必须尽可能少地重写(截止日期、营销等;你知道它是怎么回事)。

我的解决方案是扩展默认 PDOStatement 对象的功能以缓存参数化值(或引用),并在执行语句时,使用 PDO 对象的功能在将参数注入回查询时正确转义参数细绳。然后我们可以绑定语句对象的 execute 方法并记录当时执行的实际查询(或至少尽可能忠实地再现)

正如我所说,我们不想修改整个代码库来添加此功能,因此我们覆盖PDOStatement 对象的默认值bindParam()bindValue()方法,缓存绑定数据,然后调用parent::bindParam()或 parent:: bindValue()。这允许我们现有的代码库继续正常运行。

最后,当execute()调用该方法时,我们执行插值并将结果字符串作为新属性提供E_PDOStatement->fullQuery。这可以输出以查看查询,或者例如写入日志文件。

该扩展以及安装和配置说明可在 github 上找到:

https://github.com/noahheck/E_PDOStatement

免责声明
显然,正如我所提到的,我编写了这个扩展。因为它是在许多线程的帮助下开发的,所以我想在这里发布我的解决方案,以防其他人遇到这些线程,就像我一样。


小智 5

您可以扩展 PDOStatement 类来捕获有界变量并存储它们以备后用。然后可以添加 2 种方法,一种用于变量清理 ( debugBindedVariables ),另一种用于打印带有这些变量的查询 ( debugQuery ):

class DebugPDOStatement extends \PDOStatement{
  private $bound_variables=array();
  protected $pdo;

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

  public function bindValue($parameter, $value, $data_type=\PDO::PARAM_STR){
    $this->bound_variables[$parameter] = (object) array('type'=>$data_type, 'value'=>$value);
    return parent::bindValue($parameter, $value, $data_type);
  }

  public function bindParam($parameter, &$variable, $data_type=\PDO::PARAM_STR, $length=NULL , $driver_options=NULL){
    $this->bound_variables[$parameter] = (object) array('type'=>$data_type, 'value'=>&$variable);
    return parent::bindParam($parameter, $variable, $data_type, $length, $driver_options);
  }

  public function debugBindedVariables(){
    $vars=array();

    foreach($this->bound_variables as $key=>$val){
      $vars[$key] = $val->value;

      if($vars[$key]===NULL)
        continue;

      switch($val->type){
        case \PDO::PARAM_STR: $type = 'string'; break;
        case \PDO::PARAM_BOOL: $type = 'boolean'; break;
        case \PDO::PARAM_INT: $type = 'integer'; break;
        case \PDO::PARAM_NULL: $type = 'null'; break;
        default: $type = FALSE;
      }

      if($type !== FALSE)
        settype($vars[$key], $type);
    }

    if(is_numeric(key($vars)))
      ksort($vars);

    return $vars;
  }

  public function debugQuery(){
    $queryString = $this->queryString;

    $vars=$this->debugBindedVariables();
    $params_are_numeric=is_numeric(key($vars));

    foreach($vars as $key=>&$var){
      switch(gettype($var)){
        case 'string': $var = "'{$var}'"; break;
        case 'integer': $var = "{$var}"; break;
        case 'boolean': $var = $var ? 'TRUE' : 'FALSE'; break;
        case 'NULL': $var = 'NULL';
        default:
      }
    }

    if($params_are_numeric){
      $queryString = preg_replace_callback( '/\?/', function($match) use( &$vars) { return array_shift($vars); }, $queryString);
    }else{
      $queryString = strtr($queryString, $vars);
    }

    echo $queryString.PHP_EOL;
  }
}


class DebugPDO extends \PDO{
  public function __construct($dsn, $username="", $password="", $driver_options=array()) {
    $driver_options[\PDO::ATTR_STATEMENT_CLASS] = array('DebugPDOStatement', array($this));
    $driver_options[\PDO::ATTR_PERSISTENT] = FALSE;
    parent::__construct($dsn,$username,$password, $driver_options);
  }
}
Run Code Online (Sandbox Code Playgroud)

然后你可以使用这个继承的类来调试目的。

$dbh = new DebugPDO('mysql:host=localhost;dbname=test;','user','pass');

$var='user_test';
$sql=$dbh->prepare("SELECT user FROM users WHERE user = :test");
$sql->bindValue(':test', $var, PDO::PARAM_STR);
$sql->execute();

$sql->debugQuery();
print_r($sql->debugBindedVariables());
Run Code Online (Sandbox Code Playgroud)

导致

SELECT user FROM users WHERE user = 'user_test'

数组 ( [:test] => user_test )