如何通过本机ODBC绑定数组参数(存储过程)

时间:2017-11-02 21:30:52

标签: c sql-server windows stored-procedures odbc

我一直在寻找一种通过C / C ++中的ODBC API将整数数组传递给存储过程的方法。虽然VARCHAR(XX)为数组类型的字符,但整数没有数组类型,例如VARINT(XX)或类似的类型。

许多答案(例如How to send a big array to a stored procedure)建议使用表格值参数。但是,它们都使用 C#/ ADO.NET,Java或PHP 代码,其类型在C ODBC API中根本不可用。

我的存储过程:

-- "Array" Parameter type, used in the stored procedure later on
CREATE TYPE dbo.CharList
AS TABLE (Id INT)
GO

CREATE PROCEDURE [dbo].[Update_Character_Nation]
    @OldNation_Id   INT,
    @NewNation_Id   INT,
    @Excluded       dbo.CharList READONLY, -- array / table parameter
AS
BEGIN
    SET NOCOUNT ON;

    UPDATE Characters
    SET Nation_Id = @NewNation_Id
    WHERE Nation_Id = @OldNation_Id AND Id NOT IN (SELECT Id FROM @ExcludedChars) -- parameter used to exclude result sets here
END

所以我尝试了几件事:

1。直接传递数组

void UpdateCharacterNationTestArray()
{
    unsigned int old_nation_id = 2;
    unsigned int new_nation_id = 4;

    unsigned int excluded_character_ids[] = {24, 36};
    SQLLEN arr_size = ARRAYSIZE(excluded_character_ids);
    SQLINTEGER cb = SQL_NTS;

    SQLBindParameter(hstmt, 1, SQL_PARAM_INPUT, SQL_C_ULONG, SQL_INTEGER, 0, 0, &old_nation_id, 0, NULL);
    SQLBindParameter(hstmt, 2, SQL_PARAM_INPUT, SQL_C_ULONG, SQL_INTEGER, 0, 0, &new_nation_id, 0, NULL);
    SQLBindParameter(hstmt, 3, SQL_PARAM_INPUT, SQL_C_ULONG, SQL_INTEGER, arr_size, 0, excluded_character_ids, 0, &cb); // Binding array directly, like one would do with a VARCHAR type

    SQLRETURN res = SQLExecDirect(hstmt, (SQLCHAR*)"{call dbo.Update_Character_Nation(?,?,?)}", SQL_NTS);
    if (res != SQL_SUCCESS && res != SQL_SUCCESS_WITH_INFO)
    {
        printf("Error during query execution: %hd\n", res);
        ProcessLogs(SQL_HANDLE_STMT, hstmt);
    }
}

这与诊断记录失败:

  

ERROR;土生土长的:206;州:22018; msg:[Microsoft] [ODBC SQL Server   驱动程序] [SQL Server]操作数类型冲突:int与之不兼容   CharList

2。将数组作为表值参数传递(或至少尝试这样做)

void UpdateCharacterNationTestTVP()
{
    unsigned int old_nation_id = 2;
    unsigned int new_nation_id = 4;

    unsigned int excluded_character_ids[] = {24, 36};
    SQLLEN arr_size = ARRAYSIZE(excluded_character_ids);
    SQLINTEGER cb = SQL_NTS;
    SQLCHAR* tvp_name = (SQLCHAR*)"dbo.CharList";

    SQLBindParameter(hstmt, 1, SQL_PARAM_INPUT, SQL_C_ULONG, SQL_INTEGER, 0, 0, &old_nation_id, 0, NULL);
    SQLBindParameter(hstmt, 2, SQL_PARAM_INPUT, SQL_C_ULONG, SQL_INTEGER, 0, 0, &new_nation_id, 0, NULL);
    SQLBindParameter(hstmt, 3, SQL_PARAM_INPUT, SQL_C_DEFAULT, SQL_SS_TABLE, arr_size, 0, tvp_name, 0, &cb); // Binding the type here. All parameters after SQL_PARAM_INPUT are taken from MSDN documentation about TVP

    // Super scary binding stuff tvp requires you to do
    SQLINTEGER cb_rows[] = {SQL_NTS, SQL_NTS};
    SQLSetStmtAttr(hstmt, SQL_SOPT_SS_PARAM_FOCUS, (SQLPOINTER)3, SQL_IS_INTEGER);  // focusing the third parmeter (the TVP one) for the call(s) below
    SQLBindParameter(hstmt, 1, SQL_PARAM_INPUT, SQL_C_ULONG, SQL_INTEGER, arr_size /*or 0*/, 0, excluded_character_ids, sizeof(UINT), cb_rows); // binding of the actual array in a column styled fashion here
    SQLSetStmtAttr(hstmt, SQL_SOPT_SS_PARAM_FOCUS, (SQLPOINTER)0, SQL_IS_INTEGER);  // resetting the focus

    SQLRETURN res = SQLExecDirect(hstmt, (SQLCHAR*)"{call dbo.Update_Character_Nation(?,?,?)}", SQL_NTS);
    if (res != SQL_SUCCESS && res != SQL_SUCCESS_WITH_INFO)
    {
        printf("Error during query execution: %hd\n", res);
        ProcessLogs(SQL_HANDLE_STMT, hstmt);
    }
}

