Passer le paramètre de tableau dans SqlCommand

144

J'essaie de passer le paramètre de tableau à SQL commnd en C # comme ci-dessous, mais cela ne fonctionne pas. Quelqu'un l'a-t-il déjà rencontré?

string sqlCommand = "SELECT * from TableA WHERE Age IN (@Age)";
SqlConnection sqlCon = new SqlConnection(connectString);
SqlCommand sqlComm = new SqlCommand();
sqlComm.Connection = sqlCon;
sqlComm.CommandType = System.Data.CommandType.Text;
sqlComm.CommandText = sqlCommand;
sqlComm.CommandTimeout = 300;
sqlComm.Parameters.Add("@Age", SqlDbType.NVarChar);
StringBuilder sb = new StringBuilder();
foreach (ListItem item in ddlAge.Items)
{
     if (item.Selected)
     {
         sb.Append(item.Text + ",");
     }
}

sqlComm.Parameters["@Age"].Value = sb.ToString().TrimEnd(',');
Yongwei Xing
la source
11
Ce n'est pas vraiment le sujet, mais il me semble qu'avoir Age comme colonne dans un tableau est une mauvaise idée, car il devra être constamment mis à jour. Les gens vieillissent, non? Peut-être devriez-vous envisager d'avoir une colonne DateOfBirth à la place?
Kjetil Watnedal
question avec une bonne réponse ici: stackoverflow.com/questions/83471/…
Adam Butler

Réponses:

169

Vous devrez ajouter les valeurs du tableau une par une.

var parameters = new string[items.Length];
var cmd = new SqlCommand();
for (int i = 0; i < items.Length; i++)
{
    parameters[i] = string.Format("@Age{0}", i);
    cmd.Parameters.AddWithValue(parameters[i], items[i]);
}

cmd.CommandText = string.Format("SELECT * from TableA WHERE Age IN ({0})", string.Join(", ", parameters));
cmd.Connection = new SqlConnection(connStr);

MISE À JOUR: Voici une solution étendue et réutilisable qui utilise la réponse d'Adam avec sa modification suggérée. Je l'ai un peu amélioré et en ai fait une méthode d'extension pour le rendre encore plus facile à appeler.

public static class SqlCommandExt
{

    /// <summary>
    /// This will add an array of parameters to a SqlCommand. This is used for an IN statement.
    /// Use the returned value for the IN part of your SQL call. (i.e. SELECT * FROM table WHERE field IN ({paramNameRoot}))
    /// </summary>
    /// <param name="cmd">The SqlCommand object to add parameters to.</param>
    /// <param name="paramNameRoot">What the parameter should be named followed by a unique value for each value. This value surrounded by {} in the CommandText will be replaced.</param>
    /// <param name="values">The array of strings that need to be added as parameters.</param>
    /// <param name="dbType">One of the System.Data.SqlDbType values. If null, determines type based on T.</param>
    /// <param name="size">The maximum size, in bytes, of the data within the column. The default value is inferred from the parameter value.</param>
    public static SqlParameter[] AddArrayParameters<T>(this SqlCommand cmd, string paramNameRoot, IEnumerable<T> values, SqlDbType? dbType = null, int? size = null)
    {
        /* An array cannot be simply added as a parameter to a SqlCommand so we need to loop through things and add it manually. 
         * Each item in the array will end up being it's own SqlParameter so the return value for this must be used as part of the
         * IN statement in the CommandText.
         */
        var parameters = new List<SqlParameter>();
        var parameterNames = new List<string>();
        var paramNbr = 1;
        foreach (var value in values)
        {
            var paramName = string.Format("@{0}{1}", paramNameRoot, paramNbr++);
            parameterNames.Add(paramName);
            SqlParameter p = new SqlParameter(paramName, value);
            if (dbType.HasValue)
                p.SqlDbType = dbType.Value;
            if (size.HasValue)
                p.Size = size.Value;
            cmd.Parameters.Add(p);
            parameters.Add(p);
        }

        cmd.CommandText = cmd.CommandText.Replace("{" + paramNameRoot + "}", string.Join(",", parameterNames));

        return parameters.ToArray();
    }

}

Cela s'appelle comme ça ...

