【问题标题】:How can I get the SQL of a PreparedStatement?如何获取 PreparedStatement 的 SQL?
【发布时间】:2011-01-23 20:42:01
【问题描述】:

我有一个具有以下方法签名的通用 Java 方法:

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

它打开一个连接,使用 sql 语句和queryParams 可变长度数组中的参数构建一个PreparedStatement,运行它,缓存ResultSet(在CachedRowSetImpl 中),关闭连接,然后返回缓存的结果集。

我在记录错误的方法中有异常处理。我将 sql 语句记录为日志的一部分,因为它对调试非常有帮助。我的问题是记录字符串变量sql 使用?而不是实际值记录模板语句。我想记录已执行(或尝试执行)的 实际 语句。

那么...有什么方法可以得到PreparedStatement 运行的实际SQL 语句? (自己构建它。如果我找不到访问PreparedStatement's SQL 的方法,我可能最终会在我的catches 中自己构建它。)

【问题讨论】:

标签: java sql jdbc prepared-statement


【解决方案1】:

使用准备好的语句,没有“SQL查询”:

  • 您有一个包含占位符的语句
    • 发送到数据库服务器
    • 并在那里准备
    • 这意味着 SQL 语句被“分析”、解析,一些表示它的数据结构在内存中准备好
  • 然后,你有绑定变量
    • 发送到服务器
    • 并执行准备好的语句——处理这些数据

但是没有重建实际的真实 SQL 查询——既不是在 Java 端,也不是在数据库端。

因此,没有办法获取准备好的语句的 SQL——因为没有这样的 SQL。


出于调试目的,解决方案是:

  • 输出语句代码、占位符和数据列表
  • 或者“手动”“构建”一些 SQL 查询。

【讨论】:

  • 虽然这在功能上是正确的,但没有什么能阻止实用程序代码重构等效的未准备语句。例如,在 log4jdbc 中:“在记录的输出中,对于准备好的语句,绑定参数会自动插入到 SQL 输出中。这大大提高了许多情况下的可读性和调试性。”对调试非常有用,只要您知道这不是 DB 服务器实际执行语句的方式。
  • 这也取决于实现。在 MySQL 中——至少是我几年前使用的版本——JDBC 驱动程序实际上从模板和绑定变量构建了一个传统的 SQL 查询。我猜那个版本的 MySQL 本身不支持准备好的语句,所以他们在 JDBC 驱动程序中实现了它们。
  • @sidereal :这就是我所说的 "手动构建查询" 的意思;但你说得比我好;;; @Jay:我们在 PHP 中使用了相同的机制(支持时的真实准备语句;不支持它们的数据库驱动程序的伪准备语句)
  • 如果您使用 java.sql.PreparedStatement,preparedStatement 上的简单 .toString() 将包含我在 1.8.0_60 中验证过的生成的 SQL
  • @Preston 对于 Oracle DB,PreparedStatement#toString() 不显示 SQL。因此我想这取决于 DB JDBC 驱动程序。
【解决方案2】:

JDBC API 合约中没有定义,但如果幸运的话,相关的 JDBC 驱动程序可能会通过调用 PreparedStatement#toString() 来返回完整的 SQL。即

System.out.println(preparedStatement);

至少 MySQL 5.x 和 PostgreSQL 8.x JDBC 驱动程序支持它。但是,大多数其他 JDBC 驱动程序不支持它。如果你有这样的,那么你最好的选择是使用Log4jdbcP6Spy

或者,您也可以编写一个通用函数,它接受一个Connection、一个SQL 字符串和语句值,并在记录SQL 字符串和值后返回一个PreparedStatement。启动示例:

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

并将其用作

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

另一种选择是实现一个自定义PreparedStatement,它在构造时包装(装饰)real PreparedStatement 并覆盖所有方法,以便它调用 real PreparedStatement 并收集所有 setXXX() 方法中的值,并在调用 executeXXX() 方法之一时懒惰地构造“实际” SQL 字符串(这是一项工作,但大多数 IDE 为装饰器方法提供自动生成器, Eclipse 确实如此)。最后只是使用它。这基本上也是 P6Spy 及其合作伙伴在幕后所做的。

【讨论】:

  • 这类似于我正在使用的方法(您的 prepareStatement 方法)。我的问题不是如何去做——我的问题是如何log sql 语句。我知道我可以做到logger.debug(sql + " " + Arrays.asList(values)) - 我正在寻找一种方法来记录带有已集成到其中的参数的 sql 语句。无需循环自己并替换问号。
  • 然后转到我答案的最后一段或查看 P6Spy。他们为你做“讨厌的”循环和替换工作;)
  • 与 P6Spy 的链接现已断开。
  • @BalusC 我是 JDBC 的新手。我有一个疑问。如果你写这样的通用函数,那么它每次都会创建PreparedStatement。因为PreparedStatement 的全部意义在于创建一次并在任何地方重复使用它们,这不是很有效吗?
  • 这也适用于 voltdb jdbc 驱动程序,以获取准备好的语句的完整 sql 查询。
【解决方案3】:

