实体框架(Entity Framework)的联接解释

5

我有以下实体

//Active Auction Entity
public class ActiveAuction
{
    public int Id { get; set; }

    public string Title { get; set; }

    public int? FirstAuctionId { get; set; }

    public int? SecondAuctionId { get; set; }

    public int? ThirdAuctionId { get; set; }

    public virtual Auction FirstAuction { get; set; }

    public virtual Auction SecondAuction { get; set; }

    public virtual Auction ThirdAuction { get; set; }
}

// Auction Entity
public class Auction
{
    public int AuctionId { get; set; }

    public AuctionStatus AuctionStatus { get; set; }

    public int? DepartmentId { get; set; }

    public virtual Department Department { get; set; }

}

// Department Entity
public class Department
{
    public int DepartmentId { get; set; }

    public string DepartmentName { get; set; }

    public int? AdminId { get; set; }

    public virtual Admin Admin { get; set; }
}

我想要做的是获取已激活的拍卖,并加载相关的竞拍和部门信息。

我知道应该为每个需要加载的对象编写Include语句,这样EF生成的SQL语句将包含联接语句来为我选择这些对象

但这是现有的代码:

 using (var dc = DataContext())
    {
        await dc.Auction
           .Include("Department.Admin")
           .Where(i => i.Id == id && i.AuctionStatus == AuctionStatus.Accepted).ToListAsync();

        return await dc.ActiveAuction.
           SingleOrDefaultAsync(i => i.Id == id);
    }

我不知道为什么这段代码能够运行,而返回的ActiveAuctions包含了所有所需的对象。
我检查了对数据库执行的SQL语句,并且生成了两个独立的查询,正如预期的那样。
我想要一个解释,以便理解返回的ActiveAcutions是如何与其他提到的实体一起加载的!!?

Categories.Admin 是不是应该改成 Department.Admin?还是你为了缩短代码而从 Auction 对象中删除了 Categories - Johan
部门.管理员 我修复了,管理员是另一个现有实体。 - Mohamed Badr
很好,考虑使用强类型包含(即.Include(a => a.Department.Admin))。这将避免拼写错误/包含不存在的对象。 - Johan
回答你的问题,我认为这是因为你在同一上下文中执行了两个查询。EF意识到主键匹配,然后链接起你的数据。 - Johan
我想到了一些在内存中发生的合并,但我找不到任何讨论这个问题的文章。 - Mohamed Badr
显示剩余6条评论
5个回答

5
原因很简单。您可能知道,实体框架会跟踪从数据库中获取的实体,大多数情况下是为了检测对它们所做的更改,并在调用 SaveChanges 时将这些更改应用于数据库。但是,这意味着 EF 上下文具有到目前为止从数据库获取的实体的缓存。
编辑:正如@GertArnold在评论中正确指出的那样 - 我关于动态代理的解释完全错误 - 即使 ProxyCreationEnabled 为 false,它也可以工作。真正的原因是当调用 DetectChanges 时实体框架执行的关系修复(它在各种事件上隐式调用,例如将实体附加到上下文或在 DbSet 上执行查询)。在关系修复期间,EF 同步导航属性和外键,这在您的情况下导致了您观察到的行为。有关关系修复的更多信息请参阅 MSDN 要验证这一点,您可以使用此简单代码:
using (var ctx = new TestEntities()) {
     ctx.Configuration.LazyLoadingEnabled = false;                
     ctx.Configuration.ProxyCreationEnabled = false;                
     var code = ctx.Codes.First();                
     var error = ctx.Errors.First();
     Debug.Assert(Object.ReferenceEquals(error.Code, code));                                
}

这里我首先获取了一个实体(Code),然后我获取了另一个实体(Error),它具有导航属性 Code。你可以看到 延迟加载被禁用。下面的断言将会通过,因为 error.Code 和 code 是同一个 .NET 对象,这证实它是从上下文缓存中获取的。


尽管有广泛的解释,但你并没有真正了解到原因:关系修复。代理创建并不需要这样做,所以我认为提到它只会增加噪音。 - Gert Arnold
如果我理解正确的话,首先加载拍卖和部门,第二个加载活动拍卖,难道不应该是这样工作的吗? - Sandip Bantawa
@GertArnold 感谢您指出这一点 - 我对动态代理的解释完全错误。 - Evk
所以真正的原因是第一个查询(dc.Auction...)的结果恰好包括第二个查询中ActiveAuction所指的三个Auction - Gert Arnold
是的,但我认为我在原始回答中已经提到了使用动态代理。 - Evk

2
您看到的行为是因为 EF 内部针对每个 DB 上下文实例维护对象缓存。每次执行查询时,EF 首先检查内部缓存,以查看是否包含所需的实体。如果找到实体,则直接从缓存中返回,而无需查询数据库。
在您的示例中,发生了以下情况:
  • 第一个查询获取了一些拍卖实体并将它们添加到缓存中;
  • 第二个查询获取了筛选后的“活动拍卖”实体,并将它们添加到缓存中;
  • 当您检查第二个查询的结果时,您会发现属性已经被填充,因为它们是通过键在缓存中找到的;
