PDOデータベースクエリをデバッグする方法


140

PDOに移行する前に、PHPで文字列を連結してSQLクエリを作成しました。データベース構文エラーが発生した場合は、最終的なSQLクエリ文字列をエコーし​​、データベースで自分で試してみて、エラーが修正されるまで微調整してから、コードに戻すことができます。

準備されたPDOステートメントはより速く、より良く、より安全ですが、1つ気になるのは、データベースに送信される最終クエリが表示されないことです。Apacheログまたはカスタムログファイルの構文に関するエラー(catchブロック内のエラーをログに記録)を取得すると、エラーの原因となったクエリが表示されません。

PDOからデータベースに送信された完全なSQLクエリをキャプチャしてファイルに記録する方法はありますか?


4
それはされてファイルに記録します:/var/log/mysql/*。PDOにバインドされたパラメーターは構文エラーを引き起こすことはないので、必要なのは準備されたSQLクエリだけです。
Xeoncross

1
stackoverflow.com/questions/210564/…のコードを参照してください(承認された回答にはありません)。いくつかの更新が投稿されているわけではありません。
Mawgはモニカを2014

1
Composerによるシンプルな1行:github.com/panique/pdo-debug
Sliq 14

2
Xeoncrossの答えは私を助けました。この機能をオンにする方法を説明する記事は次のとおりです。多くのサーバーインストールでは、デフォルトではオフになっています。 pontikis.net/blog/how-and-when-to-enable-mysql-logs
mrbinky3000

2
試してみるvar_dump($pdo_instance->debugDumpParams())
ダニエルペトロバリエフ2015

回答:


99

あなたはこれを言う:

データベースに送信されるため、最終的なクエリは表示されません

まあ、実際には、準備済みステートメントを使用する場合、「最終クエリ」などはありません

  • まず、ステートメントがDBに送信され、そこで準備されます
    • データベースはクエリを解析し、その内部表現を構築します
  • そして、変数をバインドしてステートメントを実行すると、変数のみがデータベースに送信されます
    • そして、データベースはステートメントの内部表現に値を「注入」します


だから、あなたの質問に答えるには:

PDOからデータベースに送信された完全なSQLクエリをキャプチャしてファイルに記録する方法はありますか?

いいえ:「完全なSQLクエリ」はどこにもないため、それをキャプチャする方法はありません。


デバッグ目的で実行できる最善のことは、ステートメントのSQL文字列に値を注入することにより、「実際の」SQLクエリを「再構築」することです。

私が通常このような状況で行うことは次のとおりです。

  • プレースホルダーを使用して、ステートメントに対応するSQLコードをエコーし​​ます
  • および使用var_dump (または同等のもの)パラメータの値を表示するには、直後にします
  • 実行できる「実際の」クエリがない場合でも、これは通常、考えられるエラーを表示するには十分です。

デバッグに関しては、これは素晴らしいことではありませんが、それは準備されたステートメントの価格とそれらがもたらす利点です。


1
素晴らしい説明-ありがとう。どうやら私はこれがどのように機能するかについて曖昧な考えしか持っていなかったようです。Iステートメントが準備される場合、結果として得られるオブジェクトは、プラグインにパラメータバックデータベースに送信することができるハッシュや数値IDが含まれていると。
ネイサンロング

どういたしまして :-) ;;; これがどのように詳細に実装されているのかはわかりませんが、それはそのようなものだと思います-結果はまったく同じです;;; これは、準備済みステートメントの優れた点の1つです。同じクエリを何度も実行する必要がある場合、DBに送信されて1回だけ準備されます。実行ごとに、データのみが送信されます。
Pascal MARTIN

1
更新:Aaron PattersonはRailsconf 2011で、Railsに準備されたステートメントをさらに追加したが、その利点はMySQLよりもPostgreSQLの方がはるかに重いと述べました。これは、準備されたクエリを実行するまでMySQLが実際にクエリプランを作成しないためだと彼は言った。
Nathan Long

85

データベースログを見る

けれどもパスカルMARTINは、 PDOはすべて、一度にデータベースへの完全なクエリを送信しないことが正しいですryeguy DBのログ機能を使用するの提案は、実際に組み立てられ、データベースで実行されるように私は完全なクエリを参照することができました。

方法は次のとおりです(これらの手順はWindowsマシン上のMySQLを対象としています-走行距離は異なる場合があります)。

  • my.ini下の[mysqld]セクション、追加logのような、コマンドをlog="C:\Program Files\MySQL\MySQL Server 5.1\data\mysql.log"
  • MySQLを再起動します。
  • そのファイル内のすべてのクエリのログが開始されます。

このファイルは急速に大きくなるため、テストが終了したら必ず削除してログをオフにしてください。


1
ただのメモ-my.iniのスラッシュをエスケープする必要がありました。したがって、私のエントリは、log = "C:\\ temp \\ MySQL \\ mysql.log"のようになります。
ジム

4
これが可能の設定に応じて動作しますPDO::ATTR_EMULATE_PREPARES。詳細については、この回答を参照してください:stackoverflow.com/questions/10658865/#answer-10658929
webbiedave

23
このため、PDOは嫌いです。
Salman

1
@webbiedave-ああ、すごい!あなたのリンクされた回答は、PDOが最適に機能していない場合にのみ私の回答が機能することを意味しますが、古いバージョンのMySQLまたは古いドライバーとの下位互換性のためにクエリ全体を送信します。面白い。
Nathan Long

13
MySQL 5.5以降では、general_logではなくが必要ですlogdev.mysql.com/doc/refman/5.5/en/query-log.htmlを
Adrian

17

もちろん、このモードを使用してデバッグでき{{ PDO::ATTR_ERRMODE }} ます。クエリの前に新しい行を追加するだけで、デバッグ行が表示されます。

$db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING );
$db->query('SELECT *******');  

->query準備されたステートメントを使用しているときは呼び出しませんか?
EoghanM

17

おそらく、ステートメントハンドルでdebugDumpParams()を使用する必要があります。準備されたクエリに値をバインドした後はいつでも実行できます(execute()ステートメントは必要ありません)。

準備済みステートメントは作成されませんが、パラメーターが表示されます。


2
唯一の問題は、「エコー」せずに内部に保存するのではなく、デバッグを出力することです。この方法ではログに記録できません。
Ricardo Martins

3
出力バッファリング(ob_start()...)を使用して、出力を保存し、ログに記録できます。
Cranio 2012年

bugs.php.net/bug.php?id=52384 7.1で修正されました。値が表示されるのは少し遅れますが、phpです
Sander Visser

12

古い投稿ですが、おそらく誰かがこれを役にたつでしょう。

function pdo_sql_debug($sql,$placeholders){
    foreach($placeholders as $k => $v){
        $sql = preg_replace('/:'.$k.'/',"'".$v."'",$sql);
    }
    return $sql;
}

1
数値パラメーターも処理できる同様の関数については、私の回答を参照しください(php.netのコメント投稿者に感謝)。
Matt Browne

9

php.netの "Mark"によるコメントから抜粋した、効果的なSQLを確認するための関数を次に示します。

function sql_debug($sql_string, array $params = null) {
    if (!empty($params)) {
        $indexed = $params == array_values($params);
        foreach($params as $k=>$v) {
            if (is_object($v)) {
                if ($v instanceof \DateTime) $v = $v->format('Y-m-d H:i:s');
                else continue;
            }
            elseif (is_string($v)) $v="'$v'";
            elseif ($v === null) $v='NULL';
            elseif (is_array($v)) $v = implode(',', $v);

            if ($indexed) {
                $sql_string = preg_replace('/\?/', $v, $sql_string, 1);
            }
            else {
                if ($k[0] != ':') $k = ':'.$k; //add leading colon if it was left out
                $sql_string = str_replace($k,$v,$sql_string);
            }
        }
    }
    return $sql_string;
}

"Mark"が$ kの前にコロンを使用するのはなぜstr_replace(":$k" ....ですか?連想インデックスはすでに$ params配列にあります。
アラン

良い質問...これはそれを説明するかもしれません:stackoverflow.com/questions/9778887/…。個人的には、この関数を使用してDoctrineクエリをデバッグしました。Doctrineは名前付きパラメーターではなく番号付きパラメーターを使用していると思うので、この問題に気づきませんでした。関数を更新して、先頭のコロンの有無に関係なく機能するようにしました。
Matt Browne

ノート、このソリューションが置き換えられていること:name_long:name。少なくとも:name前に来る場合:name_long。MySQLの準備済みステートメントはこれを正しく処理できるので、混乱しないようにしてください。
Zim84

8

いいえ。PDOクエリはクライアント側で準備されていません。PDOはSQLクエリとパラメーターをデータベースサーバーに送信するだけです。データベースは、(の置換何である?のを)。次の2つのオプションがあります。

  • DBのログ機能を使用します(ただし、少なくともPostgresでは、2つの個別のステートメントとして表示されます(つまり、「最終ではない」))。
  • SQLクエリとパラメーターを出力し、自分でつなぎ合わせる

DBのログを確認することは考えていません。私はMySQLディレクトリを探し回っていて、ログファイルを表示していませんが、おそらくログをどこかでオンにする必要があるオプションです。
Nathan Long

はい、オンにする必要があります。詳細はわかりませんが、デフォルトではすべてのクエリがログに記録されるわけではありません。
ryeguy

5

エラーログの確認以外、エラーの表示についてはほとんど何も言われていませんが、かなり役立つ機能があります。

<?php
/* Provoke an error -- bogus SQL syntax */
$stmt = $dbh->prepare('bogus sql');
if (!$stmt) {
    echo "\PDO::errorInfo():\n";
    print_r($dbh->errorInfo());
}
?>

