¿Cómo puedo obtener el SQL de una declaración preparada?

160

Tengo un método Java general con la siguiente firma de método:

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

Abre una conexión, construye PreparedStatementusando la instrucción sql y los parámetros en la queryParamsmatriz de longitud variable, la ejecuta, almacena ResultSeten caché el (en a CachedRowSetImpl), cierra la conexión y devuelve el conjunto de resultados en caché.

Tengo manejo de excepciones en el método que registra errores. Registro la instrucción sql como parte del registro, ya que es muy útil para la depuración. Mi problema es que el registro de la variable String sqlregistra la declaración de la plantilla con? 'S en lugar de valores reales. Quiero registrar la declaración real que se ejecutó (o intentó ejecutar).

Entonces ... ¿Hay alguna forma de obtener la declaración SQL real que ejecutará un PreparedStatement? ( Sin construirlo yo mismo. Si no puedo encontrar una manera de acceder al PreparedStatement'sSQL, probablemente terminaré construyéndolo yo mismo catch).

froadie
fuente
1
Si está escribiendo código JDBC directo, le recomiendo mirar Apache commons-dbutils commons.apache.org/dbutils . Simplifica enormemente el código JDBC.
Ken Liu

Respuestas:

173

Usando declaraciones preparadas, no hay "consulta SQL":

  • Tiene una declaración que contiene marcadores de posición.
    • se envía al servidor de base de datos
    • y preparado allí
    • lo que significa que la instrucción SQL es "analizada", analizada, alguna estructura de datos que la representa está preparada en la memoria
  • Y, entonces, tienes variables ligadas
    • que se envían al servidor
    • y se ejecuta la declaración preparada, trabajando en esos datos

Pero no hay una reconstrucción de una consulta SQL real real, ni en el lado de Java ni en el lado de la base de datos.

Por lo tanto, no hay forma de obtener el SQL de la declaración preparada, ya que no existe tal SQL.


Para fines de depuración, las soluciones son:

  • Ouput el código de la declaración, con los marcadores de posición y la lista de datos
  • O para "construir" alguna consulta SQL "a mano".
Pascal MARTIN
fuente
22
Aunque esto es funcionalmente cierto, no hay nada que impida que el código de utilidad reconstruya una declaración equivalente no preparada. Por ejemplo, en log4jdbc: "En la salida registrada, para las declaraciones preparadas, los argumentos de enlace se insertan automáticamente en la salida SQL. Esto mejora en gran medida la legibilidad y la depuración en muchos casos". Muy útil para la depuración, siempre que sepa que no es así como el servidor de base de datos ejecuta realmente la declaración.
sideral
66
Esto también depende de la implementación. En MySQL, al menos la versión que estaba usando hace unos años, el controlador JDBC realmente construyó una consulta SQL convencional a partir de la plantilla y las variables de enlace. Supongo que la versión de MySQL no admitía declaraciones preparadas de forma nativa, por lo que las implementaron dentro del controlador JDBC.
Jay
@sidereal: a eso me refería con "compilar la consulta a mano" ; pero lo dijiste mejor que yo ;; @ Jay: tenemos el mismo tipo de mecanismo en PHP (declaraciones preparadas reales cuando se admiten; declaraciones pseudo preparadas para controladores de bases de datos que no las admiten)
Pascal MARTIN
66
Si está utilizando java.sql.PreparedStatement, un simple .toString () en el prepareStatement incluirá el SQL generado que he verificado en 1.8.0_60
Pr0n el
55
@Preston Para Oracle DB, PreparedStatement # toString () no muestra el SQL. Por lo tanto, supongo que depende del controlador DB JDBC.
Mike Argyriou
57

No está definido en ninguna parte en el contrato de la API JDBC, pero si tiene suerte, el controlador JDBC en cuestión puede devolver el SQL completo simplemente llamando PreparedStatement#toString(). Es decir

System.out.println(preparedStatement);

Al menos los controladores JDBC MySQL 5.xy PostgreSQL 8.x lo admiten. Sin embargo, la mayoría de los otros controladores JDBC no lo admiten. Si tiene uno, entonces su mejor opción es usar Log4jdbc o P6Spy .

Alternativamente, también puede escribir una función genérica que tome una Connection, una cadena SQL y los valores de la instrucción y devuelva un PreparedStatementdespués de registrar la cadena SQL y los valores. Ejemplo de inicio:

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

y úsalo como

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

Otra alternativa es implementar una costumbre PreparedStatementque envuelva (decore) lo real PreparedStatement en la construcción y anule todos los métodos para que llame a los métodos de lo real PreparedStatement y recopile los valores en todos los setXXX()métodos y construya perezosamente la cadena SQL "real" cada vez que uno de executeXXX()se llama a los métodos (todo un trabajo, pero la mayoría de los IDE proporcionan autogeneradores para métodos decoradores, Eclipse sí). Finalmente solo úsalo en su lugar. Eso también es básicamente lo que P6Spy y sus consortes ya hacen debajo de las campanas.

BalusC
fuente
Eso es similar al método que estoy usando (su método prepareStatement). Mi pregunta no es cómo hacerlo, mi pregunta es cómo registrar la instrucción sql. Sé que puedo hacerlo logger.debug(sql + " " + Arrays.asList(values)): estoy buscando una manera de registrar la instrucción sql con los parámetros ya integrados en ella. Sin repetirme y reemplazar los signos de interrogación.
froadie
Luego dirígete al último párrafo de mi respuesta o mira P6Spy. Ellos hacen el trabajo "repugnante" de bucle y reemplazo para usted;)
BalusC
El enlace a P6Spy ahora está roto.
Stephen P
@BalusC Soy novato en JDBC. Tengo una duda. Si escribe una función genérica como esa, se creará PreparedStatementcada vez. ¿No será esa una manera no tan eficiente porque el objetivo principal PreparedStatementes crearlos una vez y reutilizarlos en todas partes?
Bhushan
Esto también funciona en el controlador voltdb jdbc para obtener la consulta sql completa para una declaración preparada.
k0pernikus
37

