在多列上连接时,EF Core 2.2意外生成了SQL

时间:2019-07-07 20:35:40

标签: linq entity-framework-core

请考虑以下课程:

public class Foo
{
    public int Id { get; set; }

    public string Type { get; set; }

    public int BarId { get; set; }
}

public class Bar
{
    public int Id { get; set; }

    public string Name { get; set; }
}

和以下DbContext:

public class TestDbContext : DbContext
{
    public DbSet<Foo> Foos { get; set; }

    public DbSet<Bar> Bars { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(@"Server=.;Database=ConditionalJoinEFCoreTest;Trusted_Connection=True;");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.Entity<Foo>().HasData(new Foo { Id = 1, BarId = 1, Type = "Bar" });
        modelBuilder.Entity<Foo>().HasData(new Foo { Id = 2, BarId = 2, Type = "Bar" });
        modelBuilder.Entity<Foo>().HasData(new Foo { Id = 3, BarId = 1, Type = "Not Bar" });
        modelBuilder.Entity<Foo>().HasData(new Foo { Id = 4, BarId = 2, Type = "Not Bar" });

        modelBuilder.Entity<Bar>().HasData(new Bar { Id = 1, Name = "Bar 1" });
        modelBuilder.Entity<Bar>().HasData(new Bar { Id = 2, Name = "Bar 2" });
    }
}

现在让我们查询数据:

using (var ctx = new TestDbContext())
{
    var joinResult = ctx.Foos.GroupJoin(
      ctx.Bars,
      foo => new { Key = foo.BarId, PropName = foo.Type },
      bar => new { Key = bar.Id, PropName = "Bar" },
      (foo, bars) => new
      {
         Foo = foo,
         Bars = bars
      })
  .SelectMany(
      x => x.Bars.DefaultIfEmpty(),
      (foo, bar) => new 
      {
          Foo = foo.Foo,
          Bar = bar.Name
      });

    var result = joinResult.GroupBy(x => x.Foo.Id).Select(x => new
    {
        Id = x.Key,
        Name = x.Max(r => r.Bar)
    }).ToList();
}

此查询将按预期产生以下SQL:

SELECT [foo].[Id], [foo].[BarId], [foo].[Type], [bar].[Name] AS [Bar]
FROM [Foos] AS [foo]
LEFT JOIN [Bars] AS [bar] ON ([foo].[BarId] = [bar].[Id]) AND ([foo].[Type] = N'Bar')
ORDER BY [foo].[Id]

但是,如果我们定义类型:

public class ConditionalJoin
{
    public int Key { get; set; }

    public string PropName { get; set; }
}

...然后修改LINQ查询:

using (var ctx = new TestDbContext())
{
    var joinResult = ctx.Foos.GroupJoin(
      ctx.Bars,
      foo => new ConditionalJoin { Key = foo.BarId, PropName = foo.Type }, // <-- changed
      bar => new ConditionalJoin { Key = bar.Id, PropName = "Bar" }, // <-- changed
      (foo, bars) => new
      {
         Foo = foo,
         Bars = bars
      })
  .SelectMany(
      x => x.Bars.DefaultIfEmpty(),
      (foo, bar) => new 
      {
          Foo = foo.Foo,
          Bar = bar.Name
      });

    var result = joinResult.GroupBy(x => x.Foo.Id).Select(x => new
    {
        Id = x.Key,
        Name = x.Max(r => r.Bar)
    }).ToList();
}

然后生成的SQL如下:

SELECT [foo0].[Id], [foo0].[BarId], [foo0].[Type]
FROM [Foos] AS [foo0]

SELECT [bar0].[Id], [bar0].[Name]
FROM [Bars] AS [bar0]

为什么会这样?

1 个答案:

答案 0 :(得分:3)

正如我所怀疑的那样,问题不在于匿名类型与具体类型,而是唯一的C#编译器功能,它发出特殊的Expression.New调用,而不是这种语法Expression.MemberInit的常规调用,并且可以完成用于匿名类型。与Selection in GroupBy query with NHibernate with dynamic anonymous object中的问题完全相同,解决方案也是如此-使用参数生成类构造函数,并使用将参数映射到类成员的方法生成NewExpression

以下是有关静态类的概念证明:

public class ConditionalJoin
{
    public ConditionalJoin(int key, string property)
    {
        Key = key;
        Property = property;
    }
    public int Key { get; }
    public string Property { get; }
    public static Expression<Func<T, ConditionalJoin>> Select<T>(Expression<Func<T, int>> key, Expression<Func<T, string>> property)
    {
        var parameter = key.Parameters[0];
        var body = Expression.New(
            typeof(ConditionalJoin).GetConstructor(new[] { typeof(int), typeof(string) }),
            new[] { key.Body, Expression.Invoke(property, parameter) },
            new [] { typeof(ConditionalJoin).GetProperty("Key"), typeof(ConditionalJoin).GetProperty("Property") });
        return Expression.Lambda<Func<T, ConditionalJoin>>(body, parameter);
    }
}

及其用法:

var joinResult = ctx.Foos.GroupJoin(
    ctx.Bars,
    ConditionalJoin.Select<Foo>(foo => foo.BarId, foo => foo.Type),
    ConditionalJoin.Select<Bar>(bar => bar.Id, bar => "Bar"),
    // the rest...

当然,如果您希望查询在被评估的客户端(例如LINQ to Objects)的情况下正常工作,则该类必须更正实现GetHashCodeEquals


话虽如此,实际上EF Core支持另一种更简单的替代解决方案-在匿名/具体类型上使用Tuple(而不是ValueTuple-表达式树中仍然不支持这些)。因此,以下内容也可以在EF Core中正常运行(类似Tuple类型的情况):

var joinResult = ctx.Foos.GroupJoin(
    ctx.Bars,
    foo => new Tuple<int, string>(foo.BarId, foo.Type),
    bar => new Tuple<int, string>(bar.Id, "Bar"),
    // the rest...