web-dev-qa-db-fra.com

Comment déboguer des requêtes de base de données PDO?

Avant de passer à PDO, j'avais créé des requêtes SQL dans PHP en concaténant des chaînes. Si je rencontrais une erreur de syntaxe de base de données, je pourrais simplement faire écho à la chaîne de requête SQL finale, essayez moi-même sur la base de données et Tweak jusqu'à ce que je corrige l'erreur, puis la réintroduire dans le code.

Les instructions PDO préparées sont plus rapides, meilleures et plus sûres, mais une chose me dérange: je ne vois jamais la requête finale telle qu'elle est envoyée à la base de données. Lorsque je reçois des erreurs de syntaxe dans mon journal Apache ou mon fichier de journal personnalisé (je consigne les erreurs dans un bloc catch,), je ne peux pas voir la requête qui les a provoquées.

Existe-t-il un moyen de capturer la requête SQL complète envoyée par PDO à la base de données et de la consigner dans un fichier?

132
Nathan Long

Recherche dans le journal de la base de données

Bien que Pascal MARTIN ait raison de dire que PDO n'envoie pas la requête complète à la base de données en une fois, la suggestion de ryeguy d'utiliser la fonction de journalisation de la base de données m'a permis de voir la requête complète telle qu'assemblée et exécutée par la base de données.

Voici comment: (Ces instructions concernent MySQL sur une machine Windows - votre kilométrage peut varier.)

  • Dans my.ini, Sous la section [mysqld], Ajoutez une commande log, telle que log="C:\Program Files\MySQL\MySQL Server 5.1\data\mysql.log"
  • Redémarrez MySQL.
  • Il va commencer à enregistrer chaque requête dans ce fichier.

Ce fichier grossira rapidement. Veillez donc à le supprimer et à désactiver la journalisation lorsque vous avez terminé les tests.

84
Nathan Long

Tu dis ça :

Je ne vois jamais la requête finale car elle est envoyée à la base de données

Eh bien, en fait, lorsque vous utilisez des instructions préparées, il n’existe pas de requête " finale ":

  • Tout d'abord, une déclaration est envoyée au DB et y est préparée
    • La base de données analyse la requête et en crée une représentation interne.
  • Et, lorsque vous liez des variables et exécutez l'instruction, seules les variables sont envoyées à la base de données
    • Et la base de données "injecte" les valeurs dans sa représentation interne de l'instruction


Donc, pour répondre à votre question :

Existe-t-il un moyen de capturer la requête SQL complète envoyée par PDO à la base de données et de la consigner dans un fichier?

Non: comme il n'y a pas de " requête SQL complète " où que ce soit, il n'y a aucun moyen de la capturer.


La meilleure chose à faire, à des fins de débogage, est de "reconstruire" une requête SQL "réelle" en injectant les valeurs dans la chaîne SQL de l'instruction.

Ce que je fais habituellement, dans ce genre de situation, c'est:

  • faire écho au code SQL qui correspond à l'instruction, avec des espaces réservés
  • et utilise var_dump (ou un équivalent) juste après, pour afficher les valeurs des paramètres
  • C'est généralement suffisant pour voir une erreur possible, même si vous n'avez pas de "vraie" requête que vous puissiez exécuter.

Ce n’est pas formidable pour le débogage, mais c’est le prix des déclarations préparées et des avantages qu’elles apportent.

96
Pascal MARTIN

Bien sûr, vous pouvez déboguer en utilisant ce mode {{ PDO::ATTR_ERRMODE }} Il suffit d’ajouter une nouvelle ligne avant votre requête pour afficher les lignes de débogage.

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

Ce que vous voulez probablement faire est probablement d'utiliser debugDumParams () Il ne construit pas l'instruction préparée pour vous, mais affiche vos paramètres.

14
fijiaaron

Un ancien post, mais peut-être que quelqu'un trouvera cela utile;

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

Voici une fonction pour voir quelle sera l'efficacité du code SQL, adaptée d'un commentaire de "Mark" à l'adresse php.net :

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;
}
9
Matt Browne