您可以通过更改数据库上下文实例上的 MergeOption 设置来更改此行为。此设置更改了 EF 将结果从数据库合并到缓存的方式。
可能的值如下所示:

AppendOnly(默认值)

不存在于对象上下文中的对象将附加到上下文。如果对象已经存在于上下文中,则不会使用数据源值覆盖条目中对象属性的当前和原始值。对象条目的状态以及条目中对象属性的状态都不会改变。AppendOnly是默认的合并选项。

NoTracking

对象保持分离状态,并且不在ObjectStateManager中跟踪。但是,由Entity Framework生成的实体和带有代理的POCO实体保留对对象上下文的引用,以便加载相关对象。

OverwriteChanges

不存在于对象上下文中的对象将附加到上下文。如果对象已经存在于上下文中,则使用数据源值覆盖条目中对象属性的当前和原始值。对象条目的状态设置为未更改,没有属性标记为修改。

PreserveChanges

不存在于对象上下文中的对象将附加到上下文。

(https://msdn.microsoft.com/en-us/library/system.data.objects.mergeoption(v=vs.110).aspx)

这里有一篇很好的文章,详细描述了所有选项,并包含一些代码示例。

http://www.develop1.net/public/post/Do-you-understand-MergeOptions.aspx

编辑:只是想补充一点,如果第一个查询对实体进行了投影(例如,仅选择了某些列),则默认行为可能会导致奇怪的结果。在这种情况下,即使只有一半加载了实体,它仍将被添加到缓存中。这意味着第二个查询也将返回一半加载的实体。

ObjectContext API 已经基本上被弃用了。 - Gert Arnold
@GertArnold。这并不重要,因为DBContext只是ObjectContext的一个包装器,所以原则保持不变。 - Kaspars Ozols

0
当EF加载任何实体时,它会将它们与缓存中的现有实体连接起来,以便模型包含所有引用指向任何已加载对象。

0

你不能使用.Include("Department.Admin"),因为EF不会加载像你的内部包含,比如Admin

请注意,当你使用EF Code First时,最好确定表之间的真实关系,无论是在类中还是使用流畅的API,以满足良好的设计而没有异常。因此,在ActiveAuction中使用三个Auction而没有任何澄清可能会使你感到困扰。

另一件事是,似乎你在每个Auction中都将ActiveAuction用作外键,而没有任何声明!所以将你的Auction类更改为这样:

public class Auction
{
    [ForeignKey("ActiveOne")]
    public int AuctionId { get; set; }

    public AuctionStatus AuctionStatus { get; set; }

    public int? DepartmentId { get; set; }

    public virtual Department Department { get; set; }

    [Required]
    public virtual ActiveAuction ActiveOne { get; set; } //match this name

}

考虑到这一点,我认为您的模型存在一些不确定性。让我们来谈谈查询。就像你所说的:

我的目标是获取已激活的拍卖,并加载相关的竞拍和部门信息。

那么您可以使用类似于以下的查询:

await context.Auctions.Include("Department").Include("ActiveOne")
.Where(i => i.id == id && i.AuctionStatus == AuctionStatus.Accepted)
.Select(i => i.ActiveOne).ToListAsync();

"EF不会加载像您的内部包含这样的内部包含,因此您无法使用.Include(“Department.Admin”)。这是什么意思?请解释一下。" - Mohamed Badr
Include中不能使用嵌套成员,或者至少不会正常工作。我在使用EF特别是Code First方面已经有4年的经验。 - Amirhossein Mehrvarzi

-2

虽然 OP 的问题可能与惰性加载有关,但在这种特定情况下它实际上是如何工作的应该更加详细地解释。正如您所看到的,在这里 Auction 和 ActiveAuction 之间没有明确的关系。第一个查询似乎只是加载与 Auction 相关的所有内容,但是然后 ActiveAuction(在第二个查询中使用)似乎会自动填充已加载的数据? - Hopeless
@Hopeless 如果启用了懒加载,那么 OP 可能正在进行懒加载(而他没有注意到)... 实际上,这是我对这个问题的第一个想法。 - Jcl
@Jcl,据我所知,延迟加载将通过导航属性访问相关对象。但在这种情况下,您能指出哪些对象的相关对象是什么吗?这里加载的对象是ActiveAuction,但它们是否与Auction明确相关?我没有看到任何关系或某种导航属性访问。这就是OP问题的困惑所在。此外,我不太确定延迟加载是否与此相关,因此我只是针对此答案询问更多解释。 - Hopeless
@Hopeless,也许我误解了问题,但我认为OP的问题是这样的:“为什么在我已经在上下文中加载了这些实体(FirstAuctionSecondAuctionThirdAuction及其相关的DepartmentAdmin属性)在另一个查询中,它们已经被加载了……”如果是这样的话,那么延迟加载可能确实有关系(根据评论似乎不是,但我明白它可能与之有关)。 - Jcl

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