Come posso ottenere l'SQL di un PreparedStatement?


160

Ho un metodo Java generale con la seguente firma del metodo:

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

Apre una connessione, crea un PreparedStatementusando l'istruzione sql e i parametri nella queryParamsmatrice di lunghezza variabile, la esegue, memorizza nella cache ResultSet(in a CachedRowSetImpl), chiude la connessione e restituisce il set di risultati memorizzato nella cache.

Ho una gestione delle eccezioni nel metodo che registra gli errori. Registro l'istruzione sql come parte del registro poiché è molto utile per il debug. Il mio problema è che la registrazione della variabile String sqlregistra l'istruzione template con? 'Anziché i valori effettivi. Voglio registrare l' istruzione effettiva che è stata eseguita (o tentata di eseguire).

Quindi ... C'è un modo per ottenere l'istruzione SQL effettiva che verrà eseguita da un PreparedStatement? ( Senza costruirlo da solo. Se non riesco a trovare un modo per accedere a PreparedStatement'sSQL, probabilmente finirò per costruirlo da solo catch.


1
Se stai scrivendo direttamente codice JDBC, ti consiglio vivamente di consultare Apache commons-dbutils commons.apache.org/dbutils . Semplifica notevolmente il codice JDBC.
Ken Liu

Risposte:


173

Utilizzando le istruzioni preparate, non esiste una "query SQL":

  • Hai una dichiarazione, contenente segnaposto
    • viene inviato al server DB
    • e preparato lì
    • il che significa che l'istruzione SQL viene "analizzata", analizzata, alcune strutture di dati che la rappresentano vengono preparate in memoria
  • E, quindi, hai le variabili associate
    • che vengono inviati al server
    • e la dichiarazione preparata viene eseguita - lavorando su quei dati

Ma non è possibile ricostruire una vera query SQL reale, né sul lato Java, né sul lato database.

Pertanto, non è possibile ottenere l'istruzione SQL dell'istruzione preparata, poiché non esiste tale SQL.


A scopo di debug, le soluzioni sono:

  • Esegui il codice dell'istruzione, con i segnaposto e l'elenco dei dati
  • O "costruire" manualmente alcune query SQL ".

22
Anche se questo è funzionalmente vero, non c'è nulla che impedisca al codice di utilità di ricostruire un'istruzione non preparata equivalente. Ad esempio, in log4jdbc: "Nell'output registrato, per le istruzioni preparate, gli argomenti di bind vengono automaticamente inseriti nell'output SQL. Ciò migliora notevolmente la leggibilità e il debug per molti casi." Molto utile per il debug, purché tu sappia che non è come l'istruzione viene effettivamente eseguita dal server DB.
siderale

6
Questo dipende anche dall'implementazione. In MySQL - almeno la versione che stavo usando qualche anno fa - il driver JDBC ha effettivamente creato una query SQL convenzionale dal modello e le variabili di bind. Suppongo che la versione di MySQL non supportasse le istruzioni preparate in modo nativo, quindi le implementarono nel driver JDBC.
Jay

@sidereal: questo è ciò che intendevo per "costruire la query a mano" ; ma l'hai detto meglio di me ;;; @Jay: abbiamo lo stesso tipo di meccanismo in atto in PHP (dichiarazioni realmente preparate quando supportate; dichiarazioni pseudo-preparate per driver di database che non le supportano)
Pascal MARTIN

6
Se stai usando java.sql.PreparedStatement, un semplice .toString () sul preparatoStatement includerà il codice SQL generato che ho verificato in 1.8.0_60
Pr0n

5
@Preston Per Oracle DB PreparedStatement # toString () non mostra l'SQL. Quindi immagino che dipenda dal driver DB JDBC.
Mike Argyriou,

57

Non è definito da nessuna parte nel contratto API JDBC, ma se sei fortunato, il driver JDBC in questione potrebbe restituire l'intero SQL semplicemente chiamando PreparedStatement#toString(). ie

System.out.println(preparedStatement);

Almeno i driver MySQL 5.xe PostgreSQL 8.x JDBC lo supportano. Tuttavia, la maggior parte degli altri driver JDBC non lo supporta. Se ne hai uno, la tua scommessa migliore è usare Log4jdbc o P6Spy .

In alternativa, è anche possibile scrivere una funzione generica che accetta a Connection, una stringa SQL e i valori dell'istruzione e restituisce a PreparedStatementdopo aver registrato la stringa SQL e i valori. Esempio di avvio:

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

e usalo come

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

Un'altra alternativa è quella di implementare un'abitudine PreparedStatementche avvolge (decora) il reale PreparedStatement nella costruzione e sovrascrive tutti i metodi in modo che chiami i metodi del reale PreparedStatement e raccolga i valori in tutti i setXXX()metodi e costruisca pigramente la stringa SQL "effettiva" ogni volta che uno di i executeXXX()metodi sono chiamati (piuttosto un lavoro, ma la maggior parte degli IDE fornisce autogeneratori per i metodi di decorazione, Eclipse fa). Finalmente basta usarlo invece. Questo è fondamentalmente ciò che P6Spy e le consorti già fanno sotto i cofani.


È simile al metodo che sto usando (il tuo metodo PrepStatement). La mia domanda non è come farlo - la mia domanda è come registrare l'istruzione sql. So che posso fare logger.debug(sql + " " + Arrays.asList(values)): sto cercando un modo per registrare l'istruzione sql con i parametri già integrati in essa. Senza farmi un giro e sostituire i punti interrogativi.
froadie

Quindi vai all'ultimo paragrafo della mia risposta o guarda P6Spy. Fanno il "brutto" looping e sostituiscono il lavoro per te;)
BalusC

Il collegamento a P6Spy ora è interrotto.
Stephen P,

@BalusC Sono novizio di JDBC. Ho un dubbio Se scrivi una funzione generica in questo modo, verrà creata PreparedStatementogni volta. Non sarà un modo non così efficiente perché tutto il punto PreparedStatementè crearli una volta e riutilizzarli ovunque?
Bhushan,

Questo funziona anche sul driver jtdbc voltdb per ottenere la query sql completa per un'istruzione preparata.
k0pernikus,

37

Sto usando Java 8, driver JDBC con connettore MySQL v. 5.1.31.

Posso ottenere una vera stringa SQL usando questo metodo:

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

Quindi restituisce smth in questo modo:

INSERT INTO oc_manufacturer SET manufacturer_id = 23, name = 'Google', sort_order=0;

Questo dovrebbe avere tutti i voti in quanto è esattamente ciò che l'OP sta cercando.
HuckIt

Esiste una funzione simile per il driver Postgres?
davidwessman,

Come ottenerlo Apache Derby?
Gunasekar,

Non funziona e genera ClassCastException : java.lang.ClassCastException: oracle.jdbc.driver.T4CPreparedStatement non può essere trasmesso a com.mysql.jdbc.JDBC4PreparedStatement
Ercan

1
@ErcanDuman, la mia risposta non è universale, copre solo i driver Java 8 e MySQL JDBC.
userlond

21

Se si sta eseguendo la query e in attesa di un ResultSet(si è in questo scenario, almeno) allora si può semplicemente chiamare ResultSet's getStatement()in questo modo:

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

La variabile executedQueryconterrà l'istruzione utilizzata per creare il file ResultSet.

Ora, mi rendo conto che questa domanda è piuttosto vecchia, ma spero che questo aiuti qualcuno ..


6
@Elad Stern Stampa, oracle.jdbc.driver.OraclePreparedStatementWrapper@1b9ce4b invece di stampare l'istruzione sql eseguita! Per favore, guidaci!
AVA

@AVA, hai usato toString ()?
Elad Stern l'

@EladStern toString () è usato!
AVA

@AVA, beh non ne sono sicuro ma potrebbe avere a che fare con il tuo driver jdbc. Ho usato mysql-connettore-5 con successo.
Elad Stern,

3
rs.getStatement () restituisce semplicemente l'oggetto statement, quindi dipende dal fatto che il driver che stai utilizzando implementa .toString () che determina se tornerai a SQL
Daz

3

Ho estratto il mio sql da PreparedStatement usando preparStatement.toString () Nel mio caso toString () restituisce String in questo modo:

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

Ora ho creato un metodo (Java 8), che utilizza regex per estrarre sia query che valori e inserirli nella mappa:

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

Questo metodo restituisce la mappa in cui sono presenti coppie chiave-valore:

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

Ora - se vuoi valori come elenco puoi semplicemente usare:

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

Se readyStatement.toString () è diverso rispetto al mio caso, è solo una questione di "regolazione" di regex.


2

Utilizzando PostgreSQL 9.6.x con driver Java ufficiale 42.2.4:

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

Mostrerà l'SQL con il ?già sostituito, che è quello che stavo cercando. Ho appena aggiunto questa risposta per coprire il caso Postgres.

Non avrei mai pensato che potesse essere così semplice.


1

Ho implementato il seguente codice per la stampa di SQL da 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[" +  "] ...");
            }

    }

