Wie kann ich die SQL eines PreparedStatements abrufen?

160

Ich habe eine allgemeine Java-Methode mit der folgenden Methodensignatur:

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

Es öffnet eine Verbindung, erstellt eine PreparedStatementmit der SQL-Anweisung und den Parametern im queryParamsArray variabler Länge, führt sie aus, speichert das ResultSet(in a CachedRowSetImpl) zwischen, schließt die Verbindung und gibt die zwischengespeicherte Ergebnismenge zurück.

Ich habe Ausnahmebehandlung in der Methode, die Fehler protokolliert. Ich protokolliere die SQL-Anweisung als Teil des Protokolls, da sie beim Debuggen sehr hilfreich ist. Mein Problem ist, dass die Protokollierung der String-Variablen sqldie Template-Anweisung mit? S anstelle der tatsächlichen Werte protokolliert. Ich möchte die tatsächliche Anweisung protokollieren, die ausgeführt wurde (oder versucht wurde, ausgeführt zu werden).

Also ... Gibt es eine Möglichkeit, die tatsächliche SQL-Anweisung abzurufen, die von a ausgeführt wird PreparedStatement? ( Ohne es selbst zu erstellen. Wenn ich keinen Weg finde, auf PreparedStatement'sSQL zuzugreifen , werde ich es wahrscheinlich selbst in meinem catches erstellen.)

froadie
quelle
1
Wenn Sie reinen JDBC-Code schreiben, empfehle ich dringend, sich Apache commons-dbutils commons.apache.org/dbutils anzusehen . Dies vereinfacht den JDBC-Code erheblich.
Ken Liu

Antworten:

173

Bei vorbereiteten Anweisungen gibt es keine "SQL-Abfrage":

  • Sie haben eine Anweisung, die Platzhalter enthält
    • Es wird an den DB-Server gesendet
    • und dort vorbereitet
    • Dies bedeutet, dass die SQL-Anweisung "analysiert", analysiert und eine Datenstruktur, die sie darstellt, im Speicher vorbereitet wird
  • Und dann haben Sie Variablen gebunden
    • die an den Server gesendet werden
    • und die vorbereitete Anweisung wird ausgeführt - indem an diesen Daten gearbeitet wird

Es gibt jedoch keine Neukonstruktion einer tatsächlichen echten SQL-Abfrage - weder auf der Java-Seite noch auf der Datenbankseite.

Es gibt also keine Möglichkeit, die SQL der vorbereiteten Anweisung abzurufen, da es keine solche SQL gibt.


Für Debugging-Zwecke sind die Lösungen entweder:

  • Geben Sie den Code der Anweisung mit den Platzhaltern und der Datenliste aus
  • Oder um eine SQL-Abfrage "von Hand" zu "erstellen".
Pascal MARTIN
quelle
22
Obwohl dies funktional wahr ist, hindert nichts den Dienstprogrammcode daran, eine äquivalente unvorbereitete Anweisung zu rekonstruieren. Beispiel in log4jdbc: "In der protokollierten Ausgabe werden für vorbereitete Anweisungen die Bindungsargumente automatisch in die SQL-Ausgabe eingefügt. Dies verbessert die Lesbarkeit und das Debuggen in vielen Fällen erheblich." Sehr nützlich für das Debuggen, solange Sie wissen, dass die Anweisung nicht tatsächlich vom DB-Server ausgeführt wird.
siderisch
6
Dies hängt auch von der Implementierung ab. In MySQL - zumindest in der Version, die ich vor einigen Jahren verwendet habe - hat der JDBC-Treiber tatsächlich eine herkömmliche SQL-Abfrage aus der Vorlage erstellt und Variablen gebunden. Ich denke, diese Version von MySQL hat vorbereitete Anweisungen nicht nativ unterstützt, daher haben sie sie im JDBC-Treiber implementiert.
Jay
@sidereal: das habe ich mit "die Abfrage von Hand erstellen" gemeint ; aber du hast es besser gesagt als ich ;;; @ Jay: Wir haben die gleiche Art von Mechanismus in PHP (echte vorbereitete Anweisungen, wenn unterstützt; pseudo-vorbereitete Anweisungen für Datenbanktreiber, die sie nicht unterstützen)
Pascal MARTIN
6
Wenn Sie java.sql.PreparedStatement verwenden, enthält ein einfaches .toString () auf dem prepareStatement das generierte SQL, das ich in 1.8.0_60
Pr0n
5
@Preston Für Oracle DB zeigt das PreparedStatement # toString () das SQL nicht an. Daher denke ich, dass es vom DB JDBC-Treiber abhängt.
Mike Argyriou
57

Es ist nirgends im JDBC-API-Vertrag definiert, aber wenn Sie Glück haben, kann der betreffende JDBC-Treiber die vollständige SQL durch einfaches Aufrufen zurückgeben PreparedStatement#toString(). Dh

System.out.println(preparedStatement);

Mindestens MySQL 5.x- und PostgreSQL 8.x-JDBC-Treiber unterstützen dies. Die meisten anderen JDBC-Treiber unterstützen dies jedoch nicht. Wenn Sie eine solche haben, verwenden Sie am besten Log4jdbc oder P6Spy .

Alternativ können Sie auch eine generische Funktion schreiben, die a Connection, eine SQL-Zeichenfolge und die Anweisungswerte verwendet und PreparedStatementnach der Protokollierung der SQL-Zeichenfolge und der Werte a zurückgibt . Kickoff-Beispiel:

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

und benutze es als

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

Eine andere Alternative besteht darin, einen benutzerdefinierten PreparedStatementCode zu implementieren, der den Real PreparedStatement bei der Konstruktion umschließt (dekoriert) und alle Methoden überschreibt, sodass er die Methoden des Real aufruft PreparedStatementund die Werte in allen setXXX()Methoden sammelt und die "tatsächliche" SQL-Zeichenfolge träge konstruiert, wann immer eine von Die executeXXX()Methoden werden aufgerufen (eine ziemliche Arbeit, aber die meisten IDEs bieten Autogeneratoren für Dekorationsmethoden an, Eclipse tut dies). Verwenden Sie es schließlich einfach stattdessen. Das ist auch im Grunde das, was P6Spy und seine Kollegen bereits unter der Haube tun.

BalusC
quelle
Das ähnelt der Methode, die ich verwende (Ihre prepareStatement-Methode). Meine Frage ist nicht , wie es zu tun - meine Frage ist , wie man sich einzuloggen die SQL - Anweisung. Ich weiß, dass ich das kann logger.debug(sql + " " + Arrays.asList(values))- ich suche nach einer Möglichkeit, die SQL-Anweisung mit den bereits integrierten Parametern zu protokollieren. Ohne mich selbst zu schleifen und die Fragezeichen zu ersetzen.
Froadie
Gehen Sie dann zum letzten Absatz meiner Antwort oder schauen Sie sich P6Spy an. Sie erledigen die "fiesen" Schleifen- und Ersetzungsarbeiten für Sie;)
BalusC
Die Verbindung zu P6Spy ist jetzt unterbrochen.
Stephen P
@ BalusC Ich bin ein Neuling bei JDBC. Ich habe einen Zweifel. Wenn Sie eine solche generische Funktion schreiben, wird sie PreparedStatementjedes Mal erstellt. Wird das nicht so effizient sein, denn es PreparedStatementgeht darum, sie einmal zu erstellen und überall wiederzuverwenden?
Bhushan
Dies funktioniert auch mit dem Voltdb-JDBC-Treiber, um die vollständige SQL-Abfrage für eine vorbereitete Anweisung abzurufen.
k0pernikus
37

