Использование предложения IN с ExecuteQuery в LINQ-to-SQL - PullRequest
4 голосов
/ 20 марта 2012

LINQ to SQL выполнил ужасную работу по переводу одного из моих запросов, поэтому я переписал его вручную.Проблема в том, что перезапись обязательно включает в себя предложение IN, и я не могу понять, как передать коллекцию в ExecuteQuery для этой цели.Единственное, что я могу придумать, о чем я уже говорил, это использовать string.Format для всей строки запроса, чтобы обойти его, но это предотвратит попадание запроса в кеш запроса.

Какой правильный способ сделать это?

ПРИМЕЧАНИЕ : Обратите внимание, что Я использую необработанный SQL, переданный ExecuteQuery.Я сказал это в самом первом предложении.Говорить мне, что нужно использовать Contains, бесполезно, если только вы не знаете, как смешать Contains с необработанным SQL.

Ответы [ 5 ]

7 голосов
/ 20 марта 2012

Табличные параметры

На Cheezburger.com нам часто приходится передавать список AssetID или UserID в хранимую процедуру или запрос к базе данных.

Плохой путь: динамический SQL

Одним из способов передачи этого списка было использование динамического SQL.

 IEnumerable<long> assetIDs = GetAssetIDs();
 var myQuery = "SELECT Name FROM Asset WHERE AssetID IN (" + assetIDs.Join(",") + ")";
 return Config.GetDatabase().ExecEnumerableSql(dr=>dr.GetString("Name"), myQuery);

Это очень плохая вещь:

  1. Динамический SQL дает злоумышленникам слабость, упрощая атаки SQL-инъекций.
    Так как мы обычно просто соединяем числа вместе, это очень маловероятно, но если вы начинаете объединять строки вместе, все, что требуется, это один пользователь для ввода ';DROP TABLE Asset;SELECT ' и наш сайт мертв.
  2. Хранимые процедуры не могут иметь динамический SQL, поэтому запрос должен был храниться в коде, а не в схеме БД.
  3. Каждый раз, когда мы запускаем этот запрос, план запроса должен пересчитываться. Это может быть очень дорого для сложных запросов.

Однако у него есть то преимущество, что никакого дополнительного декодирования на стороне БД не требуется, так как идентификаторы AssetID обнаруживаются анализатором запросов.

Хороший способ: табличные параметры

SQL Server 2008 добавляет новую возможность: пользователи могут определять табличный тип базы данных. Большинство других типов являются скалярными (они возвращают только одно значение), но табличные типы могут содержать несколько значений, если значения являются табличными.

Мы определили три типа: varchar_array, int_array и bigint_array.

CREATE TYPE bigint_array AS TABLE (Id bigint NOT NULL PRIMARY KEY)

Эти хранимые типы могут использовать как хранимые процедуры, так и программно определенные запросы SQL.

  IEnumerable<long> assetIDs = GetAssetIDs();
  return Config.GetDatabase().ExecEnumerableSql(dr=>dr.GetString("Name"),
      "SELECT Name FROM Asset WHERE AssetID IN (SELECT Id FROM @AssetIDs)", 
      new Parameter("@AssetIDs", assetIDs));

Преимущества

  1. Может использоваться как в хранимых процедурах, так и в программном SQL без особых усилий
  2. Не уязвим для внедрения SQL
  3. Кэшируемые стабильные запросы
  4. Не блокирует таблицу схем
  5. Не ограничено 8 КБ данных
  6. Меньше работы, выполняемой как сервером БД, так и приложениями Mine, поскольку нет объединения или декодирования строк CSV.
  7. Анализатор запросов может получить статистику «типичного использования», что может привести к еще большей производительности.

Недостатки

  1. Работает только на SQL Server 2008 и выше.
  2. Слухи о том, что TVP предварительно буферизуются полностью перед выполнением запроса, что означает, что феноменально большие TVP могут быть отклонены сервером. Дальнейшее расследование этого слуха продолжается.

Дальнейшее чтение

Эта статья - отличный ресурс, чтобы узнать больше о TVP.

3 голосов
/ 20 марта 2012

