Передайте параметр масиву в SqlCommand


144

Я намагаюся передати параметр масиву до комісії SQL в C #, як показано нижче, але це не працює. Хтось із цим зустрічався раніше?

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
Тема насправді не є, але мені здається, що вік як стовпець у таблиці - це погана ідея, оскільки її потрібно буде постійно оновлювати. Люди старіють, правда? Можливо, вам слід розглянути можливість встановлення стовпця DateOfBirth замість цього?
Kjetil Watnedal

питання з хорошою відповіддю тут: stackoverflow.com/questions/83471/…
Адам Батлер

Відповіді:


169

Значення в масиві потрібно буде додавати по одному.

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

ОНОВЛЕННЯ: Ось розширене та багаторазове рішення, яке використовує відповідь Адама разом із запропонованою редакцією. Я трохи його вдосконалив і зробив метод розширення, щоб зробити його ще простіше телефонувати.

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

}

Це називається так ...

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

Зауважте, що "{Age}" у операторі sql є таким самим, як ім'я параметра, яке ми надсилаємо AddArrayParameters. AddArrayParameters замінить значення правильними параметрами.


11
Чи є у цього методу проблема безпеки, як введення sql?
Йонгвей Сін

7
Оскільки ви вводите значення в параметри, немає ризику введення sql.
Брайан

Це я шукав, але у мене виникло питання. Якщо в ОП було додати кілька однакових стовпців до SQL, як би ми це зробили? Приклад. ВИБІРТЕ * ІЗ Таблиці, де вік = ({0}) або вік = ({1}). (як би ми це зробили з cmd.Parameters)
Какао Дев

1
@ T2Tom, Whoops. Я полагодив це. Дякую.
Брайан

1
Мені це подобається і я здійснив наступну модифікацію лише після вилучення рядка-заповнювача до змінної: var paramPlaceholder = "{" & paramNameRoot & "}"; Debug.Assert(cmd.CommandText.Contains(paramPlaceholder), "Parameter Name Root must exist in the Source Query"); Це повинно допомогти devs, якщо вони забудуть співставити paramNameRoot із запитом.
MCattle

37

Я хотів розкрити відповідь, що Брайан сприяв зробити це легко застосованим в інших місцях.

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

Ви можете використовувати цю нову функцію наступним чином:

SqlCommand cmd = new SqlCommand();

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

cmd.CommandText = sql;


Редагувати: Ось загальна варіація, яка працює з масивом значень будь-якого типу і використовується як метод розширення:

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

Потім ви можете використовувати цей метод розширення наступним чином:

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

Переконайтеся, що ви встановили CommandText перед тим, як викликати AddArrayParameters.

Також переконайтесь, що назва вашого параметра частково не збігається ні з чим у вашому виписці (тобто @AgeOfChild)


1
Ось загальна варіація, яка працює з масивом значень будь-якого типу і може бути використана як метод розширення: загальнодоступна статична void AddArrayParameters <T> (це cmd SqlCommand, ім'я рядка, IEnumerable <T> значень) {var names = string.Join (",", values.Select ((value, i) => {var paramName = name + i; cmd.Parameters.AddWithValue (paramName, value); return paramName;})); cmd.CommandText = cmd.CommandText.Replace (ім'я, імена); }
Адам Немітовф

Незначна проблема з цією відповіддю полягає в AddWithValueфункції, будь-який шанс ви могли це виправити?
DavidG

Ця відповідь неправильна, оскільки вона має низьку масштабованість та продуктивність, а також сприяє поганій практиці кодування.
Ігор Левицький

24

Якщо ви можете використовувати такий інструмент, як "dapper", це може бути просто:

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 впорається з розгортанням цього індивідуальних параметрів для вас .


