Как я могу запустить SQL-запрос со списком строковых значений, используя формат WHERE [имя столбца] в [значения] в ASP.NET?

У меня есть SQL-запрос, который я запускаю на странице ASP.NET. Окончательный проанализированный SQL должен содержать список строковых значений в формате WHERE [columnname] IN [values]. Например, окончательный запрос может выглядеть примерно так:

SELECT PRODUCTNAME FROM PRODUCT WHERE PRODUCTCODE IN ('ABC','DEF','GHI','JKL', /* etc */);

Однако строковые значения в предложении WHERE должны быть динамическими. Обычно я использую параметризованные запросы, чтобы сделать свой код удобным и безопасным, поэтому концептуально я хотел бы сделать что-то вроде этого:

String[] productCodes = { "ABC", "DEF", "GHI", "JKL" };
SqlCommand cmd = "SELECT PRODUCTNAME FROM PRODUCT WHERE PRODUCTCODE IN (@ProductCodes)";
cmd.Parameters.Add("@ProductCodes", productCodes);

Однако похоже, что в .NET такой функциональности нет. Как мне это реализовать? Я мог бы использовать цикл foreach для массива и выполнить запрос с одним значением в качестве параметра для каждого значения, но потенциально в массиве может быть около сотни разных значений, и кажется, что запрашивать их по отдельности было бы очень неэффективно.

Я прочитал еще один вопрос, в котором кто-то предложил решение для строго типизированных параметров типа int, но этот метод заставил бы меня нервничать по поводу SQL-инъекций при использовании со строковыми значениями, тем более что клиент вполне может влиять на входные значения.

Как бы вы реализовали эту функцию запроса?

ИЗМЕНИТЬ с данными БД:

База данных — SQL Server 2005. Извините, что забыл упомянуть об этом.


person Joshua Carmody    schedule 11.03.2011    source источник
comment
Это не .NET, которому не хватает функциональности, а база данных, поэтому было бы полезно узнать, какую базу данных вы используете.   -  person Rune FS    schedule 11.03.2011
comment
@Rune FS - Упс! Это SQL Server 2005. Я добавил это к вопросу.   -  person Joshua Carmody    schedule 12.03.2011


Ответы (4)


Создайте свой базовый оператор sql в виде формата и динамически добавьте параметры, а затем установите значения в цикле.

String[] productCodes = { "ABC", "DEF", "GHI", "JKL" };
string sqlFormat = "SELECT PRODUCTNAME FROM PRODUCT WHERE PRODUCTCODE IN ({0})";
var @params = productCodes.Select((id, index) => String.Format("@id{0}", index)).ToArray();
var sql = String.Format(sqlFormat, string.Join(",", @params));

using(var cmd = new DbCommand(sql))
{
    for (int i = 0; i < productCodes.Length; i++)
        cmd.Parameters.Add(new Parameter(@params[i], DbType.String, productCodes[i]));
    // execute query
}
person NerdFury    schedule 11.03.2011
comment
Прохладный. Все еще ищу, есть ли менее грязный метод, но это похоже на хорошую идею. Как и ответ arnehehe, но без уязвимости, которую я сначала не заметил. - person Joshua Carmody; 12.03.2011

Просто идея:

String[] productCodes = { "ABC", "DEF", "GHI", "JKL" };
SqlCommand cmd = "SELECT PRODUCTNAME FROM PRODUCT WHERE PRODUCTCODE IN ("; 

for (int i=0;i<productCodes.Length;i++) {
  cmd.CommandText += "@" + productCodes[i] + ",";
  cmd.Parameters.Add("@" + productCodes[i], productCodes[i]);
}
cmd.CommandText = cmd.CommandText.SubString(0, cmd.CommandText.Length-1);
cmd.CommandText += ");"

Возможно, это не лучший способ, но, думаю, я бы попробовал именно так.

person arnehehe    schedule 11.03.2011
comment
Это интересно. Это немного грязно, но выглядит намного безопаснее, чем другие вещи, которые я рассматривал до сих пор. Я мог бы пойти по этому пути, если ни у кого нет лучшей идеи. Спасибо. - person Joshua Carmody; 12.03.2011
comment
Пожалуйста, не делайте этого. Ожидается атака с внедрением SQL-кода — возможно, не сразу, потому что коды продуктов, вероятно, сами взяты из базы данных, но в конечном итоге вы получите здесь уязвимость 2-го порядка. - person Joel Coehoorn; 12.03.2011
comment
если параметры текста команды генерируются без использования значений в именах, улучшает ли это сценарий? Разве это не то, что делают Linq2Sql и EF при создании запроса такого типа? Использовать кучу параметров @p1, @p2, @p3 и т. д., а затем назначать эти значения? - person NerdFury; 12.03.2011
comment
@Joel Coehoorn - Что ж, похоже, он использует значение в качестве имени параметра для создания команды, и ЭТО не очень хорошая идея. Но если вы замените cmd.CommandText += "@" + productCodes[i] на cmd.CommandText += "@p" + i.toString(), я думаю, это будет безопасно? - person Joshua Carmody; 12.03.2011
comment
Да, извините, я бы определенно не стал использовать это значение в качестве имени параметра. Совершенно не имеет смысла. Наверное, я просто опубликовал это как потенциальную идею. - person arnehehe; 12.03.2011

