在 SQL 服务器 JDBC 中使用 table 值参数

Using table-valued parameters with SQL Server JDBC

任何人都可以提供一些关于如何将 Table 值参数 (TVP) 与 SQL 服务器 JDBC 一起使用的指导吗?我使用的是 Microsoft 提供的 SQL 服务器驱动程序的 6.0 版本,我已经查看了 official documentation 以及更有帮助的示例

这两个示例都显示从 Connection.prepareStatement 调用中获取 SQLServerPreparedStatement 转换对象以调用 setStructured 方法。这不会阻止使用标准连接池(例如 DBCP2)吗?

我注意到其他 Stack Overflow 评论中的一条评论说可以改用 stmt.setObject 方法:

As an alternative to casting the PreparedStatement, you can pass the SQLServerDataTable instance to the PreparedStatement.setObject(int,Object) method. This worked for a TVP type that was defined in the dbo schema. – allenru Jul 15 at 19:18

虽然我在尝试这个时遇到错误,并且 dbo 架构中的类型 ...

Exception in thread "main" com.microsoft.sqlserver.jdbc.SQLServerException:  The Table-Valued Parameter must have a valid type name.

这是我的代码:

// JAVA
private static void tableParameters(DataSource ds)  throws SQLException {
    final String sql = "EXEC dbo.GetAccountsFromTable @accountIds=?";
    final List<Integer> accountIds = generateIntegers(50, 1_000_000);

    try (Connection conn = ds.getConnection();
            PreparedStatement stmt = conn.prepareStatement(sql))
    {
        SQLServerDataTable accounts = new SQLServerDataTable();
        accounts.addColumnMetadata("item", java.sql.Types.INTEGER);
        for (Integer aid : accountIds)
            accounts.addRow(aid.toString());

        stmt.setObject(1, accounts);

        try (ResultSet rs = stmt.executeQuery())
        {
            while (rs.next()) {
                System.out.println(rs.getInt(1));
            }
        }
    }
}

-- TSQL
create type dbo.IntegerTable AS TABLE (item INT);

CREATE PROCEDURE dbo.GetAccountsFromTable(@accountIds dbo.IntegerTable READONLY)
AS
BEGIN
  IF OBJECT_ID('tempdb..#AccountIds') IS NOT NULL
    DROP TABLE #AccountIds

  CREATE TABLE #AccountIds (id INTEGER)

  INSERT INTO #AccountIds
  SELECT * FROM @accountIds

  SELECT * FROM #AccountIds
END

感谢任何帮助或指导。谢谢!

回复:使用 PreparedStatement#setObject

我把它弄糊涂了一点,但我也无法让它工作。也许这是 6.0 预览版中从最终版本中删除的功能。

回复:使用 SQLServerPreparedStatement 对象

我不明白为什么这会阻止使用 DBCP2(或任何其他连接池)本身,尽管它可能对 DBCP2 的 poolPreparedStatements 选项有影响(默认情况下是 false)。尽管如此,如果有人想创建一个 "proper" PreparedStatement 对象并且仍然需要使用 setStructured 那么我刚才尝试时它工作正常:

String sql = "EXEC dbo.GetAccountsFromTable ?";
try (
        Connection conn = DriverManager.getConnection(connectionUrl);
        PreparedStatement stmt = conn.prepareStatement(sql)) {
    SQLServerDataTable accounts = new SQLServerDataTable();
    accounts.addColumnMetadata("item", java.sql.Types.INTEGER);
    accounts.addRow(123);
    accounts.addRow(234);
    ((SQLServerPreparedStatement) stmt).setStructured(1, "dbo.IntegerTable", accounts);
    try (ResultSet rs = stmt.executeQuery()) {
        while (rs.next()) {
            System.out.println(rs.getInt(1));
        }
    }
} catch (Exception e) {
    e.printStackTrace(System.err);
}

但是,如果我们正在调用存储过程,则可能 "good form" 使用 CallableStatement 对象(对于 setStructured 快速转换为 SQLServerCallableStatement调用)而不是 PreparedStatement 对象。

这是我最终使用的路线。 DBCP2 有一个 DelegatingStatement.getInnermostDelegate 方法来获取 Microsoft 驱动程序创建的 PreparedStatement 对象。我不太喜欢跳过这些障碍——即使添加了错误检查,代码看起来也很脆弱。 TVP 是特定于 SqlServer 的东西,因此使用所需的假设和强制转换可能还不错。

private static int testTvp(DataSource ds, List<Integer> accountIds)  throws SQLException {
    final String sql = "EXEC dgTest.GetAccountsFromTvp @accountIds=?";

    try (Connection conn = ds.getConnection();
            PreparedStatement stmt = conn.prepareStatement(sql)) {
        DelegatingPreparedStatement dstmt = (DelegatingPreparedStatement)stmt;
        SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)dstmt.getInnermostDelegate();

        SQLServerDataTable accounts = new SQLServerDataTable();
        accounts.addColumnMetadata("token", java.sql.Types.INTEGER);
        for (Integer aid : accountIds)
            accounts.addRow(aid);

        pstmt.setStructured(1, "dgTest.IntegerTable", accounts);

        //// NOTE: The below works for JTDS driver, official MS driver said no result sets were returned
        //try (ResultSet rs = pstmt.executeQuery()) {
        //  return sumInts(rs);
        //}

        if (pstmt.execute()) {
            try (ResultSet rs = pstmt.getResultSet()) {
                return sumInts(rs);
            }
        }
        return -1;
    }
}