每个Web请求一个DbContext ...为什么?


398

我阅读了许多文章,这些文章解释了如何设置实体框架,DbContext以便使用各种DI框架为每个HTTP Web请求创建和使用一个实体框架。

为什么这首先是个好主意?通过使用这种方法,您可以获得什么优势?在某些情况下这是个好主意吗?使用DbContext每个存储库方法调用实例化s 时,您是否可以使用该技术做一些事情?


9
Gueddari在mehdi.me/ambient-dbcontext-in-ef6调用每个仓库的方法调用一个反的DbContext实例。Quote:“通过这样做,您失去了Entity Framework通过DbContext提供的几乎所有功能,包括其一级缓存,身份映射,工作单元以及变更跟踪和延迟加载功能。 。” 优秀的文章,对处理DBContexts的生命周期提出了很好的建议。绝对值得一读。
克里斯多夫(Christoph)

Answers:


564

注意:这个答案是关于Entity Framework的DbContext,但是它适用于任何类型的工作单元实现,例如LINQ to SQL的DataContext和NHibernate的ISession

让我们从呼应伊恩开始:在DbContext整个应用程序中只有一个是一个坏主意。唯一有意义的情况是当您拥有单线程应用程序和该单个应用程序实例专用的数据库时。这DbContext不是线程安全的,并且由于DbContext缓存数据,因此很快就会过时。当多个用户/应用程序同时在该数据库上工作时,这将给您带来各种麻烦(这是很常见的)。但是我希望您已经知道这一点,并且只想知道为什么不将它的新实例(即具有短暂生活方式)DbContext注入需要的任何人。(有关为何选择一个DbContext线程(甚至每个线程的上下文)不好的,请阅读此答案)。

首先让我说,将DbContext临时工作注册为可行,但是通常您希望在一定范围内拥有一个这样的工作单元的单个实例。在Web应用程序中,在Web请求的边界上定义这样的范围可能是实用的。因此,按网络请求的生活方式。这使您可以让整套对象在同一上下文中操作。换句话说,它们在同一业务交易中运作。

如果您没有目标要在同一上下文中进行一组操作,那么在这种情况下,短暂的生活方式就可以了,但是需要注意以下几点:

  • 由于每个对象都有其自己的实例,因此更改系统状态的每个类都需要调用_context.SaveChanges()(否则更改将丢失)。这可能会使您的代码复杂化,并给代码增加第二个责任(控制上下文的责任),并且违反了“ 单一责任原则”
  • 您需要确保[由DbContext] 加载和保存的实体永远不会离开此类的范围,因为它们不能在另一个类的上下文实例中使用。这会使您的代码变得非常复杂,因为当您需要这些实体时,需要通过id重新加载它们,这也可能导致性能问题。
  • 自从DbContext实现以来IDisposable,您可能仍想处置所有创建的实例。如果要执行此操作,则基本上有两个选择。您需要在调用后立即以相同的方法处理它们context.SaveChanges(),但是在这种情况下,业务逻辑将获取对象的所有权,该对象将从外部传递出去。第二种选择是将所有创建的实例放置在Http请求的边界上,但是在那种情况下,您仍然需要某种范围设定,以使容器知道何时需要释放这些实例。

另一种选择是根本不注入a DbContext。相反,您注入了一个DbContextFactory能够创建新实例的(我过去曾经使用这种方法)。这样,业务逻辑即可明确控制上下文。如果可能看起来像这样:

public void SomeOperation()
{
    using (var context = this.contextFactory.CreateNew())
    {
        var entities = this.otherDependency.Operate(
            context, "some value");

        context.Entities.InsertOnSubmit(entities);

        context.SaveChanges();
    }
}

好的一面是您可以DbContext明确管理显式工具的生命,并且很容易进行设置。它还允许您在一定范围内使用单个上下文,这具有明显的优势,例如在单个业务事务中运行代码,并且能够传递实体,因为它们源自同一实体DbContext

缺点是您将必须在DbContextfrom方法与方法之间进行传递(这称为“方法注入”)。请注意,从某种意义上说,此解决方案与“作用域”方法相同,但是现在范围是在应用程序代码本身中控制的(并且可能会重复很多次)。负责创建和处理工作单元的是应用程序。由于DbContext是在构造依赖关系图之后创建的,因此构造函数注入不在图片之内,并且当您需要将上下文从一个类传递到另一类时,您需要遵循方法注入。

