Przekaż parametr tablicy w SqlCommand

144

Próbuję przekazać parametr tablicy do polecenia SQL w C # jak poniżej, ale to nie działa. Czy ktoś to wcześniej spotkał?

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
źródło
11
Nie jest to temat, ale wydaje mi się, że umieszczanie wieku jako kolumny w tabeli jest złym pomysłem, ponieważ będzie wymagało ciągłej aktualizacji. Ludzie się starzeją, prawda? Może zamiast tego powinieneś rozważyć posiadanie kolumny DateOfBirth?
Kjetil Watnedal
pytanie z dobrą odpowiedzią tutaj: stackoverflow.com/questions/83471/…
Adam Butler

Odpowiedzi:

169

Wartości w tablicy trzeba będzie dodawać pojedynczo.

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

AKTUALIZACJA: Oto rozszerzone i wielokrotnego użytku rozwiązanie, które wykorzystuje odpowiedź Adama wraz z jego sugerowaną edycją. Poprawiłem go trochę i uczyniłem z niego metodę rozszerzającą, aby jeszcze bardziej ułatwić wywoływanie.

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

}

Nazywa się to tak ...

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

Zauważ, że „{Age}” w instrukcji sql jest tym samym, co nazwa parametru, który wysyłamy do AddArrayParameters. AddArrayParameters zastąpi wartość prawidłowymi parametrami.

Brian
źródło
11
Czy ta metoda ma problem z bezpieczeństwem, taki jak wstrzyknięcie sql?
Yongwei Xing
7
Ponieważ umieszczasz wartości w parametrach, nie ma ryzyka wstrzyknięcia sql.
Brian
Tego szukałem, ale miałem pytanie. Gdyby OP miał kilka takich samych kolumn do dodania do SQL, jak byśmy to zrobili? Przykład. SELECT * FROM TableA WHERE Age = ({0}) OR Age = ({1}). (jak byśmy to zrobili z cmd.Parameters)
Cocoa Dev
1
@ T2Tom, Ups. Naprawiłem to. Dzięki.
Brian,
1
Podoba mi się to i wprowadziłem następującą modyfikację dopiero po wyodrębnieniu ciągu znaków zastępczych do zmiennej: var paramPlaceholder = "{" & paramNameRoot & "}"; Debug.Assert(cmd.CommandText.Contains(paramPlaceholder), "Parameter Name Root must exist in the Source Query"); Powinno to pomóc programistom, jeśli zapomną dopasować paramNameRoot do zapytania.
MCattle
37

Chciałem rozwinąć odpowiedź, że Brian przyczynił się do tego, aby było to łatwe do wykorzystania w innych miejscach.

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

Możesz użyć tej nowej funkcji w następujący sposób:

SqlCommand cmd = new SqlCommand();

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

cmd.CommandText = sql;


Edycja: Oto ogólna odmiana, która działa z tablicą wartości dowolnego typu i można jej używać jako metody rozszerzającej:

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

Następnie możesz użyć tej metody rozszerzenia w następujący sposób:

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

Upewnij się, że ustawiłeś CommandText przed wywołaniem AddArrayParameters.

Upewnij się również, że nazwa parametru nie będzie częściowo pasować do niczego innego w instrukcji (np. @AgeOfChild)

J Adam Rogers
źródło
1
Oto ogólna odmiana, która działa z tablicą wartości dowolnego typu i jest użyteczna jako metoda rozszerzająca: 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 (nazwa, nazwy); }
Adam Nemitoff
Drobny problem z tą odpowiedzią dotyczy AddWithValuefunkcji, czy jest szansa, że ​​możesz to naprawić?
DavidG,
Ta odpowiedź jest błędna, ponieważ ma słabą skalowalność i wydajność oraz promuje złe praktyki kodowania.
Igor Levicki
24

Jeśli możesz użyć narzędzia takiego jak „elegancki”, może to być po prostu:

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 zajmie się rozpakowaniem tego do indywidualnych parametrów za Ciebie .

