如何获取 PreparedStatement 的 SQL?

Posted

技术标签:

【中文标题】如何获取 PreparedStatement 的 SQL?【英文标题】:How can I get the SQL of a PreparedStatement? 【发布时间】:2010-03-04 20:35:47 【问题描述】:

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

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

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

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

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

【问题讨论】:

如果您正在编写直接的 JDBC 代码,我强烈建议您查看 Apache commons-dbutils commons.apache.org/dbutils。它极大地简化了 JDBC 代码。 复制:***.com/questions/218113/… 【参考方案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 驱动程序不支持它。如果你有这样一个,那么你最好的选择是使用Log4jdbc 或P6Spy。

或者,您也可以编写一个通用函数,它接受一个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() @latj 它给出了没有绑定变量的原始 sql。我们需要用绑定变量值。【参考方案13】:

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

看看log4jdbc

【讨论】:

看看log4jdbc然后呢?你如何使用它?你去那个网站,看到关于该项目的随意散漫,没有关于如何实际使用该技术的明确示例。

以上是关于如何获取 PreparedStatement 的 SQL?的主要内容,如果未能解决你的问题,请参考以下文章

获取使用 PreparedStatement 更新的行数

PreparedStatement - 获取最后一个 ResultSet

从 Derby 获取 PreparedStatement 查询

JDBC、MySQL:从 PreparedStatement 执行获取回行数据

java中如何获取结果集ResutSet的总条数?

插入时的 JDBC PreparedStatement 默认值 (UCanAccess)