方法注入并没有那么糟糕,但是当业务逻辑变得更加复杂,并且涉及到更多的类时,您将不得不将其从方法传递到方法,并将类传递给类,这会使代码复杂化很多(我已经看到了)过去)。对于简单的应用程序,此方法虽然会很好。

由于不利因素,这种工厂方法适用于较大的系统,另一种方法可能有用,那就是让容器或基础结构代码/ 组合根管理工作单元的方法。这是您的问题涉及的样式。

通过让容器和/或基础结构处理此问题,您的应用程序代码不会因必须创建,(可选)提交和处置UoW实例而受到污染,这使业务逻辑变得简单明了(仅是单一职责)。这种方法存在一些困难。例如,您是否提交并处置该实例?

可以在Web请求结束时完成工作单元的布置。但是,许多人错误地认为这也是提交工作单元的地方。但是,在应用程序中的那一点上,您根本无法确定应确实落实工作单元。例如,如果业务层的代码抛出异常是被提到的调用堆栈越高,你肯定希望提交。

真正的解决方案是再次明确管理某种范围,但是这次在“合成根”内部执行。在命令/处理程序模式之后抽象所有业务逻辑,您将能够编写一个装饰器,该装饰器可以包装在允许执行此操作的每个命令处理器周围。例:

class TransactionalCommandHandlerDecorator<TCommand>
    : ICommandHandler<TCommand>
{
    readonly DbContext context;
    readonly ICommandHandler<TCommand> decorated;

    public TransactionCommandHandlerDecorator(
        DbContext context,
        ICommandHandler<TCommand> decorated)
    {
        this.context = context;
        this.decorated = decorated;
    }

    public void Handle(TCommand command)
    {
        this.decorated.Handle(command);

        context.SaveChanges();
    } 
}

这样可以确保您只需要编写一次此基础结构代码。任何固态DI容器都允许您配置这样的装饰器,使其ICommandHandler<T>以一致的方式包装在所有实现中。


2
哇-感谢您的详尽回答。如果我可以投票两次,我会的。在上面,您说“ ...无意让整套操作在相同的上下文中进行操作,在这种情况下,短暂的生活方式很好...”。具体来说,“瞬态”是什么意思?
2012年

14
@Andrew:“瞬态”是一个依赖注入概念,这意味着如果将服务配置为瞬态的,则每次将其注入使用者时都会创建该服务的新实例。
史蒂文

1
@ user981375:对于CRUD操作,您可以创建一个泛型CreateCommand<TEnity>和一个泛型CreateCommandHandler<TEntity> : ICommandHandler<CreateCommand<TEntity>>(对Update,Delete和Delete进行相同的操作,并且只有一个GetByIdQuery<TEntity>查询)。不过,您应该问自己:此模型对于CRUD操作是否有用,还是是否增加了复杂性。不过,您可能会受益于使用此模型轻松添加跨领域关注点(通过装饰器)的可能性。您必须权衡利弊。
史蒂文

