【问题标题】:How to pass datatable from C# to SQL Server stored procedure如何将数据表从 C# 传递到 SQL Server 存储过程
【发布时间】:2020-08-11 12:28:50
【问题描述】:

我正在开发一个视频游戏锦标赛处理网站。 尝试获取具有指定参数的用户(他们上次登录本地网吧的城市,以及他们玩的游戏)。 为了获得最佳性能,我从编写存储过程及其工作开始。 但是在抛出异常后从代码调用时。

Exception.Message:列、参数或变量@cityIds。 : 找不到数据类型 dbo.CityIds。

SQL

CREATE TYPE [dbo].[CityIds] AS TABLE(
  [Id] [uniqueidentifier] NOT NULL
)
GO
ALTER   PROCEDURE [dbo].[spGetUsersByCityAndGame] 
  @gameProcessName NVARCHAR(50),
  @cityIds       [CityIds] READONLY
AS
BEGIN
SET NOCOUNT ON;
SELECT users.Id
FROM AspNetUsers users WITH( NOLOCK )
JOIN
( -- select below finds last log entry by startDateTime column for each user
  SELECT authLog1.*
  FROM AuthenticationLog authLog1 WITH( NOLOCK )
  LEFT OUTER JOIN
  AuthenticationLog authLog2 WITH( NOLOCK )
  ON authLog1.ClientId=authLog2.ClientId
     AND authLog1.StartDateTime<authLog2.StartDateTime
  WHERE authLog2.ClientId IS NULL
) lastAuthLog
ON users.Id=lastAuthLog.ClientId
    JOIN
    Club club
    ON lastAuthLog.ClubId=club.Id
        JOIN
        City city
        ON club.CityId=city.Id
            JOIN
            ProcessLogs processLog
            ON lastAuthLog.Id=processLog.AuthenticationLogId
            INNER JOIN @cityIds cids on city.Id = cids.Id
WHERE users.Birthday IS NOT NULL
      AND users.PhoneNumber IS NOT NULL
      AND users.UserName IS NOT NULL
      AND users.Email IS NOT NULL
      AND users.FullName IS NOT NULL
      AND processLog.ProcessName=@gameProcessName;
END;

C#

private async Task<List<ApplicationUser>> GetUserByParameters(Guid gameId, IEnumerable<Guid> citiesIds)
{
    Game game = await _gamesRepository.GetAsync(gameId);

    SqlParameter gameNameParam = new SqlParameter("@gameProcessName", SqlDbType.NVarChar, 50)
    {
        Value = (object)game.ExecutableName ?? DBNull.Value
    };

    List<SqlDataRecord> table = new List<SqlDataRecord>();

    foreach (Guid cityId in citiesIds)
    {
        SqlDataRecord tableRow = new SqlDataRecord(new SqlMetaData[] { new SqlMetaData("Id", SqlDbType.UniqueIdentifier) });

        tableRow.SetGuid(0, cityId);

        table.Add(tableRow);
    }

    SqlParameter citiesIdsParam = new SqlParameter
    {
        TypeName = "[dbo].[CityIds]",
        SqlDbType = SqlDbType.Structured,
        ParameterName = "@cityIds",
        Value = table,
    };

    string sql = @"EXECUTE dbo.spGetUsersByCityAndGame @gameProcessName, @cityIds";

    List<ApplicationUser> users = await _dbContext.Users.FromSqlRaw(sql, gameNameParam, citiesIdsParam).ToListAsync();

    return users;
}

【问题讨论】:

  • 在alter SPROC中你可以给[dbo].[CityIds]而不是[CityIds]并尝试
  • 考虑到 dbo.CityIdsTTV 从未出现在您的代码中,我觉得我们在这里遗漏了一些东西。另外,应该是EXECUTE dbo.spGetUsersByCityAndGame @gameProcessName, @cityIds READONLY
  • 您可以尝试删除您的类型并使用正确的名称重新创建并在您的 SP 中使用它吗?
  • 用户有权限吗?要使用表类型,需要对该类型具有CONTROL 权限。 (技术上 EXECUTEREFERENCES 就足够了,但这并没有太多限制。)
  • 通常您使用将数据库行链接到 c# 数据表的 DataAdapter 查询数据库并将结果放入数据表中。然后你所要做的就是使用数据表的更新方法,然后将更改存储回数据库。

标签: c# sql .net sql-server


【解决方案1】:

