Passa il parametro Array in SqlCommand


144

Sto provando a passare i parametri dell'array al comando SQL in C # come di seguito, ma non funziona. Qualcuno lo incontra prima?

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(',');

11
Non proprio l'argomento, ma mi sembra che avere Age come colonna in una tabella sia una cattiva idea, dal momento che dovrà essere aggiornato costantemente. Le persone invecchiano, vero? Forse dovresti considerare di avere una colonna DateOfBirth invece?
Kjetil Watnedal

domanda con una buona risposta qui: stackoverflow.com/questions/83471/…
Adam Butler,

Risposte:


169

Sarà necessario aggiungere i valori nell'array uno alla volta.

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

AGGIORNAMENTO: ecco una soluzione estesa e riutilizzabile che utilizza la risposta di Adam insieme alla sua modifica suggerita. L'ho migliorato un po 'e l'ho reso un metodo di estensione per rendere ancora più facile la chiamata.

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

}

Si chiama così ...

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

Si noti che "{Age}" nell'istruzione sql è lo stesso del nome del parametro che stiamo inviando a AddArrayParameters. AddArrayParameters sostituirà il valore con i parametri corretti.


11
Questo metodo ha il problema di sicurezza, come l'iniezione sql?
Yongwei Xing

7
Poiché si stanno inserendo i valori in parametri, non vi è alcun rischio di iniezione sql.
Brian

Questo è quello che stavo cercando ma avevo una domanda. Se l'OP avesse diverse colonne uguali da aggiungere all'SQL, come potremmo farlo? Esempio. SELEZIONA * DA Tabella A DOVE Età = ({0}) O Età = ({1}). (come lo faremmo con i parametri cmd.)
Cocoa Dev,

1
@ T2Tom, Whoops. L'ho riparato. Grazie.
Brian

1
Mi piace questo e ho apportato la seguente modifica solo dopo aver estratto la stringa segnaposto in una variabile: var paramPlaceholder = "{" & paramNameRoot & "}"; Debug.Assert(cmd.CommandText.Contains(paramPlaceholder), "Parameter Name Root must exist in the Source Query"); Questo dovrebbe aiutare gli sviluppatori se dimenticano di associare paramNameRoot alla query.
MCattle,

37

Volevo ampliare la risposta che Brian ha contribuito a rendere facilmente utilizzabile in altri luoghi.

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

È possibile utilizzare questa nuova funzione come segue:

SqlCommand cmd = new SqlCommand();

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

cmd.CommandText = sql;


Modifica: ecco una variante generica che funziona con una matrice di valori di qualsiasi tipo ed è utilizzabile come metodo di estensione:

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

È quindi possibile utilizzare questo metodo di estensione come segue:

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

Assicurarsi di impostare CommandText prima di chiamare AddArrayParameters.

Assicurati anche che il nome del tuo parametro non corrisponda parzialmente a qualsiasi altra cosa nella tua dichiarazione (ad esempio @AgeOfChild)


1
Ecco una variante generica che funziona con array di valori di qualsiasi tipo ed è utilizzabile come metodo di estensione: void statico pubblico AddArrayParameters <T> (questo cmd SqlCommand, nome stringa, valori IEnumerable <T>) {var names = string.Join (",", limits.Select ((value, i) => {var paramName = name + i; cmd.Parameters.AddWithValue (paramName, value); return paramName;})); cmd.CommandText = cmd.CommandText.Replace (nome, nomi); }
Adam Nemitoff,

Il problema minore con questa risposta è con la AddWithValuefunzione, qualche possibilità che tu possa risolvere?
DavidG

Questa risposta è sbagliata perché ha scarse scalabilità e prestazioni e promuove cattive pratiche di codifica.
Igor Levicki,

24

Se puoi usare uno strumento come "dapper", questo può essere semplicemente:

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 si occuperà di scartarlo su singoli parametri per te .


