Comment puis-je obtenir le SQL d'un PreparedStatement?

160

J'ai une méthode Java générale avec la signature de méthode suivante:

private static ResultSet runSQLResultSet(String sql, Object... queryParams)

Il ouvre une connexion, construit une PreparedStatementà l'aide de l'instruction sql et des paramètres du queryParamstableau de longueur variable, l'exécute, met en cache le ResultSet(dans a CachedRowSetImpl), ferme la connexion et renvoie le jeu de résultats mis en cache.

J'ai la gestion des exceptions dans la méthode qui enregistre les erreurs. Je consigne l'instruction sql dans le cadre du journal car elle est très utile pour le débogage. Mon problème est que la journalisation de la variable String sqlenregistre l'instruction de modèle avec des? Au lieu des valeurs réelles. Je veux enregistrer l' instruction réelle qui a été exécutée (ou qui a tenté d'exécuter).

Alors ... Existe-t-il un moyen d'obtenir l'instruction SQL réelle qui sera exécutée par un PreparedStatement? ( Sans le construire moi-même. Si je ne trouve pas un moyen d'accéder au PreparedStatement'sSQL, je finirai probablement par le construire moi-même dans mon catches.)

froadie
la source
1
Si vous écrivez du code JDBC directement, je vous recommande vivement regarder commons-dbUtils Apache commons.apache.org/dbutils . Cela simplifie considérablement le code JDBC.
Ken Liu

Réponses:

173

En utilisant des instructions préparées, il n'y a pas de "requête SQL":

  • Vous avez une instruction contenant des espaces réservés
    • il est envoyé au serveur DB
    • et préparé là-bas
    • ce qui signifie que l'instruction SQL est "analysée", analysée, une certaine structure de données la représentant est préparée en mémoire
  • Et puis, vous avez des variables liées
    • qui sont envoyés au serveur
    • et l'instruction préparée est exécutée - travailler sur ces données

Mais il n'y a pas de reconstruction d'une vraie requête SQL réelle - ni du côté Java, ni du côté de la base de données.

Donc, il n'y a aucun moyen d'obtenir le SQL de l'instruction préparée - car il n'y a pas de tel SQL.


À des fins de débogage, les solutions sont les suivantes:

  • Ouput le code de l'instruction, avec les espaces réservés et la liste des données
  • Ou pour "construire" une requête SQL "à la main".
Pascal MARTIN
la source
22
Bien que cela soit fonctionnellement vrai, rien n'empêche le code utilitaire de reconstruire une instruction équivalente non préparée. Par exemple, dans log4jdbc: "Dans la sortie journalisée, pour les instructions préparées, les arguments de liaison sont automatiquement insérés dans la sortie SQL. Cela améliore considérablement la lisibilité et le débogage dans de nombreux cas." Très utile pour le débogage, tant que vous savez que ce n'est pas la façon dont l'instruction est réellement exécutée par le serveur DB.
sidéral
6
Cela dépend également de la mise en œuvre. Dans MySQL - au moins la version que j'utilisais il y a quelques années - le pilote JDBC a en fait construit une requête SQL conventionnelle à partir du modèle et des variables de liaison. Je suppose que cette version de MySQL ne supportait pas les déclarations préparées de manière native, donc ils les ont implémentées dans le pilote JDBC.
Jay
@sidereal: c'est ce que j'entendais par "construire la requête à la main" ; mais tu l'as dit mieux que moi ;;; @Jay: nous avons le même type de mécanisme en place en PHP (de vraies déclarations préparées lorsqu'elles sont supportées; des déclarations pseudo-préparées pour les pilotes de base de données qui ne les supportent pas)
Pascal MARTIN
6
Si vous utilisez java.sql.PreparedStatement, un simple .toString () sur le fichier PreparedStatement inclura le SQL généré que j'ai vérifié dans 1.8.0_60
Pr0n
5
@Preston Pour Oracle DB, PreparedStatement # toString () n'affiche pas le SQL. Par conséquent, je suppose que cela dépend du pilote DB JDBC.
Mike Argyriou
57

Il n'est défini nulle part dans le contrat de l'API JDBC, mais si vous avez de la chance, le pilote JDBC en question peut renvoyer le SQL complet en appelant simplement PreparedStatement#toString(). C'est à dire

System.out.println(preparedStatement);