var cmd = new SqlCommand("SELECT * FROM TableA WHERE Age IN ({Age})");
cmd.AddArrayParameters("Age", new int[] { 1, 2, 3 });

Notez que "{Age}" dans l'instruction sql est le même que le nom du paramètre que nous envoyons à AddArrayParameters. AddArrayParameters remplacera la valeur par les paramètres corrects.

Brian
la source
11
Cette méthode a-t-elle le problème de sécurité, comme l'injection SQL?
Yongwei Xing
7
Parce que vous mettez les valeurs dans des paramètres, il n'y a aucun risque d'injection SQL.
Brian
C'est ce que je cherchais mais j'avais une question. Si l'OP avait plusieurs colonnes identiques à ajouter au SQL, comment ferions-nous cela? Exemple. SELECT * FROM TableA WHERE Age = ({0}) OU Age = ({1}). (comment le ferions-nous avec les paramètres cmd.)
Cocoa Dev
1
@ T2Tom, Oups. Je l'ai corrigé. Merci.
Brian
1
J'aime cela, et j'ai seulement apporté la modification suivante après avoir extrait la chaîne d'espace réservé en une variable: var paramPlaceholder = "{" & paramNameRoot & "}"; Debug.Assert(cmd.CommandText.Contains(paramPlaceholder), "Parameter Name Root must exist in the Source Query"); Cela devrait aider les développeurs s'ils oublient de faire correspondre paramNameRoot avec la requête.
MCattle
37

Je voulais développer la réponse que Brian a contribué à rendre cela facilement utilisable dans d'autres endroits.

/// <summary>
/// This will add an array of parameters to a SqlCommand. This is used for an IN statement.
/// Use the returned value for the IN part of your SQL call. (i.e. SELECT * FROM table WHERE field IN (returnValue))
/// </summary>
/// <param name="sqlCommand">The SqlCommand object to add parameters to.</param>
/// <param name="array">The array of strings that need to be added as parameters.</param>
/// <param name="paramName">What the parameter should be named.</param>
protected string AddArrayParameters(SqlCommand sqlCommand, string[] array, string paramName)
{
    /* An array cannot be simply added as a parameter to a SqlCommand so we need to loop through things and add it manually. 
     * Each item in the array will end up being it's own SqlParameter so the return value for this must be used as part of the
     * IN statement in the CommandText.
     */
    var parameters = new string[array.Length];
    for (int i = 0; i < array.Length; i++)
    {
        parameters[i] = string.Format("@{0}{1}", paramName, i);
        sqlCommand.Parameters.AddWithValue(parameters[i], array[i]);
    }

    return string.Join(", ", parameters);
}

Vous pouvez utiliser cette nouvelle fonction comme suit:

SqlCommand cmd = new SqlCommand();

string ageParameters = AddArrayParameters(cmd, agesArray, "Age");
sql = string.Format("SELECT * FROM TableA WHERE Age IN ({0})", ageParameters);

cmd.CommandText = sql;


Edit: Voici une variante générique qui fonctionne avec un tableau de valeurs de n'importe quel type et est utilisable comme méthode d'extension:

public static class Extensions
{
    public static void AddArrayParameters<T>(this SqlCommand cmd, string name, IEnumerable<T> values) 
    { 
        name = name.StartsWith("@") ? name : "@" + name;
        var names = string.Join(", ", values.Select((value, i) => { 
            var paramName = name + i; 
            cmd.Parameters.AddWithValue(paramName, value); 
            return paramName; 
        })); 
        cmd.CommandText = cmd.CommandText.Replace(name, names); 
    }
}

Vous pouvez ensuite utiliser cette méthode d'extension comme suit:

var ageList = new List<int> { 1, 3, 5, 7, 9, 11 };
var cmd = new SqlCommand();
cmd.CommandText = "SELECT * FROM MyTable WHERE Age IN (@Age)";    
cmd.AddArrayParameters("Age", ageList);

Assurez-vous de définir le CommandText avant d'appeler AddArrayParameters.

Assurez-vous également que le nom de votre paramètre ne correspondra partiellement à rien d'autre dans votre déclaration (par exemple @AgeOfChild)

