通过 useFmtOnly 检索 ParameterMetaData

下载 JDBC 驱动程序

The Microsoft JDBC Driver for SQL Server 提供从服务器查询参数元数据的另一种方法,即 useFmtOnly。 此功能是在驱动程序版本 7.4 中首次引入的,需要作为 sp_describe_undeclared_parameters 中已知问题的解决方法。

驱动程序主要使用存储过程 sp_describe_undeclared_parameters 来查询参数元数据。 在大多数情况下,此过程是用于检索参数元数据的推荐方法。 但是,在以下用例中,当前执行存储过程会失败:

  • 针对 Always Encrypted 列
  • 针对临时表和表变量
  • 针对视图

针对这些用例的建议解决方案是,分析用户的参数和表目标的 SQL 查询,然后在启用 FMTONLY 的情况下执行 SELECT 查询。 以下代码片段具体演示了此功能。

--create a normal table 'Foo' and a temporary table 'Bar'
CREATE TABLE Foo(c1 int);
CREATE TABLE #Bar(c1 int);

EXEC sp_describe_undeclared_parameters N'SELECT * FROM Foo WHERE c1 = @p0' --works fine
EXEC sp_describe_undeclared_parameters N'SELECT * FROM #Bar WHERE c1 = @p0' --fails with "Invalid object name '#Bar'"

SET FMTONLY ON;
SELECT c1 FROM Foo; --works
SET FMTONLY OFF;
SET FMTONLY ON;
SELECT c1 FROM #Bar; --works
SET FMTONLY OFF;

启用/禁用功能

默认情况下,useFmtOnly 功能处于禁用状态。 用户可以通过指定 useFmtOnly=true 在连接字符串中启用此功能。 例如:jdbc:sqlserver://<server>:<port>;encrypt=true;databaseName=<databaseName>;user=<user>;password=<password>;useFmtOnly=true;

也可通过 SQLServerDataSource 提供该功能。

SQLServerDataSource ds = new SQLServerDataSource();
ds.setServerName(<server>);
ds.setPortNumber(<port>);
ds.setDatabaseName("<databaseName>");
ds.setUser("<user>");
ds.setPassword("<password>");
ds.setUseFmtOnly(true);
try (Connection c = ds.getConnection()) {
    // do work with connection
}

此功能也可在语句级别提供。 用户可以通过 PreparedStatement.setUseFmtOnly(boolean) 启用/禁用该功能。

备注

驱动程序将对“语句级别”属性和“连接级别”属性设置优先级。

使用功能

启用新功能后,驱动程序将在内部开始使用该功能,而不是在查询参数元数据时使用 sp_describe_undeclared_parameters。 最终用户不需要执行任何其他操作。

final String sql = "INSERT INTO #Bar VALUES (?)";
try (Connection c = DriverManager.getConnection(URL, USERNAME, PASSWORD)) {
    try (Statement s = c.createStatement()) {
        s.execute("CREATE TABLE #Bar(c1 int)");
    }
    try (PreparedStatement p1 = c.prepareStatement(sql); PreparedStatement p2 = c.prepareStatement(sql)) {
        ((SQLServerPreparedStatement) p1).setUseFmtOnly(true);
        ParameterMetaData pmd1 = p1.getParameterMetaData();
        System.out.println(pmd1.getParameterTypeName(1)); // prints int
        ParameterMetaData pmd2 = p2.getParameterMetaData(); // throws exception, Invalid object name '#Bar'
    }
}

备注

该功能仅支持 SELECT/INSERT/UPDATE/DELETE 查询。 查询应以 4 个支持的关键字之一或公用表表达式开头,后跟一个支持的查询。 不支持使用公用表表达式中的参数。

已知问题

此功能当前存在一些问题,这些问题是由 SQL 分析逻辑中的缺陷导致的。 这些问题可能会在以后对该功能的更新中得到解决,现已在下面列出,并提供了解决方法建议。

A. 使用“前向声明”别名

CREATE TABLE Foo(c1 int)

DELETE fooAlias FROM Foo fooAlias WHERE c1 > ?; --Invalid object name 'fooAlias'

--Workaround #1: Specify AS keyword
DELETE fooAlias FROM Foo AS fooAlias WHERE c1 > ?;
--Workaround #2: Use the table name
DELETE Foo FROM Foo fooAlias WHERE c1 > ?;

B. 当表具有共享的多个列名时,列名不明确

CREATE TABLE Foo(c1 int, c2 int, c3 int)
CREATE TABLE Bar(c1 int, c2 int, c3 int)

SELECT c1,c2 FROM Foo WHERE c3 IN (SELECT c3 FROM Bar WHERE c1 > ? and c2 < ? and c3 = ?); --Ambiguous Column Name

--Workaround: Use aliases
SELECT c1,c2 FROM Foo WHERE c3 IN (SELECT c3 FROM Bar b WHERE b.c1 = ? and b.c2 = ? and b.c3 = ?);

C. 从包含参数的子查询中 SELECT


CREATE TABLE Foo(c1 int)

SELECT * FROM (SELECT * FROM Foo WHERE c1 = ?) WHERE c1 = ?; --Incorrect syntax near '?'

--Workaround: N/A

D. SET 子句中存在子查询

CREATE TABLE Foo(c1 int)

UPDATE Foo SET c1 = (SELECT c1 FROM Foo) WHERE c1 = ?; --Incorrect syntax near ')'

--Workaround: Add a 'delimiting' condition
UPDATE Foo SET c1 = (SELECT c1 FROM Foo HAVING (HASH JOIN)) WHERE c1 = ?;

另请参阅

设置连接属性