如何强制EF使用连接而不是拆分复杂的查询?

时间:2015-06-04 19:06:44

标签: c# entity-framework entity-framework-6 iqueryable deferred-execution

我有一个复杂的IQueryable,我希望EF用单个数据库查询填充,以便我可以使用延迟执行。请考虑以下示例。

对于这些模型:

public enum AlphaState { Unknown = '\0', Good = 'G', Bad = 'B' }

[Table("MY_ALPHA")]
public class Alpha
{
    [Key]
    [Column("alpha_index")]
    public long Index { get; set; }

    [Column("alpha_id")] // user-editable field that users call ID
    public string AlphaId { get; set; }

    [Column("deleted")]
    public char? Deleted { get; set; }

    [Column("state")]
    public AlphaState State { get; set; }

    [InverseProperty("Alpha")]
    public ICollection<Bravo> Bravos { get; set; }
}

[Table("MY_BRAVO")]
public class Bravo
{
    [Key]
    [Column("bravo_index")]
    public long BravoIndex { get; set; }

    [ForeignKey("Alpha")]
    [Column("alpha_index")] // actually a 1:0..1 relationship
    public long? AlphaIndex { get; set; }
    public virtual Alpha Alpha { get; set; }

    [InverseProperty("Bravo")]
    public ICollection<Charlie> Charlies { get; set; }
}

[Table("MY_CHARLIE_VIEW")]
public class Charlie
{
    [Key]
    [Column("charlie_index")]
    public int CharlieIndex { get; set; }

    [Column("deleted")]
    public char? Deleted { get; set; }

    [Column("created_at")]
    public DateTime CreatedAt { get; set; }

    [ForeignKey("Bravo")]
    [Column("bravo_index")]
    public long BravoIndex { get; set; }
    public virtual Bravo Bravo { get; set; }

    [ForeignKey("Delta")]
    [Column("delta_index")]
    public long DeltaIndex { get; set; }
    public virtual Delta Delta { get; set; }

    [InverseProperty("Charlie")]
    public virtual ICollection<Delta> AllDeltas { get; set; }
}

[Table("MY_DELTA")]
public class Delta
{
    [Key]
    [Column("delta_index")]
    public long DeltaIndex { get; set; }

    [ForeignKey("Charlie")]
    [Column("charlie_index")]
    public long CharlieIndex { get; set; }
    public virtual Charlie Charlie { get; set; }

    [InverseProperty("Delta")] // actually a 1:0..1 relationship
    public ICollection<Echo> Echoes { get; set; }
}

public enum EchoType { Unknown = 0, One = 1, Two = 2, Three = 3 }

[Table("MY_ECHOES")]
public class Echo
{
    [Key]
    [Column("echo_index")]
    public int EchoIndex { get; set; }

    [Column("echo_type")]
    public EchoType Type { get; set; }

    [ForeignKey("Delta")]
    [Column("delta_index")]
    public long DeltaIndex { get; set; }
    public virtual Delta Delta { get; set; }
}

...考虑这个问题:

IQueryable<Alpha> result = context.Alphas.Where(a => a.State == AlphaState.Good)
                                         .Where(a => !a.Deleted.HasValue)
                                         .Where(a => a.Bravos.SelectMany(b => b.Charlies)
                                                             .Where(c => !c.Deleted.HasValue)
                                                             .Where(c => c.Delta.Echoes.Any())
                                                             .OrderByDescending(c => c.CreatedAt).Take(1)
                                                             .Any(c => c.Delta.Echoes.Any(e => e.Type == EchoType.Two)))
var query = result as System.Data.Objects.ObjectQuery;
string queryString = query.ToTraceString();

注意:查理实际上是一张桌子上的视图; Delta有一个FK到Charlie的表,但是该视图为与Charlie链接的最新Delta提供了假FK,因此该模型使用它,因为该计划仅使用EF进行查询,而不是用于更新。 / p>

我希望这个查询可以通过单个数据库查询来填充,但是正如所写的那样,这不是发生了什么。如何修改此查询以获得相同的结果,但是EF只是将条件构建到results IQueryable而不是为其预先获取数据?

我如何知道它使用两种查询

我确信它已经分裂成多个查询,因为由于超出此问题范围的原因,我故意给上下文一个错误的连接字符串。 result是一个IQueryable,所以它应该使用延迟执行,并且实际上不会尝试检索任何数据,直到它被使用,但我声明它后立即获得连接失败异常。

