Comment passer des paramètres de valeur de table à une procédure stockée à partir d'un code .net

171

J'ai une base de données SQL Server 2005. Dans quelques procédures, j'ai des paramètres de table que je passe à un proc stocké sous forme de nvarchar(séparés par des virgules) et que je divise en interne en valeurs uniques. Je l'ajoute à la liste des paramètres de commande SQL comme ceci:

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

Je dois migrer la base de données vers SQL Server 2008. Je sais qu'il existe des paramètres de valeur de table et je sais comment les utiliser dans des procédures stockées. Mais je ne sais pas comment en passer un à la liste des paramètres dans une commande SQL.

Quelqu'un connaît-il la syntaxe correcte de la Parameters.Addprocédure? Ou y a-t-il une autre façon de passer ce paramètre?

Marek Kwiendacz
la source
Découvrez cette solution: procédure stockée avec paramètre table dans EF. code.msdn.microsoft.com/Stored-Procedure-with-6c194514
Carl Prothman
Dans un cas comme celui-ci, je concatène généralement des chaînes et les divise côté serveur ou je passe même un xml si j'ai plusieurs colonnes. SQL est très rapide lors du traitement de xml. Vous pouvez essayer toutes les méthodes et vérifier le temps de traitement, puis choisir la meilleure méthode. Un XML ressemblerait à <Items> <Item value = "sdadas" /> <Item value = "sadsad" /> ... </Items>. Le processus sur Sql Server est également simple. En utilisant cette méthode, vous pouvez toujours ajouter un nouvel attribut à <item> si vous avez besoin de plus d'informations.
Nițu Alexandru
4
@ NițuAlexandru, "Sql est très rapide lors du traitement de xml.". Pas même près.
nothrow

Réponses:

279

DataTable, DbDataReaderOu les IEnumerable<SqlDataRecord>objets peuvent être utilisés pour remplir un paramètre de table d'une valeur par l'article MSDN Paramètres table Valued dans SQL Server 2008 (ADO.NET) .

L'exemple suivant illustre l'utilisation de a DataTableou an IEnumerable<SqlDataRecord>:

Code 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

Code 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
la source
24
+1 Excellent exemple. Les points à retenir sont: envoyer un DataTablecomme valeur de paramètre, défini SqlDbTypesur Structuredet TypeNamevers le nom UDT de la base de données.
lc.
10
Si vous prévoyez de réutiliser une instance d'un type de référence dans une boucle (SqlDataRecord dans votre exemple), veuillez ajouter un commentaire expliquant pourquoi il est prudent de le faire dans cette instance particulière.
Søren Boisen
2
Ce code est erroné: les paramètres de table vides doivent avoir leur valeur définie sur null. CreateSqlDataRecordsne retournera jamais nullsi un idsparamètre vide est donné .
ta.speot.is
4
@Crono: DataTable(ou DataSet) l'implémentent uniquement parce qu'ils doivent prendre en charge les capacités de glisser-déposer dans Visual-Studio, ils implémentent donc les IComponentimplémentations IDisposable. Si vous n'utilisez pas le concepteur mais que vous le créez manuellement, il n'y a aucune raison de le usingsupprimer (ou d'utiliser la déclaration). C'est donc l'une des exceptions à la règle d'or "disposer de tout ce qui met en œuvre IDisposable".
Tim Schmelter
2
@TimSchmelter En règle générale, j'appelle toujours des Disposeméthodes, même si ce n'est que pour que l'analyse de code ne me prévienne pas si je ne le fais pas. Mais je suis d'accord que dans ce scénario spécifique où la base DataSetet les DataTableinstances sont utilisées, l'appel Disposene ferait rien.
Crono
31

Suite à la réponse de Ryan, vous devrez également définir la propriété DataColumn's Ordinalsi vous avez affaire table-valued parameterà plusieurs colonnes dont les ordinaux ne sont pas classés par ordre alphabétique.

Par exemple, si vous avez la valeur de table suivante qui est utilisée comme paramètre dans SQL:

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

Vous auriez besoin de classer vos colonnes en tant que telles en 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

Si vous ne le faites pas, vous obtiendrez une erreur d'analyse, échec de la conversion de nvarchar en int.

Scotty.NET
la source
15

Générique

   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
la source
Pourriez-vous s'il vous plaît me faire savoir que ce que je passe comme paramètre? Sélecteur Func <T, TProperty>? Ne peut-il pas être simplement tbl.Rows.Add (item) et pas besoin de ce paramètre.
GDroid
le selector.Invoke (item) sélectionne la propriété sur l'élément dans la plupart des cas, c'est un int, mais il vous permet également de sélectionner une propriété de chaîne
Martea
pouvez-vous s'il vous plaît fournir un exemple de la façon dont je mets le sélecteur là-bas ?? J'ai une liste <Guid> à passer au
processus
guidList.ToTabledValuedParameter (x => x), puisque x est le guid dans votre cas, le retour sera un DataTable avec une colonne (id) avec une liste de guides,
Martea
5

La façon la plus propre de travailler avec. En supposant que votre table est une liste d'entiers appelée "dbo.tvp_Int" (personnaliser pour votre propre type de table)

Créez cette méthode d'extension ...

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

Vous pouvez maintenant ajouter un paramètre de table sur une ligne n'importe où en faisant simplement ceci:

cmd.Parameters.AddWithValueFor_Tvp_Int("@IDValues", listOfIds);
Shahzad Qureshi
la source
1
et si le paramCollection est NULL? Comment passer le type vide?
Muflix
2
@Muflix Obscurément, les méthodes d'extension fonctionnent en fait avec des instances nulles. Donc, ajouter une simple if(paramCollection != null)vérification en haut de la méthode sera très bien
Rhumborl
1
Réponse mise à jour avec -si- vérification initiale
Shahzad Qureshi
2
Peut-être un peu pédant, mais j'utiliserais à la IEnumerableplace de Listla signature, de cette façon vous pouvez passer tout ce qui est IEnumerable, pas seulement des listes, puisque vous n'utilisez aucune fonction spécifique à List, je ne vois pas vraiment de raison de ne pas nousIEnumerable
Francis Lord
L'utilisation de List vous permet d'utiliser le raccourci data.ForEach (), sinon vous auriez à écrire une boucle foreach. Ce qui pourrait fonctionner aussi, mais j'aime écrire des choses aussi courtes que possible.
Shahzad Qureshi
0

Utilisez ce code pour créer un paramètre approprié à partir de votre type:

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
    };
}
bside
la source