準備されたステートメントで PDOStatement::execute() を呼び出すときに実行される生の SQL 文字列を取得する方法はありますか? デバッグ目的では、これは非常に便利です。
17 に答える
パラメーター値が補間された最終的な 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 はパラメータを SQL クエリに補間し、execute() を実行するとクエリ全体を送信します。 これは真の準備済みクエリではありません。 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 (?) などのステートメントの配列の出力を処理するようにしました。
更新: 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::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();
?>
マイクによってコードにもう少し追加されました - 値をたどって一重引用符を追加します
/**
* 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 にはパブリック プロパティ $queryString があります。それはあなたが望むものでなければなりません。
PDOStatement にドキュメント化されていないメソッド debugDumpParams() があることに気付きました。これも参照してください。
上記の $queryString プロパティは、パラメーターが値に置き換えられずに、渡されたクエリのみを返す可能性があります。.Net では、クエリ実行プログラムの catch 部分に単純な検索を実行させ、パラメータを提供された値に置き換えて、クエリに使用されていた実際の値をエラー ログに表示できるようにします。PHP でパラメーターを列挙し、パラメーターを割り当てられた値に置き換えることができるはずです。
既存の回答はどれも完全または安全ではなかったので、次の改善点があるこの関数を思いつきました。
?
名前のない ( ) パラメーターと名前付き ( ) パラメーターの両方で機能し:foo
ます。PDO::quote()を使用して
NULL
、 、int
、float
またはではない値を適切にエスケープしますbool
。"?"
プレースホルダーと":foo"
間違えることなく、文字列値を含む文字列値を適切に処理します。
function interpolateSQL(PDO $pdo, string $query, array $params) : string {
$s = chr(2); // Escape sequence for start of placeholder
$e = chr(3); // Escape sequence for end of placeholder
$keys = [];
$values = [];
// Make sure we use escape sequences that are not present in any value
// to escape the placeholders.
foreach ($params as $key => $value) {
while( mb_stripos($value, $s) !== false ) $s .= $s;
while( mb_stripos($value, $e) !== false ) $e .= $e;
}
foreach ($params as $key => $value) {
// Build a regular expression for each parameter
$keys[] = is_string($key) ? "/$s:$key$e/" : "/$s\?$e/";
// Treat each value depending on what type it is.
// While PDO::quote() has a second parameter for type hinting,
// it doesn't seem reliable (at least for the SQLite driver).
if( is_null($value) ){
$values[$key] = 'NULL';
}
elseif( is_int($value) || is_float($value) ){
$values[$key] = $value;
}
elseif( is_bool($value) ){
$values[$key] = $value ? 'true' : 'false';
}
else{
$value = str_replace('\\', '\\\\', $value);
$values[$key] = $pdo->quote($value);
}
}
// Surround placehodlers with escape sequence, so we don't accidentally match
// "?" or ":foo" inside any of the values.
$query = preg_replace(['/\?/', '/(:[a-zA-Z0-9_]+)/'], ["$s?$e", "$s$1$e"], $query);
// Replace placeholders with actual values
$query = preg_replace($keys, $values, $query, 1, $count);
// Verify that we replaced exactly as many placeholders as there are keys and values
if( $count !== count($keys) || $count !== count($values) ){
throw new \Exception('Number of replacements not same as number of keys and/or values');
}
return $query;
}
さらに改善できると確信しています。
私の場合、最終的には、実際の「準備されていないクエリ」(つまり、プレースホルダーを含む SQL) と JSON エンコードされたパラメーターをログに記録することになりました。ただし、このコードは、最終的な SQL クエリを実際に補間する必要がある一部のユース ケースで使用される可能性があります。
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;
}
誰かが役に立つことを願っています。
bind param の後に完全なクエリ文字列をログに記録する必要があるため、これは私のコードの一部です。同じ問題を抱えているすべての人に役立つことを願っています。
/**
*
* @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 のような限られたフレームワークに行き詰まっている場合に、複数の部分的な LIKE 条件を検索するには:
$pdo = $this->getDataSource()->getConnection();
$results = $this->find('all', array(
'conditions' => array(
'Model.name LIKE ' . $pdo->quote("%{$keyword1}%"),
'Model.name LIKE ' . $pdo->quote("%{$keyword2}%"),
),
);
「再利用」バインド値を使用するまで、マイクの答えはうまく機能しています。
例えば:
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;
}