Como posso obter o SQL de uma PreparedStatement?

160

Eu tenho um método Java geral com a seguinte assinatura de método:

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

Ele abre uma conexão, cria um PreparedStatementusando a instrução sql e os parâmetros na queryParamsmatriz de comprimento variável, a executa, armazena ResultSetem cache o (em a CachedRowSetImpl), fecha a conexão e retorna o conjunto de resultados em cache.

Eu tenho manipulação de exceção no método que registra erros. Registro a instrução sql como parte do log, pois é muito útil para depuração. Meu problema é que o registro da variável String sqlregistra a instrução do modelo com? Em vez de valores reais. Quero registrar a instrução real que foi executada (ou tentou executar).

Então ... Existe alguma maneira de obter a instrução SQL real que será executada por um PreparedStatement? ( Sem construí-lo eu mesmo. Se não conseguir encontrar uma maneira de acessar o PreparedStatement'sSQL, provavelmente acabarei construindo ele mesmo em meus catches.)

froadie
fonte
1
Se você estiver escrevendo um código JDBC direto, eu recomendo consultar o Apache commons-dbutils commons.apache.org/dbutils . Simplifica bastante o código JDBC.
Ken Liu

Respostas:

173

Usando instruções preparadas, não há "consulta SQL":

  • Você tem uma declaração contendo espaços reservados
    • é enviado para o servidor DB
    • e preparado lá
    • o que significa que a instrução SQL é "analisada", analisada, alguma estrutura de dados que a representa é preparada na memória
  • E então, você vinculou variáveis
    • que são enviados para o servidor
    • e a declaração preparada é executada - trabalhando nesses dados

Mas não há reconstrução de uma consulta SQL real real - nem no lado Java nem no lado do banco de dados.

Portanto, não há como obter o SQL da instrução preparada - pois não existe esse SQL.


Para fins de depuração, as soluções são:

  • Sobre o código da instrução, com os espaços reservados e a lista de dados
  • Ou para "construir" alguma consulta SQL "manualmente".
Pascal MARTIN
fonte
22
Embora isso seja funcionalmente verdadeiro, não há nada que impeça o código de utilitário de reconstruir uma instrução despreparada equivalente. Por exemplo, em log4jdbc: "Na saída registrada, para instruções preparadas, os argumentos de ligação são inseridos automaticamente na saída SQL. Isso melhora muito a legibilidade e a depuração para muitos casos." Muito útil para depuração, desde que você saiba que não é como a instrução está sendo executada pelo servidor de banco de dados.
Sidereal
6
Isso também depende da implementação. No MySQL - pelo menos a versão que eu estava usando há alguns anos atrás - o driver JDBC realmente criou uma consulta SQL convencional a partir do modelo e vinculou variáveis. Eu acho que a versão do MySQL não suportava instruções preparadas nativamente, então elas as implementaram no driver JDBC.
Jay em Jay
@ sideral: é isso que eu quis dizer com "construir a consulta manualmente" ; mas você disse isso melhor do que eu ;;; @Jay: temos o mesmo tipo de mecanism no lugar em PHP (declarações preparadas reais quando suportados; afirmações pseudo-preparado para motoristas de banco de dados que não os suportam)
Pascal MARTIN
6
Se você estiver usando java.sql.PreparedStatement, um simples .toString () no preparadoStatement incluirá o SQL gerado que eu verifiquei em 1.8.0_60
Pr0n
5
@Preston Para Oracle DB, o PreparedStatement # toString () não mostra o SQL. Portanto, acho que depende do driver DB JDBC.
Mike Argyriou 23/02
57

Não está definido em nenhum lugar no contrato da API JDBC, mas se você tiver sorte, o driver JDBC em questão pode retornar o SQL completo apenas ligando PreparedStatement#toString(). Ou seja,

System.out.println(preparedStatement);

Pelo menos os drivers MySQL 5.xe PostgreSQL 8.x JDBC suportam. No entanto, a maioria dos outros drivers JDBC não é compatível. Se você tiver esse, sua melhor aposta é usar o Log4jdbc ou o P6Spy .

Como alternativa, você também pode escrever uma função genérica que use a Connection, uma string SQL e os valores da instrução e retorne a PreparedStatementdepois de registrar a string SQL e os valores. Exemplo inicial:

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 use-o como

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

Outra alternativa é implementar um costume PreparedStatementque envolve (decora) o real PreparedStatement na construção e substitui todos os métodos para que ele chame os métodos do real PreparedStatement e colete os valores em todos os setXXX()métodos e construa preguiçosamente a string SQL "real" sempre que um dos os executeXXX()métodos são chamados (bastante trabalho, mas a maioria dos IDEs fornece geradores automáticos para métodos de decorador, o Eclipse fornece). Finalmente, basta usá-lo. Isso também é basicamente o que o P6Spy e os consortes já fazem sob o capô.

BalusC
fonte
É semelhante ao método que estou usando (seu método prepareStatement). Minha pergunta não é como fazê-lo - minha pergunta é como registrar a instrução sql. Eu sei que posso fazer logger.debug(sql + " " + Arrays.asList(values))- estou procurando uma maneira de registrar a instrução sql com os parâmetros já integrados a ela. Sem me dar laços e substituir os pontos de interrogação.
froadie
Então vá para o último parágrafo da minha resposta ou veja o P6Spy. Eles fazem o "desagradável" looping e substituindo trabalho para você;)
BalusC
O link para o P6Spy agora está quebrado.
Stephen P
@BalusC Sou novato no JDBC. Eu tenho uma duvida. Se você escrever uma função genérica assim, ela será criada PreparedStatementsempre. Não será esse o caminho não tão eficiente, porque o ponto principal PreparedStatementé criá-los uma vez e reutilizá-los em qualquer lugar?
Bhushan
Isso também funciona no driver voltdb jdbc para obter a consulta sql completa para uma instrução preparada.
K0pernikus
37

