如何获取PreparedStatement的SQL?


160

我有一个带有以下方法签名的常规Java方法:

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

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

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

所以...有什么方法可以获取将由a运行的实际SQL语句PreparedStatement?(没有自己构建它。如果我找不到访问PreparedStatement'sSQL的方法,我可能最终会在自己的catches中最终构建它。)


1
如果您正在编写直接的JDBC代码,则强烈建议您查看Apache commons-dbutils commons.apache.org/dbutils。它极大地简化了JDBC代码。
刘坚

Answers:


173

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

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

但是,无论是在Java端还是在数据库端,都没有重建实际的真实SQL查询的方法。

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


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

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

22
尽管从功能上讲这是正确的,但没有什么能阻止实用程序代码重建等效的未准备好的语句。例如,在log4jdbc中:“在记录的输出中,对于准备好的语句,将绑定参数自动插入到SQL输出中。这在许多情况下大大提高了可读性和调试性。” 只要您知道数据库服务器实际上不是执行语句的方式,它就对调试非常有用。
恒星

6
这也取决于实现方式。在MySQL(至少是我几年前使用的版本)中,JDBC驱动程序实际上是根据模板和绑定变量构建了常规的SQL查询。我猜想MySQL版本本身不支持准备好的语句,所以他们在JDBC驱动程序中实现了它们。
杰伊(Jay)

@sidereal:这就是我“手动构建查询”的意思;但是你说的比我好;@Jay:我们在PHP中使用了相同的机制(受支持时为真正的预备语句;不支持它们的数据库驱动程序为伪预备语句)
Pascal MARTIN 2010年

6
如果您使用的是java.sql.PreparedStatement,则prepareStatement上的简单.toString()将包含生成的SQL,我已在1.8.0_60
Pr0n

5
@Preston对于Oracle DB,PreparedStatement#toString()不显示SQL。因此,我想这取决于DB JDBC驱动程序。
Mike Argyriou 2016年

57

JDBC API合同中没有定义它,但是如果您幸运的话,有问题的JDBC驱动程序可以通过调用来返回完整的SQL PreparedStatement#toString()。即

System.out.println(preparedStatement);

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

或者,您也可以编写一个通用函数,该函数接受ConnectionSQL字符串和语句值,并PreparedStatement在记录SQL字符串和值后返回a 。开球示例:

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 on结构并覆盖所有方法,以便它调用real 的方法PreparedStatement并收集所有setXXX()方法中的值,并在以下任何一种情况下懒惰地构造“实际” SQL字符串这些executeXXX()方法被称为(相当有用,但是大多数IDE都为decorator方法提供了自动生成器,而Eclipse则提供了)。最后,只需使用它即可。基本上,这也是P6Spy和合作伙伴已经在后台进行的操作。


这类似于我使用的方法(您的prepareStatement方法)。我的问题不是如何执行-我的问题是如何记录 sql语句。我知道我能做的logger.debug(sql + " " + Arrays.asList(values))-我正在寻找一种方法来记录带有已集成到其中的参数的sql语句。不用循环自己和替换问号。
弗罗迪

然后转到我的答案的最后一段,或查看P6Spy。他们为您完成“讨厌的”循环和替换工作;)
BalusC 2010年

到P6Spy的链接现在已断开。
Stephen P

@BalusC我是JDBC的新手。我有一个疑问。如果您这样编写通用函数,那么它将PreparedStatement每次创建。那不是那么有效的方法PreparedStatement吗?要不是将它们创建一次并在各处重复使用?
Bhushan

这也可以在voltdb jdbc驱动程序上使用,以获取准备好的语句的完整sql查询。
k0pernikus

37

我正在使用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所寻找的。
HuckIt '16

Postgres驱动程序有类似的功能吗?
davidwessman '17

如何获得它Apache Derby
Gunasekar '18 -10-6

它不工作,并抛出ClassCastException异常:java.lang.ClassCastException:oracle.jdbc.driver.T4CPreparedStatement不能转换到com.mysql.jdbc.JDBC4PreparedStatement
埃尔詹

1
@ErcanDuman,我的回答不是通用的,它仅涵盖Java 8和MySQL JDBC驱动程序。
userlond

21

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

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

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

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


6
@Elad Stern它打印oracle.jdbc.driver.OraclePreparedStatementWrapper@1b9ce4b,而不是打印已执行的sql语句!请指导我们!
AVA 2015年

@AVA,您使用toString()吗?
伊拉德·斯特恩

使用@EladStern toString()!
AVA 2015年

@AVA,我不确定,但可能与您的jdbc驱动程序有关。我已经成功使用了mysql-connector-5。
伊拉德·斯特恩

3
rs.getStatement()仅返回语句对象,因此取决于您使用的驱动程序是否实现.toString()来确定是否要取回SQL
Daz

3

我已经使用prepareStatement.toString()从PreparedStatement中提取了我的sql,在我的情况下,toString()返回的String是这样的:

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

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

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

此方法返回具有键值对的map:

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

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


2

使用带有官方Java驱动程序的PostgreSQL 9.6.x42.2.4

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

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

我永远都不会想到它会如此简单。


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

代码段转换带有参数列表的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;
        }

1

很晚了:),但是您可以通过以下方式从OraclePreparedStatementWrapper获取原始SQL:

((OraclePreparedStatementWrapper) preparedStatement).getOriginalSql();

1
当我尝试使用包装器时,它说:oracle.jdbc.driver.OraclePreparedStatementWrapper在oracle.jdbc.driver中不是公共的。无法从外部软件包访问。您如何使用该课程?
频谱


-1

简单起作用:

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

对于prepareStatement也很好。


这只是一个简单的命令.toString(),用几行多余的行来欺骗不熟练的用户,并且早在很久以前就已经回答过。
佩雷

-1

我正在使用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可以在(同时)循环中动态构建,我只是简单地弄清楚了使用MessageFormat类充当SQL查询的字符串模板格式化程序。


4
这种炸毁了使用准备好的语句的全部原因,例如避免了sql注入和提高了性能。
壁虱2016年

1
我100%同意你的看法。我应该明确指出,对于大规模海量数据集成,我最多可以执行两次此代码,并且迫切需要一种快速的方法来获得一些日志输出,而不必花费整个log4j enchilada的时间,因为这样做过分了我需要。这不应写入生产代码中:-)
Diego Tercero

这对我适用于Oracle驱动程序(但不包括参数):((OraclePreparedStatementWrapper) myPreparedStatement).getOriginalSql()
latj

-4

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

看看log4jdbc


3
看一下log4jdbc,然后呢?你如何使用它?您可以转到该站点,并随机浏览有关该项目的内容,而没有关于如何实际使用该技术的明确示例。
霍利
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.