【问题标题】:How can I force EF to use joins instead of splitting up a complicated query?如何强制 EF 使用联接而不是拆分复杂的查询?
【发布时间】:2015-08-19 13:13:51
【问题描述】:

我有一个复杂的 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();

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

我希望这个查询由单个数据库查询填充,但正如所写的那样,这不是正在发生的事情。如何修改此查询以获得相同的结果,但让 EF 简单地将条件构建到 results IQueryable 中,而不是为其预取数据?

我怎么知道它使用两个查询

我确定它会拆分为多个查询,因为出于此问题范围之外的原因,我故意为上下文提供了错误的连接字符串。 result 是一个 IQueryable,因此它应该使用延迟执行,并且在使用它之前实际上不会尝试检索任何数据,但是我一声明它就会收到连接失败异常。

背景

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

【问题讨论】:

  • 您可能想使用以下方法检查您的假设:stackoverflow.com/a/20751723/4812586msdn.microsoft.com/en-us/data/dn469464.aspx
  • 你可以在没有子查询的情况下在 SQL 中做到这一点吗?
  • 我相信EF会在你访问ObjectQuery属性时连接到数据库。它这样做不是为了执行查询,而是为了确定您正在使用的 SQL Server 版本,以便能够为实体连接字符串提供 ProviderManifestToken 值。这是 EF 早期的遗留问题。
  • @ObliviousSage 对此进行调查。使连接有效,以免出现异常。然后使用 SQL Profiler 查看正在执行的内容。也许这只是一个真正的查询。
  • 默认情况下,EF 不会立即将您的 IQueryable&lt;&gt; 转换为提供程序查询。然而,各种方法迫使它这样做,包括任何输出 T-SQL 的跟踪方法。

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


【解决方案1】:
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,然后在需要具体化结果时用于执行查询。

【讨论】:

  • 嗯。我的 actual 查询比我使用的示例复杂得多;我认为我可以将涉及的 5 个模型简化为 2 个并保留我得到的行为,但也许不是。让我编辑我的问题。
  • 我已经编辑了我的问题,以给出一个更接近我的实际查询的示例。
【解决方案2】:

尝试使用 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();

这将被推迟到枚举。

【讨论】:

  • 只是跳过去看看我为什么投了反对票。令我惊讶的是,这个问题经过了如此多的编辑,如果不回顾历史,就不可能确定它的初始状态。因此,很难说我实际上试图回答什么问题!
【解决方案3】:

我怎么知道它使用了两个查询

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

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

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

当权者宣布它永远无法连接到数据库;我们应该使用 EF 生成一个 IQueryable,从中提取查询字符串,然后使用我们现有的 DAL 来运行查询。

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

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

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-23
    • 1970-01-01
    • 2018-05-24
    • 2011-11-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多