Повторное использование LINQ-SQL - CompiledQuery.Compile
Я играл с LINQ-SQL, пытаясь получить повторно используемые фрагменты выражений, которые я могу подключить к другим запросам. Итак, я начал с чего-то вроде этого:
Func<TaskFile, double> TimeSpent = (t =>
t.TimeEntries.Sum(te => (te.DateEnded - te.DateStarted).TotalHours));
Затем мы можем использовать приведенное выше в запросе LINQ, как показано ниже (пример LINQPad):
TaskFiles.Select(t => new {
t.TaskId,
TimeSpent = TimeSpent(t),
})
Это приводит к ожидаемому результату, за исключением того, что запрос на строку генерируется для вставленного выражения. Это видно в LINQPad. Нехорошо.
В любом случае, я заметил метод CompiledQuery.Compile
. Хотя это принимает параметр DataContext
в качестве параметра, я думал, что включу его игнорировать, и попробую те же Func
. Поэтому я получил следующее:
static Func<UserQuery, TaskFile, double> TimeSpent =
CompiledQuery.Compile<UserQuery, TaskFile, double>(
(UserQuery db, TaskFile t) =>
t.TimeEntries.Sum(te => (te.DateEnded - te.DateStarted).TotalHours));
Обратите внимание, что я не использую параметр db
. Однако теперь, когда мы используем этот обновленный параметр, генерируется только 1 SQL-запрос. Выражение успешно переведено в SQL и включено в исходный запрос.
Итак, мой последний вопрос: что делает CompiledQuery.Compile
настолько особенным? Кажется, что параметр DataContext
вообще не нужен, и в этот момент я думаю, что это более удобный параметр для генерации полных запросов.
Будет ли считаться хорошей идеей использовать метод CompiledQuery.Compile
, подобный этому? Это похоже на большой взлом, но он кажется единственным жизнеспособным маршрутом для повторного использования LINQ.
UPDATE
Используя первый Func
в статусе Where
, мы видим следующее исключение:
NotSupportedException: Method 'System.Object DynamicInvoke(System.Object[])' has no supported translation to SQL.
Как показано ниже:
.Where(t => TimeSpent(t) > 2)
Однако, когда мы используем Func
, сгенерированный CompiledQuery.Compile
, запрос выполняется успешно и создается правильный SQL.
Я знаю, что это не идеальный способ повторного использования операторов Where
, но он немного показывает, как генерируется Дерево выражений.
Ответы
Ответ 1
Резюме Exec:
Expression.Compile
генерирует CLR-метод, если CompiledQuery.Compile
создает делегат, который является заполнителем для SQL.
Одна из причин, по которым вы до сих пор не получили правильного ответа, - это то, что некоторые вещи в вашем примере кода неверны. И без базы данных или общего образца, который может сыграть кто-то другой, шансы еще больше уменьшаются (я знаю, что это трудно обеспечить, но обычно это стоит).
О фактах:
Expression<Func<TaskFile, double>> TimeSpent = (t =>
t.TimeEntries.Sum(te => (te.DateEnded - te.DateStarted).TotalHours));
Затем мы можем использовать приведенное выше в запросе LINQ, как показано ниже:
TaskFiles.Select(t => new {
t.TaskId,
TimeSpent = TimeSpent(t),
})
(Примечание. Возможно, вы использовали тип Func<>
для TimeSpent. Это дает ту же ситуацию, что и в вашем сценарии, как описано в параграфе ниже. Обязательно прочитайте и поймите это).
Нет, это не скомпилируется. Выражения не могут быть вызваны (TimeSpent
- это выражение). Сначала их необходимо скомпилировать в делегат. То, что происходит под капотом при вызове Expression.Compile()
, заключается в том, что Дерево выражений скомпилировано до IL, которое вводится в DynamicMethod
, для которого вы затем получаете делегат.
Следующие действия будут работать:
var q = TaskFiles.Select(t => new {
t.TaskId,
TimeSpent = TimeSpent.Compile().DynamicInvoke()
});
Это приводит к ожидаемому результату, за исключением того, что запрос на строку сгенерированный для подключенного выражения. Это видно в LINQPad. Нехорошо.
Почему это происходит? Ну, Linq To Sql нужно будет извлечь все TaskFiles
, обезводить TaskFile
экземпляры, а затем запустить селектор против него в памяти. Вероятно, вы получаете запрос на TaskFile, потому что они содержат одно или несколько сопоставлений 1: m.
В то время как LTS позволяет проецировать в памяти для выбора, он не делает этого для Wheres (цитата, это, насколько мне известно). Когда вы об этом думаете, это имеет смысл: вероятно, вы будете передавать намного больше данных, фильтруя всю базу данных в памяти, а затем преобразовывая подмножество в памяти. (Хотя он создает проблемы с производительностью запросов, как вы видите, что нужно знать при использовании ORM).
CompiledQuery.Compile()
делает что-то другое. Он компилирует запрос в SQL, а возвращаемый делегат - это только заполнитель Linq to SQL, который будет использоваться внутренне. Вы не можете "вызывать" этот метод в среде CLR, его можно использовать только как node в другом дереве выражений.
Итак, почему LTS генерирует эффективный запрос с выражением CompiledQuery.Compile
'd then? Потому что он знает, что делает это выражение node, потому что он знает SQL за ним. В случае Expression.Compile
это просто a InvokeExpression
, который вызывает DynamicMethod
, как я объяснял ранее.
Зачем нужен параметр DataContext? Да, это более удобно для создания полных запросов, но это также связано с тем, что компилятору дерева выражений нужно знать Mapping для использования для генерации SQL. Без этого параметра было бы больно найти это отображение, поэтому это очень разумное требование.
Ответ 2
Я удивлен, почему у вас пока нет ответов. CompiledQuery.Compile
компилирует и кэширует запрос. Вот почему вы видите только один создаваемый запрос.
Не только это НЕ взломать, это рекомендуемый способ!
Ознакомьтесь с этими статьями MSDN для получения подробной информации и примера:
Скомпилированные запросы (LINQ to Entities)
Практическое руководство. Сохранение и повторное использование запросов (LINQ to SQL)
Обновление: (превышение лимита комментариев)
Я сделал рытье в отражателе, и я вижу, что используется DataContext. В вашем примере вы просто не используете его.
Сказав это, основное различие между ними состоит в том, что первый создает делегат (для дерева выражений), а второй создает SQL, который кэшируется и фактически возвращает функцию (вид). Первые два выражения вызывают запрос, когда вы вызываете Invoke
на них, поэтому вы видите их несколько.
Если ваш запрос не изменяется, но только DataContext
и Parameters
, и если вы планируете его использовать повторно, CompiledQuery.Compile
поможет. Это дорого для компиляции, поэтому для одного запроса нет никакой выгоды.
Ответ 3
TaskFiles.Select(t => new {
t.TaskId,
TimeSpent = TimeSpent(t),
})
Это не запрос LinqToSql, поскольку нет экземпляра DataContext. Скорее всего, вы запрашиваете некоторый EntitySet, который не реализует IQueryable.
Пожалуйста, опубликуйте полные заявления, а не фрагменты инструкции. (Я вижу недопустимую запятую, точку с запятой, отсутствие назначения).
Кроме того, попробуйте следующее:
var query = myDataContext.TaskFiles
.Where(tf => tf.Parent.Key == myParent.Key)
.Select(t => new {
t.TaskId,
TimeSpent = TimeSpent(t)
});
// where myParent is the source of the EntitySet and Parent is a relational property.
// and Key is the primary key property of Parent.