我们的 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
最佳答案
2
不相关的前言——安全警告!
没什么不寻常的
这很不寻常,可能是一个巨大的安全漏洞。是否sql
包含任何未直接硬编码在源文件中的输入?任何用户输入、配置文件中的条目等等?您几乎永远不应该使用 Statement,而应该使用 PreparedStatement。请参阅。如果全部是硬编码的,基本上不用担心,但您仍然没有完全脱离危险。是否每个人都知道,当您开始使用用户提供的输入调用此方法的那一刻,您的系统就完全可以轻易被攻破?就“安全问题”而言,“易受 SQL 注入”是五级火灾。如果恶意代理发现这样的漏洞,您就彻底失败了。他们控制了您的整个数据库。
海仑出击?
的必然性:也许执行对以结尾的存储过程的调用的行为SELECT
实际上并不按规范要求返回true
,所以,这将是一个指责游戏:提交一个错误,但 MS 只会否认它说“它没有违反任何规范”。
或者…它是多结果 API!
JDBC 有点奇怪的性质是,任何对 DB 的调用都可能产生一系列“结果”。你所做的绝大多数调用都只产生一个结果,而且这个结果有两种完全不同的形式:[A] 更新计数,或 [B] ResultSet
。
当您有多个结果时,每个单独的结果可以是“更新计数”风格,也可以是“结果集”风格。
由于某些 DB 引擎上的存储过程能够产生多个结果,因此将此“多个结果”明确添加到 JDBC,这听起来就像您在此处遇到的情况。
API 已经过时;一般来说,JDBC API 根本不适合直接使用(改用 JDBI 或 JOOQ 1,一些抽象 – 它被设计为低级粘合剂;我们也不会直接用机器代码编写)。在这种情况下,对多结果的支持有点笨拙地注入了 API 的现有方面。因此,为什么它以以下奇怪的方式工作:
-
调用
execute
您的语句。这将指示执行由布尔值产生的许多潜在结果中的第一个结果的风格。false
意味着第一个结果属于“更新计数”风格,true
意味着它属于这种ResultSet
风格。但这仅指第一个结果的风格,它没有提到任何其他结果! -
按照您希望的方式处理第一个结果;大概是,仅当第一个结果的味道是味道时才
getResultSet()
调用。Statement
ResultSet
-
你还没完!调用
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
。
|
–
_CONN
是一种联系;鉴于整篇文章都在大肆宣扬原始 JDBC,要求在问题中阐明这一点是不合理的。在这种情况下,如果没有 2 个独立版本的外部依赖、整个非免费数据库引擎和非平凡的存储过程,就不可能实现最低限度的可重现示例。你这种过于急切的投票关闭行为并没有给 SO 带来任何好处;我请你重新考虑一下你的限制。SO 的声誉在这里确实没有余地。–
–
–
–
|