Для справки, запрос сотен или около того значений в массиве «не по отдельности» также может быть неэффективным (хотя и не таким неэффективным, как сотня или около того обращений к SQL Server). Однако параметризация запроса немного помогает, потому что вы можете подготовить один и тот же запрос и выполнить его несколько раз. Хранимая процедура может быть еще лучше.

Есть ли шанс, что вы можете использовать Linq или у вас есть версия pre-.NET-3.5?

Если вы не можете использовать Linq и вам абсолютно необходимо пойти по этому пути, попробуйте следующее:

SqlCommand cmd = "SELECT PRODUCTNAME FROM PRODUCT WHERE PRODUCTCODE = @ProductCode";
cmd.Prepare();
List<string> results;
foreach (string code in productCodes)
{
    cmd.Parameters.Clear();
    cmd.Parameters.Add("@ProductCodes", DbType.VarChar).Value = code;
    cmd.ExecuteQuery();
    // Add code here to add the returned values to the results list.  It's been
    // a while since I've used ADO.NET, and I don't have time to look it up
    // at the moment...
}

И тогда у вас есть список результатов.

person Andrew    schedule 11.03.2011
comment
Это .NET 2.0. Я попытался установить 3.5 или 4.0 на сервер (Windows 2003 Server, 64-разрядная версия с IIS, работающим в 32-разрядной версии), и это вызвало всевозможные проблемы (долгая история). Сетевой администратор и я договорились, что я буду программировать в .NET 2.0, пока он не придумает, как заставить 4.0 хорошо работать со всеми нашими сайтами, работающими на этой машине. - person Joshua Carmody; 12.03.2011
comment
Я упомянул возможность foreach в своем вопросе. Я думаю, что выполнение запроса в цикле было бы слишком неэффективным при работе со 100+ значениями. - person Joshua Carmody; 12.03.2011
comment
@ Джошуа Кармоди Да, вы упомянули об этом. Однако не похоже, что вы выполняли какую-либо логику подготовки/выполнения. Такой сценарий является одной из причин для подготовки/выполнения логики в первую очередь. - person Andrew; 12.03.2011
comment
Да, я понимаю о подготовке заявления. Но у меня сложилось впечатление, что если я выполняю оператор SELECT с предложением WHERE COLUMN IN (VALUES) для неиндексированного столбца, SQL Server будет выполнять поиск по таблице только один раз, тогда как если бы я выполнял подготовленный оператор в циклическом поиске неиндексированный столбец, SQL Server будет искать всю таблицу на каждой итерации в цикле. Разве это не правда? Если это так, я думаю, мне лучше избегать этого. - person Joshua Carmody; 12.03.2011
comment
@ Джошуа Кармоди Честно говоря, в SQL Server 2005 я не совсем уверен. Я знаю, что мне больше повезло с производительностью на SQL Server в целом, когда я избегал IN практически любыми возможными способами. Тем не менее, если этот столбец не проиндексирован, у вас все равно будут большие проблемы. С индексом производительность не должна быть слишком большой. Без индекса это будет медленным, независимо от того, какой метод вы используете, особенно если одновременно запрашивается сотня или более значений. - person Andrew; 12.03.2011

Вы ищете параметры, возвращающие табличное значение. Однако это не так. t доступны для сервера sql до тех пор, пока asp.net не получил широкого распространения, поэтому их поддержка в asp.net ограничена.

Вместо этого я рекомендую думать об этом как о создании тележки для покупок. Пользователи добавляют товары в корзину, и в какой-то момент вы хотите отобразить все товары в корзине. В этом случае естественным решением является то, что сама корзина находится в таблице базы данных. Вместо того, чтобы вытягивать элементы для включения в карточку как часть директивы «IN (?)», вы строите это как подзапрос для своей директивы: «WHERE X IN (SELECT X FROM ShoppingCart WHERE UserID= @UserID AND SessionKey= @SessionKey)». Даже если у вас есть сотни добавленных элементов, пользователь будет работать так быстро, а нагрузка на вставку распределяется довольно равномерно.

Конечно, вы, вероятно, строите что-то другое, а не корзину для покупок, но, тем не менее, ваш запрос почти всегда будет относиться к одной из трех категорий:

  1. Элементы для вашего списка выбираются пользователем вручную, и в этом случае вы все равно можете получить каждый результат выбора в новой записи базы данных, которая, в свою очередь, может использоваться в подзапросе.
  2. Это данные, которые уже доступны в вашей базе данных, и в этом случае вы все равно сможете использовать подзапрос (а если вы не можете, возможно, пришло время добавить где-нибудь столбец «категория», чтобы вы могли).
  3. Это данные, которые жестко закодированы в вашем приложении, и в этом случае вы также можете закодировать их в своем запросе.

Редким исключением является случай, когда запрос инициируется другим компьютерным источником, или у вас также может быть много кода, который вы не хотите переделывать, чтобы сделать это возможным.
Так что, если по какой-то причине этот подход не работает Чтобы сократить это для вас, стандартную статью о сервере sql (включая несколько альтернатив) можно найти здесь:
http://www.sommarskog.se/arrays-in-sql.html

Эта статья действительно является стандартной работой по этому вопросу и стоит вашего времени.

person Joel Coehoorn    schedule 11.03.2011
comment
Извините, я забыл упомянуть, что база данных - это SQL Server 2005. Я думаю, что табличные параметры не поддерживаются до 2008 года.... - person Joshua Carmody; 12.03.2011
comment
@Joshua Yep - прочитайте вторую половину поста для нормального обходного пути - person Joel Coehoorn; 12.03.2011