如何让EntityFramework为相关对象生成有效的SQL查询?

时间:2015-06-09 15:01:10

标签: c# .net entity-framework orm

我正在尝试研究如何使用.NET EntityFramework在获取相关实体时生成可读和自然的代码高效的SQL查询语句。例如,给出以下代码优先定义

public class WidgetContext : DbContext
{
    public DbSet<Widget> Widgets { get; set; }
    public DbSet<Gizmo> Gizmos { get; set; }
}

public class Widget
{
    public virtual int Id { get; set; }
    [Index]
    [MaxLength(512)]
    public virtual string Name { get; set; }
    public virtual ICollection<Gizmo> Gizmos { get; set; }
}

public class Gizmo
{
    public virtual long Id { get; set; }
    [Index]
    [MaxLength(512)]
    public virtual string Name { get; set; }
    public virtual Widget Widget { get; set; }
    public virtual int WidgetId { get; set; }

}

我希望能够编写像

这样的代码
using (var wc = new WidgetContext())
{
    var widget = wc.Widgets.First(x => x.Id == 123);
    var gizmo = widget.Gizmos.First(x => x.Name == "gizmo 99");
}

并查看按

行创建的SQL查询
SELECT TOP (1) * from Gizmos WHERE WidgetId = 123 AND Name = 'gizmo 99'

因此,选择正确的Gizmo的工作是由数据库执行的。这很重要,因为在我的用例中,每个Widget可能有数千个相关的Gizmo,在特定的请求中我只需要一次检索一个。不幸的是,上面的代码导致EntityFramework像这样创建SQL

SELECT * from Gizmos WHERE WidgetId = 123

然后通过扫描整套相关的Gizmo实体在内存中执行Gizmo.Name上的匹配。

经过大量的实验,我找到了在实体框架中创建高效SQL用法的方法,但只能使用难以编写的丑陋代码。下面的例子说明了这一点。

using System.Data.Entity;
using System.Data.Entity.Core.Objects.DataClasses;
using System.Linq;
static void Main(string[] args)
{
    Database.SetInitializer(new DropCreateDatabaseAlways<WidgetContext>());

    using (var wc = new WidgetContext())
    {
        var widget = new Widget() { Name = "my widget"};
        wc.Widgets.Add(widget);
        wc.SaveChanges();
    }

    using (var wc = new WidgetContext())
    {
        var widget = wc.Widgets.First();
        for (int i = 0; i < 1000; i++)
            widget.Gizmos.Add(new Gizmo() { Name = string.Format("gizmo {0}", i) });
        wc.SaveChanges();
    }

    using (var wc = new WidgetContext())
    {
        wc.Database.Log = Console.WriteLine;
        var widget = wc.Widgets.First();

        Console.WriteLine("=====> Query 1");
        // queries all gizmos associated with the widget and then runs the 'First' query in memory. Nice code, ugly database usage
        var g1 = widget.Gizmos.First(x => x.Name == "gizmo 99");

        Console.WriteLine("=====> Query 2");
        // queries on the DB with two terms in the WHERE clause - only pulls one record, good SQL, ugly code
        var g2 = ((EntityCollection<Gizmo>) widget.Gizmos).CreateSourceQuery().First(x => x.Name == "gizmo 99");

        Console.WriteLine("=====> Query 3");
        // queries on the DB with two terms in the WHERE clause - only pulls one record, good SQL, ugly code
        var g3 = wc.Gizmos.First(x => x.Name == "gizmo 99" && x.WidgetId == widget.Id);

        Console.WriteLine("=====> Query 4");
        // queries on the DB with two terms in the WHERE clause - only pulls one record, also good SQL, ugly code
        var g4 = wc.Entry(widget).Collection(x => x.Gizmos).Query().First(x => x.Name == "gizmo 99");
    }

    Console.ReadLine();
}

查询1演示了如何获取所有内容并过滤&#39;由实体对象的自然使用生成的方法。

上面的查询2,3和4都生成了我认为有效的SQL查询 - 一个返回单行并且在WHERE子句中有两个术语,但它们都涉及非常高跷的C#代码。

有没有人有一个解决方案可以编写自然的C#代码并在这种情况下生成有效的SQL利用率?

我应该注意到我已经尝试用我的Widget对象中的EntityCollection替换ICollection,以允许从上面的Query 2代码中删除强制转换。不幸的是,这会导致EntityException告诉我

  

无法将对象添加到EntityCollection或   的EntityReference。附加到ObjectContext的对象不能   被添加到不是的EntityCollection或EntityReference   与源对象相关联。

当我尝试检索任何相关对象时。

任何建议表示赞赏。

1 个答案:

答案 0 :(得分:0)

好的,进一步的挖掘让我尽可能地接近我想要的位置(重申一下,这个代码看起来像OO但生成有效的数据库使用模式)。

事实证明,上面的Query2(将相关集合转换为EntityCollection)实际上并不是一个好的解决方案,因为虽然它针对数据库生成了所需的查询类型,但仅仅是获取{{来自Gizmos的集合足以使实体框架进入数据库并获取所有相关的Gizmo - 即执行我试图避免的查询。

但是,可以在不调用collection属性的getter的情况下获取关系widget,如http://blogs.msdn.com/b/alexj/archive/2009/06/08/tip-24-how-to-get-the-objectcontext-from-an-entity.aspx所述。当您访问EntityCollection集合属性时,此方法将回避实体框架获取相关实体。

因此,可以像这样添加Widget上的附加只读属性

Gizmos

然后调用代码看起来像这样

 public IQueryable<Gizmo> GizmosQuery
 {
     get
     {
         var relationshipManager = ((IEntityWithRelationships)this).RelationshipManager;
         return (IQueryable<Gizmo>) relationshipManager.GetAllRelatedEnds().First( x => x is EntityCollection<Gizmo>).CreateSourceQuery();
     }
 }

此方法生成的SQL可以有效地从数据库中只提取一行,但取决于以下条件保持为真

  • 从源类型到目标类型只有一个关系。将Widget链接到Gizmos的多个关系意味着在var g1 = widget.GizmosQuery.First(x => x.Name == "gizmo 99"); .First()调用中需要更复杂的谓词。
  • GizmosQuery启用了代理创建,Widget类可以生成代理(https://msdn.microsoft.com/en-us/library/vstudio/dd468057%28v=vs.100%29.aspx
  • 不能在使用DbContext新创建的对象上调用GizmosQuery属性,因为它们不是代理,不会实现new Widget()。作为有效代理的新对象可以在必要时使用IEntityWithRelationships创建。