Übergeben Sie den Array-Parameter in SqlCommand

144

Ich versuche, Array-Parameter wie unten beschrieben an SQL-Befehl in C # zu übergeben, aber es funktioniert nicht. Trifft es jemand schon einmal?

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
quelle
11
Nicht wirklich das Thema, aber es scheint mir, dass es eine schlechte Idee ist, Alter als Spalte in einer Tabelle zu haben, da es ständig aktualisiert werden muss. Die Leute werden älter, oder? Vielleicht sollten Sie stattdessen eine Spalte DateOfBirth verwenden?
Kjetil Watnedal
Frage mit guter Antwort hier: stackoverflow.com/questions/83471/…
Adam Butler

Antworten:

168

Sie müssen die Werte einzeln im Array hinzufügen.

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

UPDATE: Hier ist eine erweiterte und wiederverwendbare Lösung, die Adams Antwort zusammen mit seiner vorgeschlagenen Bearbeitung verwendet. Ich habe es ein wenig verbessert und es zu einer Erweiterungsmethode gemacht, um das Aufrufen noch einfacher zu machen.

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

}

Es heißt so ...

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

Beachten Sie, dass das "{Age}" in der SQL-Anweisung mit dem Parameternamen übereinstimmt, den wir an AddArrayParameters senden. AddArrayParameters ersetzt den Wert durch die richtigen Parameter.

Brian
quelle
11
Hat diese Methode das Sicherheitsproblem wie SQL Injection?
Yongwei Xing
7
Da Sie die Werte in Parameter eingeben, besteht kein Risiko einer SQL-Injektion.
Brian
Das habe ich gesucht, aber ich hatte eine Frage. Wenn das OP mehrere der gleichen Spalten zum SQL hinzufügen würde, wie würden wir das tun? Beispiel. SELECT * FROM TableA WHERE Alter = ({0}) ODER Alter = ({1}). (Wie würden wir es mit den cmd.Parametern machen)
Cocoa Dev
1
@ T2Tom, Whoops. Ich habe es repariert. Vielen Dank.
Brian
1
Ich mag dies und habe erst die folgende Änderung vorgenommen, nachdem die Platzhalterzeichenfolge in eine Variable extrahiert wurde: var paramPlaceholder = "{" & paramNameRoot & "}"; Debug.Assert(cmd.CommandText.Contains(paramPlaceholder), "Parameter Name Root must exist in the Source Query"); Dies sollte Entwicklern helfen, wenn sie vergessen, paramNameRoot mit der Abfrage abzugleichen.
MCattle
37

Ich wollte die Antwort erweitern, die Brian dazu beigetragen hat, dies an anderen Orten leicht nutzbar zu machen.

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

Sie können diese neue Funktion wie folgt verwenden:

SqlCommand cmd = new SqlCommand();

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

cmd.CommandText = sql;


Bearbeiten: Hier ist eine generische Variante, die mit einem Array von Werten eines beliebigen Typs funktioniert und als Erweiterungsmethode verwendet werden kann:

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

Sie können diese Erweiterungsmethode dann wie folgt verwenden:

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

Stellen Sie sicher, dass Sie den CommandText festlegen, bevor Sie AddArrayParameters aufrufen.

Stellen Sie außerdem sicher, dass Ihr Parametername teilweise nicht mit anderen Elementen in Ihrer Anweisung übereinstimmt (z. B. @AgeOfChild).

J Adam Rogers
quelle
1
Hier ist eine generische Variante, die mit einem Array von Werten eines beliebigen Typs funktioniert und als Erweiterungsmethode verwendet werden kann: public static void AddArrayParameters <T> (dieser SqlCommand-Befehl cmd, Zeichenfolgenname, IEnumerable <T> -Werte) {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, Namen); }
Adam Nemitoff
Ein kleines Problem mit dieser Antwort ist die AddWithValueFunktion. Gibt es eine Chance, dass Sie das beheben können?
DavidG
Diese Antwort ist falsch, da sie eine schlechte Skalierbarkeit und Leistung aufweist und schlechte Codierungspraktiken fördert.
Igor Levicki
24

Wenn Sie ein Tool wie "dapper" verwenden können, kann dies einfach sein:

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 übernimmt das Auspacken in einzelne Parameter für Sie .