此代码(或多或少地复制了这篇MSDN文章:Use Table-Valued Parameters (ODBC))出错:

  

ERROR;原生:0;州:07002; msg:[Microsoft] [ODBC SQL Server   驱动程序] COUNT字段不正确或语法错误

第3。吗

我最终正在寻找一种使用无符号整数数组调用存储过程的方法。如果有另一种方法,请告知。如果可能的话,我想远离字符串(VARCHAR)解析。

环境

为了可见性,我没有包含ODBC初始化代码,但这些是环境规范:

  • Visual Studio 2013
  • SQL Server 2014
  • ODBC版本:SQL_OV_ODBC3
  • 驱动程序连接:TCP / IPv4
  • hstmt =已分配的SQL语句句柄
  • C ++(但是代码没有使用C ++语言功能,因此100%C兼容)

有关上述方法的任何想法或关于如何将数组绑定为存储过程参数的全新建议都表示赞赏。

2 个答案:

答案 0 :(得分:0)

您最好的选择可能是将数组解析为分隔列表并将其传递给过程。

对于如何做到这一点有一个很好的答案,在下面的问题上引用其他网站等:

T-SQL stored procedure that accepts multiple Id values

答案 1 :(得分:0)

深入挖掘,我发现第一个ODBC API调用失败实际上是对TVP列/参数的绑定调用:

SQLBindParameter(hstmt, 3, SQL_PARAM_INPUT, SQL_C_DEFAULT, SQL_SS_TABLE, arr_size, 0, tvp_name, 0, &cb);

此调用返回SQL_ERROR并检查由此发出的诊断记录给出了以下错误:

  

HY004 [Microsoft] [ODBC SQL Server驱动程序]无效的SQL数据类型

这个具体问题已经在这里提出,但遗憾的是仍未解决。最终使用过时的ODBC驱动程序是导致整个问题的原因。有关如何解决此问题的详细信息,请参阅我在其他问题上的回答:https://stackoverflow.com/a/47113255/2334932

然后由@ A.K提出两点。在他的评论中终于解决了这个问题:

<强> 1。参数长度

传递给SQLBindParameter的最后一个值,参数长度或cb,需要实际可用行数而不是SQL_NTS,因为它在这里用作输入参数。

<强> 2。传递参数名称不是必需的,因为它们受位置

的约束

我认为它可以使用或不使用,但是在这里指定TVP的名称实际上并不是必需的,可以省略。

因此,将第三个SQLBindParameter调用更改为此修复了问题的其余部分:

cb = arr_size;
SQLBindParameter(hstmt, 3, SQL_PARAM_INPUT, SQL_C_DEFAULT, SQL_SS_TABLE, arr_size, 0, NULL, 0, &cb);

完整的工作代码

void UpdateCharacterNationTestTVP()
{
    unsigned int old_nation_id = 2;
    unsigned int new_nation_id = 4;

    unsigned int excluded_character_ids[] = {24, 36};
    SQLLEN arr_size = ARRAYSIZE(excluded_character_ids);
    SQLINTEGER cb = arr_size; // Needs to have the actual amount of available rows

    SQLBindParameter(hstmt, 1, SQL_PARAM_INPUT, SQL_C_ULONG, SQL_INTEGER, 0, 0, &old_nation_id, 0, NULL);
    SQLBindParameter(hstmt, 2, SQL_PARAM_INPUT, SQL_C_ULONG, SQL_INTEGER, 0, 0, &new_nation_id, 0, NULL);
    SQLBindParameter(hstmt, 3, SQL_PARAM_INPUT, SQL_C_DEFAULT, SQL_SS_TABLE, arr_size, 0, NULL, 0, &cb); // Does not need the name of the TVP

    // Super scary binding stuff tvp requires you to do
    SQLINTEGER cb_rows[] = {SQL_NTS, SQL_NTS};
    SQLSetStmtAttr(hstmt, SQL_SOPT_SS_PARAM_FOCUS, (SQLPOINTER)3, SQL_IS_INTEGER);  // focusing the third parmeter (the TVP one) for the call(s) below
    SQLBindParameter(hstmt, 1, SQL_PARAM_INPUT, SQL_C_ULONG, SQL_INTEGER, arr_size /*or 0*/, 0, excluded_character_ids, sizeof(UINT), cb_rows); // binding of the actual array in a column styled fashion here
    SQLSetStmtAttr(hstmt, SQL_SOPT_SS_PARAM_FOCUS, (SQLPOINTER)0, SQL_IS_INTEGER);  // resetting the focus

    SQLRETURN res = SQLExecDirect(hstmt, (SQLCHAR*)"{call dbo.Update_Character_Nation(?,?,?)}", SQL_NTS);
    if (res != SQL_SUCCESS && res != SQL_SUCCESS_WITH_INFO)
    {
        printf("Error during query execution: %hd\n", res);
        ProcessLogs(SQL_HANDLE_STMT, hstmt);
    }
}