ソースリンク

このコードを変更して、例外メッセージやその他の種類のエラー処理として使用できることは明らかです。


2
これは間違った方法です。PDOは、このコードを役に立たないほどスマートです。エラーに対して例外をスローするように指示するだけです。PHPが残りの部分を実行します。この制限された機能よりもはるかに優れています。また、すべてのエラーを直接ブラウザに出力しないようにしてください。より良い方法があります。
常識

3
これは公式のドキュメントであり、もちろん本番環境では誰もそのエラーを出力する予定はありませんでした。これも公式サイト(php.net)の例です。コード例の下のリンクを参照してください。そして、PDOインスタンス化内で追加のパラメーター$ db-> setAttribute(PDO :: ATTR_ERRMODE、PDO :: ERRMODE_EXCEPTION)を使用する方が確かにはるかに優れていますが、残念ながらそのコードにアクセスできませんでした
Zippp

4

たとえば、次のpdoステートメントがあります。

$query="insert into tblTest (field1, field2, field3)
values (:val1, :val2, :val3)";
$res=$db->prepare($query);
$res->execute(array(
  ':val1'=>$val1,
  ':val2'=>$val2,
  ':val3'=>$val3,
));

これで、次のような配列を定義して、実行されたクエリを取得できます。

$assoc=array(
  ':val1'=>$val1,
  ':val2'=>$val2,
  ':val3'=>$val3,
);
$exQuery=str_replace(array_keys($assoc), array_values($assoc), $query);
echo $exQuery;

