实体框架:如果数据已经在上下文中,为什么会访问数据库?

4
为什么要查询已在ObjectContext中表示的记录,数据库会受到影响吗?
以下是我的思考过程:
假设您进行以下查询操作: SiteUser someUser = context.SiteUser.Where(role => role.UserID == 1).ToList()[0]; 这只是一个简单的示例,意思是我想从ID为1的表中获取一个用户。现在假设这是第一次查询,那么我猜测它必须在上下文中创建SiteUser列表,查询数据库,然后填充它的列表。使用分析器,我看到了这个结果:
SELECT 
 [Extent1].[UserID] AS [UserID], 
 [Extent1].[MainEmail] AS [MainEmail], 
 [Extent1].[Password] AS [Password], 
 [Extent1].[UserName] AS [UserName]
FROM [TIDBA].[TI_USER] AS [Extent1]
WHERE 1 = [Extent1].[UserID]

很好。它做到了我所期望的,并且在SiteUser列表中(如果我使用Watch深入挖掘),我可以看到上下文SiteUser列表中有一个项目,恰好是表示此数据行的水化对象。

接下来,我想修改一些内容,但不保存:

someUser.UserName = "HIHIHI";

现在假设由于某种原因我想再次使用相同的上下文来获取它(这是一个奇怪的例子,但实际上是为了证明这个情况确实发生了):
someUser = context.SiteUser.Where(role => role.UserID == 1).ToList()[0];

我认为会发生的情况是它会查看上下文中的SiteUser列表,因为生成的属性是这样说的。(如果不为空,则返回列表)然后它会查看是否存在并返回。没有数据库访问。猜猜分析器说了什么...

SELECT 
 [Extent1].[UserID] AS [UserID], 
 [Extent1].[MainEmail] AS [MainEmail], 
 [Extent1].[Password] AS [Password], 
 [Extent1].[UserName] AS [UserName]
FROM [TIDBA].[TI_USER] AS [Extent1]
WHERE 1 = [Extent1].[UserID]

嗯,我开始考虑也许这是一个肠胃检查,以查看该数据项上是否有任何更改,并仅在客户端未更改的值上更新SiteUser对象。(有点像context.Refresh(RefreshMode.ClientWins, context.SiteUser))。因此,我将其停止在:

someUser = context.SiteUser.Where(role => role.UserID == 1).ToList()[0];

我在数据库中更改了一个值(密码列),并让它命中数据库。但是对象上没有任何变化。

这里似乎有些不对劲。它命中数据库以选择我已经在上下文中填充的对象,但它不应用我在数据库中手动进行的更改。那么它为什么还要命中数据库呢?

更新 在下面一些链接的帮助下,我能够深入挖掘并找到了以下内容:

合并选项

看起来有一个枚举设置告诉如何处理负载。现在在阅读后,我看到了MergeOption.AppendOnly的相关信息:

已存在于对象上下文中的对象不会从数据源加载。这是查询或在EntityCollection<(Of <(TEntity>)>)上调用Load方法时的默认行为。

这表明如果我在上下文中拥有它,则不应该命中数据库。然而,这似乎并不正确。如果OverwriteChanges或PreserveChanges是默认设置,那就有意义了,但它们不是。这似乎与应该发生的情况相矛盾。我能想到的唯一一件事就是“已加载”的意思只是没有覆盖。然而,这并不意味着没有查询到数据库。

4个回答

8

context.SiteUser是一个类型为ObjectQuery的属性。当你执行ObjectQuery时,它将总是访问后端存储。这就是它们所做的。如果你不想执行数据库查询,那么就不要使用ObjectQuery。

听起来你真正想要的是一个函数,它能够说:“如果实体已经在上下文中被实例化,则直接返回它。如果没有,则从数据库中获取它。” 事实上,ObjectContext包括这样一个函数,名为GetObjectByKey

GetObjectByKey尝试从ObjectStateManager中检索具有指定EntityKey的对象。如果该对象当前未加载到对象上下文中,则尝试执行查询以从数据源返回该对象。


看起来默认情况下不这样做似乎有些奇怪。我猜默认的MergeOption说不要查看数据库中的更改,而只在覆盖的上下文中保留它是很烦人的。不过话说回来,它被称为MergeOption... - Programmin Tool

2

在我看来,EF第二次访问数据库的原因是为了确保没有其他行满足查询条件。自第一次查询以来,可能会有其他相关的行插入到表中,EF正在检查是否存在这些行。


1

如果我理解你的问题,这应该是答案:

实际上,实体框架默认的做法是要求实体对象向一个名为状态管理器的框架类发送更改通知,状态管理器会跟踪哪些属性被更改。原始值只在需要时复制。当更新发生时,如果更改的属性被标记为“并发令牌”,那么这些原始值将用于与服务器通信。也就是说,在创建更新语句时,实体框架将包含一个检查,以验证数据库中的行是否仍然具有原始值,如果不是,则会引发异常,通知程序其他人已经更改了数据库中的行。同时,实体框架并不完全需要从属性设置器获取通知,你也可以在应用程序代码中确定哪些内容被修改,并调用框架上的显式方法来指示哪些属性已经更改(但此时框架将只记录属性已经被修改,而不会有原始值)。

这来自于这里。更多相关信息可以在这里这里阅读。

编辑补充:

看起来,使用EF时,有一个ObjectStateManager跟踪更改,从未真正允许断开的数据。为了拥有断开的数据,您需要调用ObjectContext.Detach方法来断开对象。更多信息可以在这里这里找到。


唯一看起来不对的是这没有涉及更新。现在,我可能非常愚钝(很有可能),或者我可以假设默认情况下它始终检查数据库,无论选择、更新或其他操作。那是我该假设的吗? - Programmin Tool
这些链接提供了一些我已经在原始帖子中更新的内容,但不幸的是,它们只增加了我对正在发生的事情感到困惑的程度。 - Programmin Tool

1
如果避免使用 .ToList(),而使用 .FirstOrDefault() 会怎样呢?

不行。每次运行linq“查询”时,第一个(顶部)仍然会命中它。 - Programmin Tool
你有没有执行 context.SiteUser.FirstOrDefault(role => role.UserID == 1) 这段代码?我在想如果你获取了 otherUser 并将其与 someUser 的数据进行比较,会发生什么? - Davy Landman
即使在为someUser和otherUser进行数据填充之间,您在数据库中以某种方式更改了值,这些值仍然保持不变。我怀疑这是因为它知道它处于上下文中,并且MergeOption默认情况下会忽略更改。 - Programmin Tool
如果我没记错的话,你会得到相同的引用(因为你在相同的上下文中)。 - Davy Landman
可以希望如此。如果不行,Entity Framework 可能出现了一些可怕的问题。 - Programmin Tool

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