从PDO准备语句获取原始SQL查询字符串

有没有办法让一个准备好的语句调用PDOStatement :: execute()时执行原始SQL字符串? 出于调试目的,这将是非常有用的。

我假设你的意思是你想要最终的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 

如果您设置了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服务器的通用查询日志显示上述解决方法的原因,因为在这种情况下,即使是带有参数占位符的准备好的查询,也会在服务器上重写,并将参数值重新填充到查询字符串中。 但是这只在日志记录过程中完成,而不是在执行查询期间完成

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

我修改了方法,包括处理像WHERE IN(?)语句的数组的输出。

更新:只是添加检查NULL值和重复$ PARAMS,所以实际的$参数值不会被修改。

伟大的工作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; } 

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

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

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

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

将准备好的语句中包含的信息直接转储到输出上。 它将提供正在使用的SQL查询,使用的参数数量(Params),参数列表,其名称,类型(paramtype)作为整数,键名或位置以及查询中的位置由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(); ?> 

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

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

我的解决方案是扩展默认的PDOStatement对象的功能,以缓存参数化的值(或引用),并执行语句时,使用PDO对象的功能正确地转义参数,当它们被重新注入查询串。 然后,我们可以配合执行语句对象的方法并记录当时执行的实际查询( 或至少尽可能忠实地复制)

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

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

该扩展,以及安装和配置说明,可在github上获得:

https://github.com/noahheck/E_PDOStatement

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

您可以扩展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); } } 

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

 $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用户FROM用户WHERE user ='user_test'

Array([:test] => user_test)

上面提到的$ queryString属性可能只会返回传入的查询,而不用参数替换它们的值。 在.Net中,我有我的查询执行器的catch部分执行一个简单的搜索替换参数与它们提供的值,以便错误日志可以显示正在用于查询的实际值。 您应该能够枚举PHP中的参数,并用参数值替换参数。

有点相关…如果你只是想清理一个特定的变量,你可以使用PDO :: quote 。 例如,如果您遇到像CakePHP这样的有限框架,要搜索多个部分LIKE条件:

 $pdo = $this->getDataSource()->getConnection(); $results = $this->find('all', array( 'conditions' => array( 'Model.name LIKE ' . $pdo->quote("%{$keyword1}%"), 'Model.name LIKE ' . $pdo->quote("%{$keyword2}%"), ), ); 

绑定参数后,我需要记录完整的查询字符串,所以这是我的代码中的一块。 希望大家都有同样的问题。

 /** * * @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); } 

迈克的答案是行之有效的,直到你使用“重用”绑定值。
例如:

 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) 

迈克的答案只能取代第一:搜索,而不是第二。
所以,我重写了他的答案,使用多个可以正确重用的参数。

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

preg_replace没有为我工作,当binding_超过9,binding_1和binding_10被替换str_replace(离开0后面),所以我做了反向替换:

 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; 

}

希望有人认为它有用。