J Adam Rogers
la source
1
Voici une variante générique qui fonctionne avec un tableau de valeurs de n'importe quel type et est utilisable comme méthode d'extension: public static void AddArrayParameters <T> (this SqlCommand cmd, string name, IEnumerable <T> values) {var names = string.Join (",", values.Select ((value, i) => {var paramName = name + i; cmd.Parameters.AddWithValue (paramName, value); return paramName;})); cmd.CommandText = cmd.CommandText.Replace (nom, noms); }
Adam Nemitoff
Un problème mineur avec cette réponse est lié à la AddWithValuefonction, avez-vous une chance de résoudre ce problème?
DavidG
Cette réponse est erronée car elle présente une évolutivité et des performances médiocres et favorise de mauvaises pratiques de codage.
Igor Levicki
24

Si vous pouvez utiliser un outil comme "dapper", cela peut être simplement:

int[] ages = { 20, 21, 22 }; // could be any common list-like type
var rows = connection.Query<YourType>("SELECT * from TableA WHERE Age IN @ages",
          new { ages }).ToList();

Dapper se chargera de le déballer en paramètres individuels pour vous .

Marc Gravell
la source
Dapper tire beaucoup de dépendances: (
mlt
@mlt hein? non, ce n'est pas le cas; sur netfx: "pas de dépendances"; sur ns2.0, juste "System.Reflection.Emit.Lightweight" - et nous pourrions probablement le supprimer si nous ajoutions une cible necroreapp
Marc Gravell
Je ne voulais pas détourner la discussion, mais je l'ai fait ... Jusqu'à présent, j'utilise Npgsql qui gère correctement les tableaux comme des '{1,2,3}'arguments de style pour une fonction (pas une clause WHERE IN), mais je préfère utiliser ODBC simple sinon tracas de tableau. Je suppose que j'aurais également besoin de Dapper ODBC dans ce cas. Voici ce qu'il veut tirer. snipboard.io/HU0RpJ.jpg . Peut-être devrais-je en lire plus sur Dapper ...
mlt le
16

Si vous utilisez MS SQL Server 2008 et supérieur, vous pouvez utiliser des paramètres table comme décrit ici http://www.sommarskog.se/arrays-in-sql-2008.html .

1. Créez un type de tableau pour chaque type de paramètre que vous utiliserez

La commande suivante crée un type de table pour les entiers:

create type int32_id_list as table (id int not null primary key)

2. Mettre en œuvre des méthodes d'assistance

public static SqlCommand AddParameter<T>(this SqlCommand command, string name, IEnumerable<T> ids)
{
  var parameter = command.CreateParameter();      

  parameter.ParameterName = name;
  parameter.TypeName = typeof(T).Name.ToLowerInvariant() + "_id_list";
  parameter.SqlDbType = SqlDbType.Structured;
  parameter.Direction = ParameterDirection.Input;

  parameter.Value = CreateIdList(ids);

  command.Parameters.Add(parameter);
  return command;
}

private static DataTable CreateIdList<T>(IEnumerable<T> ids)
{
  var table = new DataTable();
  table.Columns.Add("id", typeof (T));

  foreach (var id in ids)
  {
    table.Rows.Add(id);
  }

  return table;
}

3. Utilisez-le comme ça

cmd.CommandText = "select * from TableA where Age in (select id from @age)"; 
cmd.AddParameter("@age", new [] {1,2,3,4,5});
Gregor Slavec
la source
1
La ligne table.Rows.Add(id);entraîne une légère odeur de code lors de l'utilisation de SonarQube. J'ai utilisé cette alternative à l' intérieur du foreach: var row = table.NewRow(); row["id"] = id; table.Rows.Add(row);.
pogosama
1
Cela devrait être la réponse acceptée, surtout si elle a été adaptée pour accepter plus de colonnes.
Igor Levicki
10

Puisqu'il existe une méthode sur

SqlCommand.Parameters.AddWithValue(parameterName, value)

il peut être plus pratique de créer une méthode acceptant un paramètre (nom) à remplacer et une liste de valeurs. Ce n'est pas au niveau des paramètres (comme AddWithValue ) mais sur la commande elle-même, il est donc préférable de l'appeler AddParametersWithValues et pas seulement AddWithValues :

requete:

SELECT * from TableA WHERE Age IN (@age)

usage:

sqlCommand.AddParametersWithValues("@age", 1, 2, 3);

la méthode d'extension:

public static class SqlCommandExtensions
{
    public static void AddParametersWithValues<T>(this SqlCommand cmd,  string parameterName, params T[] values)
    {
        var parameterNames = new List<string>();
        for(int i = 0; i < values.Count(); i++)
        {
            var paramName = @"@param" + i;
            cmd.Parameters.AddWithValue(paramName, values.ElementAt(i));
            parameterNames.Add(paramName);
        }

        cmd.CommandText = cmd.CommandText.Replace(parameterName, string.Join(",", parameterNames));
    }
}
tridy
la source
1
Il semble que plusieurs itérations de cette méthode d'extension existent à travers quelques réponses. J'ai utilisé celui-ci, donc je vote pour :-)
Dan Forbes
il est préférable d'utiliser un index statique pour le nom du paramètre
shmnff
6

