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.
9条答案
按热度按时间ijxebb2r1#
注意:这个答案讨论的是实体框架的
DbContext
,但是它适用于任何类型的工作单元实现,比如LINQ to SQL的DataContext
和NHibernate的ISession
。让我们从回应Ian开始:为整个应用程序使用一个
DbContext
是一个坏主意。唯一有意义的情况是当你有一个单线程应用程序和一个数据库只由这个应用程序示例使用。DbContext
不是线程安全的,并且由于DbContext
缓存数据,当多个用户/应用程序同时使用该数据库时,这会给您带来各种各样的麻烦(当然这是很常见的)。但是我希望您已经知道这一点,并且只想知道为什么不直接注入一个新示例(即短暂的生活方式)的DbContext
到任何需要它的人。(要了解为什么单个DbContext
--甚至每个线程的上下文--是坏的,请阅读this answer)。让我先说一下,将
DbContext
注册为transient是可行的,但是通常您希望在某个范围内拥有这样一个工作单元的单个示例,在Web应用程序中,在Web请求的边界上定义这样一个范围是可行的;因此是每个Web请求的生活方式。2这允许你让一整组对象在同一个上下文中操作。3换句话说,它们在同一个业务事务中操作。如果您没有在同一个上下文中运行一组操作的目标,那么在这种情况下,暂时的生活方式是可以的,但是有几件事需要注意:
_context.SaveChanges()
(否则改变会丢失),这会使代码复杂化,并给代码增加第二个责任(控制上下文的责任),这违反了Single Responsibility Principle。DbContext
加载和保存]永远不会离开这样一个类的作用域,因为它们不能在另一个类的上下文示例中使用,这会使你的代码变得非常复杂,因为当你需要这些实体时,你需要再次通过id加载它们,这也会导致性能问题。DbContext
实现了IDisposable
,所以您可能仍然希望Dispose所有创建的示例,如果您希望这样做,基本上有两种选择,您需要在调用context.SaveChanges()
之后立即在同一个方法中进行Dispose。但是在这种情况下,业务逻辑取得它从外部传递的对象的所有权。第二个选项是Dispose在Http请求边界上创建的所有示例,但在这种情况下,您仍然需要某种范围,以便让容器知道何时需要Dispose这些示例。另一种选择是完全不注入
DbContext
,而是注入一个能够创建新示例的DbContextFactory
(我过去经常使用这种方法),这样业务逻辑就可以显式地控制上下文,如下所示:这样做的好处是可以显式地管理
DbContext
的生命周期,而且设置起来很容易,还允许在某个范围内使用单个上下文,这具有明显的优势,例如在单个业务事务中运行代码,并且能够传递实体,因为它们来自同一个DbContext
。缺点是必须在方法之间传递
DbContext
(称为方法注入)注意在某种意义上该解决方案与“作用域”方法相同,但是现在范围在应用程序代码本身中控制(并且可能重复很多次)负责创建和处理工作单元的是应用程序。由于DbContext
是在依赖关系图构造之后创建的,因此构造函数注入不在考虑范围之内,当您需要将上下文从一个类传递到另一个类时,您需要遵从方法注入。方法注入并没有那么糟糕,但是当业务逻辑变得更加复杂,并且涉及到更多的类时,您将不得不在方法和类之间传递它,这会使代码变得非常复杂(我以前见过这种情况)。
由于工厂方法的缺点,对于更大的系统,另一种方法可能是有用的,那就是让容器或基础设施代码/Composition Root管理工作单元,这就是你的问题所涉及的风格。
通过让容器和/或基础结构来处理这一点,您的应用程序代码就不会因为必须创建、(可选)提交和释放UoW示例而受到污染,从而保持业务逻辑简单明了(只是一个单一责任)。这种方法存在一些困难。例如,在哪里提交和释放示例?
处理工作单元可以在Web请求结束时完成。然而,许多人 * 错误地 * 认为这也是提交工作单元的位置。然而,在应用程序中的这一点上,您根本无法确定是否应实际提交工作单元。例如,如果业务层代码引发了一个异常,该异常在调用堆栈的更高位置被捕获,你肯定不想做承诺。
真实的的解决方案是再次显式地管理某种作用域,但这次是在Composition Root中进行的。抽象command / handler pattern背后的所有业务逻辑,您将能够编写一个装饰器,它可以 Package 在允许这样做的每个命令处理程序周围。例如:
这确保了您只需要编写一次基础设施代码,任何固定的DI容器都允许您配置这样一个装饰器,以一致的方式 Package 所有
ICommandHandler<T>
实现。rekjcdws2#
微软有两个相互矛盾的建议,许多人以完全不同的方式使用DbContexts。
1.一个建议是**“尽可能快地处理DbContext”,因为使DbContext活动会占用宝贵的资源,如数据库连接等。
1.另一个声明强烈建议每个请求一个DbContext**
这两者相互矛盾,因为如果你的请求做了很多与Db无关的事情,那么你的DbContext就会无缘无故地被保留。因此,当你的请求只是在等待随机的事情完成时,让你的DbContext保持活动状态是浪费。
许多人遵循规则1,将他们的DbContext放在他们的**“存储库模式”中,并为每个数据库查询创建新示例,因此每个请求X*DbContext**
他们只是尽快获取数据并处理上下文。许多人认为这是一种可接受的做法。虽然这有占用数据库资源时间最短的好处,但显然牺牲了EF必须提供的所有UnitOfWork和Caching糖果。
保持DbContext的单个多用途示例处于活动状态可以最大限度地发挥缓存的优势,但由于DbContext不是线程安全的,并且每个Web请求都在其自己的线程上运行,因此每个请求的DbContext是您可以保留的最长示例。
EF的团队建议每个请求使用1个DbContext,这显然是基于这样一个事实,即在Web应用程序中,UnitOfWork最有可能位于一个请求中,并且该请求具有一个线程。因此,每个请求使用一个DbContext就像UnitOfWork和缓存的理想好处一样。
但在许多情况下,这是不正确的。我认为日志记录是一个单独的UnitOfWork,因此在异步线程中为请求后日志记录提供新的DbContext是完全可以接受的
最后,DbContext的生存期被限制在这两个参数上。UnitOfWork和Thread
ht4b089n3#
这里没有一个答案真正回答了这个问题。OP没有问关于单例/每个应用程序的DbContext设计,他问了关于每个(web)请求的设计以及可能存在的潜在好处。
我将引用http://mehdi.me/ambient-dbcontext-in-ef6/,因为Mehdi是一个很棒的资源:
可能的性能提升。
每个DbContext示例都维护一个一级高速缓存,其中包含从数据库加载的所有实体。每当您按实体的主键查询实体时,DbContext将首先尝试从其一级高速缓存中检索该实体,然后才默认从数据库中查询该实体。根据您的数据查询模式,由于DbContext一级高速缓存,跨多个顺序业务事务重用相同的DbContext可导致进行较少的数据库查询。
它支持延迟加载。
如果服务返回持久性实体(与返回视图模型或其他类型的DTO相反),并且您希望利用这些实体上的延迟加载,则从中检索这些实体的DbContext示例的生存期必须扩展到业务事务的范围之外。任何在返回的实体上延迟加载属性的尝试都将失败(使用惰性加载是否是个好主意完全是另一个问题,我们在这里就不讨论了)在我们的Web应用程序示例中,惰性加载通常用在由单独的服务层返回的实体上的控制器动作方法中。在这种情况下,由服务方法用来加载这些实体的DbContext示例需要在web请求的持续时间内保持活动(或者至少直到动作方法完成)。
请记住,这也有缺点。那个链接包含了许多其他关于这个主题的阅读资源。
只是张贴这一点,以防有人偶然发现这个问题,并没有得到吸收的答案,实际上并没有解决这个问题。
eoigrqb64#
我非常肯定这是因为DbContext根本不是线程安全的,所以共享它从来都不是一个好主意。
s4chpxco5#
问题或讨论中没有真正解决的一件事是DbContext不能取消更改的事实。您可以提交更改,但不能清除更改树,因此如果您使用每个请求上下文,则无论出于何种原因需要丢弃更改,您都将倒霉。
我个人在需要时创建DbContext的示例--通常附加到业务组件,这些组件能够在需要时重新创建上下文。而不是强加给我一个单独的示例。我也不必在每次控制器启动时创建DbContext,不管它实际上是否被使用。然后如果我仍然想要每个请求示例,我可以在TOR中创建它们(通过DI或手动),或者根据需要在每个控制器方法中创建它们。我个人通常采用后一种方法,以避免在实际不需要时创建DbContext示例。
这也取决于你从哪个Angular 来看待它。对我来说,每个请求示例从来就没有意义。DbContext真的属于Http请求吗?就行为而言,这是错误的地方。你的业务组件应该创建你的上下文,而不是Http请求。然后你可以根据需要创建或丢弃你的业务组件,而不必担心上下文的生命周期。
eqzww0vc6#
我同意之前的观点。如果你要在单线程应用中共享DbContext,你需要更多的内存。例如,我在Azure上的Web应用(一个额外的小示例)需要另外150 MB的内存,而我每小时有大约30个用户。
这是真实的示例图像:应用程序已在中午12点部署
qc6wkl3g7#
我喜欢它的地方在于它将工作单元(用户看到的--即页面提交)与ORM意义上的工作单元对齐。
因此,您可以使整个页面提交事务化,而如果公开CRUD方法,并且每个方法都创建一个新上下文,则无法实现这一点。
ruyhziif8#
即使在单线程单用户应用程序中,也不要使用单示例DbContext的另一个简单原因是它使用的标识Map模式。这意味着每次使用查询或按ID检索数据时,它都会将检索到的实体示例保存在缓存中。下次检索相同的实体时,它会为您提供实体的缓存示例(如果可用)。在同一会话中所做的任何修改。这是必要的,这样SaveChanges方法就不会以同一数据库记录的多个不同实体示例结束;否则,上下文将不得不以某种方式合并来自所有这些实体示例的数据。
出现问题的原因是单个DbContext可能成为一个定时炸弹,最终可能会缓存整个数据库+内存中.NET对象的开销。
有一些方法可以绕过这种行为,只使用带有
.NoTracking()
扩展方法的Linq查询。而且现在的PC有很多RAM。但通常这不是所需的行为。kcwpcxri9#
实体框架需要特别注意的另一个问题是,在使用创建新实体、延迟加载和使用这些新实体(来自同一上下文)的组合时。如果不使用IDbSet.Create(与新建相比),则在从创建实体的上下文中检索该实体时,延迟加载将不起作用。例如: