Как передать параметры табличного значения в хранимую процедуру из кода .net

171

У меня есть база данных SQL Server 2005. В нескольких процедурах у меня есть параметры таблицы, которые я передаю хранимому процессу в виде nvarchar(разделенных запятыми) и внутренне делю на отдельные значения. Я добавляю его в список параметров команды SQL следующим образом:

cmd.Parameters.Add("@Logins", SqlDbType.NVarchar).Value = "jim18,jenny1975,cosmo";

Я должен перенести базу данных в SQL Server 2008. Я знаю, что есть параметры табличных значений, и я знаю, как использовать их в хранимых процедурах. Но я не знаю, как передать его в список параметров в команде SQL.

Кто-нибудь знает правильный синтаксис Parameters.Addпроцедуры? Или есть другой способ передать этот параметр?

Марек Квендач
источник
Проверьте это решение: хранимая процедура с табличным параметром в EF. code.msdn.microsoft.com/Stored-Procedure-with-6c194514
Карл Протман
В случае, подобном этому, я обычно объединяю строки и делю их на стороне сервера или пропускаю даже XML, если у меня несколько столбцов. Sql это очень быстро при обработке XML. Вы можете попробовать все методы и проверить время обработки и после этого выбрать лучший метод. XML будет выглядеть следующим образом: <Items> <Item value = "sdadas" /> <Item value = "sadsad" />...</ Items>. Процесс на Sql Server также прост. Используя этот метод, вы всегда можете добавить новый атрибут в <item>, если вам нужна дополнительная информация.
Нику Александру
4
@ NițuAlexandru, «Sql очень быстро обрабатывает XML». Даже не близко.
Nothrow

Ответы:

279

DataTable, DbDataReaderили IEnumerable<SqlDataRecord>объекты можно использовать для заполнения табличного параметра в соответствии со статьей MSDN Табличные значения в SQL Server 2008 (ADO.NET) .

Следующий пример иллюстрирует использование a DataTableили an IEnumerable<SqlDataRecord>:

Код SQL :

CREATE TABLE dbo.PageView
(
    PageViewID BIGINT NOT NULL CONSTRAINT pkPageView PRIMARY KEY CLUSTERED,
    PageViewCount BIGINT NOT NULL
);
CREATE TYPE dbo.PageViewTableType AS TABLE
(
    PageViewID BIGINT NOT NULL
);
CREATE PROCEDURE dbo.procMergePageView
    @Display dbo.PageViewTableType READONLY
AS
BEGIN
    MERGE INTO dbo.PageView AS T
    USING @Display AS S
    ON T.PageViewID = S.PageViewID
    WHEN MATCHED THEN UPDATE SET T.PageViewCount = T.PageViewCount + 1
    WHEN NOT MATCHED THEN INSERT VALUES(S.PageViewID, 1);
END

Код C # :

private static void ExecuteProcedure(bool useDataTable, 
                                     string connectionString, 
                                     IEnumerable<long> ids) 
{
    using (SqlConnection connection = new SqlConnection(connectionString)) 
    {
        connection.Open();
        using (SqlCommand command = connection.CreateCommand()) 
        {
            command.CommandText = "dbo.procMergePageView";
            command.CommandType = CommandType.StoredProcedure;

            SqlParameter parameter;
            if (useDataTable) {
                parameter = command.Parameters
                              .AddWithValue("@Display", CreateDataTable(ids));
            }
            else 
            {
                parameter = command.Parameters
                              .AddWithValue("@Display", CreateSqlDataRecords(ids));
            }
            parameter.SqlDbType = SqlDbType.Structured;
            parameter.TypeName = "dbo.PageViewTableType";

            command.ExecuteNonQuery();
        }
    }
}

private static DataTable CreateDataTable(IEnumerable<long> ids) 
{
    DataTable table = new DataTable();
    table.Columns.Add("ID", typeof(long));
    foreach (long id in ids) 
    {
        table.Rows.Add(id);
    }
    return table;
}

private static IEnumerable<SqlDataRecord> CreateSqlDataRecords(IEnumerable<long> ids) 
{
    SqlMetaData[] metaData = new SqlMetaData[1];
    metaData[0] = new SqlMetaData("ID", SqlDbType.BigInt);
    SqlDataRecord record = new SqlDataRecord(metaData);
    foreach (long id in ids) 
    {
        record.SetInt64(0, id);
        yield return record;
    }
}
Райан Пречел
источник
24
+1 Отличный пример. Выводы: отправьте в DataTableкачестве значения параметра, задайте SqlDbTypeдля Structuredи TypeNameимя UDT базы данных.
жк
10
Если вы собираетесь повторно использовать экземпляр ссылочного типа в цикле (SqlDataRecord в вашем примере), пожалуйста, добавьте комментарий о том, почему это безопасно в данном конкретном случае.
Сорен Бойзен
2
Этот код неверен: для пустых табличных параметров их значение должно быть установлено null. CreateSqlDataRecordsникогда не вернется, nullесли дан пустой idsпараметр.
ta.speot.is
4
@Crono: DataTable(или DataSet) реализуйте его только потому, что они должны поддерживать возможности перетаскивания в Visual-Studio, поэтому они реализуют, IComponentкакие реализации IDisposable. Если вы не используете конструктор, а создаете его вручную, нет причин его утилизировать (или использовать using-statement). Так что это одно из исключений из золотого правила «распоряжаться всем, что реализует IDisposable».
Тим Шмельтер
2
@TimSchmelter Как правило, я всегда вызываю Disposeметоды, даже если это только так, что анализ кода не предупредит меня, если я этого не сделаю. Но я согласен, что в этом конкретном сценарии, где используются база DataSetи DataTableэкземпляры, вызов Disposeничего не сделает.
Crono
31