1

Snippet di codice per convertire SQL PreparedStaments con l'elenco di argomenti. Per me funziona

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

1

Molto tardi :) ma puoi ottenere l'SQL originale da un OraclePreparedStatementWrapper da

((OraclePreparedStatementWrapper) preparedStatement).getOriginalSql();

1
Quando provo ad usare il wrapper dice: oracle.jdbc.driver.OraclePreparedStatementWrappernon è pubblico in oracle.jdbc.driver. Non è possibile accedervi dal pacchetto esterno. Come stai usando quella classe?
spettro

0

Se stai usando MySQL puoi registrare le query usando il registro delle query di MySQL . Non so se altri fornitori offrono questa funzionalità, ma è probabile che lo facciano.


-1

Semplicemente funzione:

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

Va bene anche per Preparated State.


Questo è semplicemente un .toString()paio di righe in più per ingannare gli utenti inesperti, ed è già stato risposto anni fa.
Pere

-1

Sto usando Oralce 11g e non sono riuscito a ottenere l'SQL finale da PreparedStatement. Dopo aver letto la risposta @Pascal MARTIN capisco perché.

Ho appena abbandonato l'idea di utilizzare PreparedStatement e ho usato un semplice formattatore di testo che si adattava alle mie esigenze. Ecco il mio esempio:

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