Dapper тягне багато залежностей :(
mlt

@mlt так? ні, це не так; on netfx: "немає залежностей"; на ns2.0, просто "System.Reflection.Emit.Lightweight" - і ми, ймовірно, можемо зняти це, якщо ми додамо ціль некрореап
Marc Gravell

Я не хотів викрадати дискусію, але я це зробив ... Поки я використовую Npgsql, який обробляє масиви добре, як у '{1,2,3}'стилі аргументи функції (а не пункт WHERE IN), але я скоріше використовую звичайний ODBC, якщо ні клопоту з масиву. Я припускаю, що мені знадобиться також Dapper ODBC. Ось, що вона хоче тягнути. snipboard.io/HU0RpJ.jpg . Можливо, я повинен прочитати докладніше на Dapper ...
mlt

16

Якщо ви використовуєте MS SQL Server 2008 і вище, ви можете використовувати параметри з табличним значенням, як описано тут http://www.sommarskog.se/arrays-in-sql-2008.html .

1. Створіть тип таблиці для кожного типу параметрів, який ви будете використовувати

Наступна команда створює тип таблиці для цілих чисел:

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

2. Запровадити хелперні методи

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. Використовуйте так

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

1
Рядок table.Rows.Add(id);призводить до незначного запаху коду при використанні SonarQube. Я використовував цю альтернативу всередині foreach : var row = table.NewRow(); row["id"] = id; table.Rows.Add(row);.
погосама

1
Це має бути прийнятою відповіддю, особливо якщо вона була пристосована для прийняття більшої кількості стовпців.
Ігор Левицький

10

Оскільки існує метод на

SqlCommand.Parameters.AddWithValue(parameterName, value)

може бути зручніше створити метод, що приймає параметр (ім'я) для заміни та список значень. Це не на рівні параметрів (наприклад, AddWithValue ), а в самій команді, тому краще назвати це AddParametersWithValues, а не лише AddWithValues :

запит:

SELECT * from TableA WHERE Age IN (@age)

використання:

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

метод розширення:

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
Схоже, що кілька ітерацій цього методу розширення існують у кількох відповідях. Я використав цей, проте, тому я його голосую :-)
Ден Форбс

краще використовувати статичний індекс для імені параметра
shmnff

6

Я хочу запропонувати інший спосіб, як вирішити обмеження з оператором IN.

Наприклад, у нас є наступний запит

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

Ми хочемо передати кілька ідентифікаторів для фільтрації користувачів. На жаль, це неможливо зробити з C # простим способом. Але для цього я вирішив за допомогою функції "string_split". Нам потрібно переписати трохи наступний запит.

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

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

Тепер ми можемо легко пропустити один параметр перерахування значень, розділених комою.


найкращий і найчистіший спосіб, який я знайшов, за умови поточної сумісності.
користувач1040975

4

Передача масиву елементів як згорнутого параметра до пункту WHERE..IN не вдасться, оскільки запит набуде форми WHERE Age IN ("11, 13, 14, 16").

Але ви можете передати свій параметр у вигляді масиву, серіалізованого у XML або JSON:

Використовуючи nodes()метод:

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

Використовуючи OPENXMLметод:

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

Це трохи більше на стороні SQL, і вам потрібен належний XML (з коренем).

Використовуючи OPENJSONметод (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);

Зауважте, що для останнього методу вам також потрібно мати рівень сумісності на рівні 130+.


0

Огляд: Використовуйте DbType, щоб встановити тип параметра.

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

Використовуйте .AddWithValue(), отже:

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

Ви також можете скористатися цим:

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

Ваш загальний зразок коду розгляне наступне:

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

Тип вікового поля - nvchar not int. Це важливо?
Йонгвей Сін

Це не повинно. Особливо з другим методом. Ви чітко вказуєте тип.
Кайл Розендо

Я використовую обидва способи, він все ще не працює. Я не хочу маніпулювати рядком, який може призвести до проблеми безпеки
Yongwei Xing

Я вас не дуже розумію. Коли ви кажете, що це не працює, чи це викид? Що це робить?
Кайл Розендо

1
це не кидає винятку, воно нічого не повертає. Але я запускаю T-SQL в Studio Management, він повертає багато результатів.
Йонгвей Сін

-1

Ось незначний варіант відповіді Брайана про те, що хтось інший може вважати корисним. Бере список клавіш і вводить його до списку параметрів.

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

Ця відповідь неправильна, оскільки вона має низьку масштабованість та продуктивність, а також сприяє поганій практиці кодування.
Ігор Левицький


-5

спробуйте так

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
Навіщо ставити SqlConnection та SqlCommnad у циклі?
Йонгвей Сін
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.