Non, les requêtes PDO ne sont pas préparées côté client. PDO envoie simplement la requête SQL et les paramètres au serveur de base de données. Le base de données est ce que fait la substitution (du ? '). Vous avez deux options:

  • Utilisez la fonction de journalisation de votre base de données (mais même dans ce cas, elle est normalement affichée sous la forme de deux instructions distinctes (c'est-à-dire "pas final" au moins avec Postgres)
  • Produisez la requête SQL et les paramètres, puis assemblez-les vous-même.
8
ryeguy

presque rien n'a été dit sur l'affichage des erreurs, à l'exception des journaux d'erreurs de vérification, mais il existe une fonctionnalité plutôt utile:

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

( lien source )

il est clair que ce code peut être modifié pour être utilisé comme message d'exception ou tout autre type de traitement d'erreur

5
Zippp

par exemple, vous avez cette déclaration 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,
));

maintenant vous pouvez obtenir la requête exécutée en définissant un tableau comme ceci:

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

Pour vous connecter à MySQL WAMP , vous devez éditer le fichier my.ini (par exemple, sous wamp\bin\mysql\mysql5.6.17\my.ini).

et ajouter à [mysqld]:

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

En cherchant sur Internet, j'ai trouvé cela comme une solution acceptable. Une classe différente est utilisée à la place de PDO et les fonctions PDO sont appelées via des appels de fonctions magiques. Je ne suis pas sûr que cela crée de graves problèmes de performances. Mais il peut être utilisé jusqu'à ce qu'une fonctionnalité de journalisation sensible soit ajoutée à PDO.

Ainsi, conformément à ce thread , vous pouvez écrire un wrapper pour votre connexion PDO qui peut se connecter et lève une exception lorsque vous obtenez une erreur.

Voici un exemple simple:

class LoggedPDOSTatement extends PDOStatement    {

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

}

vous pouvez donc utiliser cette classe à la place de PDOStatement:

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

Voici une implémentation de décorateur AOP mentionnée:

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
bkilinc

Voici une fonction que j'ai créée pour renvoyer une requête SQL avec des paramètres "résolus".

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

En supposant que vous exécutez comme ça

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

Cette fonction n’ajoute pas de guillemets aux requêtes mais fait le travail à ma place.

1
rezden

j'utilise cette classe pour déboguer PDO (avec 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
Janos Szabo

J'ai créé un projet/référentiel moderne chargé par Composer pour exactement ceci ici:

déboguer

Trouvez le projet accueil GitHub ici , voyez un article de blog l'expliquant ici . Une ligne à ajouter à votre composer.json, et vous pourrez l’utiliser comme ceci:

echo debugPDO($sql, $parameters);

$ sql est l'instruction SQL brute, $ parameters est un tableau de vos paramètres: La clé est le nom de l'espace réservé (": user_id") ou le numéro du paramètre non nommé ("?"), la valeur est .. ainsi, le valeur.

La logique derrière: Ce script va simplement graduer les paramètres et les remplacer dans la chaîne SQL fournie. Super simple, mais super efficace pour 99% de vos cas d'utilisation. Note: Ceci est juste une émulation de base, pas un vrai débogage PDO (car ce n'est pas possible car PHP envoie du SQL brut et des paramètres au serveur MySQL séparé).

Un grand merci à bigwebguy et Mike du fil de discussion StackOverflow Obtention de la chaîne de requête SQL brute de PDO pour écrire la fonction principale entière derrière ce script. Dédicace!

0
Sliq

Le problème que j’avais avec la solution pour intercepter les exemptions de PDO à des fins de débogage est qu’il ne capturait que les exemptions de PDO (duh), mais ne détectait pas les erreurs de syntaxe enregistrées comme des erreurs php (je ne sais pas pourquoi, mais " pourquoi "n’est pas pertinent pour la solution). Tous mes appels PDO proviennent d'une classe de modèle de table unique que j'ai étendue pour toutes mes interactions avec toutes les tables ... cela compliquait les choses lorsque j'essayais de déboguer du code, car l'erreur enregistrerait la ligne de code php où se trouvait mon appel d'exécution. appelé, mais ne m’a pas dit où était l’appel. J'ai utilisé le code suivant pour résoudre ce problème:

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