Estoy usando Java 8, controlador JDBC con conector MySQL v. 5.1.31.

Puedo obtener una cadena 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());

Entonces devuelve algo como esto:

INSERT INTO oc_manufacturer SET manufacturer_id = 23, name = 'Google', sort_order=0;
userlond
fuente
Esto debería tener todos los votos positivos, ya que es exactamente lo que está buscando el OP.
HuckIt
¿Existe una función similar para el controlador Postgres?
davidwessman
¿Cómo conseguirlo Apache Derby?
Gunasekar
No funciona y arroja ClassCastException : java.lang.ClassCastException: oracle.jdbc.driver.T4CPreparedStatement no se puede enviar a com.mysql.jdbc.JDBC4PreparedStatement
Ercan
1
@ErcanDuman, mi respuesta no es universal, solo cubre Java 8 y el controlador JDBC MySQL.
userlond
21

Si va a ejecutar la consulta y esperar un ResultSet(usted está en esta situación, por lo menos), entonces puede simplemente llamar ResultSet's getStatement()de este modo:

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

La variable executedQuerycontendrá la declaración que se utilizó para crear elResultSet .

Ahora, me doy cuenta de que esta pregunta es bastante antigua, pero espero que esto ayude a alguien ...

Elad Stern
fuente
66
@Elad Stern Imprime, oracle.jdbc.driver.OraclePreparedStatementWrapper@1b9ce4b en lugar de imprimir la instrucción sql ejecutada. Por favor guíanos!
AVA
@AVA, ¿usaste toString ()?
Elad Stern
¡Se utiliza @EladStern toString ()!
AVA
@AVA, bueno, no estoy seguro, pero puede tener que ver con tu controlador jdbc. He usado mysql-connector-5 con éxito.
Elad Stern
3
rs.getStatement () solo devuelve el objeto de declaración, por lo que depende de si el controlador que está utilizando implementa .toString () que determina si recuperará el SQL
Daz
3

