Большое удобство составления запросов в строку в том, что можно динамически добавлять условия и собственно генерировать логику отбора в рантайме, так можно поступать и с LINQ, причем дополнительные условия можно накладывать не в виде вызова методов, а так же – декларативно.
При этом для уточнения условий отбора нужно просто выполнить запрос к запросу. Благодаря тому, что запрос выполняется не сразу, а в момент обращения к его результату – будет выполнено уточнение начального запроса. Если при этом тип возвращаемого результата остается тем же (выбираем тот же тип данных), то запрос можно присвоить той же переменной.
Для примера создадим в MS-SQL таблицу Test со столбцом Value типа int и ts типа TimeStamp (чтобы полей было всё же больше, чем те, с которыми нужно работать). Нам нужно выбрать из этой таблицы все четные значения больше 5, отсортированные по возрастанию.
Это можно написать в обычном запросе вот так:
var q = from item in DB.tests where item.value % 2 == 0 && item.value > 5 orderby item.value select item.value; При этом мы получим IEnumerable<int> и в базу отправится запрос
exec sp_executesql N’SELECT [t0].[value]
FROM [dbo].[test] AS [t0]
WHERE (([t0].[value] % @p0) = @p1) AND ([t0].[value] > @p2)
ORDER BY [t0].[value]’,N’@p0 int,@p1 int,@p2 int’,@p0=2,@p1=0,@p2=5
Это хорошо и удобно, но бывает что нужно уточнять параметры запроса в зависимости от каких-то условий или добавлять условия в цикле или что-то похожее, обычно это делается при формировании запроса в виде строки:
string query = "SELECT value FROM Test WHERE (1=1)"; query += " AND (Test.Value % 2 = 0@)"; if (flag) query += " AND (Test.Value > 5)"; query += " ORDER BY Test.Value";
Такую же гибкость предоставляет и LINQ, при осуществлении запроса к запросу. Сделаем тот же самый запрос на LINQ в несколько шагов:
1. Выберем базовый набор элементов с которыми хотим работать, пусть это будут вся таблица Test
var q = from item in DB.tests select item;
Это преобразуется в запрос
SELECT [t0].[value], [t0].[ts]
FROM [dbo].[test] AS [t0]
2. Но поскольку к результатам запроса мы еще не обращаемся, то к БД он не отправлен, уточняем наш запрос:
q = from item in q where item.value % 2 == 0 select item;
Мы добавили условие четности и запрос соответственно уточнился:
SELECT [t0].[value], [t0].[ts]
FROM [dbo].[test] AS [t0]
WHERE ([t0].[value] % @p0) = @p1
Теперь уточним запрос, т.к. мы хотим работать только со столбцом value – выбираем его.
var nums = from num in q select num.value;
3. Запрос уточняется до
SELECT [t0].[value]
FROM [dbo].[test] AS [t0]
т.е. если запросить данные сейчас, то выберется уже только один столбец, а не вся таблица. Но суть даже не в этом – раз мы выбрали только один целочисленный столбец мы теперь и работаем с ним просто как с коллекцией целых чилел. Обратите внимание – у нас поменялся тип результата – вместо всех столбцов мы выбираем только один, поэтому нам нужна новая переменная (используется новый тип данных)
4. Выбираем из них четные:
nums = from num in nums where num % 2 == 0 select num;
и запрос снова уточняется:
SELECT [t0].[value]
FROM [dbo].[test] AS [t0]
WHERE ([t0].[value] % @p0) = @p1
Здесь и дальше мы уже работает только с коллекцией int’ов, так что используем ту же самую переменную.
5. Теперь уточняем запрос в зависимости от какого-то нашего условия:
if (flag) nums = from num in nums where num > 5 select num;
У меня влаг выставлен в true, поэтому условие уточняется:
SELECT [t0].[value]
FROM [dbo].[test] AS [t0]
WHERE ([t0].[value] > @p0) AND (([t0].[value] % @p1) = @p2)
6. Сортируем полученный результат:
nums = from num in nums orderby num select num;
SELECT [t0].[value]
FROM [dbo].[test] AS [t0]
WHERE ([t0].[value] > @p0) AND (([t0].[value] % @p1) = @p2)
ORDER BY [t0].[value]
И наконец обращаемся к результату:
foreach (var num in nums) Console.WriteLine(num);
Согласно SQL Profiler’у туда пришел запрос:
exec sp_executesql N’SELECT [t0].[value]
FROM [dbo].[test] AS [t0]
WHERE ([t0].[value] > @p0) AND (([t0].[value] % @p1) = @p2)
ORDER BY [t0].[value]’,N’@p0 int,@p1 int,@p2 int’,@p0=5,@p1=2,@p2=0
Запрос совершенно эквивалентные, отличаются только порядком условий в AND.
В чем плюсы такого подхода?
1. Мы работает с типизированной коллекцией того что нам нужно (контроль опечаток на стадии компиляции, автодополнения и т.п.)
2. Можем уточнять и добавлять новые условия запроса в зависимости от каких-то внешних условий, так же как при формировании запроса в виде строки.
3. Каждое уточнение выглядит человеко-читабельно: из уже выбраного выбираем то, что соответствует новому условию или условиям (можно доабвлять произвольное количество условий за раз).
4. Не смотря на такую конструкцию – запрос к запросу – реально в базу отправляется только один результирующий запрос, учитывающий сразу все условия, что собственно нам и нужно.
5. Можно писать условия отбора в человеческом порядке на человеческом языке, так что это потом будет просто понять и исправить (пример приведен ниже)
P.S. После написания поста стало интересно попробовать – а что если нам нужно будет выбрать из той же таблицы четные числа больше 5, но только среди чисел, которые идут с 5-го по 25-й если их отсортировать. Т.е. при переводе на русский это получается так:
Отсортировать числа в столбце value по возрастанию, взять из них элементы с 5-го по 25-й включительно и среди них выбрать четные числа больше 5.
Написал такой кусок кода:
var nums = from item in DB.tests orderby item.value select item.value; nums = nums.Skip(4).Take(21); nums = from num in nums where num % 2 == 0 && num > 5 select num; foreach (var num in nums) Console.WriteLine(num);
из чего сгенерировался запрос:
exec sp_executesql N’SELECT [t2].[value]
FROM (
SELECT [t1].[value], [t1].[ROW_NUMBER]
FROM (
SELECT ROW_NUMBER() OVER (ORDER BY [t0].[value]) AS [ROW_NUMBER], [t0].[value]
FROM [dbo].[test] AS [t0]
) AS [t1]
WHERE [t1].[ROW_NUMBER] BETWEEN @p0 + 1 AND @p0 + @p1
) AS [t2]
WHERE (([t2].[value] % @p2) = @p3) AND ([t2].[value] > @p4)
ORDER BY [t2].[ROW_NUMBER]’,N’@p0 int,@p1 int,@p2 int,@p3 int,@p4 int’,@p0=4,@p1=21,@p2=2,@p3=0,@p4=5
Вот фиг бы я такой запрос вручную написал, да если и написал бы, то не слишком он читабельный и правибельный, в LINQ как-то проще.
2 комментария на «“Генерирование LINQ-запросов в runtime”»
Спасибо за статью, интересно
Благодарю за материал. Пригодится.