我正在使用 Java 8,带有 MySQL 连接器 v. 5.1.31 的 JDBC 驱动程序。

我可以使用这种方法得到真正的 SQL 字符串:

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

所以它会像这样返回:

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

【讨论】:

  • 这应该有所有的赞成票,因为它正是 OP 正在寻找的。​​span>
  • Postgres-driver 有类似的功能吗?
  • 如何获得Apache Derby
  • 它不起作用并抛出 ClassCastException: java.lang.ClassCastException: oracle.jdbc.driver.T4CPreparedStatement 无法转换为 com.mysql.jdbc.JDBC4PreparedStatement跨度>
  • @ErcanDuman,我的回答并不通用,它仅涵盖 Java 8 和 MySQL JDBC 驱动程序。
【解决方案4】:

如果您正在执行查询并期待 ResultSet(至少您在这种情况下),那么您可以像这样简单地调用 ResultSetgetStatement()

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

变量executedQuery 将包含用于创建ResultSet 的语句。

现在,我意识到这个问题已经很老了,但我希望这对某人有所帮助..

【讨论】:

  • @Elad Stern 它打印的是 oracle.jdbc.driver.OraclePreparedStatementWrapper@1b9ce4b 而不是打印执行的 sql 语句!请指导我们!
  • @AVA,你用过 toString() 吗?
  • @EladStern toString() 被使用!
  • @AVA,我不确定,但这可能与您的 jdbc 驱动程序有关。我已经成功使用了mysql-connector-5。
  • rs.getStatement() 只返回语句对象,因此取决于您使用的驱动程序是否实现 .toString() 来确定您是否会取回 SQL
【解决方案5】:

我已经使用preparedStatement.toString() 从PreparedStatement 中提取了我的sql 在我的例子中toString() 返回这样的字符串:

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

现在我创建了一个方法(Java 8),它使用正则表达式来提取查询和值并将它们放入映射中:

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

此方法返回我们有键值对的地图:

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

现在 - 如果你想将值作为列表,你可以简单地使用:

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

如果您的preparedStatement.toString() 与我的情况不同,那只是“调整”正则表达式的问题。

【讨论】:

    【解决方案6】:

    使用 PostgreSQL 9.6.x 和官方 Java 驱动程序42.2.4:

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

    将显示已替换 ? 的 SQL,这正是我所寻找的。 刚刚添加了这个答案来涵盖 postgres 案例。

    我从没想过会这么简单。

    【讨论】:

      【解决方案7】:

      使用参数列表转换 SQL PreparedStaments 的代码片段。它对我有用

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

      【讨论】:

        【解决方案8】:

        很晚 :) 但您可以通过

        从 OraclePreparedStatementWrapper 获取原始 SQL
        ((OraclePreparedStatementWrapper) preparedStatement).getOriginalSql();
        

        【讨论】:

        • 当我尝试使用包装器时,它说:oracle.jdbc.driver.OraclePreparedStatementWrapper 在 oracle.jdbc.driver 中不公开。无法从外部包访问。您如何使用该类?
        【解决方案9】:

        我实现了以下代码,用于从 PrepareStatement 打印 SQL

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

        【讨论】:

          【解决方案10】:

          如果您使用的是 MySQL,您可以使用 MySQL's query log 记录查询。我不知道其他供应商是否提供此功能,但他们很有可能提供。

          【讨论】:

            【解决方案11】:

            简单函数:

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

            preparedStatement 也可以。

            【讨论】:

            • 这只是一个 .toString() 加上几行来愚弄不熟练的用户,并且在很久以前就已经得到了回答。
            【解决方案12】:

            我正在使用 Oralce 11g,但无法从 PreparedStatement 中获取最终 SQL。阅读@Pascal MARTIN 的回答后,我明白了原因。

            我只是放弃了使用 PreparedStatement 的想法,而是使用了一个适合我需要的简单文本格式化程序。这是我的例子:

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

            您发现 sqlInParam 可以在 (for,while) 循环中动态构建,我只是简单明了地使用 MessageFormat 类作为 SQL 查询的字符串模板格式化程序。

            【讨论】:

            • 这样就搞砸了使用prepared statements的全部原因,例如避免sql注入和提高性能。
            • 我 100% 同意你的看法。我应该明确表示,我让这段代码最多执行几次,以进行大规模的批量数据集成,并且迫切需要一种快速的方法来获得一些日志输出,而无需进入整个 log4j enchilada,这对于什么来说太过分了我需要。这不应该进入生产代码:-)
            • 这适用于我的 Oracle 驱动程序(但不包括参数):((OraclePreparedStatementWrapper) myPreparedStatement).getOriginalSql()
            【解决方案13】:

            为此,您需要一个支持在低级别记录 sql 的 JDBC 连接和/或驱动程序。

            看看log4jdbc

            【讨论】:

            • 看看log4jdbc然后呢?你如何使用它?您访问该站点并看到关于该项目的随机散漫,没有关于如何实际使用该技术的明确示例。
            猜你喜欢
            • 1970-01-01
            • 2011-06-09
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多