Ainsi, le code ci-dessus intercepte les DEUX exceptions PDO ET les erreurs de syntaxe php et les traite de la même manière. Mon gestionnaire d'erreur ressemble à ceci:

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

Si quelqu'un a de meilleures idées sur la manière d'obtenir des informations pertinentes pour mon gestionnaire d'erreurs que de définir le modèle de table en tant que variable globale, je serais heureux de l'entendre et de modifier mon code.

0
Troy Knapp

Dans l’environnement Debian NGINX, j’ai fait ce qui suit.

Aller à /etc/mysql/mysql.conf.d modifier mysqld.cnf si tu trouves log-error = /var/log/mysql/error.log ajoutez les 2 lignes suivantes ci-dessous.

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

Pour voir les journaux goto /var/log/mysql et tail -f mysql.log

N'oubliez pas de commenter ces lignes une fois que vous avez terminé le débogage si vous êtes dans un environnement de production delete mysql.log car ce fichier journal va grandir rapidement et peut être énorme.

0
khan

ce code fonctionne très bien pour moi:

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

N'oubliez pas de remplacer $ data et $ query par vos noms

0
user3553866

Comment déboguer les requêtes de la base de données mysql PDO sous Ubuntu

TL; DR Enregistrez toutes vos requêtes et réduisez le journal mysql.

Ces instructions concernent l’installation d’Ubuntu 14.04. Émettre la commande lsb_release -a pour obtenir votre version. Votre installation peut être différente.

Activer la connexion à mysql

  1. Accédez à la ligne de commande du serveur de développement
  2. Changer de répertoire cd /etc/mysql. Vous devriez voir un fichier nommé my.cnf. C’est le fichier que nous allons changer.
  3. Vérifiez que vous êtes au bon endroit en tapant cat my.cnf | grep general_log. Ceci filtre le my.cnf fichier pour vous. Vous devriez voir deux entrées: #general_log_file = /var/log/mysql/mysql.log && #general_log = 1.
  4. Décommentez ces deux lignes et enregistrez-les via votre éditeur de choix.
  5. Redémarrez mysql: Sudo service mysql restart.
  6. Vous devrez peut-être également redémarrer votre serveur Web. (Je ne me souviens pas de la séquence que j’ai utilisée). Pour mon installation, c’est nginx: Sudo service nginx restart.

Bon travail! Vous êtes tous ensemble. Tout ce que vous avez à faire est d’afficher le fichier journal afin que vous puissiez voir en temps réel les requêtes PDO de votre application.

Suivez le journal pour voir vos requêtes

Entrez cette cmd tail -f /var/log/mysql/mysql.log.

Votre sortie ressemblera à ceci:

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='[email protected]' LIMIT 5
75 Close stmt   
75 Quit 
73 Quit 

Toutes les nouvelles requêtes créées par votre application s'afficheront automatiquement , tant que vous continuez à suivre le journal. Pour sortir de la queue, appuyez sur cmd/ctrl c.

Remarques

  1. Attention: ce fichier journal peut devenir énorme. Je ne l’utilise que sur mon serveur de développement.
  2. Le fichier journal devient trop gros? Tronquez-le. Cela signifie que le fichier reste, mais que le contenu est supprimé. truncate --size 0 mysql.log.
  3. Cool que le fichier journal répertorie les connexions mysql. Je sais que l'un de ceux-ci provient de mon ancien code mysqli à partir duquel je suis en train de faire la transition. Le troisième provient de ma nouvelle connexion PDO. Cependant, je ne sais pas d'où vient la seconde. Si vous connaissez un moyen rapide de le trouver, faites le moi savoir.

Crédit et merci

Crier énorme à réponse de Nathan Long ci-dessus pour que l'inspecteur découvre ceci sur Ubuntu. Merci également à dikirill pour son commentaire sur le message de Nathan, qui m’a amené à cette solution.

Je vous aime stackoverflow!

0