Повторное использование 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.