1
私のために働いた。2番目のコードサンプルに誤りがあります:である));必要があります);(丸括弧が1つだけ)。
Jasom Dotnet、2015

2

インターネットで検索すると、これは許容できる解決策であることがわかりました。PDOの代わりに別のクラスが使用され、PDO関数はマジック関数呼び出しを通じて呼び出されます。これが深刻なパフォーマンスの問題を引き起こすかどうかはわかりません。ただし、PDOに適切なログ機能が追加されるまで使用できます。

したがって、このスレッドに従って、PDO接続のラッパーを記述して、エラーが発生したときにログに記録して例外をスローすることができます。

これは簡単な例です:

class LoggedPDOSTatement extends PDOStatement    {

function execute ($array)    {
    parent::execute ($array);
    $errors = parent::errorInfo();
    if ($errors[0] != '00000'):
        throw new Exception ($errors[2]);
    endif;
  }

}

したがって、PDOStatementの代わりにそのクラスを使用できます。

$this->db->setAttribute (PDO::ATTR_STATEMENT_CLASS, array ('LoggedPDOStatement', array()));

ここで言及されたPDOデコレータの実装:

class LoggedPDOStatement    {

function __construct ($stmt)    {
    $this->stmt = $stmt;
}

function execute ($params = null)    {
    $result = $this->stmt->execute ($params); 
    if ($this->stmt->errorCode() != PDO::ERR_NONE):
        $errors = $this->stmt->errorInfo();
        $this->paint ($errors[2]);
    endif;
    return $result;
}

function bindValue ($key, $value)    {
    $this->values[$key] = $value;    
    return $this->stmt->bindValue ($key, $value);
}

function paint ($message = false)    {
    echo '<pre>';
    echo '<table cellpadding="5px">';
    echo '<tr><td colspan="2">Message: ' . $message . '</td></tr>';
    echo '<tr><td colspan="2">Query: ' . $this->stmt->queryString . '</td></tr>';
    if (count ($this->values) > 0):
    foreach ($this->values as $key => $value):
    echo '<tr><th align="left" style="background-color: #ccc;">' . $key . '</th><td>' . $value . '</td></tr>';
    endforeach;
    endif;
    echo '</table>';
    echo '</pre>';
}

function __call ($method, $params)    {
    return call_user_func_array (array ($this->stmt, $method), $params); 
}

}