如果你想做“基于集合”的 sql,你可以“转换”你的 DataTable 到 xml(通过一个假的 DataSet)

            System.Data.DataTable dbfacs = System.Data.Common.DbProviderFactories.GetFactoryClasses(); /* replace this with YOUR datatable code */
            System.Data.DataSet ds1 = new DataSet();
            ds1.Tables.Add(dbfacs);
            string xml = ds1.GetXml();

现在发送这个 xml 执行您的存储过程。在存储过程中,您将“切碎” xml 到 @variable 或 #temp 表中......并从那里执行 CUD(创建/更新/删除)或(根据需要),可以执行 JOINS 或 EXISTS 子句在@variable 或#temp 表上。

在此处查看完整示例:

https://www.sqlservercentral.com/articles/the-zero-to-n-parameter-problem-sql-server-2005-and-up-update

通用示例:(使用 Northwind 数据库)(此处为 DDL:https://github.com/Microsoft/sql-server-samples/tree/master/samples/databases/northwind-pubs

IF EXISTS (
            SELECT * FROM INFORMATION_SCHEMA.ROUTINES
            WHERE ROUTINE_TYPE = N'PROCEDURE' and ROUTINE_SCHEMA = N'dbo' and ROUTINE_NAME = N'uspCustomerFindByXml'  
        )   
BEGIN
    DROP PROCEDURE [dbo].[uspCustomerFindByXml]
END

GO

CREATE PROCEDURE dbo.uspCustomerFindByXml (
    @xmlSource xml , 
    @numberRowsAffected int output  --return
)

AS 

SET NOCOUNT ON 

DECLARE @errorTracker int -- used to "remember" the @@ERROR

DECLARE @updateRowCount int
DECLARE @insertRowCount int 


-- build a table (variable table) to store the xml-based result set
DECLARE @CustomerHolder TABLE (  
    identityid int IDENTITY (1,1) , 
CustomerID varchar(6) 
)


INSERT @CustomerHolder
    (
        CustomerID 
    )
SELECT 
    T.parameter.value('(CustomerID)[1]', 'varchar(6)') AS CustomerID

FROM @xmlSource.nodes('/CustomersDS/Customers') AS T(parameter);



select * from @CustomerHolder



SET NOCOUNT OFF


Select @updateRowCount = @@ROWCOUNT

SELECT cust.CustomerID, cust.CompanyName, cust.ContactName FROM

    dbo.Customers cust
WHERE
    exists (   select null from @CustomerHolder holder where ltrim(rtrim(upper(holder.CustomerID))) = ltrim(rtrim(upper(cust.CustomerID)))   )

Select @insertRowCount = @@ROWCOUNT

select @numberRowsAffected = @insertRowCount + @updateRowCount

--select * from Customers

SET NOCOUNT OFF


GO




GRANT EXECUTE on dbo.uspCustomerFindByXml TO public



GO





declare @numberRowsAffected int

EXEC dbo.uspCustomerFindByXml
'

<CustomersDS>
  <Customers>
    <CustomerID>ALFKI</CustomerID>
  </Customers>
  <Customers>
    <CustomerID>ANATR</CustomerID>
  </Customers>
  <Customers>
    <CustomerID>ANTON</CustomerID>
  </Customers>
</CustomersDS>

' , @numberRowsAffected OUT

print '/@numberRowsAffected/'
print @numberRowsAffected
print ''


GO

这里也是一个显示“基础知识”的链接。来自微软:

https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql/specifying-xml-values-as-parameters

额外:

这种“风格”也可以用于 CRUD 功能。您也可以通过加入或在碎 xml 上存在的位置来创建/读取/更新/删除。

CU(创建/更新)通用示例如下:

IF EXISTS (
            SELECT * FROM INFORMATION_SCHEMA.ROUTINES
            WHERE ROUTINE_TYPE = N'PROCEDURE' and ROUTINE_SCHEMA = N'dbo' and ROUTINE_NAME = N'uspTitleUpdate'  
        )   
BEGIN
    DROP PROCEDURE [dbo].[uspTitleUpdate]
END

GO

CREATE PROCEDURE dbo.uspTitleUpdate (
    @xmlSource xml , 
    @numberRowsAffected int output  --return
)

AS 

SET NOCOUNT ON 

DECLARE @errorTracker int -- used to "remember" the @@ERROR

DECLARE @updateRowCount int
DECLARE @insertRowCount int 


-- build a table (variable table) to store the xml-based result set
DECLARE @TitleHolder TABLE (  
    identityid int IDENTITY (1,1) , 
 
title_id varchar(6) , 
title varchar(80) , 
type varchar(32) , 
pub_id varchar(32) , 
price money , 
advance money , 
royalty varchar(32) , 
ytd_sales varchar(32) , 
notes TEXT , 
pubdate datetime
)


INSERT @TitleHolder
    (
        title_id ,
        title ,
        [type] ,
        pub_id ,
        price ,
        advance ,
        royalty ,
        ytd_sales ,
        notes ,
        pubdate
    )
SELECT 
    T.parameter.value('(title_id)[1]', 'varchar(6)') AS title_id
    , T.parameter.value('(title)[1]', 'varchar(80)') AS title
    , T.parameter.value('(type)[1]', 'varchar(32)') AS [type] 
    , T.parameter.value('(pub_id)[1]', 'varchar(32)') AS pub_id
    , T.parameter.value('(price)[1]', 'money') AS price
    , T.parameter.value('(advance)[1]', 'money') AS advance
    , T.parameter.value('(royalty)[1]', 'varchar(32)') AS royalty
    , T.parameter.value('(ytd_sales)[1]', 'varchar(32)') AS ytd_sales
    , T.parameter.value('(notes)[1]', 'varchar(max)') AS notes
    , dbo.udf_convert_xml_date_to_datetime (T.parameter.value('(pubdate)[1]', 'varchar(64)') ) AS pubdate
FROM @xmlSource.nodes('/TitlesDS/Titles') AS T(parameter);


/* 
select * from @TitleHolder
*/


SET NOCOUNT OFF


Update 
    titles 
set 
    title = tu.title , 
    [type]  = tu.[type]  , 
    pub_id = tu.pub_id , 
    price = tu.price , 
    advance  = tu.advance , 
    royalty  = tu.royalty , 
    ytd_sales  = tu.ytd_sales , 
    notes  = tu.notes , 
    pubdate  = tu.pubdate 
FROM
    @TitleHolder tu , titles
WHERE
    ltrim(rtrim(upper(titles.title_id))) = ltrim(rtrim(upper(tu.title_id)))


Select @updateRowCount = @@ROWCOUNT

INSERT INTO titles
    (
        title_id ,
        title ,
        [type]  ,
        pub_id ,
        price ,
        advance ,
        royalty ,
        ytd_sales ,
        notes ,
        pubdate
    )
Select
    title_id ,
    title ,
    [type]  ,
    pub_id ,
    price ,
    advance ,
    royalty ,
    ytd_sales ,
    notes ,
    pubdate
FROM
    @TitleHolder tu
WHERE
    not exists (   select null from dbo.titles innerRealTable where ltrim(rtrim(upper(innerRealTable.title_id))) = ltrim(rtrim(upper(tu.title_id)))   )

Select @insertRowCount = @@ROWCOUNT

select @numberRowsAffected = @insertRowCount + @updateRowCount

--select * from titles

SET NOCOUNT OFF


GO




GRANT EXECUTE on dbo.uspTitleUpdate TO public



GO





/* 


declare @numberRowsAffected int

EXEC dbo.uspTitleUpdate
'

<TitlesDS>
  <Titles>
    <title_id>BU1032</title_id>
    <title>The Busy Executives Database Guide</title>
    <type>business    </type>
    <price>19.99</price>
    <pubdate>2013-12-10T13:42:27.020604-05:00</pubdate>
  </Titles>
  <Titles>
    <title_id>BU1111</title_id>
    <title>Cooking with Computers: Surreptitious Balance Sheets</title>
    <type>business    </type>
    <price>11.95</price>
    <pubdate>2013-12-10T13:42:27.021604-05:00</pubdate>
  </Titles>
  <Titles>
    <title_id>BU2075</title_id>
    <title>You Can Combat Computer Stress!</title>
    <type>business    </type>
    <price>2.99</price>
    <pubdate>2013-12-10T13:42:27.021604-05:00</pubdate>
  </Titles>
</TitlesDS>

' , @numberRowsAffected OUT

print '/@numberRowsAffected/'
print @numberRowsAffected
print ''

*/

GO

【讨论】:

  • 现在是 2020 年。用户定义的表类型是将表值数据传递给 SQL 而不是 XML 的正确方法。而且通过 SQL 解析 XML 是 CPU 密集型的,而且很慢。
  • 粉碎有点慢。但是与一些粉碎工作相比,RBAR 要慢得多。 TVP 和 UDT 是特定于 sql-server 的。 xml 更可能是跨 rdbms 兼容的。所以有利有弊。
猜你喜欢
  • 2012-06-21
  • 2014-09-26
  • 2017-02-26
  • 2011-04-05
  • 2017-03-30
相关资源
最近更新 更多