【问题标题】:SingleOrDefault and FirstOrDefault returning cached dataSingleOrDefault 和 FirstOrDefault 返回缓存数据
【发布时间】:2021-01-04 19:05:04
【问题描述】:

我之前编写的一些代码使用Find() 方法通过主键检索单个实体:

return myContext.Products.Find(id)

这很有效,因为我将这段代码放入了一个泛型类中,并且每个实体都有不同的字段名称作为其主键。

但我不得不替换代码,因为我注意到它正在返回缓存数据,并且我需要它在每次调用时从数据库中返回数据。微软的文档证实这是Find()的行为。

所以我将代码更改为使用SingleOrDefaultFirstOrDefault。我没有在文档中找到任何说明这些方法返回缓存数据的内容。

现在我正在执行这些步骤:

  1. 通过 EF 保存实体。
  2. 在 SSMS 中执行 UPDATE 语句来更新最近保存的 记录的描述字段。
  3. 使用SingleOrDefault 将实体检索到新的实体变量中 或FirstOrDefault

返回的实体在 Description 字段中仍然具有旧值。

我已运行 SQL 跟踪,并验证在第 3 步期间正在查询数据。这让我感到困惑 - 如果 EF 正在往返于数据库,为什么它会返回缓存数据?

我在网上搜索过,大多数答案都适用于Find() 方法。此外,他们提出了一些解决方案,这些解决方案仅仅是变通方法(处理 DbContext 并实例化一个新的)或对我不起作用的解决方案(使用 AsNoTracking() 方法)。

如何从数据库中检索我的实体并绕过 EF 缓存?

【问题讨论】:

  • 工作单元意味着如果你在上下文中有实体,你将从上下文中接收实体;因此 EF 在 DB 中找到实体并使用该 ID 从上下文中返回实体。你有很多选择,比如在查询之前从上下文中分离“缓存”实体,或者将实体加载到新上下文然后附加它等等,但是所有这些变通方法都在喊“你用错了”
  • 如果没有更多信息,我同意@Lanorkin。这很少成为问题的原因是因为 DbContext 对象应该有范围并且生命周期很短(比如只有一个 HTTP 请求的生命周期)。在这么短的时间内,没有理由返回数据库两次获取相同的记录。我怀疑你的 DbContext 对象已经存在太久了。
  • 你找到合适的解决方案了吗@ChadSC???

标签: c# entity-framework-core


【解决方案1】:

您看到的行为在 Microsoft 的 How Queries Work 文章第 3 点下进行了描述:

  1. 对于结果集中的每一项

一个。如果这是一个跟踪查询,EF 会检查数据是否代表上下文实例的更改跟踪器中已经存在的实体

  • 如果是,则返回现有实体

this blog post 中描述的好一点:

原来实体框架使用Identity Map 模式。这意味着一旦具有给定键的实体加载到上下文的缓存中,只要该上下文存在,它就永远不会再次加载。因此,当我们第二次访问数据库以获取客户时,它从数据库中检索了更新后的851 记录,但是由于客户851 已经加载到上下文中,它忽略了数据库中的更新记录(@987654324 @details)。

所有这些都是说,如果您进行查询,它会首先检查主键以查看它是否已经在缓存中。如果是这样,它会使用缓存中的内容。

你如何避免它?首先是确保你不会让你的DbContext 对象存活太久。 DbContext 对象仅设计用于一个工作单元。如果保存时间过长,就会发生不好的事情,例如过多的内存消耗。

  • 是否需要检索数据以显示给用户?创建一个DbContext 以获取数据并丢弃该DbContext
  • 您需要更新记录吗?创建一个新的DbContext,更新记录并丢弃该DbContext

这就是为什么当您在 ASP.NET Core 中使用 EF Core with dependency injection 时,它会以 scoped 的生命周期创建,因此任何 DbContext 对象只能在一个 HTTP 请求的生命周期内存活。

在极少数情况下,您确实需要为已有对象的记录获取新数据,您可以像这样使用EntityEntry.Reload()/EntityEntry.ReloadAsync

myContext.Entry(myProduct).Reload();

如果您只知道 ID,这对您没有帮助。

如果你真的真的需要重新加载一个你只有 ID 的实体,你可以做一些奇怪的事情:

private Product GetProductById(int id) {
    //check if it's in the cache already
    var cachedEntity = myContext.ChangeTracker.Entries<Product>()
                           .FirstOrDefault(p => p.Entity.Id == id);
    if (cachedEntity == null) {
        //not in cache - get it from the database
        return myContext.Products.Find(id);
    } else {
        //we already have it - reload it
        cachedEntity.Reload();
        return cachedEntity.Entity;
    }
}

但同样,这只能在有限的情况下使用,当您已经解决了任何长期存在的 DbContext 对象的情况时,因为不需要的缓存不是唯一的后果。

【讨论】:

  • 最后如何检索最新的更改并绕过 EF 缓存?我有同样的问题和困惑:(
  • @PeymanMajidi 这个答案描述了如何做到这一点。但是您的问题很可能是您在不应该重用 DbContext 对象时重用。
  • 多么棒的答案!谢谢。
【解决方案2】:

好的,我也遇到了同样的问题,终于找到答案了,
你做的一切都是正确的,这就是 EF 的工作方式。 您可以将.AsNoTracking() 用于您的目的:

return myContext.Products.AsNoTracking().Find(id)

确保您在顶部添加了using Microsoft.EntityFrameworkCore;

它就像魔术一样工作

【讨论】:

  • 请记住,如果您这样做并对该对象进行更改,则在您调用 SaveChanges() 时您的更改将不会被保存。
  • 真的吗?为什么 dotnet core 让一切都连线:(
  • 这并不特定于 .NET Core。在旧的实体框架中也是如此,这是您链接到的文档。 This is the documentation for AsNoTracking() in EF Core。如果您想了解何时使用AsNoTracking(),可以阅读这篇文章:Tracking vs. No-Tracking Queries
  • 您几乎不应该使用相同的DbContext 再次检索相同的记录。你可能在不应该重复使用你的DbContext
猜你喜欢
  • 1970-01-01
  • 2011-12-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多