Extraje mi sql de PreparedStatement usando prepareStatement.toString () En mi caso toString () devuelve String de esta manera:

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

Ahora he creado un método (Java 8), que utiliza expresiones regulares para extraer tanto la consulta como los valores y ponerlos en el 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 devuelve el mapa donde tenemos pares clave-valor:

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

Ahora, si desea valores como lista, simplemente puede usar:

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

Si su prepareStatement.toString () es diferente que en mi caso, es solo cuestión de "ajustar" la expresión regular.

RichardK
fuente
2

Usando PostgreSQL 9.6.x con el controlador oficial de Java 42.2.4:

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

Mostrará el SQL con el ? ya reemplazado, que es lo que estaba buscando. Acabo de agregar esta respuesta para cubrir el caso de postgres.

Nunca hubiera pensado que podría ser tan simple.

Christophe Roussy
fuente
1

Implementé el siguiente código para imprimir SQL desde 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[" +  "] ...");
            }

    }

fuente
1

Fragmento de código para convertir SQL PreparedStaments con la lista de argumentos. Esto funciona para mi

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

Muy tarde :) pero puede obtener el SQL original de un OraclePreparedStatementWrapper por

((OraclePreparedStatementWrapper) preparedStatement).getOriginalSql();
user497087
fuente
1
Cuando intento usar el contenedor, dice: oracle.jdbc.driver.OraclePreparedStatementWrapperno es público en oracle.jdbc.driver. No se puede acceder desde un paquete externo. ¿Cómo estás usando esa clase?
espectro
0

Si está utilizando MySQL, puede registrar las consultas utilizando el registro de consultas de MySQL . No sé si otros proveedores ofrecen esta función, pero es probable que lo hagan.

Ionuț G. Stan
fuente
-1

Simplemente funciona:

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

Está bien también para la declaración preparada.

Hobson
fuente
Esto es simplemente .toString()con un par de líneas adicionales para engañar a los usuarios inexpertos, y ya fue respondido hace años.
Pere
-1

Estoy usando Oralce 11g y no pude obtener el SQL final de PreparedStatement. Después de leer la respuesta de @Pascal MARTIN, entiendo por qué.

Simplemente abandoné la idea de usar PreparedStatement y usé un simple formateador de texto que se ajustaba a mis necesidades. Aquí está mi ejemplo:

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

Se da cuenta de que sqlInParam se puede construir dinámicamente en un bucle (for, while). Simplemente hice que sea sencillo llegar al punto de usar la clase MessageFormat para que sirva como formateador de plantillas de cadena para la consulta SQL.

Diego Tercero
fuente
44
Este tipo explota la razón completa para usar declaraciones preparadas, como evitar la inyección de sql y mejorar el rendimiento.
ticktock
1
Estoy de acuerdo contigo al 100%. Debería haber dejado en claro que hice que este código se ejecutara un par de veces como máximo para una integración masiva de datos masivos y necesitaba desesperadamente una forma rápida de obtener algo de salida de registro sin entrar en toda la enchilada log4j, lo que habría sido excesivo para lo que Lo necesitaba. Esto no debería entrar en el código de producción :-)
Diego Tercero
Esto funciona para mí con el controlador de Oracle (pero no incluye parámetros):((OraclePreparedStatementWrapper) myPreparedStatement).getOriginalSql()
latj
-4

Para hacer esto, necesita una conexión JDBC y / o un controlador que admita registrar el sql en un nivel bajo.

Echa un vistazo a log4jdbc

sideral
fuente
3
Echa un vistazo a log4jdbc y luego ¿qué? ¿Como lo usas? Vaya a ese sitio y vea divagaciones aleatorias sobre el proyecto sin un ejemplo claro sobre cómo usar realmente la tecnología.
Hooli