Estou usando o Java 8, driver JDBC com conector MySQL v. 5.1.31.

Eu posso obter uma string SQL real usando este método:

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

Então, ele retorna smth assim:

INSERT INTO oc_manufacturer SET manufacturer_id = 23, name = 'Google', sort_order=0;
userlond
fonte
Isso deve ter todos os votos positivos, pois é exatamente o que o OP está procurando.
precisa saber é o seguinte
Existe uma função semelhante para o driver do Postgres?
Davidwessman
Como conseguir isso Apache Derby?
9138 Gunasekar #
Ele não funciona e joga ClassCastException : java.lang.ClassCastException: oracle.jdbc.driver.T4CPreparedStatement não pode ser convertido para com.mysql.jdbc.JDBC4PreparedStatement
Ercan
1
@ ErcanDuman, minha resposta não é universal, abrange apenas os drivers JDBC Java 8 e MySQL.
precisa saber é o seguinte
21

Se você estiver executando a consulta e esperando uma ResultSet(você está nesse cenário, pelo menos), basta chamar ResultSetda seguinte getStatement()maneira:

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

A variável executedQueryirá conter a afirmação de que foi usado para criar o ResultSet.

Agora, percebo que essa pergunta é bastante antiga, mas espero que isso ajude alguém.

Elad Stern
fonte
6
@Elad Stern Imprime, oracle.jdbc.driver.OraclePreparedStatementWrapper@1b9ce4b em vez de imprimir a instrução sql executada! Por favor, nos guie!
AVA
@AVA, você usou toString ()?
Elad Stern
@EladStern toString () é usado!
AVA
@ AVA, bem, eu não tenho certeza, mas pode ter a ver com o seu driver jdbc. Eu usei o mysql-connector-5 com sucesso.
Elad Stern
3
rs.getStatement () apenas retorna o objeto de instrução, portanto, depende do driver que você está usando implementa .toString () que determina se você voltará ao SQL
Daz
3

Eu extraí meu sql do PreparedStatement usando o preparadoStatement.toString () No meu caso, toString () retorna String assim:

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

Agora eu criei um método (Java 8), que está usando o regex para extrair a consulta e os valores e colocá-los no mapa:

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

Este método retorna o mapa onde temos pares de valores-chave:

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

Agora - se você deseja valores como lista, pode simplesmente usar:

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

Se o seu preparadoStatement.toString () for diferente do meu caso, é apenas uma questão de "ajustar" a expressão regular.

RichardK
fonte
2

Usando o PostgreSQL 9.6.x com o driver Java oficial 42.2.4:

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

Mostrará o SQL com o ?já substituído, que é o que eu estava procurando. Acabei de adicionar esta resposta para cobrir o caso do postgres.

Eu nunca teria pensado que poderia ser tão simples.

Christophe Roussy
fonte
1

Eu implementei o seguinte código para imprimir SQL a 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[" +  "] ...");
            }

    }

fonte
1

Snippet de código para converter SQL PreparedStaments com a lista de argumentos. Funciona para mim

  /**
         * 
         * 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
fonte
1

Muito tarde :), mas você pode obter o SQL original de um OraclePreparedStatementWrapper por

((OraclePreparedStatementWrapper) preparedStatement).getOriginalSql();
user497087
fonte
1
Quando tento usar o wrapper, ele diz: oracle.jdbc.driver.OraclePreparedStatementWrappernão é público no oracle.jdbc.driver. Não pode ser acessado a partir do pacote externo. Como você está usando essa classe?
espectro
0

Se você estiver usando o MySQL, poderá registrar as consultas usando o log de consultas do MySQL . Não sei se outros fornecedores fornecem esse recurso, mas é provável que sim.

Ionuț G. Stan
fonte
-1

Simplesmente função:

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

Também está bom para a declaração preparada.

Hobson
fonte
Este é simplesmente um exemplo .toString()com algumas linhas extras para enganar usuários inexperientes, e já foi respondido há séculos.
Pere
-1

Estou usando o Oralce 11g e não consegui obter o SQL final do PreparedStatement. Depois de ler a resposta do @Pascal MARTIN, entendo o porquê.

Acabei de abandonar a ideia de usar PreparedStatement e usei um formatador de texto simples que atendesse às minhas necessidades. Aqui está o meu exemplo:

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

Você descobre que o sqlInParam pode ser criado dinamicamente em um loop (for, while). Simplesmente simplifiquei o ponto de usar a classe MessageFormat para servir como formador de modelo de string para a consulta SQL.

Diego Tercero
fonte
4
Isso explode todo o motivo do uso de instruções preparadas, como evitar a injeção de sql e melhorar o desempenho.
TickTart1
1
Eu concordo com você 100%. Eu deveria ter deixado claro que fiz esse código para ser executado algumas vezes, no máximo, para uma integração massiva de dados em massa e precisava desesperadamente de uma maneira rápida de obter alguma saída de log sem entrar em toda a enchilada do log4j, o que seria um exagero para o que Eu precisei. Isso não deve entrar em código de produção :-)
Diego Tercero
Isso funciona para mim com o driver Oracle (mas não inclui parâmetros):((OraclePreparedStatementWrapper) myPreparedStatement).getOriginalSql()
latj
-4

Para fazer isso, você precisa de uma conexão JDBC e / ou driver que suporte o log do sql em um nível baixo.

Dê uma olhada no log4jdbc

sideral
fonte
3
Dê uma olhada no log4jdbc e depois o que? como você usa isso? Você acessa esse site e vê divagações aleatórias sobre o projeto, sem um exemplo claro de como realmente usar a tecnologia.
Hooli