3
+1您是否相信我在实际阅读本文之前已经写下了所有答案?顺便说一句,IMO,我认为对您而言,在最后讨论DbContext的处置非常重要(尽管它的好处是您
始终与

1
但是您没有将上下文传递给修饰的类,修饰的类如何与传递给的相同上下文一起工作TransactionCommandHandlerDecorator?例如,如果装饰的类是InsertCommandHandlerclass,它如何将插入操作注册到上下文(EF中的DbContext)?
Masoud 2014年

34

这里没有一个答案实际上可以回答问题。OP没有询问单例/每个应用程序的DbContext设计,而是询问了每个(Web)请求的设计以及可能存在的潜在好处。

我会参考 http://mehdi.me/ambient-dbcontext-in-ef6/,因为Mehdi是一个很棒的资源:

可能的性能提升。

每个DbContext实例都维护其从数据库中加载的所有实体的一级缓存。每当您通过实体的主键查询实体时,DbContext都会首先尝试从其一级缓存中检索它,然后默认从数据库中查询它。根据您的数据查询模式,由于DbContext一级缓存,在多个顺序业务交易中重复使用相同的DbContext可能会导致进行较少的数据库查询。

它启用了延迟加载。

如果您的服务返回了持久实体(而不是返回视图模型或其他类型的DTO),并且您想利用这些实体上的延迟加载,则从中检索这些实体的DbContext实例的生命周期必须超出业务交易的范围。如果服务方法在返回之前将其使用的DbContext实例进行了处置,则任何在返回的实体上延迟加载属性的尝试都将失败(无论是否使用延迟加载都是一个好主意,这是一个完全不同的争论,我们将不再讨论这里)。在我们的Web应用程序示例中,通常在控制器操作方法中对由单独的服务层返回的实体使用延迟加载。在这种情况下,

请记住,还有缺点。该链接包含许多其他资源可供阅读。

如果其他人偶然发现了这个问题,而又不会被那些无法真正解决该问题的答案所困扰,则只需发布此内容即可。


好的链接!显式管理DBContext似乎是最安全的方法。
aggsol

34

微软有两个相互矛盾的建议,许多人以完全不同的方式使用DbContext。

  1. 一项建议是“尽快处理DbContext”。 因为拥有DbContext Alive会占用数据库连接等宝贵资源。
  2. 另一个声明高度建议每个请求一个DbContext

两者相互矛盾,因为如果您的Request做很多与Db无关的事情,那么您的DbContext将被无故保留。因此,在您的请求只是等待随机操作完成时,保持DbContext处于活动状态是浪费的……

如此多遵循规则1的人将其DbContexts置于其“存储库模式”中,根据数据库查询创建一个新实例,因此每个请求X * DbContext

他们只是获取数据并尽快处理上下文。许多人认为这是可接受的做法。尽管这样做的好处是可以在最短的时间内占用数据库资源,但这显然会牺牲EF必须提供的所有UnitOfWork缓存糖果。

使DbContext 的一个多用途实例保持活动状态可以最大程度地提高缓存的好处,但是由于DbContext 并不是线程安全的,并且每个Web请求都在其自己的线程上运行,因此每个请求的DbContext是您可以保留的最长时间

因此,EF团队建议每个请求使用1个Db上下文,这显然是基于以下事实:在Web应用程序中,一个UnitOfWork最有可能在一个请求内,并且该请求只有一个线程。因此,每个请求一个DbContext就像UnitOfWork和Caching的理想好处。

但是在很多情况下,这是不正确的。我考虑记录一个单独的UnitOfWork,因此在异步线程中具有一个用于请求后记录的新DbContext 是完全可以接受的

因此最后,它拒绝了DbContext的生存期仅限于这两个参数。工作单元线程


3
公平地说,您的HTTP请求应该很快完成(几毫秒)。如果它们要比这更长,那么您可能要考虑使用诸如外部作业调度程序之类的东西进行一些后台处理,以便请求可以立即返回。就是说,您的架构也不应该真正依赖HTTP。总体而言,这是一个很好的答案。
美眉

22

我可以肯定这是因为DbContext根本不是线程安全的。因此,共享事物绝不是一个好主意。


您是说在HTTP请求之间共享它从来不是一个好主意吗?
安德鲁(Andrew)

2
是的,安德鲁就是他的意思。共享上下文仅适用于单线程桌面应用程序。
伊丽莎白2013年

10
共享一个请求的上下文呢?因此,对于一个请求,我们可以访问不同的存储库,并通过共享一个相同的上下文在它们之间进行事务?
Lyubomir Velchev

16

问题或讨论中未真正解决的一件事是DbContext无法取消更改。您可以提交更改,但不能清除更改树,因此,如果使用基于请求的上下文,那么无论出于何种原因都需要放弃更改,那么您将很不走运。

我个人在需要时创建DbContext实例-通常附加到能够根据需要重新创建上下文的业务组件。这样,我可以控制该过程,而不必强加给我一个实例。我也不必在每次控制器启动时都创建DbContext,无论是否实际使用它。然后,如果我仍然希望按请求拥有实例,则可以在CTOR中创建它们(通过DI或手动),也可以根据需要在每种控制器方法中创建它们。就我个人而言,我通常采用后一种方法,以避免在实际上不需要它们时创建DbContext实例。

这也取决于您从哪个角度看。对我来说,每个请求实例从来没有道理。DbContext是否真的属于Http请求?就行为而言,这是错误的地方。您的业​​务组件应该创建您的上下文,而不是Http请求。然后,您可以根据需要创建或丢弃业务组件,而不必担心上下文的生命周期。


1
这是一个有趣的答案,我部分同意您的看法。对我而言,DbContext不必绑定到Web请求,但是它总是像一个“业务交易”中那样键入到单个“请求”中。而且,当您将上下文与业务交易联系在一起时,取消更改就变得很奇怪。但是,如果不在Web请求边界上使用它,并不意味着业务组件(BC)应该创建上下文。我认为这不是他们的责任。相反,您可以使用BC周围的装饰器来应用范围。这样,您甚至可以更改作用域,而无需更改任何代码。
史蒂文

1
在那种情况下,注入业务对象应该处理生命周期管理。在我看来,业务对象拥有上下文,因此应该控制生命周期。
里克·斯特拉尔

简而言之,当您说“如果需要,可以重新创建上下文”是什么意思?您是否正在滚动自己的回滚功能?你能详细点吗?
tntwyckoff

2
就我个人而言,我认为在此开始强制DbContext有点麻烦。不能保证您甚至需要访问数据库。也许您正在呼叫一个第三方服务,该服务会改变那一侧的状态。或者,也许您实际上同时有2个或3个数据库。您一开始就不会创建一堆DbContext,以防万一您最终使用它们。企业知道它正在使用的数据,因此它属于该数据。如果需要,只需将TransactionScope放在开始位置即可。我认为并非所有电话都需要一个。它确实占用资源。
丹尼尔·洛伦兹

这就是是否允许容器控制dbcontext的生存期的问题,该生存期然后控制了父控件的生存期,有时是不适当的。假设如果我希望将简单的服务单例注入到控制器中,则由于每个请求的语义,我将无法使用构造函数注入。
davidcarr

10

我同意以前的意见。可以说,如果要在单线程应用程序中共享DbContext,则需要更多内存。例如,我在Azure上的Web应用程序(一个额外的小型实例)需要另外150 MB的内存,而我每小时大约有30个用户。 应用程序在HTTP请求中共享DBContext

这是真实的示例图像:应用程序已在12PM部署


可能的想法是共享一个请求的上下文。如果我们访问不同的存储库和-DBSet类,并希望对它们的操作具有事务性,那么这将是一个很好的解决方案。看一下开源项目mvcforum.com,我认为这是在其实施Unit Of Work设计模式时完成的。
Lyubomir Velchev

3

我喜欢它的原因是它使工作单位(如用户所见-即页面提交)与ORM意义上的工作单位对齐。

因此,您可以使整个页面提交都具有事务性,如果在每个CRUD方法都创建一个新上下文的情况下公开它,则无法做到这一点。


2

即使在单线程单用户应用程序中也不使用单例DbContext的另一个低估原因是由于它使用的身份映射模式。这意味着每次使用查询或按ID检索数据时,它将检索到的实体实例保存在缓存中。下次您检索同一实体时,它将为您提供该实体的缓存实例(如果有),并具有您在同一会话中所做的任何修改。这是必需的,因此SaveChanges方法不会以同一数据库记录的多个不同实体实例结尾。否则,上下文将必须以某种方式合并所有那些实体实例中的数据。

问题的原因在于单例DbContext可能成为定时炸弹,最终可能会缓存整个数据库+ .NET对象在内存中的开销。

通过仅将Linq查询与.NoTracking()扩展方法结合使用,可以解决此问题。同样,这些天PC具有大量RAM。但是通常这不是期望的行为。


这是正确的,但是您必须假定垃圾收集器可以正常工作,从而使此问题比实际更虚拟。
tocqueville '16

2
垃圾收集器不会收集活动的静态/单个对象持有的任何对象实例。它们将最终出现在堆的第2代中。
德米特里S.

1

特别要注意的是,使用Entity Framework的另一个问题是结合使用创建新实体,延迟加载,然后使用这些新实体(来自同一上下文)的组合。如果不使用IDbSet.Create(相对于刚刚使用的),则从实体创建时从该实体上检索到该实体时,延迟加载将不起作用。

 public class Foo {
     public string Id {get; set; }
     public string BarId {get; set; }
     // lazy loaded relationship to bar
     public virtual Bar Bar { get; set;}
 }
 var foo = new Foo {
     Id = "foo id"
     BarId = "some existing bar id"
 };
 dbContext.Set<Foo>().Add(foo);
 dbContext.SaveChanges();

 // some other code, using the same context
 var foo = dbContext.Set<Foo>().Find("foo id");
 var barProp = foo.Bar.SomeBarProp; // fails with null reference even though we have BarId set.
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.