Entity Framework - 并发使用容器

Entity Framework - concurrent use of containers

在基于 Entity Framework 的应用程序的业务逻辑层中,所有作用于 DB 的方法都应该(据我所知)包含在:

using(FunkyContainer fc = new FunkyContainer())
{
    // do the thing

    fc.SaveChanges();
}

当然,为了方便起见,这些方法经常相互使用,不再赘述。我在这里看到的风险如下:

public void MainMethod()
{
    using(FunkyContainer fc = new FunkyContainer())
    {
        // perform some operations on fc
        // modify a few objects downloaded from DB

        int x = HelperMethod();

        // act on fc again

        fc.SaveChanges();
    }
}
public int HelperMethod()
{
    using(FunkyContainer fc2 = new FunkyContainer())
    {
        // act on fc2 an then:

        fc2.SaveChanges();

        return 42;
    } 
}

我觉得不太好,创建容器 fc2 时,而 fc 仍处于打开状态且尚未保存。所以这引出了我的第一个问题:

  1. 同时打开多个容器并随意操作是否可以接受?

我得出一个结论,我可以像这样写一个简单的守卫风格的对象:

public sealed class FunkyContainerAccessGuard : IDisposable
{
    private static FunkyContainer GlobalContainer { get; private set; }
    public FunkyContainer Container // simply a non-static adapter for syntactic convenience
    {
        get
        {
            return GlobalContainer;
        }
    }

    private bool IsRootOfHierarchy { get; set; }

    public FunkyContainerAccessGuard()
    {
        IsRootOfHierarchy = (GlobalContainer == null);

        if (IsRootOfHierarchy)
            GlobalContainer = new FunkyContainer();
    }

    public void Dispose()
    {
        if (IsRootOfHierarchy)
        {
            GlobalContainer.Dispose();

            GlobalContainer = null;
        }
    }
}

现在的用法如下:

public void MainMethod()
{
    using(FunkyContainerAccessGuard guard = new FunkyContainerAccessGuard())
    {
        FunkyContainer fc = guard.Container;

        // do anything with fc

        int x = HelperMethod();

        fc.SaveChanges();
    }
}
public int HelperMethod()
{
    using(FunkyContainerAccessGuard guard = new FunkyContainerAccessGuard())
    {
        FunkyContainer fc2 = guard.Container;

        // do anything with fc2

        fc2.SaveChanges();
    }
}

HelperMethodMainMethod调用时,GlobalContainer已经创建,并且被两个方法使用,所以没有冲突。而且HelperMethod也可以单独使用,然后自己创建容器

然而,这对我来说似乎是一个巨大的矫枉过正;所以:

  1. 这个问题是否已经以某些 class(IoC?)或至少一些不错的设计模式的形式得到解决?

谢谢。

  1. 同时打开多个容器并随意操作是否可以接受?

通常这是完全可以接受的,有时甚至是必要的,但你必须谨慎对待。同时拥有多个容器在进行多线程操作时特别方便。由于 db 通常的工作方式,每个线程都应该有自己的 DbContext,不应与其他线程共享。同时使用多个 DbContext 的缺点是它们中的每一个都将使用单独的数据库连接,并且有时它们是有限的,这可能导致应用程序偶尔无法连接到数据库。另一个缺点是一个 DbContext 生成的实体可能无法与其他 DbContext 生成的实体一起使用。在您的示例 HelperMethod returns 原始类型中,所以这是完全安全的,但是如果它将 return 一些实体对象,您希望在 MainMethod 中分配给例如某些导航 属性由 MainMethod DbContext 创建的实体,那么您将收到一个异常。要在 MainMethod 中克服这个问题,您必须使用实体的 ID return 由 HelperMethod 再次检索该实体,这次使用 fc 上下文。另一方面,使用多个上下文有一个优点——如果一个上下文有一些问题,例如它试图保存违反索引约束的内容,那么接下来所有保存更改的尝试都会导致与错误更改相同的异常仍然悬而未决。如果您使用多个 DbContext,那么如果其中一个失败,则第二个将独立运行 - 这就是为什么 DbContext 不应该存在太久的原因。所以通常我会说最好的使用规则是:

  • 每个线程应该使用一个单独的 DbContext
  • 在同一个线程上执行的所有方法应该共享同一个 DbContext

当然,如果要完成的工作很短,上述内容也适用。 DbContext 不应该长寿。最好的例子是 web 应用程序——每个服务器请求都由单独的线程处理,并且生成响应的操作通常不会花费很长时间。在这种情况下,为了方便起见,为生成一个响应而执行的所有方法都应该共享同一个 DbContext。但是每个请求都应该由单独的 DbContext 提供服务。

  1. 这个问题是否已经以某些 class(IoC?)或至少一些不错的设计模式的形式得到解决?

您需要确保您的 DbContext class 是每个线程的单例,但每个线程都有自己的 class 实例。在我看来,确保这一点的最佳方法是使用 IoC。例如,在 Autofac 的 Web 应用程序中,我使用以下规则注册我的 DbContext:

builder
    .RegisterType<MyDbContext>()
    .InstancePerHttpRequest();

通过这种方式,autofac IoC 为每个请求生成一个 DbContext,并在请求服务线程中共享现有实例。您无需在此处关心处置 DbContext。当您的线程结束时,您的 IoC 将执行此操作。

在大多数情况下,同时在多个连接中工作并不是正确的方法,因为:

  1. 您可能会遇到 SQL 服务器无法解决的分布式死锁。
  2. 您可能看不到之前写入但尚未提交的数据。
  3. 您不能跨上下文边界共享实体(此处:方法)。
  4. 更多的资源使用。
  5. 无法跨上下文边界进行交易(此处:方法)。

这些都是非常严重的缺点。通常,最好的模型是为应用正在处理的请求(HTTP 或 WCF 请求)提供一个上下文、连接和事务。这设置起来非常简单,并且避免了很多问题。

EF 应该用作活动对象模型。不要通过将其减少为 CRUD 来削弱它。

static FunkyContainer GlobalContainer

那是行不通的。您不应该跨请求共享上下文。超级危险。考虑将上下文存储在 HttpContext.Items 或您的应用程序中的每个请求存储中。