Getting raw SQL query string from PDO prepared statements
在对准备好的语句调用pdoStatement::execute()时,是否有方法执行原始SQL字符串?出于调试目的,这将非常有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | /** * 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; } |
号
我假设您的意思是需要最后一个SQL查询,其中插入了参数值。我知道这对调试很有用,但这不是准备好的语句的工作方式。参数不与客户端上准备好的语句组合在一起,因此PDO不应该访问与其参数组合在一起的查询字符串。
SQL语句在您执行prepare()时发送到数据库服务器,参数在您执行()时单独发送。mysql的常规查询日志确实显示了最终的SQL,其中的值是在执行()之后插入的。下面是我的常规查询日志的摘录。我从mysql cli运行查询,而不是从pdo运行,但原理是相同的。
1 2 3 4 5 | 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 |
如果设置pdo属性pdo::attr_仿真_准备,也可以获得所需的。在此模式下,pdo将参数插入到SQL查询中,并在执行()时发送整个查询。这不是真正准备好的查询。您将通过在execute()之前将变量插入SQL字符串来规避准备好的查询的好处。
@afilina回复评论:
不,执行期间文本SQL查询不与参数组合。所以PDO没有什么可以给你看的。
在内部,如果使用pdo::attr_仿真_准备,pdo会复制SQL查询,并在准备和执行之前将参数值插入其中。但是PDO不会公开这个修改过的SQL查询。
pPostatement对象具有属性$queryString,但它仅在pPostatement的构造函数中设置,并且在用参数重写查询时不会更新。
对于PDO来说,要求他们公开重写的查询是一个合理的特性请求。但即使这样,也不能提供"完整"的查询,除非您使用pdo::attr_仿真_准备。
这就是我上面展示使用mysql服务器的常规查询日志的解决方法的原因,因为在这种情况下,即使是带有参数占位符的准备好的查询也会在服务器上重写,参数值会被反写到查询字符串中。但这只在日志记录期间完成,而不是在查询执行期间。
我修改了该方法,将处理数组输出的语句包括在内(?).
更新:只是增加了对空值和重复的$params的检查,所以实际的$param值不会被修改。
干得好,BigWebguy,谢谢!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | /** * 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; } |
可能有点晚了,但现在有了
Dumps the informations contained by a prepared statement directly on
the output. It will provide the SQL query in use, the number of
parameters used (Params), the list of parameters, with their name,
type (paramtype) as an integer, their key name or position, and the
position in the query (if this is supported by the PDO driver,
otherwise, it will be -1).
号
您可以在官方的php文档中找到更多信息。
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?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(); ?> |
pPostStatement具有公共属性$queryString。这应该是你想要的。
我刚刚注意到pDestatement有一个未记录的debugdumpParams()方法,您可能还需要查看它。
Mike在代码中添加了更多的内容-通过值添加单引号
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | /** * 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; } |
。
您可以扩展pPostatement类来捕获有界变量并存储它们以供以后使用。然后可以添加两个方法,一个用于变量清理(debugbinderables),另一个用于打印带有这些变量的查询(debugquery):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | 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); } } |
。
然后您可以使用这个继承的类来调试目的。
1 2 3 4 5 6 7 8 9 | $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()); |
导致
SELECT user FROM users WHERE user = 'user_test'
Array (
[:test] => user_test
)
号
为了我自己的需要,我花了很多时间研究这种情况。这个和其他几个这样的线程帮助了我很多,所以我想分享我的想法。
虽然在故障排除时可以访问插入的查询字符串是一项重要的好处,但我们希望能够只维护某些查询的日志(因此,为此目的使用数据库日志并不理想)。我们还希望能够在任何给定时间使用日志重新创建表的条件,因此,我们需要确保正确地转义了内插字符串。最后,我们希望将此功能扩展到整个代码库,必须尽可能少地重新编写代码(截止日期、市场营销等;您知道它是怎样的)。
我的解决方案是扩展默认pDestatement对象的功能以缓存参数化值(或引用),在执行语句时,使用pdo对象的功能在参数被注入回查询字符串时正确地转义这些参数。然后我们可以连接到语句对象的execute方法,并记录当时执行的实际查询(或者至少尽可能忠实于复制)。
如我所说,我们不想修改整个代码库来添加这个功能,所以我们覆盖pdoStatement对象的默认
最后,当调用
该扩展以及安装和配置说明在GitHub上提供:
https://github.com/noaheck/e pdoStatement(网址:http://github.com/noaheck/e pdoStatement)
免责声明:显然,正如我提到的,我编写了这个扩展。因为它是在许多线程的帮助下开发的,所以我想在这里发布我的解决方案,以防其他人遇到这些线程,就像我所做的那样。
解决方法是自动在查询中输入错误并打印错误消息:
1 2 3 4 5 6 7 8 9 10 11 12 13 | //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(); } |
。
标准输出:
SQLSTATE[42000]: Syntax error or access violation: [...] near 'ELECT * FROM Person WHERE age=18' at line 1
号
需要注意的是,它只打印查询的前80个字符。
上面提到的$querystring属性可能只返回传入的查询,而不使用其值替换参数。在.NET中,我让查询执行器的catch部分用提供的值对参数进行简单的搜索替换,以便错误日志可以显示用于查询的实际值。您应该能够在PHP中枚举参数,并用它们的赋值替换这些参数。
我知道这个问题有点老了,但是,我从很久以前就开始使用这个代码(我使用了@chris go的response),现在,这些代码在php 7.2中已经过时了。
我将发布这些代码的更新版本(主要代码来自@bigwebguy、@mike和@chris go,它们都是这个问题的答案):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | /** * 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, function(&$v, $k) { if (!is_numeric($v) && $v !="NULL") $v ="\'" . $v ."\'"; }); $query = preg_replace($keys, $values, $query, 1, $count); return $query; } |
注意,代码的更改是在array_walk()函数上进行的,用匿名函数替换create_函数。这使得这些代码具有良好的功能性,并与php 7.2兼容(也希望将来的版本)。
我需要在绑定参数之后记录完整的查询字符串,所以这是我代码中的一部分。希望,它对每个有相同问题的人都有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | /** * * @param string $str * @return string */ public function quote($str) { if (!is_array($str)) { return $this->pdo->quote($str); } else { $str = implode(',', array_map(function($v) { return $this->quote($v); }, $str)); if (empty($str)) { return 'NULL'; } return $str; } } /** * * @param string $query * @param array $params * @return string * @throws Exception */ public function interpolateQuery($query, $params) { $ps = preg_split("/'/is", $query); $pieces = []; $prev = null; foreach ($ps as $p) { $lastChar = substr($p, strlen($p) - 1); if ($lastChar !="\") { if ($prev === null) { $pieces[] = $p; } else { $pieces[] = $prev ."'" . $p; $prev = null; } } else { $prev .= ($prev === null ? '' :"'") . $p; } } $arr = []; $indexQuestionMark = -1; $matches = []; for ($i = 0; $i < count($pieces); $i++) { if ($i % 2 !== 0) { $arr[] ="'" . $pieces[$i] ."'"; } else { $st = ''; $s = $pieces[$i]; while (!empty($s)) { if (preg_match("/(\?|:[A-Z0-9_\-]+)/is", $s, $matches, PREG_OFFSET_CAPTURE)) { $index = $matches[0][1]; $st .= substr($s, 0, $index); $key = $matches[0][0]; $s = substr($s, $index + strlen($key)); if ($key == '?') { $indexQuestionMark++; if (array_key_exists($indexQuestionMark, $params)) { $st .= $this->quote($params[$indexQuestionMark]); } else { throw new Exception('Wrong params in query at ' . $index); } } else { if (array_key_exists($key, $params)) { $st .= $this->quote($params[$key]); } else { throw new Exception('Wrong params in query with key ' . $key); } } } else { $st .= $s; $s = null; } } $arr[] = $st; } } return implode('', $arr); } |
。
有点关联…如果您只是想清理一个特定的变量,那么可以使用pdo::quote。例如,如果您使用有限的框架(如cakephp)搜索多个部分类似的条件:
1 2 3 4 5 6 7 |
号
preg_replace对我不起作用,当binding_超过9时,binding_1和binding_10被str_replace替换(将0留在后面),因此我向后替换:
1 2 3 4 5 6 7 8 | public function interpolateQuery($query, $params) { $keys = array(); $length = count($params)-1; for ($i = $length; $i >=0; $i--) { $query = str_replace(':binding_'.(string)$i, '\''.$params[$i]['val'].'\'', $query); } // $query = str_replace('SQL_CALC_FOUND_ROWS', '', $query, $count); return $query; |
}
希望有人发现它有用。
在您使用"重用"绑定值之前,Mike的答案是有效的。例如:
1 | SELECT * FROM `an_modules` AS `m` LEFT JOIN `an_module_sites` AS `ms` ON m.module_id = ms.module_id WHERE 1 AND `module_enable` = :module_enable AND `site_id` = :site_id AND (`module_system_name` LIKE :search OR `module_version` LIKE :search) |
麦克的回答只能取代第一个:搜索,而不是第二个。所以,我重写了他的答案,使其能够正确地使用多个参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | public function interpolateQuery($query, $params) { $keys = array(); $values = $params; $values_limit = []; $words_repeated = array_count_values(str_word_count($query, 1, ':_')); # build a regular expression for each parameter foreach ($params as $key => $value) { if (is_string($key)) { $keys[] = '/:'.$key.'/'; $values_limit[$key] = (isset($words_repeated[':'.$key]) ? intval($words_repeated[':'.$key]) : 1); } else { $keys[] = '/[?]/'; $values_limit = []; } if (is_string($value)) $values[$key] ="'" . $value ."'"; if (is_array($value)) $values[$key] ="'" . implode("','", $value) ."'"; if (is_null($value)) $values[$key] = 'NULL'; } if (is_array($values)) { foreach ($values as $key => $val) { if (isset($values_limit[$key])) { $query = preg_replace(['/:'.$key.'/'], [$val], $query, $values_limit[$key], $count); } else { $query = preg_replace(['/:'.$key.'/'], [$val], $query, 1, $count); } } unset($key, $val); } else { $query = preg_replace($keys, $values, $query, 1, $count); } unset($keys, $values, $values_limit, $words_repeated); return $query; } |
。