Pass Dictionary <string, int> для хранимой процедуры T-SQL
У меня есть приложение mvc. В действии у меня есть Dictionary<string,int>
. Key
- идентификатор, а Value
- sortOrderNumber. Я хочу создать хранимую процедуру, которая будет получать ключ (id), найти эту запись в базе данных и сохранить столбцы orderNumber
на Value
из словаря. Я хочу вызвать хранимую процедуру один раз и передать данные, вместо того, чтобы многократно звонить для обновления данных.
Есть ли у вас идеи?
Спасибо!
Ответы
Ответ 1
Использование табличных значений действительно не так сложно.
с учетом этого 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
Ответ 2
Принятый ответ на использование TVP, как правило, правильный, но требует некоторого уточнения на основе количества передаваемых данных. Использование DataTable в порядке (не говоря уже о быстром и простом) для небольших наборов данных, но для более крупных устанавливает, что он не масштабируется, учитывая, что он дублирует набор данных, помещая его в DataTable просто для средств передачи его на SQL Server. Таким образом, для более крупных наборов данных существует возможность потока содержимого любой пользовательской коллекции. Единственное реальное требование состоит в том, что вам нужно определить структуру в терминах типов SqlDb и итерации по коллекции, причем оба из них являются довольно тривиальными шагами.
Ниже представлен упрощенный обзор минимальной структуры, который является адаптацией ответа, который я разместил на Как я могу вставить 10 миллионов записей в кратчайшие сроки?, который занимается импортом данных из файла и, следовательно, немного отличается, поскольку данные в настоящее время не хранятся в памяти. Как видно из приведенного ниже кода, эта настройка не слишком сложная, но очень гибкая, а также эффективная и масштабируемая.
Объект 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
Код С#, часть 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;
}
}
Код С#, часть 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();
}
}
Ответ 3
Хранимые процедуры не поддерживают массивы в качестве входных данных. Googling дает пару хаков с использованием XML или разделенных запятыми строк, но это хаки.
Более SQLish способ сделать это - создать временную таблицу (например, #Orders
) и вставить все данные в нее. Затем вы можете вызвать sp, используя одно и то же открытое соединение Sql и вставить SP в таблицу #Orders
, чтобы прочитать значения.
Другим решением является использование Table-Valued Parameters, но для этого требуется еще несколько SQL-запросов, поэтому я думаю, что, вероятно, проще использовать temp табличный подход.