Je veux proposer une autre façon, comment résoudre la limitation avec l'opérateur IN.

Par exemple, nous avons la requête suivante

select *
from Users U
WHERE U.ID in (@ids)

Nous voulons passer plusieurs identifiants pour filtrer les utilisateurs. Malheureusement, il n'est pas possible de faire avec C # de manière simple. Mais j'ai une solution de contournement pour cela en utilisant la fonction "string_split". Nous devons réécrire un peu notre requête pour suivre.

declare @ids nvarchar(max) = '1,2,3'

SELECT *
FROM Users as U
CROSS APPLY string_split(@ids, ',') as UIDS
WHERE U.ID = UIDS.value

Maintenant, nous pouvons facilement passer une énumération de paramètres de valeurs séparées par une virgule.

utilisateur2399170
la source
le meilleur et le plus propre que j'ai trouvé, à condition que votre compatibilité soit à jour.
user1040975
4

La transmission d'un tableau d'éléments en tant que paramètre réduit à la clause WHERE..IN échouera car la requête prendra la forme de WHERE Age IN ("11, 13, 14, 16").

Mais vous pouvez passer votre paramètre en tant que tableau sérialisé en XML ou JSON:

En utilisant la nodes()méthode:

StringBuilder sb = new StringBuilder();

foreach (ListItem item in ddlAge.Items)
  if (item.Selected)
    sb.Append("<age>" + item.Text + "</age>"); // actually it's xml-ish

sqlComm.CommandText = @"SELECT * from TableA WHERE Age IN (
    SELECT Tab.col.value('.', 'int') as Age from @Ages.nodes('/age') as Tab(col))";
sqlComm.Parameters.Add("@Ages", SqlDbType.NVarChar);
sqlComm.Parameters["@Ages"].Value = sb.ToString();

En utilisant la OPENXMLméthode:

using System.Xml.Linq;
...
XElement xml = new XElement("Ages");

