如何获取PreparedStatement的SQL?

192

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

private static ResultSet runSQLResultSet(String sql, Object... queryParams)
它打开一个连接,使用sql语句和queryParams变量长度数组中的参数构建一个PreparedStatement,运行它,将ResultSet(在CachedRowSetImpl中缓存)保存,关闭连接,并返回缓存的结果集。
我在这个方法中有异常处理,记录错误日志。由于SQL语句对于调试非常有用,因此我将SQL语句作为日志的一部分进行记录。我的问题是,记录String变量sql会记录带有?的模板语句而不是实际值。我想记录被执行(或尝试执行)的实际语句。
那么... 有没有办法获取将由PreparedStatement运行的实际SQL语句? (不必自己构建。如果我找不到访问PreparedStatement SQL的方法,我最终可能会在我的catch块中自己构建它。)

1
如果你正在编写直接的JDBC代码,我强烈建议你看看Apache commons-dbutils http://commons.apache.org/dbutils/。它可以大大简化JDBC代码。 - Ken Liu
4
好的,我会尽力为您进行翻译。以下是需要翻译的内容:Dupe: https://dev59.com/TnVC5IYBdhLWcg3wqzLV - Pascal Thivent
15个回答

202

使用预处理语句,没有“SQL查询”:

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

但是,实际上没有重建SQL查询 -- 无论是在Java端还是在数据库端。

因此,无法获得预处理语句的SQL - 因为不存在这样的SQL。


为了调试目的,解决方案要么是:

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

31
尽管这在功能上是正确的,但并没有阻止实用程序代码重构等效的未准备好语句。例如,在log4jdbc中:“对于准备好的语句,在记录的输出中,绑定参数会自动插入到SQL输出中。这极大地提高了许多情况下的可读性和调试能力。”非常有用于调试,只要您意识到这不是DB服务器实际执行语句的方式即可。 - sidereal
6
这还要取决于具体的实现方式。在MySQL中,至少是我几年前使用的那个版本中,JDBC驱动程序实际上会根据模板和绑定变量构建一个传统的SQL查询语句。我猜测那个MySQL版本并没有本地支持预编译语句,因此他们在JDBC驱动程序中实现了它们。 - Jay
你有一个语句 - 好的,当我只有PreparedStatement实例时,我该如何将其打印到控制台? - O. R. Mapper
8
如果您正在使用java.sql.PreparedStatement,简单地在preparedStatement上使用.toString()将包含生成的SQL。我已经在1.8.0_60中进行验证。 - Preston
11
对于Oracle数据库,PreparedStatement#toString()不会显示SQL语句。因此,我猜这取决于DB JDBC驱动程序。 - Mike Argyriou
显示剩余3条评论

71

虽然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,它在构造时包装(装饰)了真实的PreparedStatement并重写所有方法,以便调用真实的PreparedStatement的方法并收集所有setXXX()方法中的值,并在任何executeXXX()方法之一被调用时惰性地构造出“实际”的SQL字符串(相当繁琐,但大多数IDE都提供了装饰方法的自动生成器,例如Eclipse)。最后只需使用它即可。这基本上也是P6Spy和类似工具在幕后所做的。


这与我正在使用的方法(您的prepareStatement方法)类似。我的问题不是如何执行它 - 我的问题是如何记录sql语句。我知道我可以做 logger.debug(sql + " " + Arrays.asList(values)) - 我正在寻找一种将参数已经集成到其中的sql语句记录的方法,而无需循环自己并替换问号。 - froadie
然后请转到我的答案的最后一段或查看P6Spy。他们会为您完成“恶心”的循环和替换工作 ;) - BalusC
P6Spy的链接现在已经失效了。 - Stephen P
@BalusC 我是JDBC的新手。我有一个疑问。如果你编写像那样的通用函数,那么它将每次创建PreparedStatement。这不是一种不太高效的方式吗?因为PreparedStatement的整个重点是在一个地方创建它们并在任何地方重复使用它们。 - Bhushan
这也适用于voltdb jdbc驱动程序,以获取准备语句的完整SQL查询。 - k0pernikus

40

我正在使用Java 8、JDBC驱动程序和MySQL连接器v.5.1.31。

我可以使用这种方法获取实际的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 所寻找的。 - Joshua Stafford
Postgres驱动程序中是否有类似的函数? - davidwessman
如何获取Apache Derby - Gunasekar
1
@ErcanDuman,我的答案不是通用的,它仅涵盖Java 8和MySQL JDBC驱动程序。 - userlond
@HuckIt 它适用于MySQL和PostgreSQL JDBC驱动程序,但不适用于SQLServer,因此它不是通用的。 - Matthieu
显示剩余3条评论

26

如果你正在执行查询并且期望得到一个 ResultSet (至少在这种情况下),那么你可以简单地调用 ResultSetgetStatement() 方法,如下所示:

ResultSet rs = pstmt.executeQuery();
String executedQuery = rs.getStatement().toString();
变量executedQuery将包含用于创建ResultSet的语句。
现在,我意识到这个问题很老了,但我希望这能帮助某人...

10
@Elad Stern,它打印的是"oracle.jdbc.driver.OraclePreparedStatementWrapper@1b9ce4b",而不是执行的SQL语句!请指导我们! - AVA
@AVA,你使用了toString()吗? - Elad Stern
@EladStern 使用toString()! - AVA
@AVA,嗯,我不确定,但可能与你的jdbc驱动程序有关。我已经成功使用了mysql-connector-5。 - Elad Stern
3
rs.getStatement() 只会返回语句对象,因此取决于您使用的驱动程序是否实现了 .toString() 方法,从而决定是否能够获得 SQL 语句。 - Daz
显示剩余2条评论

4
我已经通过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()与我的情况不同,那只是需要“调整”正则表达式的问题。

4

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

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

将已经替换了?的SQL语句显示出来,这正是我所需要的。为了覆盖postgres情况,我添加了这个答案。

我从未想过它可以如此简单。


2

非常晚 :) 但是您可以通过OraclePreparedStatementWrapper获得原始SQL,只需

((OraclePreparedStatementWrapper) preparedStatement).getOriginalSql();

4
当我尝试使用包装器时,它显示:“oracle.jdbc.driver.OraclePreparedStatementWrapper”在“oracle.jdbc.driver”中不是public的,不能从包外访问。你是如何使用这个类的?请说明。 - spectrum

2

代码片段,用于将带有参数列表的SQL PreparedStatements转换。它对我有效。

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

1

我实现了以下代码以从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[" +  "] ...");
            }

    }

1
使用Oracle:
public static String getSQLFromPreparedStatement(PreparedStatement preparedStatement) {
    if (preparedStatement instanceof OraclePreparedStatement) {
        OraclePreparedStatement oraclePreparedStatement = (OraclePreparedStatement) preparedStatement;
        try {
            return oraclePreparedStatement.getOriginalSql();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    return null;
}

你的回答可以通过提供更多支持性信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人能够确认你的回答是否正确。你可以在帮助中心找到关于如何撰写好回答的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接