Если вы не можете использовать табличные параметры, этот параметр немного быстрее, чем параметр xml, но при этом позволяет избежать динамического sql: передать объединенный список значений в качестве строкового параметра и проанализировать разделенный символ вернитесь к значениям в вашем запросе. см. эту статью для получения инструкций о том, как эффективно выполнять синтаксический анализ.

2 голосов
/ 20 марта 2012

У меня есть подозрение, что вы работаете на SQL Server 2005. Табличные параметры не добавлялись до 2008 года, но вы все равно можете использовать тип данных XML для передачи наборов между клиентом и сервер.

0 голосов
/ 07 февраля 2013

Я не уверен на 100%, что правильно понимаю проблему, но у ExecuteQuery в LinqToSql есть перегрузка для параметров, и в запросе предполагается использовать формат, подобный string.Format.

Использование этой перегрузки безопасно для внедрения SQL-кода, и LinqToSql за кадром транслирует его для использования sp_executesql с параметрами.

Вот пример:

string sql = "SELECT * FROM city WHERE city LIKE {0}";
db.ExecuteQuery(sql, "Lon%"); //Note that we don't need the single quotes 

Таким образом, можно использовать преимущества параметризованных запросов даже при использовании динамического sql.

Однако, когда дело доходит до использования IN с динамическим числом параметров, есть две опции:

  1. Создайте строку динамически, а затем передайте значения в виде массива, как в:

    string sql = "SELECT * FROM city WHERE zip IN (";
    List<string> placeholders = new List<string>();
    for(int i = 0; i < zips.Length;i++)
    {
          placeholders.Add("{"+i.ToString()+"}");
    }
    sql += string.Join(",",placeholders.ToArray());
    sql += ")";
    db.ExecuteQuery(sql, zips.ToArray());
    
  2. Мы можем использовать более компактный подход, используя методы расширения Linq, как в

    string sql = "SELECT * FROM city WHERE zip IN ("+
      string.Join("," , zips.Select(z => "{" + zips.IndexOf(f).ToString() + "}"))
    +")";
    db.ExecuteQuery(sql, zips.ToArray());
    
0 голосов
/ 09 апреля 2012

Это работает для SQL Server 2005 (и более поздних версий):

create procedure IGetAListOfValues
   @Ids xml -- This will recevie a List of values
as
begin
    -- You can load then in a temp table or use it as a subquery:
    create table #Ids (Id int);
    INSERT INTO #Ids
    SELECT DISTINCT params.p.value('.','int') 
    FROM @Ids.nodes('/params/p') as params(p);
    ...
end

Вы должны вызвать эту процедуру с параметром, подобным следующему:

exec IGetAListOfValues
@Ids = '<params> <p>1</p> <p>2</p> </params>' -- xml parameter

Функция node использует xPathвыражение.В данном случае это /params/p, и поэтому XML использует <params> в качестве корня и <p> в качестве элемента.

Функция значения преобразует текст внутри каждого элемента p в int, но выможет использовать его с другими типами данных легко.В этом примере есть DISTINCT, чтобы избежать повторяющихся значений, но, конечно, вы можете удалить его в зависимости от того, чего вы хотите достичь.

У меня есть вспомогательный (дополнительный) метод, который преобразует IEnumerable<T> встрока, похожая на ту, что показана в примере выполнения.Его легко создать, и он сделает всю работу за вас, когда вам это нужно.(Вы должны проверить тип данных T и преобразовать в адекватную строку, которая может быть проанализирована на стороне SQL Server).Таким образом, ваш код C # становится чище, а ваши SP следуют той же схеме для получения параметров (вы можете передать столько списков, сколько необходимо).

Одним из преимуществ является то, что вам не нужно делать ничего особенного вваша база данных, чтобы она работала.

Конечно, вам не нужно создавать временную таблицу, как это сделано в моем примере, но вы можете использовать запрос непосредственно как подзапрос внутри предиката IN1021 *

    WHERE MyTableId IN (SELECT DISTINCT params.p.value('.','int') 
    FROM @Ids.nodes('/params/p') as params(p) )
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...