Au moins les pilotes JDBC MySQL 5.x et PostgreSQL 8.x le prennent en charge. Cependant, la plupart des autres pilotes JDBC ne le prennent pas en charge. Si vous en avez un, votre meilleur pari est d'utiliser Log4jdbc ou P6Spy .

Vous pouvez également écrire une fonction générique qui prend a Connection, une chaîne SQL et les valeurs de l'instruction et retourne a PreparedStatementaprès avoir consigné la chaîne SQL et les valeurs. Exemple de lancement:

public static PreparedStatement prepareStatement(Connection connection, String sql, Object... values) throws SQLException {
    PreparedStatement preparedStatement = connection.prepareStatement(sql);
    for (int i = 0; i < values.length; i++) {
        preparedStatement.setObject(i + 1, values[i]);
    }
    logger.debug(sql + " " + Arrays.asList(values));
    return preparedStatement;
}

et utilisez-le comme

try {
    connection = database.getConnection();
    preparedStatement = prepareStatement(connection, SQL, values);
    resultSet = preparedStatement.executeQuery();
    // ...

Une autre alternative consiste à implémenter une personnalisation PreparedStatementqui enveloppe (décore) le réel PreparedStatement lors de la construction et remplace toutes les méthodes afin d'appeler les méthodes du réel PreparedStatement et de collecter les valeurs dans toutes les setXXX()méthodes et de construire paresseusement la chaîne SQL "réelle" chaque fois que l'un des les executeXXX()méthodes sont appelées (tout un travail, mais la plupart des IDE fournissent des générateurs automatiques pour les méthodes décoratrices, Eclipse le fait). Enfin, utilisez-le à la place. C'est aussi fondamentalement ce que P6Spy et ses consorts font déjà sous le capot.

BalusC
la source
C'est similaire à la méthode que j'utilise (votre méthode prepareStatement). Ma question n'est pas de savoir comment le faire - ma question est de savoir comment enregistrer l'instruction SQL. Je sais que je peux le faire logger.debug(sql + " " + Arrays.asList(values))- je cherche un moyen de consigner l'instruction SQL avec les paramètres déjà intégrés. Sans me boucler et remplacer les points d'interrogation.
froadie
Ensuite, dirigez-vous vers le dernier paragraphe de ma réponse ou regardez P6Spy. Ils font le "vilain" travail de bouclage et de remplacement pour vous;)
BalusC
Le lien vers P6Spy est maintenant rompu.
Stephen P
@BalusC Je suis novice en JDBC. J'ai un doute. Si vous écrivez une fonction générique comme ça, elle créera à PreparedStatementchaque fois. N'est-ce pas une manière pas si efficace PreparedStatementde les créer une fois et de les réutiliser partout?
Bhushan
Cela fonctionne également sur le pilote voltdb jdbc pour obtenir la requête SQL complète pour une instruction préparée.
k0pernikus
37

J'utilise Java 8, pilote JDBC avec connecteur MySQL v. 5.1.31.

Je peux obtenir une vraie chaîne SQL en utilisant cette méthode:

// 1. make connection somehow, it's conn variable
// 2. make prepered statement template
PreparedStatement stmt = conn.prepareStatement(
    "INSERT INTO oc_manufacturer" +
    " SET" +
    " manufacturer_id = ?," +
    " name = ?," +
    " sort_order=0;"
);
// 3. fill template
stmt.setInt(1, 23);
stmt.setString(2, 'Google');
// 4. print sql string
System.out.println(((JDBC4PreparedStatement)stmt).asSql());

Donc, il retourne comme ceci:

INSERT INTO oc_manufacturer SET manufacturer_id = 23, name = 'Google', sort_order=0;
userlond
la source
Cela devrait avoir tous les votes positifs car c'est exactement ce que le PO recherche.
HuckIt
Existe-t-il une fonction similaire pour le pilote Postgres?
davidwessman
Comment l'obtenir pour Apache Derby ?
Gunasekar
Ça ne marche pas et ça jette ClassCastException : java.lang.ClassCastException: oracle.jdbc.driver.T4CPreparedStatement ne peut pas être converti en com.mysql.jdbc.JDBC4PreparedStatement
Ercan
1
@ErcanDuman, ma réponse n'est pas universelle, elle ne couvre que les pilotes Java 8 et MySQL JDBC.
userlond
21

