PDO準備済みステートメントから生のSQLクエリ文字列を取得する
-
03-07-2019 - |
質問
準備されたステートメントでPDOStatement :: execute()を呼び出すときに実行される生のSQL文字列を取得する方法はありますか?デバッグの目的では、これは非常に便利です。
解決
パラメータ値が補間された最終的なSQLクエリが必要だということです。これはデバッグに役立つことを理解していますが、プリペアドステートメントの動作方法ではありません。パラメーターは、クライアント側で準備されたステートメントと結合されないため、PDOはパラメーターと結合されたクエリ文字列にアクセスすることはできません。
prepare()を実行するとSQLステートメントがデータベースサーバーに送信され、execute()を実行するとパラメーターが個別に送信されます。 MySQLの一般的なクエリログには、execute()の後に値が補間された最終的なSQLが表示されます。以下は、私の一般的なクエリログからの抜粋です。 PDOではなくmysql CLIからクエリを実行しましたが、原理は同じです。
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はexecute()時にパラメーターをSQLクエリに補間し、クエリ全体を送信します。 これは真のプリペアドクエリではありません。 execute()の前に変数をSQL文字列に挿入することにより、プリペアドクエリの利点を回避できます。
@afilinaからのコメント:
いいえ、テキストSQLクエリは実行中にパラメーターと結合されません 。したがって、PDOに表示するものは何もありません。
内部では、PDO :: ATTR_EMULATE_PREPARESを使用する場合、PDOはSQLクエリのコピーを作成し、準備と実行を行う前にパラメータ値をそれに挿入します。ただし、PDOはこの変更されたSQLクエリを公開しません。
PDOStatementオブジェクトには$ queryStringプロパティがありますが、これはPDOStatementのコンストラクターでのみ設定され、クエリがパラメーターで書き換えられても更新されません。
書き換えられたクエリを公開するようにPDOに要求するのは、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(?)などのステートメントの配列の出力を処理するようにメソッドを変更しました。
UPDATE: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;
}
PDOStatementには、パブリックプロパティ$ queryStringがあります。それはあなたが望むものでなければなりません。
PDOStatementにはドキュメント化されていないメソッドdebugDumpParams()があり、これも参照してください。
おそらく少し遅れましたが、今は PDOStatement :: debugDumpParams
で詳細を確認できます。準備されたステートメントに含まれる情報を直接ダンプします 出力。使用中のSQLクエリ、 使用されるパラメーター(パラメーター)、パラメーターのリスト、名前、 整数としてのタイプ(paramtype)、それらのキー名または位置、および クエリ内の位置(これがPDOドライバーでサポートされている場合、 それ以外の場合、-1)になります。
例:
<?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();
?>
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;
}
私は自分のニーズに合わせてこの状況を調査するのにかなりの時間を費やしました。このスレッドと他のいくつかのSOスレッドは私を大いに助けてくれたので、思いついたことを共有したいと思いました。
トラブルシューティング中に補間されたクエリ文字列にアクセスできることは大きな利点ですが、特定のクエリのみのログを維持できるようにしたかったのです(したがって、この目的でデータベースログを使用するのは理想的ではありませんでした)。また、ログを使用して任意の時点でテーブルの状態を再作成できるようにしたかったため、補間された文字列が適切にエスケープされたことを確認する必要がありました。最後に、この機能をコードベース全体に拡張し、可能な限り書き直さずに済むようにしました(期限、マーケティングなど。ご存知の通りです)。
私の解決策は、パラメーター化された値(または参照)をキャッシュするようにデフォルトのPDOStatementオブジェクトの機能を拡張し、ステートメントが実行されるときに、PDOオブジェクトの機能を使用してパラメーターが適切にエスケープされて注入されることでしたクエリ文字列に。次に、ステートメントオブジェクトのメソッドを実行して、その時点で実行された実際のクエリを記録します(または少なくとも可能な限り忠実に再現する。
前述したように、この機能を追加するためにコードベース全体を変更したくなかったため、デフォルトの bindParam()
および bindValue()
メソッドを上書きしますPDOStatementオブジェクトのバインドされたデータのキャッシュを実行し、 parent :: bindParam()
またはparent :: bindValue()
を呼び出します。これにより、既存のコードベースが通常どおり機能し続けることができました。
最後に、 execute()
メソッドが呼び出されると、補間を実行し、結果の文字列を新しいプロパティ E_PDOStatement-&gt; fullQuery
として提供します。これは、クエリを表示するために出力することも、たとえばログファイルに書き込むこともできます。
拡張機能は、インストールおよび構成の手順とともに、githubで利用可能です:
https://github.com/noahheck/E_PDOStatement
免責事項:
明らかに、私が言ったように、私はこの拡張機能を書きました。ここにある多くのスレッドの助けを借りて開発されたので、他の誰かがこれらのスレッドに出くわした場合に備えて、ここに自分のソリューションを投稿したいと思いました。
PDOStatementクラスを拡張して、境界変数をキャプチャし、後で使用するために保存できます。次に、2つのメソッドを追加できます。1つは変数のサニタイズ用(debugBindedVariables)、もう1つはこれらの変数を使用してクエリを出力するため(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);
}
}
そして、この継承されたクラスを使用して、目的をデバッグできます。
$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());
結果
ユーザーからユーザーを選択WHERE user = 'user_test'
配列( [:test] =&gt; user_test )
解決策は、自発的にクエリにエラーを入れて、エラーのメッセージを出力することです:
//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]:構文エラーまたはアクセス違反:[...] 'ELECT * FROM Person WHERE age = 18' の1行目
クエリの最初の80文字のみを印刷することに注意することが重要です。
上記の$ 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);
}
この質問は少し古いことは知っていますが、私はずっと前からこのコードを使用しています(@ chris-goからの応答を使用しています)。そして、これらのコードはPHP 7.2で廃止されました
これらのコードの更新版を投稿します(メインコードのクレジットは @bigwebguy 、 @mike および @ chris-go 、全員がこの質問の答えです):
/**
* 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_functionを匿名関数に置き換えていることに注意してください。これにより、これらの優れたコードが機能し、PHP 7.2と互換性があります(将来のバージョンも期待しています)。
sprintf(str_replace( '?'、 '&quot;%s&quot;'、$ sql)、... $ params);
例を次に示します。
function mysqli_prepared_query($link, $sql, $types='', $params=array()) {
echo sprintf(str_replace('?', '"%s"', $sql), ...$params);
//prepare, bind, execute
}
$link = new mysqli($server, $dbusername, $dbpassword, $database);
$sql = "SELECT firstname, lastname FROM users WHERE userage >= ? AND favecolor = ?";
$types = "is"; //integer and string
$params = array(20, "Brown");
if(!$qry = mysqli_prepared_query($link, $sql, $types, $params)){
echo "Failed";
} else {
echo "Success";
}
これはPHPでのみ機能します&gt; = 5.6
マイクの答えは、「再利用」を使用するまで有効です。バインド値。
例:
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)
マイクの答えは、最初の:searchのみを置き換えることができ、2番目の検索は置き換えません。
そこで、私は彼の答えを書き直して、適切に再利用できる複数のパラメーターを処理します。
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;
}
誰かが便利だと思ってほしい。