Marc Gravell
quelle
Dapper zieht viele Abhängigkeiten :(
mlt
@mlt huh? nein, das tut es nicht; auf netfx: "keine Abhängigkeiten"; auf ns2.0 nur "System.Reflection.Emit.Lightweight" - und wir könnten das wahrscheinlich entfernen, wenn wir ein necroreapp-Ziel hinzufügen
Marc Gravell
Ich wollte die Diskussion nicht entführen, aber ich tat es ... Bisher verwende ich Npgsql, das Arrays gut wie Stilargumente '{1,2,3}'für eine Funktion behandelt (keine WHERE IN-Klausel), aber ich würde lieber einfaches ODBC verwenden, wenn nicht Array Ärger. Ich nehme an, ich würde in diesem Fall auch Dapper ODBC benötigen. Hier ist was es ziehen will. snipboard.io/HU0RpJ.jpg . Vielleicht sollte ich mehr über Dapper lesen ...
mlt
16

Wenn Sie MS SQL Server 2008 und höher verwenden, können Sie tabellenwertige Parameter verwenden, wie hier beschrieben: http://www.sommarskog.se/arrays-in-sql-2008.html .

1. Erstellen Sie einen Tabellentyp für jeden Parametertyp, den Sie verwenden

Der folgende Befehl erstellt einen Tabellentyp für Ganzzahlen:

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

2. Implementieren Sie Hilfsmethoden

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. Verwenden Sie es so

cmd.CommandText = "select * from TableA where Age in (select id from @age)"; 
cmd.AddParameter("@age", new [] {1,2,3,4,5});
Gregor Slavec
quelle
1
Die Zeile table.Rows.Add(id);führt bei Verwendung von SonarQube zu einem geringfügigen Codegeruch. Ich habe diese Alternative im foreach verwendet : var row = table.NewRow(); row["id"] = id; table.Rows.Add(row);.
Pogosama
1
Dies sollte die akzeptierte Antwort sein, insbesondere wenn sie angepasst wurde, um mehr Spalten zu akzeptieren.
Igor Levicki
10

Da gibt es eine Methode auf

SqlCommand.Parameters.AddWithValue(parameterName, value)

Es ist möglicherweise bequemer, eine Methode zu erstellen, die einen zu ersetzenden Parameter (Namen) und eine Liste von Werten akzeptiert. Es befindet sich nicht auf der Parameterebene (wie AddWithValue ), sondern auf Befehl selbst. Es ist daher besser, es AddParametersWithValues und nicht nur AddWithValues ​​zu nennen :

Abfrage:

SELECT * from TableA WHERE Age IN (@age)

Verwendung:

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

die Erweiterungsmethode:

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
quelle
1
Es sieht so aus, als ob mehrere Iterationen dieser Erweiterungsmethode in einigen Antworten vorhanden sind. Ich habe dieses verwendet, also stimme ich es ab :-)
Dan Forbes
Es ist besser, einen statischen Index für den Parameternamen zu verwenden
shmnff
6

Ich möchte einen anderen Weg vorschlagen, wie man die Einschränkung mit dem IN-Operator löst.

Zum Beispiel haben wir folgende Abfrage

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

Wir möchten mehrere IDs an Filterbenutzer übergeben. Leider ist es nicht einfach, mit C # umzugehen. Ich habe jedoch eine umfassende Problemumgehung, indem ich die Funktion "string_split" verwende. Wir müssen unsere Abfrage ein wenig umschreiben, um Folgendes zu tun.

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

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

Jetzt können wir leicht eine Parameteraufzählung von durch Komma getrennten Werten übergeben.

user2399170
quelle
Der beste und sauberste Weg, den ich gefunden habe, vorausgesetzt, Ihre Kompatibilität ist aktuell.
user1040975
4

Das Übergeben eines Arrays von Elementen als reduzierter Parameter an die WHERE..IN-Klausel schlägt fehl, da die Abfrage die Form von hat WHERE Age IN ("11, 13, 14, 16").

Sie können Ihren Parameter jedoch als Array übergeben, das in XML oder JSON serialisiert ist:

Mit nodes()Methode:

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

Mit OPENXMLMethode:

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

Das ist ein bisschen mehr auf der SQL-Seite und Sie benötigen ein richtiges XML (mit root).

Verwenden der OPENJSONMethode (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);

Beachten Sie, dass für die letzte Methode die Kompatibilitätsstufe mindestens 130 sein muss.

Lukasz Matysiak
quelle
0

Übersicht: Verwenden Sie den DbType, um den Parametertyp festzulegen.

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()
Goldener Löwe
quelle
-1

Verwenden Sie .AddWithValue()also:

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

Alternativ können Sie Folgendes verwenden:

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

Ihr gesamtes Codebeispiel sieht dann wie folgt aus:

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
quelle
Der Typ des Feldes Alter ist nvchar nicht int. Ist das wichtig?
Yongwei Xing
Es sollte nicht. Besonders bei der zweiten Methode. Sie geben den Typ explizit an.
Kyle Rosendo
Ich benutze beide Methoden, es funktioniert immer noch nicht. Ich möchte die Zeichenfolge nicht manipulieren, die zu Sicherheitsproblemen führen kann
Yongwei Xing
Ich verstehe dich nicht wirklich. Wenn Sie sagen, dass es nicht funktioniert, löst es eine Ausnahme aus? Was tut es?
Kyle Rosendo
1
es löst keine Ausnahme aus, es gibt nichts zurück. Aber ich führe das T-SQL in Studio Management aus, es gibt viele Ergebnisse zurück.
Yongwei Xing
-1

Hier ist eine kleine Variante von Brians Antwort, die jemand anderes nützlich finden könnte. Nimmt eine Liste von Schlüsseln und legt sie in der Parameterliste ab.

//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
quelle
Diese Antwort ist falsch, da sie eine schlechte Skalierbarkeit und Leistung aufweist und schlechte Codierungspraktiken fördert.
Igor Levicki
-3

Versuchen

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

versuche es so

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
quelle
2
Warum sollten Sie SqlConnection und SqlCommnad in die Schleife einfügen?
Yongwei Xing