Dapper tira molte dipendenze :(
mlt il

@mlt eh? no, non lo fa; su netfx: "nessuna dipendenza"; su ns2.0, solo "System.Reflection.Emit.Lightweight" - e probabilmente potremmo rimuoverlo se aggiungessimo un obiettivo necroreapp
Marc Gravell

Non intendevo dirottare la discussione, ma l'ho fatto ... Finora uso Npgsql che gestisce bene le matrici come negli '{1,2,3}'argomenti di stile per una funzione (non una clausola WHERE IN), ma preferirei usare ODBC semplice se no seccatura dell'array. Presumo che avrei bisogno di Dapper ODBC anche in questo caso. Ecco cosa vuole tirare. snipboard.io/HU0RpJ.jpg . Forse dovrei leggere di più su Dapper ...
mlt

16

Se si utilizza MS SQL Server 2008 e versioni successive è possibile utilizzare parametri con valori di tabella come descritto qui http://www.sommarskog.se/arrays-in-sql-2008.html .

1. Creare un tipo di tabella per ciascun tipo di parametro che verrà utilizzato

Il seguente comando crea un tipo di tabella per numeri interi:

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

2. Implementare metodi di supporto

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. Usalo in questo modo

cmd.CommandText = "select * from TableA where Age in (select id from @age)"; 
cmd.AddParameter("@age", new [] {1,2,3,4,5});

1
La riga table.Rows.Add(id);produce un odore di codice minore quando si utilizza SonarQube. Ho usato questa alternativa all'interno del foreach: var row = table.NewRow(); row["id"] = id; table.Rows.Add(row);.
pogosama,

1
Questa dovrebbe essere la risposta accettata, soprattutto se è stata adattata per accettare più colonne.
Igor Levicki,

10

Dal momento che esiste un metodo

SqlCommand.Parameters.AddWithValue(parameterName, value)

potrebbe essere più comodo creare un metodo che accetti un parametro (nome) da sostituire e un elenco di valori. Non è a livello di parametri (come AddWithValue ) ma su comando stesso, quindi è meglio chiamarlo AddParametersWithValues e non solo AddWithValues :

query:

SELECT * from TableA WHERE Age IN (@age)

utilizzo:

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

il metodo di estensione:

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

1
Sembra che esistano diverse iterazioni di questo metodo di estensione in alcune risposte. Ho usato questo, però, quindi sto votando a favore :-)
Dan Forbes

è meglio usare un indice statico per il nome del parametro
shmnff

6

Voglio proporre un altro modo, come risolvere la limitazione con l'operatore IN.

Ad esempio abbiamo la seguente query

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

Vogliamo passare diversi ID per filtrare gli utenti. Sfortunatamente non è possibile fare con C # in modo semplice. Ma ho una soluzione alternativa per questo usando la funzione "string_split". Dobbiamo riscrivere un po 'la nostra query a seguire.

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

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

Ora possiamo facilmente passare un elenco di parametri di valori separati da virgola.


il modo migliore e più pulito che ho trovato, a condizione che la tua compatibilità sia aggiornata.
user1040975

4

Il passaggio di una matrice di elementi come parametro compresso alla clausola WHERE..IN avrà esito negativo poiché la query prenderà forma WHERE Age IN ("11, 13, 14, 16").

Ma puoi passare il tuo parametro come un array serializzato in XML o JSON:

Utilizzando il nodes()metodo:

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();

Utilizzando il OPENXMLmetodo:

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();

Questo è un po 'più sul lato SQL e hai bisogno di un XML adeguato (con root).

Utilizzando il OPENJSONmetodo (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);

Nota che per l'ultimo metodo devi avere anche Livello di compatibilità a 130+.


0

Panoramica: utilizzare DbType per impostare il tipo di parametro.

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()

-1

Usa .AddWithValue(), quindi:

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

In alternativa, è possibile utilizzare questo:

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

L'esempio di codice totale esaminerà quindi quanto segue:

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

Il tipo di campo Età è nvchar non int. Importa?
Yongwei Xing

Non dovrebbe. Soprattutto con il secondo metodo. Si specifica il tipo in modo esplicito.
Kyle Rosendo

Uso entrambi i metodi, non funziona ancora. Non voglio manipolare la stringa che potrebbe causare problemi di sicurezza
Yongwei Xing

Non ti sto davvero capendo. Quando dici che non funziona, genera un'eccezione? Che cosa fa?
Kyle Rosendo

1
non genera eccezioni, non restituisce nulla. Ma eseguo il T-SQL in Studio Management, restituisce molti risultati.
Yongwei Xing

-1

Ecco una variante minore della risposta di Brian che qualcun altro potrebbe trovare utile. Prende un elenco di chiavi e lo inserisce nell'elenco dei parametri.

//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(',') + ")";

Questa risposta è sbagliata perché ha scarse scalabilità e prestazioni e promuove cattive pratiche di codifica.
Igor Levicki,

-3

provare

sqlComm.Parameters["@Age"].Value = sb.ToString().Replace(","," ");

-5

provalo così

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(',');
     } 
} 

2
Perché mettere SqlConnection e SqlCommnad nel loop?
Yongwei Xing
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.