【问题标题】:Single DbContext instance in multi-thread application多线程应用程序中的单个 DbContext 实例
【发布时间】:2019-09-09 17:22:18
【问题描述】:

在我目前正在处理的一个 MVVM WPF 应用程序中,Entity Framework 6 的使用如下:

  • 代码优先实体模型
  • 在应用程序启动时创建一个 DbContext 并在其间共享(通常在构造函数中传递)
  • 某些实体的某些属性直接绑定到 UI - 或者未映射并保存与 UI 相关的内容,例如。在此类实体的构造函数中创建的用户控件
  • 有一个单独的线程刷新“作业”供我们的应用程序执行 - 这些“作业”由外部应用程序直接写入数据库
  • UI 线程使用相同的 DbContext 来添加、删除或更改那些“作业”和其他实体在 UI 中的点击操作
  • 还有另一个单独的线程用于刷新和管理其他实体
  • 实体之间利用延迟加载的优势相互链接

首先,我们遇到了 context.SaveChanges() 的问题 - 我们遇到了各种不同的异常,例如:

"New transaction is not allowed because there are other threads running in the session"

"The property "ID" is part of the object's key information and cannot be modified"

"The transaction operation cannot be performed because there are pending requests working on this transaction"

因此,我们在上下文类中为此实现了简单的锁定,希望能解决这个问题:

public override int SaveChanges()
{
    lock (this)
    {
        return base.SaveChanges();
    }
}

这只是部分帮助,因为现在我们遇到了以下异常,该异常出现的频率较低:

"An error occurred while saving entities that do not expose foreign key properties for their relationships"

此外,我们有时会遇到链接属性的问题。尽管它们都被定义为虚拟以启用延迟加载,但有时我们确实会收到空引用异常,因为它们不会被链接。

我主要担心的是:

  • 经过一些研究,我发现 EF 的这种实现并不像它应该的那样(上下文应该是短暂的)
  • 在模型中包含 UI 绑定打破了 SoC 范式
  • DbContext 不是线程安全的

我认为理想情况下我们应该重构架构——也许通过开发一些单独的层来处理这些问题,但这在我们的案例中会很耗时,而不是更可取的解决方案。

有没有一种方法可以像在我们的应用程序中设计的那样使用 DbContext 和 EF6,并进行一些更改来解决问题?

【问题讨论】:

  • 你在使用 DI 吗?如果是这样,您可以将上下文注册为作用域,以便在请求的生命周期内重复使用。
  • 在桌面应用程序中,DbContext 不需要是短暂的,但您需要每个线程单独的 DbContext。但是,您可以拥有一个 DbContext,其生命周期范围为 ViewModel,在桌面应用程序中,它可能长达几分钟。

标签: c# wpf multithreading entity-framework dbcontext


【解决方案1】:

到目前为止,您对问题的分析是正确的。 DbContexts 应该是短暂的。 UI 不应绑定到实体(即使 很多 示例这样做,甚至来自 Microsoft),而且 DbContext 肯定不是线程安全的。坏消息是,不可能有一个简单的方法来解决这个问题。

从一个长期存在的 DbContext 切换到短期的 DbContext 是一件相当简单的事情。但是,处理可能在 DbContext 实例之间传递的实体引用并不是那么简单。这直接与绑定到实体的第二个缺陷融为一体,因为这些实体是从域/业务逻辑到视图并返回的。短期上下文应该在很大程度上解决线程安全问题,只要实体不在上下文之间移动。

根据应用程序的成熟程度和时间压力,在已建立的应用程序中解决此类问题可能很困难,但这并非不可能。关键是确定应用程序中出现最多问题的领域,并首先解决这些问题。一个相当小但被大量使用的区域将是测试重构代码库所涉及的水域的理想选择。为了避免破坏一切,您可以利用有界上下文定义来拆分应用程序的每个区域的 DbContext 定义,以便对于一组相关视图,您可以组成视图模型结构并从新的、短暂的 DbContext 填充它管理检索这些屏幕所需的所有实体以及所有持久性。如果应用程序结构正在传递大量实体,困难的部分将是为您的代码建立边界。在这些边界处,方法签名将需要更改,以便原始 DbContext 中的实体不会流入新代码,并且对旧代码的调用可以将视图模型转换回原始 DbContext 范围内的实体。最终,尽管您使用的方法需要反映应用程序的确切结构。严重依赖抽象、泛型等的代码可能更难重构。

另一种方法是使用Detach/AttachAsNoTracking 开始处理分离的实体。不过,在我看来,这可能会使您陷入更深的复杂性兔子洞,因此我会谨慎地将其视为可能的解决方案。 AsNoTracking 在您从可能很大的记录子集读取数据并避免将它们与 DbContext 关联的成本的情况下是一个不错的选择。通常,DbContext 跟踪的实体越多,针对上下文的操作就越慢。因此,搜索和报告等操作受益于AsNoTracking。但是,在分离/附加实体时,您需要特意检查上下文实例,以查看它是否在附加之前尚未跟踪具有相同 PK 的实例,并确保提供的实体尚未被另一个 DbContext 跟踪。处理分离/附加嵌套实体图也很痛苦。

流行语录的变体。 “有人认为他们可以通过使用分离的实体来解决问题,但现在有两个问题。” :)

【讨论】:

    【解决方案2】:

    根据您的用例,在启动查询时,使用 .AsNoTracking() 扩展方法可能会有所帮助。这使得 DbContext 不会跟踪对对象属性的更改,因此如果一个线程使用 .AsNoTracking() 获取数据,而另一个线程更新一组可能存在冲突的数据(不使用 .AsNoTracking()),则这两个操作不应干扰彼此。

    【讨论】:

      猜你喜欢
      • 2023-02-10
      • 2012-12-02
      • 2013-06-02
      • 2019-05-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多