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