每个 Web 请求一个 DbContext ......为什么? [英] One DbContext per web request... why?

查看:25
本文介绍了每个 Web 请求一个 DbContext ......为什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have been reading a lot of articles explaining how to set up Entity Framework's DbContext so that only one is created and used per HTTP web request using various DI frameworks.

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

Why is this a good idea in the first place? What advantages do you gain by using this approach? Are there certain situations where this would be a good idea? Are there things that you can do using this technique that you can't do when instantiating DbContexts per repository method call?

推荐答案

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

NOTE: This answer talks about the Entity Framework's DbContext, but it is applicable to any sort of Unit of Work implementation, such as LINQ to SQL's DataContext, and NHibernate's ISession.

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

Let start by echoing Ian: Having a single DbContext for the whole application is a Bad Idea. The only situation where this makes sense is when you have a single-threaded application and a database that is solely used by that single application instance. The DbContext is not thread-safe and and since the DbContext caches data, it gets stale pretty soon. This will get you in all sorts of trouble when multiple users/applications work on that database simultaneously (which is very common of course). But I expect you already know that and just want to know why not to just inject a new instance (i.e. with a transient lifestyle) of the DbContext into anyone who needs it. (for more information about why a single DbContext -or even on context per thread- is bad, read this answer).

首先让我说将 DbContext 注册为瞬态是可行的,但通常您希望在某个范围内拥有这样一个工作单元的单个实例.在 Web 应用程序中,在 Web 请求的边界上定义这样的范围是可行的;因此,每个 Web 请求的生活方式.这允许您让一整套对象在同一上下文中运行.换句话说,他们在同一个商业交易中运作.

Let me start by saying that registering a DbContext as transient could work, but typically you want to have a single instance of such a unit of work within a certain scope. In a web application, it can be practical to define such a scope on the boundaries of a web request; thus a Per Web Request lifestyle. This allows you to let a whole set of objects operate within the same context. In other words, they operate within the same business transaction.

如果您没有让一组操作在同一上下文中运行的目标,在这种情况下,短暂的生活方式没有问题,但有几点需要注意:

If you have no goal of having a set of operations operate inside the same context, in that case the transient lifestyle is fine, but there are a few things to watch:

  • 由于每个对象都有自己的实例,因此每个更改系统状态的类都需要调用 _context.SaveChanges()(否则更改会丢失).这可能会使您的代码复杂化,并为代码添加第二个责任(控制上下文的责任),并且违反了 Single责任原则.
  • 您需要确保实体 [由 DbContext 加载和保存] 永远不会离开此类类的范围,因为它们不能在另一个类的上下文实例中使用.这会使您的代码变得非常复杂,因为当您需要这些实体时,您需要通过 id 再次加载它们,这也会导致性能问题.
  • 由于 DbContext 实现了 IDisposable,您可能仍然希望 Dispose 所有创建的实例.如果你想这样做,你基本上有两个选择.您需要在调用 context.SaveChanges() 后立即以相同的方法处理它们,但在这种情况下,业务逻辑会获得从外部传递的对象的所有权.第二种选择是在 Http 请求的边界上处理所有创建的实例,但在这种情况下,您仍然需要某种范围来让容器知道何时需要处理这些实例.
  • Since every object gets its own instance, every class that changes the state of the system, needs to call _context.SaveChanges() (otherwise changes would get lost). This can complicate your code, and adds a second responsibility to the code (the responsibility of controlling the context), and is a violation of the Single Responsibility Principle.
  • You need to make sure that entities [loaded and saved by a DbContext] never leave the scope of such a class, because they can't be used in the context instance of another class. This can complicate your code enormously, because when you need those entities, you need to load them again by id, which could also cause performance problems.
  • Since DbContext implements IDisposable, you probably still want to Dispose all created instances. If you want to do this, you basically have two options. You need to dispose them in the same method right after calling context.SaveChanges(), but in that case the business logic takes ownership of an object it gets passed on from the outside. The second option is to Dispose all created instances on the boundary of the Http Request, but in that case you still need some sort of scoping to let the container know when those instances need to be Disposed.

另一个选择是注入DbContext.相反,您注入一个能够创建新实例的 DbContextFactory(我过去曾使用这种方法).通过这种方式,业务逻辑显式地控制上下文.如果可能看起来像这样:

Another option is to not inject a DbContext at all. Instead, you inject a DbContextFactory that is able to create a new instance (I used to use this approach in the past). This way the business logic controls the context explicitly. If might look like this:

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.

The plus side of this is that you manage the life of the DbContext explicitly and it is easy to set this up. It also allows you to use a single context in a certain scope, which has clear advantages, such as running code in a single business transaction, and being able to pass around entities, since they originate from the same DbContext.

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

The downside is that you will have to pass around the DbContext from method to method (which is termed Method Injection). Note that in a sense this solution is the same as the 'scoped' approach, but now the scope is controlled in the application code itself (and is possibly repeated many times). It is the application that is responsible for creating and disposing the unit of work. Since the DbContext is created after the dependency graph is constructed, Constructor Injection is out of the picture and you need to defer to Method Injection when you need to pass on the context from one class to the other.

方法注入并没有那么糟糕,但是当业务逻辑变得更复杂,涉及更多类时,您将不得不将其从方法传递到方法,从类传递到类,这会使代码复杂化很多(我过去见过这个).对于一个简单的应用程序,这种方法就可以了.

Method Injection isn't that bad, but when the business logic gets more complex, and more classes get involved, you will have to pass it from method to method and class to class, which can complicate the code a lot (I've seen this in the past). For a simple application, this approach will do just fine though.

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

Because of the downsides, this factory approach has for bigger systems, another approach can be useful and that is the one where you let the container or the infrastructure code / Composition Root manage the unit of work. This is the style that your question is about.

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

By letting the container and/or the infrastructure handle this, your application code is not polluted by having to create, (optionally) commit and Dispose a UoW instance, which keeps the business logic simple and clean (just a Single Responsibility). There are some difficulties with this approach. For instance, were do you Commit and Dispose the instance?

处理工作单元可以在 Web 请求结束时完成.然而,许多人错误地认为这也是提交工作单元的地方.但是,在应用程序中的那个点,您根本无法确定实际上应该提交工作单元.例如如果业务层代码抛出的异常在调用堆栈的上层被捕获,您绝对不想想要提交.

Disposing a unit of work can be done at the end of the web request. Many people however, incorrectly assume that this is also the place to Commit the unit of work. However, at that point in the application, you simply can't determine for sure that the unit of work should actually be committed. e.g. If the business layer code threw an exception that was caught higher up the callstack, you definitely don't want to Commit.

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

The real solution is again to explicitly manage some sort of scope, but this time do it inside the Composition Root. Abstracting all business logic behind the command / handler pattern, you will be able to write a decorator that can be wrapped around each command handler that allows to do this. Example:

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

This ensures that you only need to write this infrastructure code once. Any solid DI container allows you to configure such a decorator to be wrapped around all ICommandHandler<T> implementations in a consistent manner.

这篇关于每个 Web 请求一个 DbContext ......为什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