LINQ-SQL重用-CompiledQuery.Compile

14

我一直在尝试使用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重用的唯一可行路线。

更新

Where语句中使用第一个Func时,我们会看到以下异常:

NotSupportedException: Method 'System.Object DynamicInvoke(System.Object[])' has no supported translation to SQL.

就像下面这样:

.Where(t => TimeSpent(t) > 2)

然而,当我们使用由CompiledQuery.Compile生成的Func时,查询成功执行并生成了正确的SQL。

我知道这不是重复使用Where语句的理想方式,但它展示了一下表达式树是如何生成的。

3个回答

3

执行摘要:

Expression.Compile 生成一个CLR方法,而 CompiledQuery.Compile 生成一个占位符为SQL的委托。


你之前没有得到正确答案的原因之一是你样例代码中有些错误。如果没有数据库或通用样例供他人使用,机会就更小了(我知道提供这些很困难,但通常是值得的)。

接下来是事实:

Expression<Func<TaskFile, double>> TimeSpent = (t =>
    t.TimeEntries.Sum(te => (te.DateEnded - te.DateStarted).TotalHours));

Then, we can use the above in a LINQ query like the below:

TaskFiles.Select(t => new {
    t.TaskId,
    TimeSpent = TimeSpent(t),
})
(注:也许您在TimeSpent中使用了Func<>类型。这会导致与下面所述的情况相同的情况。但请确保阅读并理解它。)
不,这样不会编译。表达式不能被调用(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允许在选择时进行内存投影,但对于Where语句却不这样做(需要引证,据我所知)。当您考虑这一点时,这是非常合理的:通过在内存中过滤整个数据库,您可能会传输更多的数据,而不是通过在内存中转换其子集。 (尽管它创建了查询性能问题,但在使用ORM时要注意这一点)。

CompiledQuery.Compile()执行不同的操作。它将查询编译为SQL,并且返回的委托只是Linq to SQL内部使用的占位符。您无法在CLR中“调用”此方法,它只能用作另一个表达式树中的节点。

那么为什么LTS使用CompiledQuery.Compile的表达式生成高效的查询呢?因为它知道这个表达式节点的作用,因为它知道其背后的SQL。在Expression.Compile的情况下,它只是调用我之前解释的DynamicMethodInvokeExpression

为什么需要DataContext参数?是的,这更方便创建完整的查询,但也是因为表达式树编译器需要知道用于生成SQL的映射。没有这个参数,找到这个映射将是一件麻烦的事情,所以这是一个非常明智的要求。


我不同意这个结论。在非编译查询转换期间,所有映射和表达式信息都可以在CompiledQuery.Compile中使用。 - Amy B
@David:正如我在答案中所概述的,他的代码实际上无法编译。你不能在上面的代码中使用这样的表达式。而结论仍然有效:Expression.Compile=CLR, CompiledQuery.Compile和DataContext query = SQL(在OP证明其他情况之前,我宣布这是相同的)。他得到不同的SQL的原因是因为他没有调用一个Expression,而是一个CLR委托。 - Johannes Rudolph
直接使用“Expression”将无法编译。我意识到我的第一个示例不够清晰,但实际上我正在使用“Func”。第一个示例使用普通的“Func”,它会产生每行一个查询。使用经过“CompiledQuery.Compile”处理的相同“Func”,我们只看到生成了一个查询,而不使用签名中的DataContext!从我的有限测试中可以看出,在“Where”和“Select”中都可以工作,但我确信它在任何地方都可以工作。我假设解析器会忽略普通的“Func”直到稍后的阶段。 - Hux
@MiG,好的,那就是我想的。你能仔细再读一遍我的回答,看看是否明白为什么会出现这种预期行为吗?从我的角度来看,这很清楚,但不要犹豫,如果有问题我会尝试用不同的方式解释。 - Johannes Rudolph
是的,我现在明白了,虽然我希望CompiledQuery.Compile的行为是默认行为。我将不得不使用反射器亲眼看看确切的科学原理。感谢您的回答。 - Hux

1

我很惊讶为什么到现在为止你还没有得到任何答案。 CompiledQuery.Compile 编译并缓存查询。这就是为什么你只看到生成了一个查询。

不仅如此,这不是黑客行为,而是推荐的方法!

查看这些 MSDN 文章以获取详细信息和示例:

已编译查询(LINQ to Entities)
如何:存储和重用查询(LINQ to SQL)

更新:(超出评论限制)
我在反编译器中进行了一些挖掘,我确实看到 DataContext 被使用了。在你的示例中,你只是没有使用它。

话虽如此,两者之间的主要区别在于前者创建一个委托(用于表达式树),而后者创建被缓存的 SQL 并实际返回一个函数(有点像)。前两个表达式在你调用它们的 Invoke 时产生查询,这就是为什么你看到了多个查询。

如果您的查询不变,但只有DataContextParameters发生变化,并且您计划重复使用它,则CompiledQuery.Compile会有所帮助。编译是昂贵的,因此对于一次性查询,没有任何好处。

我之前看过那些例子,但都没有解答我所提出的具体问题。为什么根据我的示例不需要DataContext参数?为什么前两个示例中的普通Func没有内置到完整表达式树中?这两者有何不同?在我看来,我期望前两个示例生成1个查询。 - Hux
好的。这似乎提供了更多的见解。但我本以为在生成任何SQL之前,表达式树将完全构建。你的答案很有道理,因为Select似乎会忽略第一组Func直到迭代。此外,在Where中使用Func会产生不同的行为。Linq查询甚至无法运行(我将更新我的原始问题)。 - Hux

0
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.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接