将 Dictionary<string,int> 传递给存储过程 T-SQL

Posted

技术标签:

【中文标题】将 Dictionary<string,int> 传递给存储过程 T-SQL【英文标题】:Pass Dictionary<string,int> to Stored Procedure T-SQL 【发布时间】:2013-11-26 06:03:01 【问题描述】:

我有 mvc 应用程序。在行动中,我有Dictionary&lt;string,int&gt;Key 是 ID,Value 是 sortOrderNumber。我想创建存储过程,它将获取 key(id) 在数据库中找到此记录并从 Dictionary 中通过value 保存orderNumber 列。我想一次调用存储过程并将数据传递给它,而不是多次调用来更新数据。

你有什么想法吗? 谢谢!

【问题讨论】:

【参考方案1】:

使用 TVP 的公认答案通常是正确的,但需要根据传入的数据量进行一些澄清。对于较小的数据集,使用 DataTable 很好(更不用说快速和简单),但对于较大的数据集设置它不会缩放,因为它通过将数据集放置在 DataTable 中来复制数据集,只是为了将其传递给 SQL Server。因此,对于较大的数据集,可以选择流式传输任何自定义集合的内容。唯一真正的要求是您需要根据 SqlDb 类型定义结构并遍历集合,这两个步骤都相当简单。

下面显示了最小结构的简单概述,这是对我在How can I insert 10 million records in the shortest time possible? 上发布的答案的改编,该答案处理从文件中导入数据,因此由于数据当前不在内存中,因此略有不同。从下面的代码中可以看出,这种设置并不过分复杂,而且非常灵活,而且高效且可扩展。

SQL 对象 #1:定义结构

-- First: You need a User-Defined Table Type
CREATE TYPE dbo.IDsAndOrderNumbers AS TABLE
(
   ID NVARCHAR(4000) NOT NULL,
   SortOrderNumber INT NOT NULL
);
GO

SQL 对象 #2:使用结构

-- Second: Use the UDTT as an input param to an import proc.
--         Hence "Tabled-Valued Parameter" (TVP)
CREATE PROCEDURE dbo.ImportData (
   @ImportTable    dbo.IDsAndOrderNumbers READONLY
)
AS
SET NOCOUNT ON;

-- maybe clear out the table first?
TRUNCATE TABLE SchemaName.TableName;

INSERT INTO SchemaName.TableName (ID, SortOrderNumber)
    SELECT  tmp.ID,
            tmp.SortOrderNumber
    FROM    @ImportTable tmp;

-- OR --

some other T-SQL

-- optional return data
SELECT @NumUpdates AS [RowsUpdated],
       @NumInserts AS [RowsInserted];
GO

C# 代码,第 1 部分:定义迭代器/发送器

using System.Collections;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using Microsoft.SqlServer.Server;

private static IEnumerable<SqlDataRecord> SendRows(Dictionary<string,int> RowData)

   SqlMetaData[] _TvpSchema = new SqlMetaData[] 
      new SqlMetaData("ID", SqlDbType.NVarChar, 4000),
      new SqlMetaData("SortOrderNumber", SqlDbType.Int)
   ;
   SqlDataRecord _DataRecord = new SqlDataRecord(_TvpSchema);
   StreamReader _FileReader = null;

      // read a row, send a row
      foreach (KeyValuePair<string,int> _CurrentRow in RowData)
      
         // You shouldn't need to call "_DataRecord = new SqlDataRecord" as
         // SQL Server already received the row when "yield return" was called.
         // Unlike BCP and BULK INSERT, you have the option here to create an
         // object, do manipulation(s) / validation(s) on the object, then pass
         // the object to the DB or discard via "continue" if invalid.
         _DataRecord.SetString(0, _CurrentRow.ID);
         _DataRecord.SetInt32(1, _CurrentRow.sortOrderNumber);

         yield return _DataRecord;
      

C# 代码,第 2 部分:使用迭代器/发送器

public static void LoadData(Dictionary<string,int> MyCollection)

   SqlConnection _Connection = new SqlConnection("connection string");
   SqlCommand _Command = new SqlCommand("ImportData", _Connection);
   SqlDataReader _Reader = null; // only needed if getting data back from proc call

   SqlParameter _TVParam = new SqlParameter();
   _TVParam.ParameterName = "@ImportTable";
// _TVParam.TypeName = "IDsAndOrderNumbers"; //optional for CommandType.StoredProcedure
   _TVParam.SqlDbType = SqlDbType.Structured;
   _TVParam.Value = SendRows(MyCollection); // method return value is streamed data
   _Command.Parameters.Add(_TVParam);
   _Command.CommandType = CommandType.StoredProcedure;

   try
   
      _Connection.Open();

      // Either send the data and move on with life:
      _Command.ExecuteNonQuery();
      // OR, to get data back from a SELECT or OUTPUT clause:
      SqlDataReader _Reader = _Command.ExecuteReader();
      
       Do something with _Reader: If using INSERT or MERGE in the Stored Proc, use an
       OUTPUT clause to return INSERTED.[RowNum], INSERTED.[ID] (where [RowNum] is an
       IDENTITY), then fill a new Dictionary<string, int>(ID, RowNumber) from
       _Reader.GetString(0) and _Reader.GetInt32(1). Return that instead of void.
      
   
   finally
   
      _Reader.Dispose(); // optional; needed if getting data back from proc call
      _Command.Dispose();
      _Connection.Dispose();
   