foreach (ListItem item in ddlAge.Items)
  if (item.Selected)
    xml.Add(new XElement("age", item.Text);

sqlComm.CommandText = @"DECLARE @idoc int;
    EXEC sp_xml_preparedocument @idoc OUTPUT, @Ages;
    SELECT * from TableA WHERE Age IN (
    SELECT Age from OPENXML(@idoc, '/Ages/age') with (Age int 'text()')
    EXEC sp_xml_removedocument @idoc";
sqlComm.Parameters.Add("@Ages", SqlDbType.Xml);
sqlComm.Parameters["@Ages"].Value = xml.ToString();

C'est un peu plus du côté SQL et vous avez besoin d'un XML approprié (avec root).

Utilisation de la OPENJSONméthode (SQL Server 2016+):

using Newtonsoft.Json;
...
List<string> ages = new List<string>();

foreach (ListItem item in ddlAge.Items)
  if (item.Selected)
    ages.Add(item.Text);

sqlComm.CommandText = @"SELECT * from TableA WHERE Age IN (
    select value from OPENJSON(@Ages))";
sqlComm.Parameters.Add("@Ages", SqlDbType.NVarChar);
sqlComm.Parameters["@Ages"].Value = JsonConvert.SerializeObject(ages);

Notez que pour la dernière méthode, vous devez également avoir un niveau de compatibilité à 130+.

Lukasz Matysiak
la source
0

Vue d'ensemble: utilisez le DbType pour définir le type de paramètre.

var parameter = new SqlParameter();
parameter.ParameterName = "@UserID";
parameter.DbType = DbType.Int32;
parameter.Value = userID.ToString();

var command = conn.CreateCommand()
command.Parameters.Add(parameter);
var reader = await command.ExecuteReaderAsync()
Lion d'or
la source
-1

Utilisez .AddWithValue(), donc:

sqlComm.Parameters.AddWithValue("@Age", sb.ToString().TrimEnd(','));

Vous pouvez également utiliser ceci:

sqlComm.Parameters.Add(
    new SqlParameter("@Age", sb.ToString().TrimEnd(',')) { SqlDbType = SqlDbType. NVarChar }
    );

Votre échantillon de code total examinera ensuite:

string sqlCommand = "SELECT * from TableA WHERE Age IN (@Age)";
SqlConnection sqlCon = new SqlConnection(connectString);
SqlCommand sqlComm = new SqlCommand();
sqlComm.Connection = sqlCon;
sqlComm.CommandType = System.Data.CommandType.Text;
sqlComm.CommandText = sqlCommand;
sqlComm.CommandTimeout = 300;

StringBuilder sb = new StringBuilder();
foreach (ListItem item in ddlAge.Items)
{
     if (item.Selected)
     {
         sb.Append(item.Text + ",");
     }
}

sqlComm.Parameters.AddWithValue("@Age", sb.ToString().TrimEnd(','));

// OR

// sqlComm.Parameters.Add(new SqlParameter("@Age", sb.ToString().TrimEnd(',')) { SqlDbType = SqlDbType. NVarChar });
Kyle Rosendo
la source
Le type du champ Age est nvchar et non int. Est-ce que ça importe?
Yongwei Xing
Ça ne devrait pas. Surtout avec la deuxième méthode. Vous spécifiez le type explicitement.
Kyle Rosendo
J'utilise les deux méthodes, cela ne fonctionne toujours pas. Je ne veux pas manipuler la chaîne qui peut poser un problème de sécurité
Yongwei Xing
Je ne te comprends pas vraiment. Lorsque vous dites que cela ne fonctionne pas, cela lance-t-il une exception? Qu'est ce que ça fait?
Kyle Rosendo
1
il ne lance pas d'exception, il ne renvoie rien. Mais j'exécute le T-SQL dans Studio Management, il renvoie de nombreux résultats.
Yongwei Xing
-1

Voici une variante mineure de la réponse de Brian que quelqu'un d'autre pourrait trouver utile. Prend une liste de clés et la dépose dans la liste des paramètres.

//keyList is a List<string>
System.Data.SqlClient.SqlCommand command = new System.Data.SqlClient.SqlCommand();
string sql = "SELECT fieldList FROM dbo.tableName WHERE keyField in (";
int i = 1;
foreach (string key in keyList) {
    sql = sql + "@key" + i + ",";
    command.Parameters.AddWithValue("@key" + i, key);
    i++;
}
sql = sql.TrimEnd(',') + ")";
Jeff
la source
Cette réponse est erronée car elle présente une évolutivité et des performances médiocres et favorise de mauvaises pratiques de codage.
Igor Levicki
-3

essayer

sqlComm.Parameters["@Age"].Value = sb.ToString().Replace(","," ");
Ballin
la source
-5

essayez-le comme ça

StringBuilder sb = new StringBuilder(); 
foreach (ListItem item in ddlAge.Items) 
{ 
     if (item.Selected) 
     { 
          string sqlCommand = "SELECT * from TableA WHERE Age IN (@Age)"; 
          SqlConnection sqlCon = new SqlConnection(connectString); 
          SqlCommand sqlComm = new SqlCommand(); 
          sqlComm.Connection = sqlCon; 
          sqlComm.CommandType = System.Data.CommandType.Text; 
          sqlComm.CommandText = sqlCommand; 
          sqlComm.CommandTimeout = 300; 
          sqlComm.Parameters.Add("@Age", SqlDbType.NVarChar);
          sb.Append(item.Text + ","); 
          sqlComm.Parameters["@Age"].Value = sb.ToString().TrimEnd(',');
     } 
} 
Ballin
la source
2
Pourquoi mettre SqlConnection et SqlCommnad dans la boucle?
Yongwei Xing