Si vous exécutez la requête et attendez un ResultSet(vous êtes au moins dans ce scénario), vous pouvez simplement appeler ResultSetles getStatement()comme ceci:

ResultSet rs = pstmt.executeQuery();
String executedQuery = rs.getStatement().toString();

La variable executedQuerycontiendra l'instruction qui a été utilisée pour créer leResultSet .

Maintenant, je réalise que cette question est assez ancienne, mais j'espère que cela aidera quelqu'un.

Elad Stern
la source
6
@Elad Stern Il imprime, oracle.jdbc.driver.OraclePreparedStatementWrapper@1b9ce4b au lieu d'imprimer l'instruction sql exécutée! Veuillez nous guider!
AVA
@AVA, avez-vous utilisé toString ()?
Elad Stern
@EladStern toString () est utilisé!
AVA
@AVA, eh bien je ne suis pas sûr mais cela peut avoir à voir avec votre pilote jdbc. J'ai utilisé mysql-connector-5 avec succès.
Elad Stern
3
rs.getStatement () renvoie juste l'objet de l'instruction, donc c'est à savoir si le pilote que vous utilisez implémente .toString () qui détermine si vous récupérerez le SQL
Daz
3

J'ai extrait mon sql de PreparedStatement en utilisant prepareStatement.toString () Dans mon cas, toString () retourne String comme ceci:

org.hsqldb.jdbc.JDBCPreparedStatement@7098b907[sql=[INSERT INTO 
TABLE_NAME(COLUMN_NAME, COLUMN_NAME, COLUMN_NAME) VALUES(?, ?, ?)],
parameters=[[value], [value], [value]]]

Maintenant, j'ai créé une méthode (Java 8), qui utilise regex pour extraire à la fois la requête et les valeurs et les mettre dans la carte:

private Map<String, String> extractSql(PreparedStatement preparedStatement) {
    Map<String, String> extractedParameters = new HashMap<>();
    Pattern pattern = Pattern.compile(".*\\[sql=\\[(.*)],\\sparameters=\\[(.*)]].*");
    Matcher matcher = pattern.matcher(preparedStatement.toString());
    while (matcher.find()) {
      extractedParameters.put("query", matcher.group(1));
      extractedParameters.put("values", Stream.of(matcher.group(2).split(","))
          .map(line -> line.replaceAll("(\\[|])", ""))
          .collect(Collectors.joining(", ")));
    }
    return extractedParameters;
  }

Cette méthode renvoie la carte où nous avons des paires clé-valeur:

"query" -> "INSERT INTO TABLE_NAME(COLUMN_NAME, COLUMN_NAME, COLUMN_NAME) VALUES(?, ?, ?)"
"values" -> "value,  value,  value"

Maintenant, si vous voulez des valeurs sous forme de liste, vous pouvez simplement utiliser:

List<String> values = Stream.of(yourExtractedParametersMap.get("values").split(","))
    .collect(Collectors.toList());

Si votre prepareStatement.toString () est différent de mon cas, c'est juste une question de "réglage" de regex.

RichardK
la source
2

Utilisation de PostgreSQL 9.6.x avec le pilote Java officiel 42.2.4:

...myPreparedStatement.execute...
myPreparedStatement.toString()

Montrera le SQL avec le ?déjà remplacé, ce que je cherchais. Je viens d'ajouter cette réponse pour couvrir l'affaire postgres.

Je n'aurais jamais pensé que cela pouvait être aussi simple.

Christophe Roussy
la source
1

J'ai implémenté le code suivant pour imprimer SQL à partir de PrepareStatement

public void printSqlStatement(PreparedStatement preparedStatement, String sql) throws SQLException{
        String[] sqlArrya= new String[preparedStatement.getParameterMetaData().getParameterCount()];
        try {
               Pattern pattern = Pattern.compile("\\?");
               Matcher matcher = pattern.matcher(sql);
               StringBuffer sb = new StringBuffer();
               int indx = 1;  // Parameter begin with index 1
               while (matcher.find()) {
             matcher.appendReplacement(sb,String.valueOf(sqlArrya[indx]));
               }
               matcher.appendTail(sb);
              System.out.println("Executing Query [" + sb.toString() + "] with Database[" + "] ...");
               } catch (Exception ex) {
                   System.out.println("Executing Query [" + sql + "] with Database[" +  "] ...");
            }

    }