Ich verwende Java 8, JDBC-Treiber mit MySQL Connector Version 5.1.31.

Ich kann echte SQL-Zeichenfolge mit dieser Methode erhalten:

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

Es gibt also Folgendes zurück:

INSERT INTO oc_manufacturer SET manufacturer_id = 23, name = 'Google', sort_order=0;
userlond
quelle
Dies sollte alle positiven Stimmen haben, da es genau das ist, wonach das OP sucht.
HuckIt
Gibt es eine ähnliche Funktion für den Postgres-Treiber?
Davidwessman
Wie bekomme ich es Apache Derby?
Gunasekar
Es funktioniert nicht und löst ClassCastException : java.lang.ClassCastException: oracle.jdbc.driver.T4CPreparedStatement kann nicht in com.mysql.jdbc.JDBC4PreparedStatement umgewandelt werden
Ercan
1
@ErcanDuman, meine Antwort ist nicht universell, sondern bezieht sich nur auf Java 8 und den MySQL JDBC-Treiber.
Userlond
21

Wenn Sie die Abfrage sind die Ausführung und erwartet ein ResultSet(Sie in diesem Szenario sind, zumindest) , dann können Sie einfach anrufen ResultSet‚s getStatement()etwa so:

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

Die Variable executedQueryenthält die Anweisung, mit der die erstellt wurde ResultSet.

Jetzt ist mir klar, dass diese Frage ziemlich alt ist, aber ich hoffe, das hilft jemandem.

Elad Stern
quelle
6
@Elad Stern Es wird gedruckt, oracle.jdbc.driver.OraclePreparedStatementWrapper@1b9ce4b anstatt die ausgeführte SQL-Anweisung zu drucken! Bitte führen Sie uns!
AVA
@AVA, hast du toString () verwendet?
Elad Stern
@EladStern toString () wird verwendet!
AVA
@ AVA, ich bin mir nicht sicher, aber es kann mit Ihrem JDBC-Treiber zu tun haben. Ich habe MySQL-Connector-5 erfolgreich verwendet.
Elad Stern
3
rs.getStatement () gibt nur das Anweisungsobjekt zurück. Es hängt also davon ab, ob der von Ihnen verwendete Treiber .toString () implementiert, der bestimmt, ob Sie SQL
Daz
3