Marc Gravell
źródło
Dapper pobiera wiele zależności :(
mlt
@mlt huh? nie, nie ma; na netfx: "brak zależności"; na ns2.0 wystarczy „System.Reflection.Emit.Lightweight” - i prawdopodobnie moglibyśmy to usunąć, dodając cel necroreapp
Marc Gravell
Nie chciałem przechwytywać dyskusji, ale zrobiłem ... Jak dotąd używam Npgsql, który obsługuje tablice dobrze, jak w '{1,2,3}'argumentach stylu funkcji (nie klauzuli WHERE IN), ale wolę używać zwykłego ODBC, jeśli nie problemy z tablicą. Przypuszczam, że w tym przypadku również potrzebowałbym Dapper ODBC. Oto, co chce ciągnąć. snipboard.io/HU0RpJ.jpg . Może powinienem przeczytać więcej o Dapper ...
mlt
16

Jeśli używasz MS SQL Server 2008 i nowszych, możesz używać parametrów wycenianych w tabeli, jak opisano tutaj http://www.sommarskog.se/arrays-in-sql-2008.html .

1. Utwórz typ tabeli dla każdego typu parametru, którego będziesz używać

Następujące polecenie tworzy typ tabeli dla liczb całkowitych:

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

2. Wdrożenie metod pomocniczych

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. Używaj tego w ten sposób

cmd.CommandText = "select * from TableA where Age in (select id from @age)"; 
cmd.AddParameter("@age", new [] {1,2,3,4,5});
Gregor Slavec
źródło
1
Ta linia table.Rows.Add(id);powoduje delikatny zapach kodu podczas korzystania z SonarQube. Użyłem tej alternatywy wewnątrz foreach: var row = table.NewRow(); row["id"] = id; table.Rows.Add(row);.
pogosama,
1
Powinna to być akceptowana odpowiedź, zwłaszcza jeśli została dostosowana do przyjmowania większej liczby kolumn.
Igor Levicki
10

Ponieważ jest metoda

SqlCommand.Parameters.AddWithValue(parameterName, value)

wygodniej byłoby utworzyć metodę akceptującą parametr (nazwę) do zastąpienia i listę wartości. Nie jest na poziomie parametrów (jak AddWithValue ), ale na samym poleceniu, więc lepiej jest nazwać go AddParametersWithValues, a nie tylko AddWithValues :

pytanie:

SELECT * from TableA WHERE Age IN (@age)

stosowanie:

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

metoda rozszerzenia:

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
źródło
1
Wygląda na to, że istnieje kilka iteracji tej metody rozszerzenia w kilku odpowiedziach. Jednak użyłem tego, więc głosuję za nim :-)
Dan Forbes
lepiej jest użyć statycznego indeksu dla nazwy parametru
shmnff
6

Chcę zaproponować inny sposób rozwiązania ograniczenia za pomocą operatora IN.

Na przykład mamy następujące zapytanie

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

Chcemy przekazać kilka identyfikatorów, aby filtrować użytkowników. Niestety nie da się tego zrobić w C # w łatwy sposób. Ale mam obejście tego problemu za pomocą funkcji „string_split”. Musimy nieco przepisać nasze zapytanie na następujące.

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

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

Teraz możemy łatwo przekazać jeden parametr wyliczający wartości oddzielone przecinkami.

user2399170
źródło
najlepszy i najczystszy sposób, jaki znalazłem, pod warunkiem, że Twoja zgodność jest aktualna.
user1040975
4

Przekazanie tablicy elementów jako zwiniętego parametru do klauzuli WHERE..IN nie powiedzie się, ponieważ zapytanie będzie miało postać WHERE Age IN ("11, 13, 14, 16").

Ale możesz przekazać swój parametr jako tablicę serializowaną do XML lub JSON:

Za pomocą nodes()metody:

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

Za pomocą OPENXMLmetody:

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

To trochę więcej po stronie SQL i potrzebujesz odpowiedniego XML (z rootem).

Przy użyciu OPENJSONmetody (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);

Zauważ, że w przypadku ostatniej metody musisz również mieć poziom zgodności na poziomie 130+.

Łukasz Matysiak
źródło
0

Omówienie: Użyj DbType, aby ustawić typ parametru.

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()
złoty Lew
źródło
-1

Użyj .AddWithValue(), więc:

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

Alternatywnie możesz użyć tego:

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

Twój całkowity przykład kodu będzie wyglądał następująco:

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
źródło
Typ pola Age to nvchar not int. Czy to ma znaczenie?
Yongwei Xing
Nie powinno. Zwłaszcza w przypadku drugiej metody. Ty określasz typ jawnie.
Kyle Rosendo
Używam obu metod, ale nadal nie działa. Nie chcę manipulować sznurkiem, co może powodować problemy z bezpieczeństwem
Yongwei Xing
Naprawdę cię nie rozumiem. Kiedy mówisz, że to nie działa, czy zgłasza wyjątek? Co to robi?
Kyle Rosendo
1
nie zgłasza wyjątku, nic nie zwraca. Ale uruchamiam T-SQL w Studio Management, zwraca wiele wyników.
Yongwei Xing
-1

Oto niewielki wariant odpowiedzi Briana, który może być przydatny dla kogoś innego. Pobiera listę kluczy i upuszcza ją na listę parametrów.

//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
źródło
Ta odpowiedź jest błędna, ponieważ ma słabą skalowalność i wydajność oraz promuje złe praktyki kodowania.
Igor Levicki
-3

próbować

sqlComm.Parameters["@Age"].Value = sb.ToString().Replace(","," ");
Ballin
źródło
-5

spróbuj tego w ten sposób

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
źródło
2
Po co umieszczać SqlConnection i SqlCommnad w pętli?
Yongwei Xing