我一直在阅读很多文章,解释如何设置Entity Framework的DbContext
,以便每个HTTP Web请求只使用各种DI框架创建和使用一个。
为什么这首先是一个好主意?使用这种方法有什么好处?在某些情况下这是个好主意吗?在使用每个存储库方法调用实例化DbContext
时,您是否可以使用此技术执行某些操作?
答案 0 :(得分:524)
注意:这个答案谈到了实体框架的
DbContext
,但是 它适用于任何类型的工作单元实施,例如 LINQ to SQLDataContext
和NHibernate'ISession
。
让我们回首伊恩:对整个应用程序来说只有一个DbContext
是一个坏主意。唯一有意义的情况是,您拥有单线程应用程序和仅由该单个应用程序实例使用的数据库。 DbContext
不是线程安全的,并且由于DbContext
缓存数据,它很快就会变得陈旧。当多个用户/应用程序同时处理该数据库时,这会让您遇到各种麻烦(当然这很常见)。但是我希望你已经知道这一点,并且只想知道为什么不向需要它的人注入DbContext
的新实例(即具有短暂的生活方式)。 (有关单个DbContext
- 甚至每个线程的上下文 - 为什么不好的更多信息,请阅读this answer)。
首先让我说,将DbContext
注册为瞬态可能有效,但通常您希望在特定范围内拥有此类工作单元的单个实例。在Web应用程序中,在Web请求的边界上定义这样的范围是可行的;因此,每Web请求的生活方式。这允许您让整组对象在相同的上下文中运行。换句话说,它们在同一商业交易中运作。
如果你没有让一组操作在同一个环境中运行的目标,那么短暂的生活方式很好,但有一些事情值得关注:
_context.SaveChanges()
(否则更改会丢失)。这可能会使您的代码复杂化,并对代码(控制上下文的责任)增加了第二个责任,并且违反了Single Responsibility Principle。DbContext
实现DbContext
,您可能仍希望Dispose所有已创建的实例。如果你想这样做,你基本上有两个选择。您需要在调用IDisposable
之后立即将它们置于同一方法中,但在这种情况下,业务逻辑将获取从外部传递的对象的所有权。第二个选项是在Http请求的边界上处理所有创建的实例,但在这种情况下,您仍然需要某种范围,以便让容器知道这些实例何时需要处置。另一种选择是不注入context.SaveChanges()
。相反,您注入了一个能够创建新实例的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
。 / p>
缺点是您必须将DbContext
从方法传递给方法(称为方法注入)。请注意,从某种意义上说,这个解决方案与'作用范围相同。方法,但现在范围在应用程序代码本身中控制(并且可能重复多次)。应用程序负责创建和处理工作单元。由于DbContext
是在构造依赖图之后创建的,因此构造函数注入不在图片中,当您需要将上下文从一个类传递到另一个类时,需要遵循方法注入。
方法注入并不是那么糟糕,但是当业务逻辑变得更复杂,涉及更多类时,你必须将它从方法传递给方法,将类传递给类,这会使代码复杂化很多(我过去见过这个)。对于一个简单的应用程序,这种方法可以做得很好。
由于缺点,这种工厂方法适用于更大的系统,另一种方法可能很有用,您可以让容器或基础设施代码/ Composition Root管理工作单元。这就是你的问题的风格。
通过让容器和/或基础架构处理这个问题,您的应用程序代码不会因为必须创建,(可选)提交和配置UoW实例而受到污染,这样可以保持业务逻辑的简单和干净(只需一个单一的责任) 。这种方法存在一些困难。例如,您是否提交并处置实例?
可以在Web请求结束时处理一个工作单元。然而,许多人错误地认为这也是提交工作单元的地方。但是,在应用程序的这一点上,您无法确定是否应该实际提交工作单元。例如如果业务层代码抛出了一个在callstack上面被捕获的异常,那么你肯定不想要提交。
真正的解决方案是再次显式管理某种范围,但这次是在Composition Root中进行的。抽象command / handler pattern背后的所有业务逻辑,您将能够编写一个装饰器,可以围绕每个允许执行此操作的命令处理程序。例如:
DbContext
这确保您只需编写一次此基础结构代码。任何可靠的DI容器都允许您以一致的方式配置这样的装饰器以包裹所有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();
}
}
实现。
答案 1 :(得分:29)
这里没有一个答案实际上回答了这个问题。 OP没有询问单个/每个应用程序的DbContext设计,他询问了每个(web)请求设计以及可能存在的潜在好处。
我将参考http://mehdi.me/ambient-dbcontext-in-ef6/,因为Mehdi是一个很棒的资源:
可能的性能提升。
每个DbContext实例都维护其从数据库加载的所有实体的第一级缓存。每当您通过主键查询实体时,DbContext将首先尝试从其第一级缓存中检索它,然后默认从数据库中查询它。根据您的数据查询模式,在多个顺序业务事务中重复使用相同的DbContext可能会导致由于DbContext第一级缓存而进行的数据库查询更少。
启用延迟加载。
如果您的服务返回持久性实体(而不是返回视图模型或其他类型的DTO)并且您希望利用这些实体上的延迟加载,那么DbContext实例的生命周期就是这些实体的生命周期被检索的必须超出业务交易的范围。如果服务方法在返回之前放置了它使用的DbContext实例,那么任何对返回实体的延迟加载属性的尝试都将失败(无论是否使用延迟加载是一个好主意,我们赢得了一个完全不同的辩论。进入这里)。在我们的Web应用程序示例中,延迟加载通常用于由单独服务层返回的实体的控制器操作方法。在这种情况下,服务方法用于加载这些实体的DbContext实例需要在Web请求期间保持活动状态(或者至少在操作方法完成之前)。
请记住,也有缺点。该链接包含许多其他资源以阅读该主题。
只是发布这个以防万一其他人偶然发现这个问题并且没有专注于那些实际上没有解决问题的答案。
答案 2 :(得分:26)
微软有两个矛盾的建议,许多人以完全不同的方式使用DbContexts。
那些相互矛盾的原因是因为如果你的请求与Db的东西有很多无关,那么你的DbContext就会无缘无故地保留下来。 因此,当你的请求只是等待随机的东西完成时,保持你的DbContext活着是浪费......
许多关注规则1 的人在其&#34;存储库模式&#34; 中包含他们的DbContexts,并为每个数据库查询创建一个新实例< / strong>所以 X * DbContext 每个请求
他们只是获取他们的数据并尽快处理上下文。 这被 MANY 人认为是可接受的做法。 虽然这样可以在最短的时间内占用您的数据库资源,但它明显牺牲了EF提供的所有 UnitOfWork 和缓存糖果。
保持DbContext的单个多用途实例最大化缓存的好处,但由于DbContext 不是线程安全,并且每个Web请求都在运行它是自己的主题,每个请求的DbContext是最长的,你可以保留它。
因此,EF团队建议每个请求使用1 Db Context,这显然基于以下事实:在Web应用程序中,UnitOfWork最有可能在一个请求中并且该请求有一个线程。因此,每个请求一个DbContext就像UnitOfWork和Caching的理想好处。
但在很多情况下并非如此。 我认为登录一个单独的UnitOfWork,因此在异步线程中有一个新的DbContext用于后请求记录是完全可以接受的
所以最后它拒绝DbContext的生命周期限于这两个参数。 UnitOfWork 和主题
答案 3 :(得分:22)
我很确定这是因为DbContext根本不是线程安全的。所以分享这件事永远不是一个好主意。
答案 4 :(得分:13)
在问题或讨论中没有真正解决的一件事是DbContext无法取消更改。您可以提交更改,但是您无法清除更改树,因此如果您使用每个请求上下文,如果您因任何原因需要更改,那么您将无法运气。
我个人在需要时创建DbContext的实例 - 通常附加到能够在需要时重新创建上下文的业务组件。这样我就可以控制这个过程,而不是让一个实例强加给我。我也不必在每个控制器启动时创建DbContext,无论它是否真正被使用。然后,如果我仍然想要每个请求实例,我可以在CTOR中创建它们(通过DI或手动),或者在每个控制器方法中根据需要创建它们。就个人而言,我通常采用后一种方法,以避免在实际不需要时创建DbContext实例。
这取决于您从哪个角度看待它。对我而言,每个请求实例从未有意义。 DbContext真的属于Http请求吗?在行为方面,这是错误的地方。您的业务组件应该创建您的上下文,而不是Http请求。然后,您可以根据需要创建或丢弃业务组件,而不必担心上下文的生命周期。
答案 5 :(得分:9)
我同意以前的意见。很好的说,如果您要在单线程应用程序中共享DbContext,您将需要更多内存。例如,我在Azure上的一个Web应用程序(一个额外的小实例)需要另外150 MB的内存,我每小时有大约30个用户。
这是真实示例图片:应用程序已在12PM部署
答案 6 :(得分:3)
我喜欢它的是它将工作单元(用户看到它 - 即页面提交)与ORM意义上的工作单元对齐。
因此,您可以使整个页面提交事务性,如果您在创建新上下文时公开CRUD方法,则无法执行此操作。
答案 7 :(得分:2)
即使在单线程单用户应用程序中,不使用单例DbContext的另一个低调原因是它使用的身份映射模式。这意味着每次使用query或id检索数据时,它都会将检索到的实体实例保留在缓存中。下次检索同一实体时,它将为您提供实体的缓存实例(如果可用),以及您在同一会话中所做的任何修改。这是必要的,因此SaveChanges方法不会以相同数据库记录的多个不同实体实例结束;否则,上下文必须以某种方式合并来自所有这些实体实例的数据。
问题的原因是单例DbContext可以成为一个定时炸弹,最终可以缓存整个数据库+内存中.NET对象的开销。
通过仅使用.NoTracking()
扩展方法的Linq查询,可以解决此问题。这些天PC也有很多内存。但通常这不是理想的行为。
答案 8 :(得分:1)
要特别注意实体框架的另一个问题是使用创建新实体,延迟加载,然后使用这些新实体(来自相同的上下文)的组合。如果你不使用IDbSet.Create(vs只是new),那么当该实体从其创建的上下文中检索到时,该实体上的延迟加载不起作用。例如:
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.