Using table-valued parameters with SQL Server JDBC

久未见 提交于 2021-02-07 23:54:28

问题


Could anyone provide some guidance on how to use Table Value Parameters (TVP) with SQL Server JDBC? I am using the 6.0 version of the SQL Server driver provided by Microsoft, and I have reviewed the official documentation as well as the more helpful example

How to pass Table-Valued parameters from java to sql server stored procedure?

Both of the examples show getting a SQLServerPreparedStatement casted object from a Connection.prepareStatement call in order to call the setStructured method. Wouldn't this prevent usage of standard connection pools such as DBCP2?

I noticed a comment from the other Stack Overflow comment saying it might be possible to use the stmt.setObject method instead:

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

Though I got an error when I tried this, and the type is in the dbo schema...

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

Here's my code:

// 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

Any help or guidance is appreciated. Thanks!


回答1:


Here's the route that I ended up using. DBCP2 has a DelegatingStatement.getInnermostDelegate method to get the PreparedStatement object created by the Microsoft driver. I'm not a huge fan of the hoops needed to jump through - even if error checking was added the code seems brittle. TVP is a SqlServer specific thing so maybe it's not so bad to use the required assumptions and casts.

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



回答2:


re: using PreparedStatement#setObject

I fooled around with it a bit and I could not get it to work either. Perhaps it was a feature in the 6.0 preview that got pulled from the final release.

re: using a SQLServerPreparedStatement object

I don't see why that would prevent using DBCP2 (or any other connection pool) per se, although it might have implications for DBCP2's poolPreparedStatements option (which is false by default). Still, if one wanted to create a "proper" PreparedStatement object and still needed to use setStructured then this worked fine when I tried it just now:

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

However, given that we are calling a stored procedure it might be "good form" to use a CallableStatement object (with a quickie cast to SQLServerCallableStatement for the setStructured call) instead of a PreparedStatement object.



来源:https://stackoverflow.com/questions/40387895/using-table-valued-parameters-with-sql-server-jdbc

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!