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

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

你有什么想法吗? 谢谢!

【问题讨论】:

    标签: sql-server asp.net-mvc tsql import


    【解决方案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:您不需要创建数据表。我刚刚发布了一个答案(正如斯蒂芬在上面的评论中要求我提出的那样),它显示了一个完全流式传输的结构:stackoverflow.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 比所有这些选项都慢。
      猜你喜欢
      • 1970-01-01
      • 2015-07-02
      • 2010-09-17
      • 2021-12-30
      • 2012-05-13
      • 1970-01-01
      • 2012-12-02
      • 2013-04-16
      • 2018-07-06
      相关资源
      最近更新 更多