Ich habe meine SQL aus PreparedStatement mit prepareStatement.toString () extrahiert. In meinem Fall gibt toString () einen String wie folgt zurück:

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

Jetzt habe ich eine Methode (Java 8) erstellt, die Regex verwendet, um sowohl Abfragen als auch Werte zu extrahieren und in die Map einzufügen:

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

Diese Methode gibt map zurück, wo wir Schlüssel-Wert-Paare haben:

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

Jetzt - wenn Sie Werte als Liste möchten, können Sie einfach verwenden:

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

Wenn Ihr prepareStatement.toString () anders ist als in meinem Fall, müssen Sie nur den regulären Ausdruck "anpassen".

RichardK
quelle
2

Verwenden von PostgreSQL 9.6.x mit dem offiziellen Java-Treiber 42.2.4:

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

Zeigt die SQL mit der ?bereits ersetzten, was ich gesucht habe. Habe gerade diese Antwort hinzugefügt, um den Postgres-Fall abzudecken.

Ich hätte nie gedacht, dass es so einfach sein könnte.

Christophe Roussy
quelle
1

Ich habe den folgenden Code zum Drucken von SQL aus PrepareStatement implementiert

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[" +  "] ...");
            }

    }

quelle
1

Code-Snippet zum Konvertieren von SQL PreparedStaments mit der Liste der Argumente. Für mich geht das

  /**
         * 
         * 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;
        }
Harter Maheswari
quelle
1

Sehr spät :), aber Sie können die ursprüngliche SQL von einem OraclePreparedStatementWrapper von erhalten

((OraclePreparedStatementWrapper) preparedStatement).getOriginalSql();
user497087
quelle
1
Wenn ich versuche, den Wrapper zu verwenden, heißt es: oracle.jdbc.driver.OraclePreparedStatementWrapperist in oracle.jdbc.driver nicht öffentlich. Kann nicht von außerhalb des Pakets zugegriffen werden. Wie benutzt du diese Klasse?
Spektrum
0

Wenn Sie MySQL verwenden, können Sie die Abfragen mithilfe des Abfrageprotokolls von MySQL protokollieren . Ich weiß nicht, ob andere Anbieter diese Funktion anbieten, aber die Chancen stehen gut, dass sie dies tun.

Ionuț G. Stan
quelle
-1

Einfach funktionieren:

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

Es ist auch gut für die vorbereitete Erklärung.

Hobson
quelle
Dies ist einfach eine .toString()mit ein paar zusätzlichen Zeilen, um unerfahrene Benutzer zu täuschen, und wurde bereits vor Ewigkeiten beantwortet.
Pere
-1

Ich verwende Oralce 11g und konnte das endgültige SQL nicht aus dem PreparedStatement abrufen. Nachdem ich die Antwort von @Pascal MARTIN gelesen habe, verstehe ich warum.

Ich habe gerade die Idee der Verwendung von PreparedStatement aufgegeben und einen einfachen Textformatierer verwendet, der meinen Anforderungen entsprach. Hier ist mein Beispiel:

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

Sie haben herausgefunden, dass sqlInParam dynamisch in einer (for, while) -Schleife erstellt werden kann. Ich habe es einfach gemacht, die MessageFormat-Klasse als Formatierungsvorlage für Zeichenfolgenvorlagen für die SQL-Abfrage zu verwenden.

Diego Tercero
quelle
4
Diese Art sprengt den gesamten Grund für die Verwendung vorbereiteter Anweisungen, z. B. das Vermeiden von SQL-Injection und eine verbesserte Leistung.
Ticktock
1
Ich stimme Ihnen zu 100% zu. Ich hätte klarstellen müssen, dass ich diesen Code höchstens ein paar Mal für eine massive Integration von Massendaten ausführen musste und dringend einen schnellen Weg brauchte, um eine Protokollausgabe zu erhalten, ohne auf die gesamte log4j-Enchilada einzugehen, die für was übertrieben gewesen wäre Ich brauchte. Dies sollte nicht in den Produktionscode gehen :-)
Diego Tercero
Dies funktioniert für mich mit Oracle-Treiber (enthält jedoch keine Parameter):((OraclePreparedStatementWrapper) myPreparedStatement).getOriginalSql()
latj
-4

Dazu benötigen Sie eine JDBC-Verbindung und / oder einen JDBC-Treiber, der die Protokollierung der SQL auf einer niedrigen Ebene unterstützt.

Schauen Sie sich log4jdbc an

siderisch
quelle
3
Schauen Sie sich log4jdbc an und was dann? Wie benutzt man es? Sie gehen zu dieser Site und sehen zufällige Streifzüge durch das Projekt ohne klares Beispiel für die tatsächliche Verwendung der Technologie.
Hooli