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

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.


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)


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 .


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

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

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.


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+.


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

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

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

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

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

2
Po co umieszczać SqlConnection i SqlCommnad w pętli?
Yongwei Xing
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.