Jak mogę uzyskać SQL PreparedStatement?

160

Mam ogólną metodę Java z następującą sygnaturą metody:

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

Otwiera połączenie, buduje za PreparedStatementpomocą instrukcji sql i parametrów w queryParamstablicy o zmiennej długości, uruchamia je, buforuje ResultSet(w a CachedRowSetImpl), zamyka połączenie i zwraca zbuforowany zestaw wyników.

Mam obsługę wyjątków w metodzie, która rejestruje błędy. Loguję instrukcję sql jako część dziennika, ponieważ jest ona bardzo pomocna przy debugowaniu. Mój problem polega na tym, że rejestrowanie zmiennej String sqlrejestruje instrukcję szablonu z? Zamiast rzeczywistych wartości. Chcę zarejestrować rzeczywistą instrukcję, która została wykonana (lub próbowała wykonać).

Więc ... Czy istnieje sposób, aby uzyskać rzeczywistą instrukcję SQL, która zostanie uruchomiona przez PreparedStatement? ( Bez budowania go samodzielnie. Jeśli nie mogę znaleźć sposobu na uzyskanie dostępu do PreparedStatement'sSQL, prawdopodobnie utworzę go samodzielnie w moich catches).

froadie
źródło
1
Jeśli piszesz prosty kod JDBC, zdecydowanie polecam zajrzenie do Apache commons-dbutils commons.apache.org/dbutils . Znacznie upraszcza kod JDBC.
Ken Liu,

Odpowiedzi:

173

Korzystając z przygotowanych wyciągów nie ma „zapytania SQL”:

  • Masz instrukcję zawierającą symbole zastępcze
    • jest wysyłany do serwera DB
    • i tam przygotowane
    • co oznacza, że ​​instrukcja SQL jest „analizowana”, parsowana, a reprezentująca ją struktura danych jest przygotowywana w pamięci
  • A zatem masz powiązane zmienne
    • które są wysyłane na serwer
    • i przygotowana instrukcja jest wykonywana - praca na tych danych

Ale nie ma rekonstrukcji rzeczywistego zapytania SQL - ani po stronie Java, ani po stronie bazy danych.

Nie ma więc możliwości uzyskania SQL przygotowanej instrukcji - bo takiego SQL nie ma.


W celu debugowania rozwiązania są następujące:

  • Wyjdź z kodu instrukcji, z symbolami zastępczymi i listą danych
  • Lub „ręcznie” zbudować zapytanie SQL.
Pascal MARTIN
źródło
22
Chociaż jest to funkcjonalnie prawdziwe, nic nie stoi na przeszkodzie, aby kod narzędziowy zrekonstruował równoważną nieprzygotowaną instrukcję. Na przykład w log4jdbc: „W logowanym wyjściu dla przygotowanych instrukcji argumenty bind są automatycznie wstawiane do wyniku SQL. To znacznie poprawia czytelność i debugowanie w wielu przypadkach”. Bardzo przydatne do debugowania, o ile jesteś świadomy, że nie jest to sposób, w jaki instrukcja jest faktycznie wykonywana przez serwer bazy danych.
gwiazdorstwo
6
Zależy to również od implementacji. W MySQL - przynajmniej w wersji, której używałem kilka lat temu - sterownik JDBC faktycznie zbudował konwencjonalne zapytanie SQL z szablonu i przypisania zmiennych. Wydaje mi się, że ta wersja MySQL nie wspierała natywnie przygotowanych instrukcji, więc zaimplementowała je w sterowniku JDBC.
Jay
@sidereal: to właśnie miałem na myśli, mówiąc „ręcznie buduj zapytanie” ; ale powiedziałeś to lepiej niż ja ;;; @Jay: mamy ten sam rodzaj mekanizmu w PHP (prawdziwe przygotowane instrukcje, jeśli są obsługiwane; pseudo przygotowane instrukcje dla sterowników baz danych, które ich nie obsługują)
Pascal MARTIN
6
Jeśli używasz java.sql.PreparedStatement, prosta .toString () na przygotowanymStatement będzie zawierać wygenerowany kod SQL, który zweryfikowałem w 1.8.0_60
Pr0n
5
@Preston Dla Oracle DB PreparedStatement # toString () nie pokazuje SQL. Dlatego myślę, że zależy to od sterownika DB JDBC.
Mike Argyriou
57

Nigdzie nie jest to zdefiniowane w umowie JDBC API, ale jeśli masz szczęście, dany sterownik JDBC może zwrócić pełny kod SQL, po prostu wywołując PreparedStatement#toString(). To znaczy

System.out.println(preparedStatement);

Obsługują go przynajmniej sterowniki JDBC MySQL 5.xi PostgreSQL 8.x. Jednak większość innych sterowników JDBC go nie obsługuje. Jeśli masz taki, najlepszym rozwiązaniem jest użycie Log4jdbc lub P6Spy .

Alternatywnie można również napisać funkcję ogólną, która pobiera Connectionciąg znaków SQL i wartości instrukcji i zwraca PreparedStatementpo zarejestrowaniu ciągu SQL i wartości. Przykład rozpoczęcia:

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

i używaj go jako

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

Inną alternatywą jest zaimplementowanie niestandardowego, PreparedStatementktóry zawija (ozdabia) rzeczywistość PreparedStatement w konstrukcji i przesłania wszystkie metody, tak że wywołuje metody wartości rzeczywistej PreparedStatement i zbiera wartości we wszystkich setXXX()metodach i leniwie konstruuje „rzeczywisty” ciąg SQL za każdym razem, gdy jeden z te executeXXX()metody nazywa się (dość pracy, ale większość IDE zapewnia autogenerators metod dekorator, Eclipse robi). Wreszcie po prostu użyj go. To także w zasadzie to, co P6Spy i konsorci już robią pod maskami.

BalusC
źródło
Jest to podobne do metody, której używam (metoda readyStatement). Moje pytanie nie dotyczy tego, jak to zrobić - moje pytanie brzmi, jak zarejestrować instrukcję sql. Wiem, że dam radę logger.debug(sql + " " + Arrays.asList(values))- szukam sposobu na zalogowanie instrukcji sql z parametrami już w niej zintegrowanymi. Bez zapętlania się i zastępowania znaków zapytania.
froadie
Następnie przejdź do ostatniego akapitu mojej odpowiedzi lub spójrz na P6Spy. Robią za ciebie "paskudne" zapętlanie i zastępowanie pracy;)
BalusC
Link do P6Spy jest teraz uszkodzony.
Stephen P
@BalusC Jestem nowicjuszem w JDBC. Mam jedną wątpliwość. Jeśli napiszesz taką funkcję ogólną, będzie ona tworzona za PreparedStatementkażdym razem. Czy nie będzie to tak wydajne, ponieważ chodzi o PreparedStatementto, aby je raz stworzyć i wykorzystać wszędzie?
Bhushan,
Działa to również w przypadku sterownika voltdb jdbc, aby uzyskać pełne zapytanie sql dla przygotowanej instrukcji.
k0pernikus
37

