我们的 Java 代码使用Statement如下方式:

Statement stmt = _CONN.createStatement();
boolean isRst = stmt.execute(sql);
if (isRst) {
    do {
        ResultSet rs = stmt.getResultSet();
        setColumnInfo(rs);
        while (rs.next())
            outputOneRecord(rs);
        rs.close();
    } while (fullerror && stmt.getMoreResults());
} else {
    int r = stmt.getUpdateCount();
    if (r != -1)
        logger.info("Update count: {}", r);
}

没什么不寻常的;它已经工作了很多年。最近,我们将 JDBC 提供程序 JAR 从旧的改为了旧的:

<dependency>
    <groupId>com.microsoft</groupId>
    <artifactId>sqljdbc4</artifactId>
    <version>4.0.2206.100</version>
</dependency>

新的:

<dependency>
    <groupId>com.microsoft.sqlserver</groupId>
    <artifactId>mssql-jdbc</artifactId>
    <version>12.4.1.jre11</version>
</dependency>

几乎所有内容都像以前一样继续工作,除了一个存储过程,它首先执行一些更新,然后以SELECT语句结束。

旧的 JDBC 实现曾经返回这些结果SELECT,但是,当使用新的 JAR 时,我们的stmt.execute(sql)返回是 false ——代码报告更新计数并退出而不输出任何行。

替换 CLASSPATH 中的 JDBC JAR 路径,放入旧的依赖项而无需重新编译任何内容,即可解决问题。

这是记录在案的行为改变吗stmt.execute("EXEC sp_Foo")?还是微软新代码中的一个错误?

8

  • 你要导入什么包? _CONN 是什么? 添加一个


    – 

  • 3
    @aled 此时此刻,这只是充满敌意的评论。_CONN是一种联系;鉴于整篇文章都在大肆宣扬原始 JDBC,要求在问题中阐明这一点是不合理的。在这种情况下,如果没有 2 个独立版本的外部依赖、整个非免费数据库引擎和非平凡的存储过程,就不可能实现最低限度的可重现示例。你这种过于急切的投票关闭行为并没有给 SO 带来任何好处;我请你重新考虑一下你的限制。SO 的声誉在这里确实没有余地。


    – 


  • 1
    更一般地,由于观察到的故障与特定的 SP 相关,您可能应该考虑(并告诉我们)哪些特征将该 SP 与系统中所有其他继续按预期工作的 SP 区分开来。


    – 

  • 2
    @rzwitserloot 我没有对这个问题投票。此外,MRE 应该包括对这个问题来说很重要的储存程序。


    – 

  • 1
    @MikhailT.,您告诉我们这是相关 SP 的行为。您没有告诉我们这种行为是否将此 SP 与所有其他继续按预期工作的 SP 区分开来。无论如何,我很高兴您找到了解决方案。


    – 


最佳答案
2

不相关的前言——安全警告!

没什么不寻常的

这很不寻常,可能是一个巨大的安全漏洞。是否sql包含任何未直接硬编码在源文件中的输入?任何用户输入、配置文件中的条目等等?您几乎永远不应该使用 Statement,而应该使用 PreparedStatement。请参阅。如果全部是硬编码的,基本上不用担心,但您仍然没有完全脱离危险。是否每个人都知道,当您开始使用用户提供的输入调用此方法的那一刻,您的系统就完全可以轻易被攻破?就“安全问题”而言,“易受 SQL 注入”是五级火灾。如果恶意代理发现这样的漏洞,您就彻底失败了。他们控制了您的整个数据库。

海仑出击?

的必然性:也许执行对以结尾存储过程的调用的行为SELECT实际上并不按规范要求返回true,所以,这将是一个指责游戏:提交一个错误,但 MS 只会否认它说“它没有违反任何规范”。

或者…它是多结果 API!

JDBC 有点奇怪的性质是,任何对 DB 的调用都可能产生一系列结果”。你所做的绝大多数调用都只产生一个结果,而且这个结果有两种完全不同的形式:[A] 更新计数,或 [B] ResultSet

当您有多个结果时,每个单独的结果可以是“更新计数”风格,也可以是“结果集”风格。

由于某些 DB 引擎上的存储过程能够产生多个结果,因此将此“多个结果”明确添加到 JDBC,这听起来就像您在此处遇到的情况

API 已经过时;一般来说,JDBC API 根本不适合直接使用(改用 JDBI 或 JOOQ 1,一些抽象 – 它被设计为低级粘合剂;我们也不会直接用机器代码编写)。在这种情况下,对多结果的支持有点笨拙地注入了 API 的现有方面。因此,为什么它以以下奇怪的方式工作:

  1. 调用execute您的语句。这将指示执行由布尔值产生的许多潜在结果中的第一个结果的风格。false意味着第一个结果属于“更新计数”风格,true意味着它属于这种ResultSet风格。但这仅指第一个结果的风格,它没有提到任何其他结果!

  2. 按照您希望的方式处理第一个结果;大概是,仅当第一个结果的味道是味道时才getResultSet()调用StatementResultSet

  3. 你还没完!调用getMoreResults()返回一个boolean。这个布尔值可能是你在 API 中见过的最奇怪的布尔值。true意思是:

  • 还有另外一个结果。
  • 它是一个结果集。
  • 如果您为刚刚经过的结果打开了一个结果集,那么现在该结果集已关闭。