【讨论】:

【参考方案2】:

使用表值参数真的没那么复杂。

给定这个 SQL:

CREATE TYPE MyTableType as TABLE (ID nvarchar(25),OrderNumber int) 


CREATE PROCEDURE MyTableProc (@myTable MyTableType READONLY)    
   AS
   BEGIN
    SELECT * from @myTable
   END

这将显示它是多么容易,它只是选择您发送的值用于演示目的。我相信您可以轻松地将其抽象出来。

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;

namespace TVPSample

    class Program
    
        static void Main(string[] args)
        
            //setup some data
            var dict = new Dictionary<string, int>();
            for (int x = 0; x < 10; x++)
            
                dict.Add(x.ToString(),x+100);
            
            //convert to DataTable
            var dt = ConvertToDataTable(dict);
            using (SqlConnection conn = new SqlConnection("[Your Connection String here]"))
            
                conn.Open();
                using (SqlCommand comm = new SqlCommand("MyTableProc",conn))
                
                    comm.CommandType=CommandType.StoredProcedure;
                    var param = comm.Parameters.AddWithValue("myTable", dt);
                    //this is the most important part:
                    param.SqlDbType = SqlDbType.Structured;
                    var reader = comm.ExecuteReader(); //or NonQuery, etc.
                    while (reader.Read())
                    
                        Console.WriteLine("0 1", reader["ID"], reader["OrderNumber"]);
                    

                
            
        

        //I am sure there is a more elegant way of doing this.
        private static DataTable ConvertToDataTable(Dictionary<string, int> dict)
        
            var dt = new DataTable();
            dt.Columns.Add("ID",typeof(string));
            dt.Columns.Add("OrderNumber", typeof(Int32));
            foreach (var pair in dict)
            
                var row = dt.NewRow();
                row["ID"] = pair.Key;
                row["OrderNumber"] = pair.Value;
                dt.Rows.Add(row);
            
            return dt;
        
    

生产

0 100
1 101
2 102
3 103
4 104
5 105
6 106
7 107
8 108
9 109

【讨论】:

如果传入大量数据,那么通过 IEnumerable 接口将数据流式传输会比以 DataTable 的形式制作原始数据的第二个副本更有效.我写了一篇关于这种方法的文章,包括一个工作示例:sqlservercentral.com/articles/SQL+Server+2008/66554 @srutzky - 这是一篇非常好的文章!你会考虑在这里放一个样本作为答案吗? 为什么一定要创建DataTable?为什么不能直接传字典? @Aref - 我想您必须与 System.Data 的作者一起讨论这个问题:) 也许未来版本的 SQL Server 将与 .NET 更紧密地集成,谁知道... @Aref:您不需要创建数据表。我刚刚发布了一个答案(正如斯蒂芬在上面的评论中要求我提出的那样),它显示了一个完全流式传输的结构:***.com/questions/19957132/…。有 3 种类型可以作为 TVP 发送:DataTableDbDataReaderIEnumerable&lt;SqlDataRecord&gt;。它们的共同点是它们包含 SqlMetaData 以根据 T-SQL 定义结构。我的示例显示了最接近直接传递 Dictionary 的方法。【参考方案3】:

存储过程不支持将数组作为输入。谷歌搜索提供了一些使用 XML 或逗号分隔字符串的技巧,但这些都是技巧。

执行此操作的一种更 SQLish 的方法是创建一个临时表(例如命名为 #Orders)并将所有数据插入到该表中。然后你可以调用 sp,使用 同一个打开的 Sql Connection 并且 insie SP 使用#Orders 表来读取值。

另一个解决方案是使用Table-Valued Parameters,但这需要更多的 SQL 来设置,所以我认为使用临时表方法可能更容易。

【讨论】:

嗯,他们做到了:“表值参数”——这不是微不足道的 @MarcGravell 是的,你是对的 - 当你发表评论时,我将其添加为编辑。对我来说,临时表方法看起来更简单——你更喜欢哪一种? 另请注意,它们仅支持来自 SqlServer2008 的表值参数 -1 我尽量不投反对票,但这是一个不好的建议,而赞成票表明其他人没有意识到这一点。 1) XML 是传递多维数组的有效、灵活的方式。我已经多次使用 XML,在吞吐量和重构的便利性方面取得了很大的成功(不是 hack)。 2) 发送一个简单的数字列表的 CSV 列表以在 proc 中拆分为临时表是非常快速且最少的代码(不是 hack)。 3) TVP 是以强类型流方式发送数组的最有效方式。唯一的 SQL 设置是 1 次 CREATE TYPE 语句。许多 INSERT 比所有这些选项都慢。

以上是关于将 Dictionary<string,int> 传递给存储过程 T-SQL的主要内容,如果未能解决你的问题,请参考以下文章

将 [Dictionary<String,Any?>] 转换为 [Dictionary<String,Any!>] ?迅速 3

如何将 List<Dictionary<string, byte[]> 对象添加到 Dictionary<string, byte[]> [重复]

524. Longest Word in Dictionary through Deleting

如何将 Optional<Dictionary<String, Any>> 转换为 Dictionary<String, Any> 以发送带有 json 参数的 A

C# 将 List<string> 转换为 Dictionary<string, string>

将 XML 转换为 Dictionary<String,String>