Übergeben von Tabellenwertparametern an gespeicherte Prozeduren aus .net-Code

171

Ich habe eine SQL Server 2005-Datenbank. In einigen Prozeduren habe ich Tabellenparameter, die ich als nvarchar(durch Kommas getrennt) an einen gespeicherten Prozess übergebe und intern in einzelne Werte teile. Ich füge es der SQL-Befehlsparameterliste wie folgt hinzu:

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

Ich muss die Datenbank auf SQL Server 2008 migrieren. Ich weiß, dass es Tabellenwertparameter gibt, und ich weiß, wie man sie in gespeicherten Prozeduren verwendet. Ich weiß jedoch nicht, wie ich einen in einem SQL-Befehl an die Parameterliste übergeben soll.

Kennt jemand die korrekte Syntax der Parameters.AddProzedur? Oder gibt es eine andere Möglichkeit, diesen Parameter zu übergeben?

Marek Kwiendacz
quelle
Schauen Sie sich diese Lösung an: Gespeicherte Prozedur mit tabellenwertigem Parameter in EF. code.msdn.microsoft.com/Stored-Procedure-with-6c194514
Carl Prothman
In einem Fall wie diesem verkette ich normalerweise Zeichenfolgen und teile sie auf der Serverseite oder übergebe sogar eine XML, wenn ich mehrere Spalten habe. SQL ist es sehr schnell bei der Verarbeitung von XML. Sie können alle Methoden ausprobieren und die Verarbeitungszeit überprüfen und anschließend die beste Methode auswählen. Ein XML würde wie folgt aussehen: <Items> <Item value = "sdadas" /> <Item value = "sadsad" /> ... </ Items>. Der Prozess auf SQL Server ist ebenfalls einfach. Mit dieser Methode können Sie <item> jederzeit ein neues Attribut hinzufügen, wenn Sie weitere Informationen benötigen.
Nițu Alexandru
4
@ NițuAlexandru, "SQL ist es sehr schnell bei der Verarbeitung von XML." Nicht annähernd.
Nothrow

Antworten:

278

DataTable,, DbDataReaderoder IEnumerable<SqlDataRecord>Objekte können verwendet werden, um einen Parameter mit Tabellenwert gemäß dem MSDN-Artikel Tabellenparameter in SQL Server 2008 (ADO.NET) zu füllen .

Das folgende Beispiel zeigt die Verwendung von a DataTableoder an IEnumerable<SqlDataRecord>:

SQL-Code :

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 # -Code :

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
quelle
24
+1 Exzellentes Beispiel. Imbissbuden sind: Senden Sie a DataTableals Parameterwert, setzen Sie SqlDbTypeauf Structuredund TypeNamean den UDT-Namen der Datenbank.
lc.
10
Wenn Sie eine Instanz eines Referenztyps in einer Schleife wiederverwenden möchten (in Ihrem Beispiel SqlDataRecord), fügen Sie bitte einen Kommentar hinzu, warum dies in dieser speziellen Instanz sicher ist.
Søren Boisen
2
Dieser Code ist falsch: Für leere Tabellenwertparameter sollte der Wert festgelegt werden null. CreateSqlDataRecordswird niemals zurückkehren, nullwenn ein leerer idsParameter angegeben wird.
ta.speot.is
4
@Crono: DataTable(oder DataSet) implementieren Sie es nur, weil sie Drag & Drop-Funktionen in Visual-Studio unterstützen müssen, damit sie implementieren, IComponentwelche implementiert werden IDisposable. Wenn Sie den Designer nicht verwenden, sondern manuell erstellen, gibt es keinen Grund, ihn zu entsorgen (oder die usingAnweisung zu verwenden). Dies ist also eine der Ausnahmen der goldenen Regel "Entsorgen Sie alles, was implementiert wird IDisposable".
Tim Schmelter
2
@TimSchmelter Als Faustregel rufe ich immer DisposeMethoden auf, auch wenn es nur so ist, dass mich die Code-Analyse nicht warnt, wenn ich es nicht tue. Ich stimme jedoch zu, dass in diesem speziellen Szenario, in dem Basis DataSetund DataTableInstanzen verwendet werden, das Aufrufen Disposenichts bewirken würde.
Crono
31

Neben Ryans Antwort müssen Sie auch die Eigenschaft des DataColumn' festlegen , Ordinalwenn Sie table-valued parametermit mehreren Spalten arbeiten, deren Ordnungszahlen nicht in alphabetischer Reihenfolge sind.

Wenn Sie beispielsweise den folgenden Tabellenwert haben, der in SQL als Parameter verwendet wird:

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

Sie müssten Ihre Spalten als solche in C # bestellen:

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

Wenn Sie dies nicht tun, wird ein Analysefehler angezeigt, bei dem nvarchar nicht in int konvertiert werden konnte.

Scotty.NET
quelle
15

Generisch

   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
quelle
Würden Sie mir bitte mitteilen, was ich als Parameter übergebe? Func <T, TProperty> Selektor? Kann es nicht einfach tbl.Rows.Add (item) sein und dieser Parameter wird nicht benötigt.
GDroid
Der Selector.Invoke (Element) wählt die Eigenschaft für das Element in den meisten Fällen als Int aus. Sie können jedoch auch eine Zeichenfolgeeigenschaft auswählen
Martea
Können Sie bitte ein Beispiel geben, wie ich den Selektor dort drüben platziere? Ich habe eine Liste <Guid>, die an den gespeicherten
Prozess
guidList.ToTabledValuedParameter (x => x), da x in Ihrem Fall die Guid ist, ist die Rückgabe eine DataTable mit einer Spalte (ID) mit einer Liste von Guids.
Martea
5

Der sauberste Weg, damit zu arbeiten. Angenommen, Ihre Tabelle enthält eine Liste von Ganzzahlen mit dem Namen "dbo.tvp_Int" (An Ihren eigenen Tabellentyp anpassen)

Erstellen Sie diese Erweiterungsmethode ...

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

Jetzt können Sie einfach einen Tabellenwertparameter in einer Zeile an einer beliebigen Stelle hinzufügen:

cmd.Parameters.AddWithValueFor_Tvp_Int("@IDValues", listOfIds);
Shahzad Qureshi
quelle
1
Was ist, wenn die paramCollection NULL ist? Wie übergebe ich einen leeren Typ?
Muflix
2
@Muflix Obskurerweise funktionieren Erweiterungsmethoden tatsächlich gegen Nullinstanzen. Das Hinzufügen eines einfachen if(paramCollection != null)Checks am Anfang der Methode ist also in Ordnung
Rhumborl
1
Aktualisierte Antwort mit anfänglichem -if- Scheck
Shahzad Qureshi
2
Vielleicht ein bisschen pedantisch, aber ich würde IEnumerableanstelle Listder Signatur verwenden, damit Sie alles übergeben können, was IEnumerablenicht nur Listen sind. Da Sie keine spezifische Funktion verwenden List, sehe ich keinen Grund, dies nicht zu tun unsIEnumerable
Francis Lord
Mit List können Sie die Kurzdaten verwenden. ForEach (), andernfalls müssten Sie tatsächlich eine foreach-Schleife schreiben. Was auch funktionieren könnte, aber ich schreibe gerne Dinge so kurz wie möglich.
Shahzad Qureshi
0

Verwenden Sie diesen Code, um einen geeigneten Parameter aus Ihrem Typ zu erstellen:

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
    };
}
b Seite
quelle