Capisci che sqlInParam può essere creato dinamicamente in un ciclo (per, mentre) Ho appena reso semplice arrivare al punto di usare la classe MessageFormat per fungere da formatore di modelli di stringa per la query SQL.


4
Questo tipo di esplode l'intero motivo dell'utilizzo di dichiarazioni preparate, come evitare l'iniezione sql e migliorare le prestazioni.
ticktock

1
Sono d'accordo con te al 100%. Avrei dovuto chiarire che avevo eseguito questo codice un paio di volte al massimo per una massiccia integrazione di dati di massa e avevo un disperato bisogno di un modo rapido per avere un po 'di output del log senza andare nell'intera enchilada log4j che sarebbe stata eccessiva per quello che Avevo bisogno. Questo non dovrebbe andare nel codice di produzione :-)
Diego Tercero

Questo funziona per me con il driver Oracle (ma non include i parametri):((OraclePreparedStatementWrapper) myPreparedStatement).getOriginalSql()
latj

-4

Per fare ciò è necessaria una connessione JDBC e / o un driver che supporti la registrazione di sql a un livello basso.

Dai un'occhiata a log4jdbc


3
Dai un'occhiata a log4jdbc e poi cosa? Come lo usi? Vai su quel sito e vedi una confusione casuale sul progetto senza un chiaro esempio su come utilizzare effettivamente la tecnologia.
Hooli,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.