la source
1

Extrait de code pour convertir SQL PreparedStaments avec la liste des arguments. Ça marche pour moi

  /**
         * 
         * formatQuery Utility function which will convert SQL
         * 
         * @param sql
         * @param arguments
         * @return
         */
        public static String formatQuery(final String sql, Object... arguments) {
            if (arguments != null && arguments.length <= 0) {
                return sql;
            }
            String query = sql;
            int count = 0;
            while (query.matches("(.*)\\?(.*)")) {
                query = query.replaceFirst("\\?", "{" + count + "}");
                count++;
            }
            String formatedString = java.text.MessageFormat.format(query, arguments);
            return formatedString;
        }
Harsh Maheswari
la source
1

Très tard :) mais vous pouvez obtenir le SQL d'origine à partir d'un OraclePreparedStatementWrapper par

((OraclePreparedStatementWrapper) preparedStatement).getOriginalSql();
user497087
la source
1
Lorsque j'essaie d'utiliser le wrapper, il dit: oracle.jdbc.driver.OraclePreparedStatementWrappern'est pas public dans oracle.jdbc.driver. Ne peut pas être accédé depuis un package extérieur. Comment utilisez-vous cette classe?
spectre
0

Si vous utilisez MySQL, vous pouvez enregistrer les requêtes en utilisant le journal des requêtes de MySQL . Je ne sais pas si d'autres fournisseurs proposent cette fonctionnalité, mais il y a de fortes chances qu'ils le fassent.

Ionuț G. Stan
la source
-1

Fonctionne simplement:

public static String getSQL (Statement stmt){
    String tempSQL = stmt.toString();

    //please cut everything before sql from statement
    //javadb...: 
    int i1 = tempSQL.indexOf(":")+2;
    tempSQL = tempSQL.substring(i1);

    return tempSQL;
}

C'est bien aussi pour PreparStatement.

Hobson
la source
Ceci est simplement un .toString()avec quelques lignes supplémentaires pour tromper les utilisateurs inexpérimentés, et a déjà été répondu il y a longtemps.
Pere
-1

J'utilise Oralce 11g et je n'ai pas réussi à obtenir le SQL final à partir du PreparedStatement. Après avoir lu la réponse de @Pascal MARTIN, je comprends pourquoi.

J'ai juste abandonné l'idée d'utiliser PreparedStatement et utilisé un formateur de texte simple qui répondait à mes besoins. Voici mon exemple:

//I jump to the point after connexion has been made ...
java.sql.Statement stmt = cnx.createStatement();
String sqlTemplate = "SELECT * FROM Users WHERE Id IN ({0})";
String sqlInParam = "21,34,3434,32"; //some random ids
String sqlFinalSql = java.text.MesssageFormat(sqlTemplate,sqlInParam);
System.out.println("SQL : " + sqlFinalSql);
rsRes = stmt.executeQuery(sqlFinalSql);

Vous comprenez que sqlInParam peut être construit de manière dynamique dans une boucle (for, while).

Diego Tercero
la source
4
Ce genre d'explosion explose toute la raison de l'utilisation d'instructions préparées, comme éviter l'injection SQL et améliorer les performances.
ticktock
1
Je suis d'accord avec vous à 100%. J'aurais dû préciser que j'avais fait exécuter ce code plusieurs fois au plus pour une intégration massive de données en masse et que j'avais désespérément besoin d'un moyen rapide d'avoir une sortie de journal sans entrer dans l'ensemble de l'enchilada log4j, ce qui aurait été excessif pour quoi J'ai eu besoin. Cela ne devrait pas entrer dans le code de production :-)
Diego Tercero
Cela fonctionne pour moi avec le pilote Oracle (mais n'inclut pas de paramètres):((OraclePreparedStatementWrapper) myPreparedStatement).getOriginalSql()
latj
-4

Pour ce faire, vous avez besoin d'une connexion JDBC et / ou d'un pilote qui prend en charge la journalisation du SQL à un niveau bas.

Jetez un œil à log4jdbc

sidéral
la source
3
Jetez un œil à log4jdbc et puis quoi? Comment l'utilisez-vous? Vous allez sur ce site et voyez des divagations aléatoires sur le projet sans exemple clair sur la façon d'utiliser réellement la technologie.
Hooli