В дополнение к ответу Райана вам также необходимо установить свойство DataColumn's, Ordinalесли вы имеете дело table-valued parameterс несколькими столбцами, порядковые номера которых расположены не в алфавитном порядке.

Например, если у вас есть следующее табличное значение, которое используется в качестве параметра в SQL:

CREATE TYPE NodeFilter AS TABLE (
  ID int not null
  Code nvarchar(10) not null,
);

Вам нужно будет заказать ваши столбцы как таковые в C #:

table.Columns["ID"].SetOrdinal(0);
// this also bumps Code to ordinal of 1
// if you have more than 2 cols then you would need to set more ordinals

Если вы этого не сделаете, вы получите ошибку разбора, не удалось преобразовать nvarchar в int.

Scotty.NET
источник
15

общий

   public static DataTable ToTableValuedParameter<T, TProperty>(this IEnumerable<T> list, Func<T, TProperty> selector)
    {
        var tbl = new DataTable();
        tbl.Columns.Add("Id", typeof(T));

        foreach (var item in list)
        {
            tbl.Rows.Add(selector.Invoke(item));

        }

        return tbl;

    }
Martea
источник
Пожалуйста, дайте мне знать, что я передаю в качестве параметра? Функциональный селектор <T, TProperty>? Разве это не может быть просто tbl.Rows.Add (item) и нет необходимости в этом параметре.
GDroid
selector.Invoke (item) выбирает свойство элемента в большинстве случаев его int, но оно также позволяет вам выбрать строковое свойство
Martea
Можете ли вы привести пример того, как я могу поставить селектор там ?? У меня есть список <Guid> для передачи в сохраненный
процесс
guidList.ToTabledValuedParameter (x => x), поскольку x - это guid в вашем случае, возвращением будет DataTable с одним столбцом (id) со списком направляющих,
Martea
5

Самый чистый способ работы с ним. Предполагая, что ваша таблица представляет собой список целых чисел с именем "dbo.tvp_Int" (Настройте для своего собственного типа таблицы)

Создать этот метод расширения ...

public static void AddWithValue_Tvp_Int(this SqlParameterCollection paramCollection, string parameterName, List<int> data)
{
   if(paramCollection != null)
   {
       var p = paramCollection.Add(parameterName, SqlDbType.Structured);
       p.TypeName = "dbo.tvp_Int";
       DataTable _dt = new DataTable() {Columns = {"Value"}};
       data.ForEach(value => _dt.Rows.Add(value));
       p.Value = _dt;
   }
}

Теперь вы можете добавить табличный параметр в одну строку в любом месте, просто выполнив это:

cmd.Parameters.AddWithValueFor_Tvp_Int("@IDValues", listOfIds);
Шахзад Куреши
источник
1
что если paramCollection имеет значение NULL? Как пройти пустой тип?
Muflix
2
@Muflix Очевидно, что методы расширения работают против нулевых экземпляров. Так что добавление простой if(paramCollection != null)проверки в начале метода будет хорошо
Rhumborl
1
Обновленный ответ с первоначальной проверкой
Шахзад Куреши
2
Может быть, немного педантичный, но я бы использовал IEnumerableвместо Listподписи, чтобы вы могли передавать все, что угодно IEnumerable, а не только списки. Так как вы не используете какую-либо функцию, специфичную для List, я не вижу причины не насIEnumerable
Фрэнсис Лорд
Использование List позволяет вам использовать сокращенную запись data.ForEach (), в противном случае вам придется написать цикл foreach. Это тоже может сработать, но мне нравится писать вещи как можно короче.
Шахзад Куреши
0

Используйте этот код для создания подходящего параметра из вашего типа:

private SqlParameter GenerateTypedParameter(string name, object typedParameter)
{
    DataTable dt = new DataTable();

    var properties = typedParameter.GetType().GetProperties().ToList();
    properties.ForEach(p =>
    {
        dt.Columns.Add(p.Name, Nullable.GetUnderlyingType(p.PropertyType) ?? p.PropertyType);
    });
    var row = dt.NewRow();
    properties.ForEach(p => { row[p.Name] = (p.GetValue(typedParameter) ?? DBNull.Value); });
    dt.Rows.Add(row);

    return new SqlParameter
    {
        Direction = ParameterDirection.Input,
        ParameterName = name,
        Value = dt,
        SqlDbType = SqlDbType.Structured
    };
}
б сторона
источник