Używam Java 8, sterownika JDBC ze złączem MySQL v. 5.1.31.

Mogę uzyskać prawdziwy ciąg SQL za pomocą tej metody:

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

Więc zwraca coś takiego:

INSERT INTO oc_manufacturer SET manufacturer_id = 23, name = 'Google', sort_order=0;
userlond
źródło
Powinno to mieć wszystkie głosy za, ponieważ jest to dokładnie to, czego szuka PO.
HuckIt
Czy istnieje podobna funkcja dla sterownika Postgres?
davidwessman
Jak to zdobyć Apache Derby?
Gunasekar
To nie działa i zgłasza ClassCastException : java.lang.ClassCastException: oracle.jdbc.driver.T4CPreparedStatement nie można przesłać do com.mysql.jdbc.JDBC4PreparedStatement
Ercan
1
@ErcanDuman, moja odpowiedź nie jest uniwersalna, obejmuje tylko sterownik Java 8 i MySQL JDBC.
userlond
21

Jeśli wykonanie zapytania i spodziewa się ResultSet(jesteś w takiej sytuacji, co najmniej), to możesz po prostu zadzwonić ResultSet„s getStatement()tak:

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

Zmienna executedQuerybędzie zawierać instrukcję, która została użyta do utworzenia pliku ResultSet.

Teraz zdaję sobie sprawę, że to pytanie jest dość stare, ale mam nadzieję, że to komuś pomoże ...

Elad Stern
źródło
6
@Elad Stern Drukuje, oracle.jdbc.driver.OraclePreparedStatementWrapper@1b9ce4b zamiast wypisywać wykonaną instrukcję sql! Proszę, prowadź nas!
AVA,
@AVA, czy użyłeś toString ()?
Elad Stern
@EladStern toString () jest używane!
AVA
@AVA, cóż, nie jestem pewien, ale może to mieć coś wspólnego z twoim sterownikiem jdbc. Z powodzeniem używałem mysql-connector-5.
Elad Stern
3
rs.getStatement () po prostu zwraca obiekt instrukcji, więc zależy to od tego, czy sterownik, którego używasz, implementuje .toString (), który określa, czy odzyskasz SQL
Daz,
3

