每个web请求一个DbContext…为什么?

我读了很多文章,解释如何设置实体框架的DbContext,以便每个HTTP web请求使用各种DI框架只创建和使用一个。

为什么一开始这是个好主意?使用这种方法有什么好处?在某些情况下,这是个好主意吗?在每个存储库方法调用实例化DbContexts时,是否有一些事情是你可以使用这种技术做而不能做的?

113444 次浏览

我喜欢它的地方在于,它将工作单元(正如用户看到的那样——即页面提交)与ORM意义上的工作单元对齐。

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

我很确定这是因为DbContext根本不是线程安全的。所以分享从来不是个好主意。

注意:这个答案谈论的是实体框架的DbContext,但是 它适用于任何类型的工作单元实现,例如 LINQ到SQL的DataContext和NHibernate的ISession.

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

首先我要说的是,将DbContext注册为瞬态对象是可行的,但通常情况下,您希望在特定范围内拥有这样一个工作单元的单个实例。在web应用程序中,在web请求的边界上定义这样一个作用域是可行的;因此是Per Web Request生活方式。这允许您让一整套对象在同一上下文中操作。换句话说,它们在同一个业务事务中运行。

如果您没有在同一上下文中运行一组操作的目标,在这种情况下,暂时的生活方式是可以的,但有几件事情需要注意:

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

另一个选项是注入一个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

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

方法注入并不是那么糟糕,但是当业务逻辑变得更加复杂,涉及到更多的类时,您将不得不从一个方法传递到另一个方法,从一个类传递到另一个类,这可能会使代码变得非常复杂(我以前见过这种情况)。对于一个简单的应用程序,这种方法就可以了。

由于这种工厂方法的缺点,它适用于更大的系统,另一种方法可能是有用的,那就是让容器或基础设施代码/ 根组成管理工作单元。这就是你的问题所涉及的风格。

通过让容器和/或基础设施处理这个问题,您的应用程序代码不会因为必须创建、(可选地)提交和处置UoW实例而受到污染,这使业务逻辑简单而干净(只有一个Single Responsibility)。这种方法有一些困难。例如,你是提交和处置实例?

处理一个工作单元可以在web请求的末尾完成。然而,许多人认为不正确也是提交工作单元的地方。然而,在应用程序的这一点上,您根本不能确定工作单元实际上应该被提交。例:如果业务层代码抛出了一个异常,该异常在调用堆栈的上层被捕获,你肯定想要提交。

真正的解决方案仍然是显式地管理某种范围,但这次在Composition Root中进行。抽象命令/处理程序模式背后的所有业务逻辑,你将能够编写一个装饰器,它可以包裹在允许这样做的每个命令处理程序周围。例子:

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>实现。

我同意之前的观点。可以说,如果你打算在单线程应用中共享DbContext,你将需要更多的内存。例如,我在Azure上的web应用程序(一个额外的小实例)需要另外150mb的内存,而我每小时大约有30个用户。  HTTP Request中的应用程序共享DBContext

.

这是一个真实的例子:应用程序已经在中午12点部署

另一个不使用单例DbContext(即使是在单线程单用户应用程序中)的低调原因是它使用的标识映射模式。这意味着每次使用查询或通过id检索数据时,它都会将检索到的实体实例保存在缓存中。下次检索相同的实体时,它将为您提供该实体的缓存实例(如果可用的话)以及您在同一会话中所做的任何修改。这是必要的,这样SaveChanges方法就不会得到同一个数据库记录的多个不同实体实例;否则,上下文将不得不以某种方式合并来自所有这些实体实例的数据。

这是一个问题,因为单例DbContext会成为一个定时炸弹,最终会缓存整个数据库和。net对象在内存中的开销。

有一些方法可以绕过这种行为,只需使用带有.NoTracking()扩展方法的Linq查询。而且现在的电脑有很多内存。但这通常不是理想的行为。

这里没有一个答案能真正回答这个问题。OP并没有询问单例/每个应用程序的DbContext设计,他询问了每个(web)请求的设计以及可能存在的潜在好处。

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

可能的性能提升。

每个DbContext实例维护它从数据库加载的所有实体的一级缓存。每当您通过主键查询一个实体时,DbContext将首先尝试从一级缓存中检索它,然后默认从数据库中查询它。根据您的数据查询模式,由于DbContext一级缓存,跨多个连续业务事务重用相同的DbContext可能会导致较少的数据库查询。

它支持延迟加载。

如果您的服务返回持久实体(而不是返回视图模型或其他类型的dto),并且您希望利用这些实体的惰性加载,那么从中检索这些实体的DbContext实例的生命周期必须扩展到业务事务的范围之外。如果service方法在返回之前释放了它使用的DbContext实例,那么任何延迟加载返回实体属性的尝试都将失败(是否使用延迟加载是一个好主意是完全不同的争论,我们不会在这里讨论)。在我们的web应用程序示例中,延迟加载通常用于由单独的服务层返回的实体上的控制器动作方法。在这种情况下,服务方法用来加载这些实体的DbContext实例需要在web请求期间保持活动(或者至少在动作方法完成之前)。

记住,也有缺点。这个链接包含了许多关于这个主题的其他资源。

我把这篇文章贴出来,以防别人偶然发现这个问题,并没有被那些并没有真正解决这个问题的答案所吸引。

在这个问题或讨论中没有真正解决的一件事是DbContext不能取消更改的事实。您可以提交更改,但不能清除更改树,因此如果您使用了每个请求上下文,那么无论出于何种原因需要丢弃更改,都是不走运的。

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

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

实体框架需要特别注意的另一个问题是,当组合使用创建新实体、延迟加载,然后使用这些新实体时(从相同的上下文中)。如果你不使用IDbSet。创建(vs只是新建),当它从创建它的上下文检索时,对该实体的惰性加载将不起作用。例子:

 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.

微软有两个矛盾推荐,许多人以完全不同的方式使用dbcontext。

  1. 一个建议是"Dispose DbContexts as soon as possible " 因为DbContext激活会占用有价值的资源,比如db 李连接等等…< / >
  2. 另一个声明每个请求一个DbContext是high 李推荐< /强> < / >
这些相互矛盾,因为如果你的请求做了很多与Db无关的事情,那么你的DbContext就会被毫无理由地保留。 因此,当你的请求只是等待随机的东西完成时,让你的DbContext保持活跃是浪费…< / p >

所以许多遵循规则1的人在他们的“库模式”中有他们的dbcontext,并在每个请求中创建每个数据库查询一个新的实例,即X * DbContext

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

保留DbContext的单个多用途实例最大限度地提高了缓存的好处,但由于DbContext是不线程安全,并且每个Web请求运行在它自己的线程上,每个请求的DbContext是你可以保留的最长的

所以EF的团队建议每个请求使用1 Db Context,这显然是基于这样一个事实:在一个Web应用程序中,一个UnitOfWork最有可能是在一个请求中,这个请求有一个线程。所以每个请求一个DbContext就像UnitOfWork和缓存的理想好处。

在许多情况下这是不正确的。 我认为日志记录是一个单独的UnitOfWork,因此在异步线程中有一个新的请求后日志记录DbContext是完全可以接受的

最后,DbContext的生命周期被限制在这两个参数上。UnitOfWork线程