背景

我们有一个现有的数据库结构,数据库访问层和使用所述结构的几十万行代码。 DAL。我们想添加一个UI以允许用户构建自己的复杂查询,而EF似乎是为此构建底层模型的好方法。但是,我们之前从未使用EF,所以Powers That Be宣布它无法连接到数据库;我们应该使用EF生成IQueryable,从中提取查询字符串,并使用我们现有的DAL来运行查询。

3 个答案:

答案 0 :(得分:3)

  

我如何知道它使用两个查询

您正在观察的不是EF开始运行您的查询。将查询分配给result变量后,仍然有查询定义,而不是结果集。如果您将分析器附加到数据库,您将看到没有为您的查询执行SELECT语句。

那么,为什么作为与数据库的连接?原因是,第一次为给定的派生DbContext类型构建查询时,EF会为该类型构建并缓存其内存模型。它通过对您定义的类型,属性和属性应用各种约定来实现此目的。理论上,此过程不需要连接到数据库,但SQL Server的提供程序无论如何都是这样做的。它这样做是为了确定您正在使用的SQL Server的版本,以便它可以确定它是否可以在它构建的模型中使用更新的SQL Server功能。

有趣的是,此模型是为类型缓存的,而不是上下文的实例。你可以通过处理context然后创建一个新的并重复构建查询的代码行来看到这一点。在第二个实例中,根本不会看到与数据库的连接,因为EF会将其缓存模型用于您的上下文类型。

  已宣布无法连接数据库的权力;我们应该使用EF生成IQueryable,从中提取查询字符串,并使用我们现有的DAL来运行查询。

由于您需要避免EF连接到数据库),您可以看到我的帖子here,其中包含有关如何在代码中预先提供此信息的详细信息

另外,请注意,EF第一次遇到DbContext类型时可能会连接到您的服务器的另一个原因是:初始化。除非您已禁用初始化(使用Database.SetInitializer<MyContext>(null)之类的内容),否则它将检查数据库是否存在,如果没有则尝试创建它。

请注意,您可以直接在EF代码优先查询上调用ToString()以获取T-SQL查询。您不需要通过中间ObjectQuery.ToTraceString()方法,这实际上是旧版EF API的一部分。但是,这两种方法都是用于调试和记录目的。使用EF构建查询但不执行它们是相当不寻常的。你很可能会遇到这种方法的问题 - 当EF确定应该生成参数化查询时,这显然很明显。此外,不能保证不同版本的EF会为同一输入生成类似的T-SQL,因此当您更新到较新的EF版本时,您的代码可能会变得相当脆弱。确保你有足够的测试!

如果您担心让用户直接连接到数据库 - 这是一个完全合法的安全问题 - 您可能会考虑另一种方法。我没有多少经验,但似乎OData可能是一个很好的选择。这允许您在客户端上构建查询,通过远程连接对它们进行序列化,然后在服务器上重新创建它们。然后,在服务器上,您可以对数据库执行它们。您的客户无需了解任何数据库。

如果您确定(或被指示)坚持您在问题中详述的方法,请花时间学习how to profile a SQL Server connection。这将是您了解EF如何翻译查询的绝对必要工具。

答案 1 :(得分:2)

context.Foos.Where(f => f.Bars.Any(b => b.SomeOtherData == "baz"));

我在我拥有的数据库上尝试了类似于你的查询(使用LINQPad),我最终得到了

SELECT 
  [Extent1].[Property1] AS [Property1]
  -- other properties of Foo
  FROM [dbo].[Foos] AS [Extent1]
  WHERE EXISTS (SELECT 
    1 AS [C1]
    FROM [dbo].[Bars] AS [Extent2]
    WHERE ([Extent1].[FooId] = [Extent2].[FooId]) AND (N'baz' = [Extent2].[SomeOtherData])
  )

...这对我来说绝对是一个查询。

IQueryable函数中的表达式不能直接执行 - 它们被用于生成SQL,然后在需要实现结果时用于执行查询。

答案 2 :(得分:-1)

尝试使用LINQ查询语法。

var result = (
    from f in foo
    join b in bar
        on f.fooid equals b.fooid
    where b.someotherdata = "baz"
    select new { f.fooid, f.somedata }
).Distinct().ToEnumerable();

这将推迟到枚举。