Wyodrębniłem mój sql z PreparedStatement przy użyciu przygotowanejStatement.toString () W moim przypadku toString () zwraca String w następujący sposób:

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

Teraz stworzyłem metodę (Java 8), która używa wyrażenia regularnego do wyodrębnienia zarówno zapytania, jak i wartości i umieszczenia ich w mapie:

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

Ta metoda zwraca mapę, w której mamy pary klucz-wartość:

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

Teraz - jeśli chcesz, aby wartości były listą, możesz po prostu użyć:

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

Jeśli twoja metoda readyStatement.toString () jest inna niż w moim przypadku, jest to tylko kwestia „dostosowania” wyrażenia regularnego.

Richard K.
źródło
2

Używanie PostgreSQL 9.6.x z oficjalnym sterownikiem Java 42.2.4:

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

Pokaże SQL z ?już zastąpionym, czego szukałem. Właśnie dodałem tę odpowiedź, aby objąć przypadek postgres.

Nigdy bym nie pomyślał, że to może być takie proste.

Christophe Roussy
źródło
1

Zaimplementowałem następujący kod do drukowania SQL z 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[" +  "] ...");
            }

    }

źródło
1

Fragment kodu do konwersji SQL PreparedStaments z listą argumentów. Mi to pasuje

  /**
         * 
         * 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;
        }
Surowe Maheswari
źródło
1

Bardzo późno :), ale oryginalny SQL można pobrać z OraclePreparedStatementWrapper przez

((OraclePreparedStatementWrapper) preparedStatement).getOriginalSql();
user497087
źródło
1
Kiedy próbuję użyć opakowania, mówi: oracle.jdbc.driver.OraclePreparedStatementWrappernie jest publicznie w oracle.jdbc.driver. Nie można uzyskać dostępu z zewnątrz pakietu. Jak korzystasz z tej klasy?
widmo
0

Jeśli używasz MySQL, możesz rejestrować zapytania za pomocą dziennika zapytań MySQL . Nie wiem, czy inni dostawcy zapewniają tę funkcję, ale są szanse, że tak.

Ionuț G. Stan
źródło
-1

Po prostu działaj:

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

Jest dobrze, jak również dla przygotowanego oświadczenia.

Hobson
źródło
To jest po prostu .toString()z kilkoma dodatkowymi wierszami, aby oszukać niedoświadczonych użytkowników, i odpowiedź została udzielona już wieki temu.
Pere
-1

Używam Oralce 11g i nie udało mi się uzyskać końcowego kodu SQL z PreparedStatement. Po przeczytaniu odpowiedzi @ Pascal MARTIN rozumiem dlaczego.

Po prostu porzuciłem pomysł korzystania z PreparedStatement i użyłem prostego programu do formatowania tekstu, który pasował do moich potrzeb. Oto mój przykład:

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

Dowiesz się, że sqlInParam można zbudować dynamicznie w pętli (for, while). Po prostu sprawiłem, że dotarcie do punktu użycia klasy MessageFormat jako elementu formatującego szablon ciągu dla zapytania SQL jest bardzo proste.

Diego Tercero
źródło
4
Ten rodzaj wysadza cały powód używania przygotowanych instrukcji, takich jak unikanie wstrzykiwania sql i zwiększona wydajność.
ticktock
1
Zgadzam się z tobą w 100%. Powinienem był wyjaśnić, że wykonałem ten kod najwyżej kilka razy w celu masowej integracji danych masowych i rozpaczliwie potrzebowałem szybkiego sposobu, aby uzyskać wyjście dziennika bez wchodzenia w całą enchiladę log4j, która byłaby przesada Potrzebowałem. To nie powinno przejść do kodu produkcyjnego :-)
Diego Tercero
To działa dla mnie ze sterownikiem Oracle (ale nie zawiera parametrów):((OraclePreparedStatementWrapper) myPreparedStatement).getOriginalSql()
latj
-4

Aby to zrobić, potrzebujesz połączenia JDBC i / lub sterownika obsługującego logowanie sql na niskim poziomie.

Spójrz na log4jdbc

gwiezdny
źródło
3
Spójrz na log4jdbc i co potem? Jak tego używasz? Wchodzisz na tę stronę i widzisz przypadkowe błądzenie o projekcie bez wyraźnego przykładu, jak faktycznie korzystać z technologii.
Hooli