web-dev-qa-db-fra.com

Passer la liste <> à la procédure stockée SQL

J'ai souvent dû charger plusieurs éléments dans un enregistrement particulier de la base de données. Par exemple: une page Web affiche les éléments à inclure pour un seul rapport, qui sont tous des enregistrements dans la base de données (le rapport est un enregistrement dans le tableau Rapport, les articles sont des enregistrements dans le tableau Article). Un utilisateur sélectionne des éléments à inclure dans un seul rapport via une application Web, et disons qu'il sélectionne 3 éléments et les soumet. Le processus ajoutera ces 3 éléments à ce rapport en ajoutant des enregistrements à une table appelée ReportItems (ReportId, ItemId).

Actuellement, je ferais quelque chose comme ça dans le code:

public void AddItemsToReport(string connStr, int Id, List<int> itemList)
{
    Database db = DatabaseFactory.CreateDatabase(connStr);

    string sqlCommand = "AddItemsToReport"
    DbCommand dbCommand = db.GetStoredProcCommand(sqlCommand);

    string items = "";
    foreach (int i in itemList)
        items += string.Format("{0}~", i);

    if (items.Length > 0)
        items = items.Substring(0, items.Length - 1);

    // Add parameters
    db.AddInParameter(dbCommand, "ReportId", DbType.Int32, Id);
    db.AddInParameter(dbCommand, "Items", DbType.String, perms);
    db.ExecuteNonQuery(dbCommand);
}

et ceci dans la procédure stockée:

INSERT INTO ReportItem (ReportId,ItemId)
SELECT  @ReportId,
          Id
FROM     fn_GetIntTableFromList(@Items,'~')

Où la fonction renvoie une table d'entiers à une colonne.

Ma question est la suivante: existe-t-il une meilleure façon de gérer quelque chose comme ça? Remarque, je ne pose pas de question sur la normalisation de la base de données ou quelque chose comme ça, ma question concerne spécifiquement le code.

50
Ryan Abbott

Si le passage à SQL Server 2008 est une option pour vous, il existe une nouvelle fonctionnalité appelée "Paramètres table" pour résoudre ce problème exact.

Découvrez plus de détails sur TVP ici et ici ou demandez simplement à Google les "paramètres de table SQL Server 2008" - vous trouverez de nombreuses informations et des exemples.

Fortement recommandé - si vous pouvez passer à SQL Server 2008 ...

34
marc_s

Votre logique de jointure de chaîne peut probablement être simplifiée:

string items = 
    string.Join("~", itemList.Select(item=>item.ToString()).ToArray());

Cela vous évitera une certaine concaténation de chaînes, ce qui est cher dans .Net.

Je ne pense pas que la façon dont vous enregistrez les articles soit incorrecte. Vous limitez les déplacements vers la base de données, ce qui est une bonne chose. Si votre structure de données était plus complexe qu'une liste d'entiers, je suggérerais XML.

Remarque: On m'a demandé dans les commentaires si cela nous permettrait d'économiser toute concaténation de chaîne (cela ne nécessite pas). Je pense que c'est une excellente question et j'aimerais poursuivre sur cette lancée.

Si vous décollez la chaîne ouverte, joignez-vous à Reflector , vous verrez que Microsoft utilise quelques techniques dangereuses (au sens .Net du mot), notamment l'utilisation d'un pointeur char et d'une structure appelée UnSafeCharBuffer. Ce qu'ils font, lorsque vous le réduisez vraiment, utilise des pointeurs pour parcourir une chaîne vide et créer la jointure. N'oubliez pas que la concaténation de chaînes principale est si coûteuse dans .Net parce qu'un nouvel objet chaîne est placé sur le tas pour chaque concaténation, car la chaîne est immuable. Ces opérations de mémoire sont coûteuses. String.Join (..) alloue essentiellement la mémoire une fois, puis opère dessus avec un pointeur. Très vite.

19
Jason Jackson

Un problème potentiel avec votre technique est qu'elle ne gère pas de très grandes listes - vous pouvez dépasser la longueur de chaîne maximale pour votre base de données. J'utilise une méthode d'assistance qui concatène les valeurs entières en une énumération de chaînes, chacune étant inférieure à un maximum spécifié (l'implémentation suivante vérifie et supprime également facultativement les identifiants en double):

public static IEnumerable<string> ConcatenateValues(IEnumerable<int> values, string separator, int maxLength, bool skipDuplicates)
{
    IDictionary<int, string> valueDictionary = null;
    StringBuilder sb = new StringBuilder();
    if (skipDuplicates)
    {
        valueDictionary = new Dictionary<int, string>();
    }
    foreach (int value in values)
    {
        if (skipDuplicates)
        {
            if (valueDictionary.ContainsKey(value)) continue;
            valueDictionary.Add(value, "");
        }
        string s = value.ToString(CultureInfo.InvariantCulture);
        if ((sb.Length + separator.Length + s.Length) > maxLength)
        {
            // Max length reached, yield the result and start again
            if (sb.Length > 0) yield return sb.ToString();
            sb.Length = 0;
        }
        if (sb.Length > 0) sb.Append(separator);
        sb.Append(s);
    }
    // Yield whatever's left over
    if (sb.Length > 0) yield return sb.ToString();
}

Ensuite, vous l'utilisez quelque chose comme:

using(SqlCommand command = ...)
{
    command.Connection = ...;
    command.Transaction = ...; // if in a transaction
    SqlParameter parameter = command.Parameters.Add("@Items", ...);
    foreach(string itemList in ConcatenateValues(values, "~", 8000, false))
    {
        parameter.Value = itemList;
        command.ExecuteNonQuery();
    }
}
8
Joe

Soit vous faites ce que vous avez déjà, passez une chaîne délimitée, puis analysez vers une valeur de table, ou l'autre choix passe dans un wodge de XML et un peu la même chose:

http://weblogs.asp.net/jgalloway/archive/2007/02/16/passing-lists-to-sql-server-2005-with-xml-parameters.aspx

Je n'ai pas encore eu l'occasion de regarder SQL 2008 pour voir s'ils ont ajouté de nouvelles fonctionnalités pour gérer ce type de chose.

5
Kev
5
GaTechThomas

Voici une explication très claire des paramètres de valeur de table de sqlteam.com: Paramètres de valeur de table

2
dotnetN00b

Voir http://www.sommarskog.se/arrays-in-sql-2005.html pour une discussion détaillée de ce problème et des différentes approches que vous pourriez utiliser.

2
Phillip Wells

Interroger un seul champ pour plusieurs valeurs dans une procédure stockée
http://www.norimek.com/blog/post/2008/04/Query-a-Single-Field-for-Multiple-Values-in-a-Stored-Procedure.aspx

1
Robert C. Barth