我阅读了许多文章,这些文章解释了如何设置实体框架,DbContext
以便使用各种DI框架为每个HTTP Web请求创建和使用一个实体框架。
为什么这首先是个好主意?通过使用这种方法,您可以获得什么优势?在某些情况下这是个好主意吗?使用DbContext
每个存储库方法调用实例化s 时,您是否可以使用该技术做一些事情?
我阅读了许多文章,这些文章解释了如何设置实体框架,DbContext
以便使用各种DI框架为每个HTTP Web请求创建和使用一个实体框架。
为什么这首先是个好主意?通过使用这种方法,您可以获得什么优势?在某些情况下这是个好主意吗?使用DbContext
每个存储库方法调用实例化s 时,您是否可以使用该技术做一些事情?
Answers:
注意:这个答案是关于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
。
缺点是您将必须在DbContext
from方法与方法之间进行传递(这称为“方法注入”)。请注意,从某种意义上说,此解决方案与“作用域”方法相同,但是现在范围是在应用程序代码本身中控制的(并且可能会重复很多次)。负责创建和处理工作单元的是应用程序。由于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>
以一致的方式包装在所有实现中。
CreateCommand<TEnity>
和一个泛型CreateCommandHandler<TEntity> : ICommandHandler<CreateCommand<TEntity>>
(对Update,Delete和Delete进行相同的操作,并且只有一个GetByIdQuery<TEntity>
查询)。不过,您应该问自己:此模型对于CRUD操作是否有用,还是是否增加了复杂性。不过,您可能会受益于使用此模型轻松添加跨领域关注点(通过装饰器)的可能性。您必须权衡利弊。
TransactionCommandHandlerDecorator
?例如,如果装饰的类是InsertCommandHandler
class,它如何将插入操作注册到上下文(EF中的DbContext)?
这里没有一个答案实际上可以回答问题。OP没有询问单例/每个应用程序的DbContext设计,而是询问了每个(Web)请求的设计以及可能存在的潜在好处。
我会参考 http://mehdi.me/ambient-dbcontext-in-ef6/,因为Mehdi是一个很棒的资源:
可能的性能提升。
每个DbContext实例都维护其从数据库中加载的所有实体的一级缓存。每当您通过实体的主键查询实体时,DbContext都会首先尝试从其一级缓存中检索它,然后默认从数据库中查询它。根据您的数据查询模式,由于DbContext一级缓存,在多个顺序业务交易中重复使用相同的DbContext可能会导致进行较少的数据库查询。
它启用了延迟加载。
如果您的服务返回了持久实体(而不是返回视图模型或其他类型的DTO),并且您想利用这些实体上的延迟加载,则从中检索这些实体的DbContext实例的生命周期必须超出业务交易的范围。如果服务方法在返回之前将其使用的DbContext实例进行了处置,则任何在返回的实体上延迟加载属性的尝试都将失败(无论是否使用延迟加载都是一个好主意,这是一个完全不同的争论,我们将不再讨论这里)。在我们的Web应用程序示例中,通常在控制器操作方法中对由单独的服务层返回的实体使用延迟加载。在这种情况下,
请记住,还有缺点。该链接包含许多其他资源可供阅读。
如果其他人偶然发现了这个问题,而又不会被那些无法真正解决该问题的答案所困扰,则只需发布此内容即可。
微软有两个相互矛盾的建议,许多人以完全不同的方式使用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的生存期仅限于这两个参数。工作单元和线程
我可以肯定这是因为DbContext根本不是线程安全的。因此,共享事物绝不是一个好主意。
问题或讨论中未真正解决的一件事是DbContext无法取消更改。您可以提交更改,但不能清除更改树,因此,如果使用基于请求的上下文,那么无论出于何种原因都需要放弃更改,那么您将很不走运。
我个人在需要时创建DbContext实例-通常附加到能够根据需要重新创建上下文的业务组件。这样,我可以控制该过程,而不必强加给我一个实例。我也不必在每次控制器启动时都创建DbContext,无论是否实际使用它。然后,如果我仍然希望按请求拥有实例,则可以在CTOR中创建它们(通过DI或手动),也可以根据需要在每种控制器方法中创建它们。就我个人而言,我通常采用后一种方法,以避免在实际上不需要它们时创建DbContext实例。
这也取决于您从哪个角度看。对我来说,每个请求实例从来没有道理。DbContext是否真的属于Http请求?就行为而言,这是错误的地方。您的业务组件应该创建您的上下文,而不是Http请求。然后,您可以根据需要创建或丢弃业务组件,而不必担心上下文的生命周期。
我同意以前的意见。可以说,如果要在单线程应用程序中共享DbContext,则需要更多内存。例如,我在Azure上的Web应用程序(一个额外的小型实例)需要另外150 MB的内存,而我每小时大约有30个用户。
这是真实的示例图像:应用程序已在12PM部署
即使在单线程单用户应用程序中也不使用单例DbContext的另一个低估原因是由于它使用的身份映射模式。这意味着每次使用查询或按ID检索数据时,它将检索到的实体实例保存在缓存中。下次您检索同一实体时,它将为您提供该实体的缓存实例(如果有),并具有您在同一会话中所做的任何修改。这是必需的,因此SaveChanges方法不会以同一数据库记录的多个不同实体实例结尾。否则,上下文将必须以某种方式合并所有那些实体实例中的数据。
问题的原因在于单例DbContext可能成为定时炸弹,最终可能会缓存整个数据库+ .NET对象在内存中的开销。
通过仅将Linq查询与.NoTracking()
扩展方法结合使用,可以解决此问题。同样,这些天PC具有大量RAM。但是通常这不是期望的行为。
特别要注意的是,使用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.