F#使用多个条件进行左联接,作为查询表达式

3

在 C# 中,我可以执行带有多个条件的左连接查询。像这个运行良好的 C# 示例一样:

using (var db = new CompanyContext())
{
    var q =
        from d in db.deps
        from e in db.emps.Where(e => d.id==e.dep_id && d.start_time<e.modified).DefaultIfEmpty() // left join
        select new { d, e };
    var result = q.ToList();
    // ...
}

我已尝试用F#将此翻译为以下内容:

use db = new CompanyContext()
let q = query {
    for d in db.deps do
    for e in db.emps.Where(fun e -> d.id=e.dep_id && d.start_time<e.modified).DefaultIfEmpty() do // left join
    select ( d, e )}
let result = q.ToList()
// ...

但是 F# 版本会导致异常:

System.InvalidOperationException:“从范围''引用的类型为'MyLib.dep'的变量'_arg1'未定义”

我的 F# 查询有什么问题吗?

编辑:根据建议,使用匿名记录 select {| d = d; e = e |} 替代 select (d, e),结果相同出现异常。

编辑:完整异常(没有内部异常。它为空):

System.InvalidOperationException: variable '_arg1' of type 'MyLib.dep' referenced from scope '', but it is not defined
   at System.Linq.Expressions.Compiler.VariableBinder.Reference(ParameterExpression node, VariableStorageKind storage)
   at System.Linq.Expressions.Compiler.VariableBinder.VisitParameter(ParameterExpression node)
   at System.Linq.Expressions.Compiler.VariableBinder.Visit(Expression node)
   at System.Linq.Expressions.Compiler.VariableBinder.VisitUnary(UnaryExpression node)
   at System.Linq.Expressions.Compiler.VariableBinder.Visit(Expression node)
   at System.Linq.Expressions.ExpressionVisitor.Visit(ReadOnlyCollection`1 nodes)
   at System.Linq.Expressions.ExpressionVisitor.VisitNewArray(NewArrayExpression node)
   at System.Linq.Expressions.Compiler.VariableBinder.Visit(Expression node)
   at System.Linq.Expressions.ExpressionVisitor.VisitArguments(IArgumentProvider nodes)
   at System.Linq.Expressions.ExpressionVisitor.VisitMethodCall(MethodCallExpression node)
   at System.Linq.Expressions.Compiler.VariableBinder.Visit(Expression node)
   at System.Linq.Expressions.ExpressionVisitor.VisitArguments(IArgumentProvider nodes)
   at System.Linq.Expressions.ExpressionVisitor.VisitMethodCall(MethodCallExpression node)
   at System.Linq.Expressions.Compiler.VariableBinder.Visit(Expression node)
   at System.Linq.Expressions.ExpressionVisitor.Visit(ReadOnlyCollection`1 nodes)
   at System.Linq.Expressions.Compiler.VariableBinder.VisitLambda[T](Expression`1 node)
   at System.Linq.Expressions.Compiler.VariableBinder.Visit(Expression node)
   at System.Linq.Expressions.Compiler.LambdaCompiler.Compile(LambdaExpression lambda, DebugInfoGenerator debugInfoGenerator)
   at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.GetLambdaExpression(Expression argument)
   at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.GetLambdaExpression(MethodCallExpression callExpression, Int32 argumentOrdinal)
   at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.MethodCallTranslator.OneLambdaTranslator.Translate(ExpressionConverter parent, MethodCallExpression call, DbExpression& source, DbExpressionBinding& sourceBinding, DbExpression& lambda)
   at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.MethodCallTranslator.OneLambdaTranslator.Translate(ExpressionConverter parent, MethodCallExpression call)
   at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.MethodCallTranslator.TypedTranslate(ExpressionConverter parent, MethodCallExpression linq)
   at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.TranslateExpression(Expression linq)
   at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.MethodCallTranslator.DefaultIfEmptyTranslator.Translate(ExpressionConverter parent, MethodCallExpression call)
   at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.MethodCallTranslator.TypedTranslate(ExpressionConverter parent, MethodCallExpression linq)
   at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.TranslateExpression(Expression linq)
   at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.TranslateLambda(LambdaExpression lambda, DbExpression input)
   at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.MethodCallTranslator.OneLambdaTranslator.Translate(ExpressionConverter parent, MethodCallExpression call, DbExpression& source, DbExpressionBinding& sourceBinding, DbExpression& lambda)
   at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.MethodCallTranslator.SelectManyTranslator.Translate(ExpressionConverter parent, MethodCallExpression call)
   at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.MethodCallTranslator.TypedTranslate(ExpressionConverter parent, MethodCallExpression linq)
   at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.TranslateExpression(Expression linq)
   at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.MethodCallTranslator.OneLambdaTranslator.Translate(ExpressionConverter parent, MethodCallExpression call, DbExpression& source, DbExpressionBinding& sourceBinding, DbExpression& lambda)
   at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.MethodCallTranslator.SelectTranslator.Translate(ExpressionConverter parent, MethodCallExpression call)
   at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.MethodCallTranslator.TypedTranslate(ExpressionConverter parent, MethodCallExpression linq)
   at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.TranslateExpression(Expression linq)
   at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.Convert()
   at System.Data.Entity.Core.Objects.ELinq.ELinqQueryState.GetExecutionPlan(Nullable`1 forMergeOption)
   at System.Data.Entity.Core.Objects.ObjectQuery`1.<>c__DisplayClass41_0.<GetResults>b__1()
   at System.Data.Entity.Core.Objects.ObjectContext.ExecuteInTransaction[T](Func`1 func, IDbExecutionStrategy executionStrategy, Boolean startLocalTransaction, Boolean releaseConnectionOnSuccess)
   at System.Data.Entity.Core.Objects.ObjectQuery`1.<>c__DisplayClass41_0.<GetResults>b__0()
   at System.Data.Entity.Core.Objects.ObjectQuery`1.GetResults(Nullable`1 forMergeOption)
   at System.Data.Entity.Core.Objects.ObjectQuery`1.<System.Collections.Generic.IEnumerable<T>.GetEnumerator>b__31_0()
   at System.Data.Entity.Internal.LazyEnumerator`1.MoveNext()
   at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
   at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at ConsoleApplication1.main(String[] argv) in C:\Temp\ConsoleApplication1\ConsoleApplication1\Program.fs:line 780

groupJoin 似乎无法编译我的额外条件。Visual Studio 抱怨它不认识 d,并在 d.start_time<e.modified 下划线:

use db = new CompanyContext()
let q = query {
    for d in db.deps do
    groupJoin e in db.emps.Where(fun e -> d.start_time<e.modified) on (d.id=e.dep_id) into es // left join
    for e in es.DefaultIfEmpty() do
    select ( d, e )}
let result = q.ToList()

FS0039 值、命名空间、类型或模块“d”未定义。

尝试使用匿名记录select {| d = d; e = e |} - dbc
你使用 DefaultIfEmpty() 进行左外连接时可能出现问题,参见 leftOuterJoin is different from C# version, workaround doesn't work for double left join #6552 - dbc
@dbc我不知道F#中也存在匿名记录(谢谢!)。然而结果是相同的。我正在看你提供的第二个关于DefaultIfEmpty()的链接。它看起来非常相似,但那里的例子没有使用多个条件。我还没有弄清楚是否可以在那里找到适用于我的问题的任何内容。 - symbiont
你能分享一下完整的 ToString() 异常输出吗?包括异常类型、消息、回溯和内部异常(如果有的话)。回溯可能会提供一些关于出错位置的提示。 - dbc
@dbc 我添加了信息。 - symbiont
显示剩余2条评论
1个回答

1

我想我找到了解决方案。使用groupJoin,似乎可以产生预期结果:

use db = new CompanyContext()
let q = query {
    for d in db.deps do
    groupJoin e in db.emps on (d.id=e.dep_id) into es
    for e in es.Where(fun e -> d.start_time<e.modified).DefaultIfEmpty() do
    select ( d , e )}
let result = q.ToList()

您不必使用on子句,可以使用一个始终为真的虚拟条件,例如0=0

use db = new CompanyContext()
let q = query {
    for d in db.deps do
    groupJoin e in db.emps on (0=0) into es
    for e in es.Where(fun e -> d.id=e.dep_id && d.start_time<e.modified).DefaultIfEmpty() do
    select ( d , e )}
let result = q.ToList()

这些示例也适用于leftOuterJoin而不是groupJoin

观点:

我不喜欢leftOuterJoin隐式地将.DefaultIfEmpty()应用于into部分(上面的示例中的into es)。因为如果您想在后续的for子句中添加更多条件,例如使用.Where,那么您仍然需要应用.DefaultIfEmpty(),以便再次至少有1个结果,因为for是下一个和单独的操作。最好使用groupJoin,它总是需要.DefaultIfEmpty()来执行左连接。


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