web-dev-qa-db-ja.com

PDO準備済みステートメントから生のSQLクエリ文字列を取得する

準備されたステートメントでPDOStatement :: execute()を呼び出すときに、生のSQL文字列を実行する方法はありますか?デバッグの目的では、これは非常に便利です。

122
Wilco

パラメーター値が補間された最終的な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クエリは実行中にパラメーターとnot結合されます。したがって、PDOに表示するものは何もありません。

内部的に、PDO :: ATTR_EMULATE_PREPARESを使用する場合、PDOはSQLクエリのコピーを作成し、準備と実行を行う前にパラメータ値をそれに挿入します。ただし、PDOはこの変更されたSQLクエリを公開しません。

PDOStatementオブジェクトには$ queryStringプロパティがありますが、これはPDOStatementのコンストラクターでのみ設定され、クエリがパラメーターで書き換えられても更新されません。

書き換えられたクエリを公開するようにPDOに要求するのは、PDOにとって合理的な機能要求です。しかし、それでも、PDO :: ATTR_EMULATE_PREPARESを使用しない限り、「完全な」クエリは得られません。

これが、MySQLサーバーの一般的なクエリログを使用する上記の回避策を示す理由です。この場合、パラメータープレースホルダーを使用した準備済みのクエリでさえ、サーバー上で書き換えられ、パラメーター値がクエリ文字列に埋め戻されます。ただし、これはクエリ実行中ではなく、ロギング中にのみ行われます。

106
Bill Karwin
/**
 * 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;
}
105
bigwebguy

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

おそらく少し遅れますが、今は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();

?>
8
Jimmy Kane

PDOStatementには、パブリックプロパティ$ queryStringがあります。それはあなたが望むものでなければなりません。

PDOStatementには文書化されていないメソッドdebugDumpParams()があり、これも参照してください。

8
Glass Robot

マイクによるコードにもう少し追加-値を調べて一重引用符を追加

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

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] => user_test)

4
Otamay

私は自分のニーズに合わせてこの状況を調査するのにかなりの時間を費やしました。これと他のいくつかのSOスレッドは私を大いに助けてくれたので、思いついたことを共有したかった。

トラブルシューティング中に補間されたクエリ文字列にアクセスできることは大きな利点ですが、特定のクエリのみのログを保持できるようにしたかったのです(したがって、この目的でデータベースログを使用するのは理想的ではありませんでした)。また、ログを使用していつでもテーブルの状態を再現できるようにしたかったため、補間された文字列が適切にエスケープされていることを確認する必要がありました。最後に、この機能をコードベース全体に拡張し、可能な限り最小限に書き換える必要がありました(期限、マーケティングなど。ご存知の通りです)。

私の解決策は、デフォルトのPDOStatementオブジェクトの機能を拡張してパラメーター化された値(または参照)をキャッシュし、ステートメントが実行されたときに、PDOオブジェクトの機能を使用して、パラメーターがクエリに戻されたときにパラメーターを適切にエスケープすることでした文字列。次に、ステートメントオブジェクトのメソッドを実行して、その時点で実行された実際のクエリ(または少なくとも可能な限り忠実に再現する)を記録します

前述したように、この機能を追加するためにコードベース全体を変更したくなかったため、PDOStatementオブジェクトのデフォルトのbindParam()およびbindValue()メソッドを上書きし、キャッシュを行いますバインドされたデータを呼び出し、parent::bindParam()またはparent :: bindValue()を呼び出します。これにより、既存のコードベースが通常どおり機能し続けることができました。

最後に、execute()メソッドが呼び出されると、補間を実行し、結果の文字列を新しいプロパティE_PDOStatement->fullQuery。これは、クエリを表示するために出力することも、たとえばログファイルに書き込むこともできます。

拡張機能は、インストールおよび構成の手順とともに、githubで入手できます。

https://github.com/noahheck/E_PDOStatement

[〜#〜]免責事項[〜#〜]
明らかに、私が言ったように、私はこの拡張機能を書きました。ここで多くのスレッドの助けを借りて開発されたので、他の誰かがこれらのスレッドに出くわした場合に備えて、ここに自分のソリューションを投稿したかったのです。

4
myesain

解決策は、自発的にクエリにエラーを入れ、エラーのメッセージを出力することです:

//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]:構文エラーまたはアクセス違反:[...] near 'ELECT * FROM Person WHERE age = 18' at 1行目

クエリの最初の80文字のみを印刷することに注意することが重要です。

4
JacopoStanchi

上記の$ queryStringプロパティは、おそらく、渡されたクエリのみを返し、パラメータは値に置き換えられません。 .Netでは、エラーログにクエリで使用された実際の値を表示できるように、クエリ実行プログラムのcatch部分に、指定された値でパラメータを簡単に検索置換する機能があります。 PHPでパラメーターを列挙し、パラメーターを割り当てられた値に置き換えることができるはずです。

1
Kibbee

sprintf(str_replace('?', '"%s"', $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> = 5.6

0
kurdtpage

私はこの質問が少し古いことを知っていますが、私はずっと前からこのコードを使用しています(@ 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と互換性があります(将来のバージョンも期待しています)。

0
Sakura Kinomoto

やや関連しています...特定の変数をサニタイズしようとしている場合は、 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}%"),
    ),
);
0
Synexis

Bindパラメータの後にクエリ文字列全体を記録する必要があるため、これはコードの一部です。帽子が同じ問題を抱えているすべての人に役立つことを願っています。

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