Jak przekazać parametry wartości tabeli do procedury składowanej z kodu .net

171

Mam bazę danych SQL Server 2005. W kilku procedurach mam parametry tabeli, które przekazuję do przechowywanego procesu jako nvarchar(oddzielone przecinkami) i wewnętrznie dzielę na pojedyncze wartości. Dodaję go do listy parametrów polecenia SQL w następujący sposób:

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

Muszę przeprowadzić migrację bazy danych do SQL Server 2008. Wiem, że istnieją parametry wartości tabeli i wiem, jak ich używać w procedurach składowanych. Ale nie wiem, jak przekazać je do listy parametrów w poleceniu SQL.

Czy ktoś zna poprawną składnię Parameters.Addprocedury? Czy jest inny sposób przekazania tego parametru?

Marek Kwiendacz
źródło
Sprawdź to rozwiązanie: procedura składowana z parametrem z wartościami przechowywanymi w tabeli w EF. code.msdn.microsoft.com/Stored-Procedure-with-6c194514
Carl Prothman
W takim przypadku zazwyczaj łączę ciągi znaków i dzielę je po stronie serwera lub przekazuję nawet plik XML, jeśli mam wiele kolumn. Sql jest bardzo szybki podczas przetwarzania XML. Możesz wypróbować wszystkie metody i sprawdzić czas przetwarzania, a następnie wybrać najlepszą metodę. XML wyglądałby następująco: <Items> <Item value = "sdadas" /> <Item value = "sadsad" /> ... </Items>. Proces na serwerze Sql jest również prosty. Korzystając z tej metody, zawsze możesz dodać nowy atrybut do <item>, jeśli potrzebujesz więcej informacji.
Nițu Alexandru
4
@ NițuAlexandru, "Sql jest bardzo szybki podczas przetwarzania xml.". Nawet nie blisko.
nothrow

Odpowiedzi:

279

DataTable, DbDataReaderlub IEnumerable<SqlDataRecord>obiektów można użyć do wypełnienia parametru wycenianego w tabeli zgodnie z artykułem MSDN Parametry z wartościami przechowywanymi w tabeli w programie SQL Server 2008 (ADO.NET) .

Poniższy przykład ilustruje użycie a DataTablelub an IEnumerable<SqlDataRecord>:

Kod 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

Kod 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;
    }
}
Ryan Prechel
źródło
24
+1 Doskonały przykład. Wnioski to: wyślij a DataTablejako wartość parametru, ustaw SqlDbTypena Structuredi TypeNamedo nazwy UDT bazy danych.
lc.
10
Jeśli zamierzasz ponownie użyć instancji typu referencyjnego w pętli (w Twoim przykładzie SqlDataRecord), dodaj komentarz wyjaśniający, dlaczego jest to bezpieczne w tym konkretnym przypadku.
Søren Boisen
2
Ten kod jest nieprawidłowy: parametry wartościowane w pustej tabeli powinny mieć ustawioną wartość null. CreateSqlDataRecordsnigdy nie zwróci, nulljeśli otrzyma pusty idsparametr.
ta.speot.is
4
@Crono: DataTable(lub DataSet) implementują go tylko dlatego, że muszą obsługiwać możliwości przeciągania i upuszczania w Visual-Studio, więc implementują, IComponentktóre implementują IDisposable. Jeśli nie używasz projektanta, ale tworzysz go ręcznie, nie ma powodu, aby go pozbyć (lub użyć instrukcji using-statement). Jest to więc jeden z wyjątków od złotej zasady „pozbądź się wszystkiego, co wdraża IDisposable”.
Tim Schmelter
2
@TimSchmelter Z reguły zawsze wywołuję Disposemetody, nawet jeśli jest to tylko po to, aby Analiza kodu nie ostrzegła mnie, jeśli tego nie zrobię. Ale zgadzam się, że w tym konkretnym scenariuszu, w którym używana jest baza DataSeti DataTableinstancje, wywołanie Disposenic nie da.
Crono,
31

W nawiązaniu do odpowiedzi Ryana, będziesz musiał również ustawić właściwość DataColumn', Ordinaljeśli masz do czynienia table-valued parameterz wieloma kolumnami, których liczby porządkowe nie są w porządku alfabetycznym.

Na przykład, jeśli masz następującą wartość tabeli, która jest używana jako parametr w języku SQL:

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

Musisz uporządkować kolumny jako takie w 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

Jeśli tego nie zrobisz, pojawi się błąd analizy, nie udało się przekonwertować nvarchar na int.

Scotty.NET
źródło
15

Ogólny

   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
źródło
Czy mógłbyś dać mi znać, co mam podać jako parametr? Selektor Func <T, TProperty>? Czy nie może to być po prostu tbl.Rows.Add (pozycja) i nie potrzeba tego parametru.
GDroid
the selector.Invoke (item) wybiera właściwość elementu w większości przypadków jest to int, ale pozwala również na wybranie właściwości ciągu
Martea
czy możesz podać przykład, jak mam tam umieścić selektor? Mam listę <Guid> do przekazania do przechowywanego proc ...
GDroid
guidList.ToTabledValuedParameter (x => x), ponieważ x jest guid w twoim przypadku, wynikiem będzie DataTable z jedną kolumną (id) z listą przewodników,
Martea
5

Najczystszy sposób na pracę. Zakładając, że Twoja tabela to lista liczb całkowitych o nazwie „dbo.tvp_Int” (dostosuj do własnego typu tabeli)

Utwórz tę metodę rozszerzenia ...

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;
   }
}

Teraz możesz dodać parametr wartości tabeli w jednym wierszu w dowolnym miejscu, wykonując następujące czynności:

cmd.Parameters.AddWithValueFor_Tvp_Int("@IDValues", listOfIds);
Shahzad Qureshi
źródło
1
co, jeśli paramCollection ma wartość NULL? Jak przekazać pusty typ?
Muflix
2
@Muflix Co oczywiste, metody rozszerzające faktycznie działają na wystąpieniach o wartości null. Tak więc dodanie prostego if(paramCollection != null)czeku na górze metody będzie w porządku
Rhumborl
1
Zaktualizowana odpowiedź z początkowym -if- sprawdzeniem
Shahzad Qureshi
2
Może trochę pedantyczny, ale użyłbym IEnumerablezamiast Listw podpisie, w ten sposób możesz przekazać wszystko, co jest IEnumerable, nie tylko listy, Ponieważ nie używasz żadnej funkcji specyficznej dla List, naprawdę nie widzę powodu, aby tego nie robić nasIEnumerable
Francis Lord
Korzystanie z listy pozwala na użycie skróconej metody data.ForEach (), w przeciwnym razie musiałbyś faktycznie napisać pętlę foreach. Co też może zadziałać, ale lubię pisać rzeczy tak krótkie, jak to tylko możliwe.
Shahzad Qureshi
0

Użyj tego kodu, aby utworzyć odpowiedni parametr z Twojego typu:

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
    };
}
Strona B
źródło