到目前为止,这还算合理。但是,这false意味着:

  • updatecount 风格还有另一个结果。
  • …或者,没有其他结果。
  • 与以前一样,任何打开的 ResultSet 都已关闭。

因此,现在您需要消除这两个歧义!您可以通过调用 来做到这一点getUpdateCount()。如果返回-1,则表示“没有其他结果”。如果返回其他任何内容,则可能还有更多结果。

是的,您可以调用getMoreResults(),获取false,然后再次调用,然后获取true。如果您的存储过程返回:

5(更新计数) 一个结果集 3(更新计数) 另一个结果集

那么事件的顺序如下:

statement.execute();        // false (5 is UpdCount flavour)
statement.getUpdateCount(); // 5
statement.getMoreResults(); // true
statement.getResultSet();   // results retrievable here
statement.getMoreResults(); // false
statement.getUpdateCount(); // 3
statement.getMoreResults(); // true. true? What the.. yes.
statement.getResultSet();   // results retrievable here
statement.getMoreResults(); // false
statement.getUpdateCount(); // -1

相应地更新您的代码,不要只是说:哦,exec返回 false 因此没有结果集。相反,您需要编写一些循环。

你应该在这段代码中写大量的注释,因为期望一个月后看到这段代码的工程师知道 JDBC 的“多结果” API 是如此奇怪是不合理的。

请记住:hasMoreResults() 正在骗你。如果它false这么说,并不意味着没有更多结果。

如果您能给我们反馈,我将非常高兴:这是答案吗?我将其作为答案发布是因为它似乎相关(由于您使用存储过程),并且根据规范,这很可能是解释,但是,我决定将其作为答案发布,部分原因是不知道任何其他非完全奇特的解释。但是,这可能只是缺乏对 MSSQL 的 JDBC 驱动程序的了解。


[1] JDBI 和 JOOQ 几乎公开了 JDBC 的所有方面,但为了提供适合当前任务的 API(即让应用程序开发人员继续使用 SQL 与数据库引擎进行对话),他们已经删除了 JDBC 中一些非常奇特的角落,而这(多结果支持)可能就是其中之一,在这种情况下,这是非常卑鄙的建议 – 这项特定工作无法通过 JOOQ 或 JDBI 完成。我只是不知道 – 这个脚注的存在只是为了强调我不保证 JDBI 或 JOOQ 可以做到这一点。

7

  • 1
    这看起来确实像,而且是旧的实现,有缺陷。在如果第一个结果是 ResultSet 对象,则应该Statement.execute()返回;如果是更新计数或没有结果,则返回 false。新的 JAR 修复了我们在不知不觉中依赖的缺陷 🙁 但是,是的,查看结果是没有意义的——必须继续尝试…true execute


    – 


  • @MikhailT.


    – 

  • @MikhailT. 查看的结果execute()并非毫无意义,只是因为 而复杂,这false仅意味着您应该调用getUpdateCount(),而有关结果的实际信息是是否getUpdateCount()返回-1(没有更多结果)或更新计数。


    – 

  • @rzwitserloot,我发布了自己的答案,详细说明了不同驱动程序之间可观察到的行为差异——正如您所要求的那样 🙂 在我看来,新版本实际上是正确的。我不明白,为什么旧​​版本在几年前没有被谴责为有缺陷的……


    – 

  • @MikhailT 谢谢 🙂 – 如果这个hasMoreResults()东西比较新(JDBC4.2 或 JDBC5 或诸如此类),而旧驱动程序已经尽了最大努力,我不会感到惊讶,也就是说,如果众多结果中至少有一个是结果集,则只返回该结果集,而不是第一个结果,因为这可能是调用者正在寻找的。很高兴它成功了!


    – 

重写了原始代码如下:

Statement stmt = _CONN.createStatement();
boolean isRst = stmt.execute(sql);
logger.debug("stmt.execute() returned {}", isRst);
for (int i = 0;; i++) {
    if (isRst) {
        ResultSet rs = stmt.getResultSet();
        int columns = rs.getMetaData().
            getColumnCount();
        logger.debug("{}: a resultSet " +
            "with {} column{}", i, columns,
            columns == 1 ? "" : 's');
        setColumnInfo(rs);
        int rows = 0;
        while (rs.next()) {
            outputOneRecord(rs);
            rows++;
        }
        rs.close();
        logger.debug("{}: {} row{}", i,
            rows, rows == 1 ? "" : 's');
    } else {
        int r = stmt.getUpdateCount();
        if (r == -1)
            break;
        logger.debug("{}: update-count: {}",
            i, r);
    }
    isRst = stmt.getMoreResults();
}

我使用两种 JDBC 后端实现对同一存储过程重新运行了该程序。丰富的调试日志记录说明了驱动程序之间的差异。

使用旧版本 4,我们只需:

stmt.execute() returned true
0: a resultSet with 5 columns
0: 61 rows
1: update-count: 0

新版本还有更多内容:

stmt.execute() returned false
0: update-count: 61
1: update-count: 120060
2: update-count: 120000
3: update-count: 60
4: update-count: 60
5: update-count: 1
6: a resultSet with 5 columns
6: 61 rows
7: update-count: 0

新实现的结果与存储过程内的实际 SQL 代码相匹配。旧实现似乎跳过了所有初始“更新”——其结果 0 实际上是结果 6。

我不知道为什么——但是无论底层后端是什么,我们的新代码都应该正常工作。新的行为确实看起来是正确的——并且符合——但它破坏了我们的旧代码,因为第一个结果是更新,导致execute()返回false