2

MySQLをWAMPに記録するには、my.iniを編集する必要があります(例:wamp \ bin \ mysql \ mysql5.6.17 \ my.ini)

に追加[mysqld]

general_log = 1
general_log_file="c:\\tmp\\mysql.log"

1

これは、「解決された」パラメータを使用してSQLクエリを返すために作成した関数です。

function paramToString($query, $parameters) {
    if(!empty($parameters)) {
        foreach($parameters as $key => $value) {
            preg_match('/(\?(?!=))/i', $query, $match, PREG_OFFSET_CAPTURE);
            $query = substr_replace($query, $value, $match[0][1], 1);
        }
    }
    return $query;
    $query = "SELECT email FROM table WHERE id = ? AND username = ?";
    $values = [1, 'Super'];

    echo paramToString($query, $values);

あなたがこのように実行すると仮定します

$values = array(1, 'SomeUsername');
$smth->execute($values);

この関数はクエリに引用符を追加しませんが、私のために仕事をします。


0

デバッグ目的でPDO免除をキャッチするソリューションで私が抱えていた問題は、PDO免除(duh)のみをキャッチしましたが、phpエラーとして登録された構文エラーをキャッチしなかったことです(これがなぜなのかはわかりませんが、 "理由」はソリューションには関係ありません)。私のすべてのPDO呼び出しは、すべてのテーブルとのすべてのやり取りのために拡張した単一のテーブルモデルクラスからのものです...コードをデバッグしようとしたとき、これは複雑なことでした。エラーが実行呼び出しがあったphpコードの行を登録するためです。と呼ばれましたが、実際にはどこから電話がかかってきたかはわかりませんでした。この問題を解決するために次のコードを使用しました。

/**
 * Executes a line of sql with PDO.
 * 
 * @param string $sql
 * @param array $params
 */
class TableModel{
    var $_db; //PDO connection
    var $_query; //PDO query

    function execute($sql, $params) { 
        //we're saving this as a global, so it's available to the error handler
        global $_tm;
        //setting these so they're available to the error handler as well
        $this->_sql = $sql;
        $this->_paramArray = $params;            

        $this->_db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->_query = $this->_db->prepare($sql);

        try {
            //set a custom error handler for pdo to catch any php errors
            set_error_handler('pdoErrorHandler');

            //save the table model object to make it available to the pdoErrorHandler
            $_tm = $this;
            $this->_query->execute($params);

            //now we restore the normal error handler
            restore_error_handler();
        } catch (Exception $ex) {
            pdoErrorHandler();
            return false;
        }            
    }
}

したがって、上記のコードはPDO例外とphp構文エラーの両方をキャッチし、同じように処理します。私のエラーハンドラは次のようになります。

function pdoErrorHandler() {
    //get all the stuff that we set in the table model
    global $_tm;
    $sql = $_tm->_sql;
    $params = $_tm->_params;
    $query = $tm->_query;

    $message = 'PDO error: ' . $sql . ' (' . implode(', ', $params) . ") \n";

    //get trace info, so we can know where the sql call originated from
    ob_start();
    debug_backtrace(); //I have a custom method here that parses debug backtrace, but this will work as well
    $trace = ob_get_clean();

    //log the error in a civilized manner
    error_log($message);

    if(admin(){
        //print error to screen based on your environment, logged in credentials, etc.
        print_r($message);
    }
}

テーブルモデルをグローバル変数として設定するよりも、エラーハンドラーに関連する情報を取得する方法について誰かがより良いアイデアを持っている場合、私はそれを聞いてコードを編集したいと思います。


0

このコードは私にとってはうまくいきます:

echo str_replace(array_keys($data), array_values($data), $query->queryString);

$ dataと$ queryを自分の名前に置き換えることを忘れないでください


0

このクラスを使用してPDOをデバッグします(Log4PHPを使用

<?php

/**
 * Extends PDO and logs all queries that are executed and how long
 * they take, including queries issued via prepared statements
 */
class LoggedPDO extends PDO
{

    public static $log = array();

    public function __construct($dsn, $username = null, $password = null, $options = null)
    {
        parent::__construct($dsn, $username, $password, $options);
    }

    public function query($query)
    {
        $result = parent::query($query);
        return $result;
    }

    /**
     * @return LoggedPDOStatement
     */
    public function prepare($statement, $options = NULL)
    {
        if (!$options) {
            $options = array();
        }
        return new \LoggedPDOStatement(parent::prepare($statement, $options));
    }
}

/**
 * PDOStatement decorator that logs when a PDOStatement is
 * executed, and the time it took to run
 * @see LoggedPDO
 */
class LoggedPDOStatement
{

    /**
     * The PDOStatement we decorate
     */
    private $statement;
    protected $_debugValues = null;

    public function __construct(PDOStatement $statement)
    {
        $this->statement = $statement;
    }

    public function getLogger()
    {
        return \Logger::getLogger('PDO sql');
    }

    /**
     * When execute is called record the time it takes and
     * then log the query
     * @return PDO result set
     */
    public function execute(array $params = array())
    {
        $start = microtime(true);
        if (empty($params)) {
            $result = $this->statement->execute();
        } else {
            foreach ($params as $key => $value) {
                $this->_debugValues[$key] = $value;
            }
            $result = $this->statement->execute($params);
        }

        $this->getLogger()->debug($this->_debugQuery());

        $time = microtime(true) - $start;
        $ar = (int) $this->statement->rowCount();
        $this->getLogger()->debug('Affected rows: ' . $ar . ' Query took: ' . round($time * 1000, 3) . ' ms');
        return $result;
    }

    public function bindValue($parameter, $value, $data_type = false)
    {
        $this->_debugValues[$parameter] = $value;
        return $this->statement->bindValue($parameter, $value, $data_type);
    }

    public function _debugQuery($replaced = true)
    {
        $q = $this->statement->queryString;

        if (!$replaced) {
            return $q;
        }

        return preg_replace_callback('/:([0-9a-z_]+)/i', array($this, '_debugReplace'), $q);
    }

    protected function _debugReplace($m)
    {
        $v = $this->_debugValues[$m[0]];

        if ($v === null) {
            return "NULL";
        }
        if (!is_numeric($v)) {
            $v = str_replace("'", "''", $v);
        }

        return "'" . $v . "'";
    }

    /**
     * Other than execute pass all other calls to the PDOStatement object
     * @param string $function_name
     * @param array $parameters arguments
     */
    public function __call($function_name, $parameters)
    {
        return call_user_func_array(array($this->statement, $function_name), $parameters);
    }
}

0

私はこれのために、Composerがロードした最新のプロジェクト/リポジトリを作成しました:

pdo-debug

ここでプロジェクトのGitHubホームを見つけてくださいそれを説明するブログ投稿をここで参照してください。composer.jsonに1行追加すると、次のように使用できます。

echo debugPDO($sql, $parameters);

$ sqlは生のSQLステートメント、$ parametersはパラメーターの配列です。キーはプレースホルダー名( ":user_id")または名前のないパラメーターの数( "?")で、値は..です。値。

背後にあるロジック:このスクリプトは単にパラメーターを調整し、提供されたSQL文字列に置き換えます。非常にシンプルですが、99%のユースケースで非常に効果的です。注:これは単なる基本的なエミュレーションであり、実際のP​​DOデバッグではありません(PHPが生のSQLとパラメーターをMySQLサーバーに分離して送信するため、これは不可能です)。

StackOverflowスレッドのbigwebguyMikeに感謝します。基本的に、このスクリプトの背後にあるメイン関数全体を作成するためにPDOから生のSQLクエリ文字列を取得します。ビッグアップ!


0

PDO mysqlデータベースクエリをデバッグする方法 Ubuntuで

TL; DRすべてのクエリをログに記録し、mysqlログをテールします。

これらの指示は、Ubuntu 14.04をインストールするためのものです。コマンドlsb_release -aを発行して、バージョンを取得します。インストールが異なる場合があります。

mysqlでログをオンにする

  1. 開発サーバーのcmd行に移動します
  2. ディレクトリを変更しcd /etc/mysqlます。というファイルが表示されますmy.cnfます。これが、変更するファイルです。
  3. と入力して、正しい場所にいることを確認しますcat my.cnf | grep general_log。これにより、my.cnfファイルがフィルタリングされます。#general_log_file = /var/log/mysql/mysql.log&&の2つのエントリが表示されます#general_log = 1
  4. これらの2行のコメントを外して、選択したエディターで保存します。
  5. mysqlを再起動しますsudo service mysql restart
  6. ウェブサーバーを再起動する必要があるかもしれません。(使用したシーケンスを思い出せません)。私のインストールでは、それはnginx:sudo service nginx restartです。

よくやった!これで準備は完了です。これで、ログファイルを追跡するだけで、アプリがリアルタイムで実行するPDOクエリを確認できます。

ログをテールしてクエリを確認する

このコマンドを入力してください tail -f /var/log/mysql/mysql.logます。

出力は次のようになります。

73 Connect  xyz@localhost on your_db
73 Query    SET NAMES utf8mb4
74 Connect  xyz@localhost on your_db
75 Connect  xyz@localhost on your_db
74 Quit 
75 Prepare  SELECT email FROM customer WHERE email=? LIMIT ?
75 Execute  SELECT email FROM customer WHERE email='a@b.co' LIMIT 5
75 Close stmt   
75 Quit 
73 Quit 

ログを追跡し続ける限り、アプリが行う新しいクエリは自動的に表示されます。尾を出るには、cmd/ctrl cます。

ノート

  1. 注意:このログファイルは非常に大きくなる可能性があります。私はこれを私の開発サーバーでのみ実行しています。
  2. ログファイルが大きくなりすぎていませんか?それを切り捨てます。つまり、ファイルは残りますが、内容は削除されます。truncate --size 0 mysql.log
  3. ログファイルにmysql接続がリストされていることを確認してください。私はそれらの1つが私の移行元のレガシーmysqliコードからのものであることを知っています。3番目は、新しいPDO接続からのものです。ただし、2番目がどこから来ているのかはわかりません。あなたがそれを見つける簡単な方法を知っているなら、私に知らせてください。

クレジットと感謝

inspoがUbuntuでこれを理解するために、上記のネイサンロングの回答に大声で叫びます。また、にdikirillこの解決策に私を導いたネイサンの投稿への彼のコメントに。

Stackoverflowが大好きです!


0

Debian NGINX環境では、次のことを行いました。

後藤/etc/mysql/mysql.conf.d編集mysqld.cnfあなたが見つけた場合log-error = /var/log/mysql/error.log、以下の2行がそれを怒鳴る追加します。

general_log_file        = /var/log/mysql/mysql.log
general_log             = 1

ログ後藤を参照してください/var/log/mysqlし、 tail -f mysql.log

mysql.logこのログファイルは急速に大きくなり、巨大になる可能性があるため、本番環境で削除している場合は、デバッグが完了したらこれらの行をコメントアウトしてください。


誰もがmysqlを使用するわけではありません。
恐怖のセミコロン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.