(译者注:使用EF开发应用程序的一个难点就在于对其DbContext的生命周期管理,你的管理策略是否能很好的支持上层服务 使用独立事务,使用嵌套事务,并行执行,异步执行等需求? Mehdi El Gueddari对此做了深入研究和优秀的工作并且写了一篇优秀的文章,现在我将其翻译为中文分享给大家。由于原文太长,所以翻译后的文章将分为四篇。你看到的这篇就是是它的第二篇。原文地址:http://mehdi.me/ambient-dbcontext-in-ef6/)
DbContext的默认行为
通常来说,DbContext的默认行为可以被描述为:“默认情况下就能做正确的事”。
下面是你应该记在脑海里面的几个关于EntityFramework的重要行为。这个列表描述了EF访问SqlServer的行为。用其它的数据库可能会略有差异。
DbContext不是线程安全的
你千万不要从多个线程同时去访问DbContext派生类实例。这可能导致将多个查询通过一个相同的数据库连接被同时发送了出去——它将破坏DbContext维护的一级缓存的状态——它们被用来提供标识映射(Identity Map),变更追踪和工作单元的功能。
在一个多线程应用程序中,你必须为每一个线程创建一个独立的DbContext派生类实例。
问题来了,如果DbContext不是线程安全的,那么它怎么支持EF6的异步功能呢?其实很简单:只需要保证在任何时刻只有一个异步操作被执行(就像EF的支持异步模式的规范描述的那样)。如果你尝试在同一个DbContext实例上并发的执行多个操作,比如通过DbSet<T>.ToListAsync()方法并发地执行多个查询语句,你将会得到一个带有下面消息的NotSupportedException。
A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread safe.
EF的异步功能是为了支持异步编程模型,而不是并发编程模型。
当且仅当SaveChanges()方法被调用的时候,修改才会被持久化
任何对实体的修改,包括更新,插入或者删除,当且仅当DbContext.SaveChanges()被调用的时候才会被持久化到数据库。如果DbContext实例在SaveChanges()方法被调用之前就被释放掉了,那么这些更新操作,插入操作,删除操作没有一条能持久化到底层数据库。
下面是用EF来实现一个业务事务的规范方式:
using (var context = new MyDbContext(ConnectionString)) { /* * 业务逻辑放在这儿. 通过context添加,修改,删除数据。 * * 抛出任何异常就可以回滚所有变化。 * * 直到业务事务完成,否则不能调用SaveChanges()方法 * 也就是说不能部分或者中间保存。 * 每一个业务事务只能刚好调用一次SaveChanges()方法 。 * * 如果你发现你自己需要在一个业务事务里面多次调用 * SaveChanges()方法,那就意味着你在一个服务方法 * 里面实现多个业务事务。这绝对是灾难的“必备良药”。 * 调用你的服务的客户端会很自然的假定你的服务方法 * 以原子的行为提交或者回滚——但你的服务却可能 * 部分提交,让系统处于一个不一致的状态。 * * 在这种情况下,将你的服务方法重构成多个服务方法—— * 每一个服务方法刚好实现了刚好一个业务事务。 */ [...] // 完成业务事务并且持久化所有变化 。 context.SaveChanges(); // 在这行代码之后变化不可能回滚了。 // context.SaveChanges()应当是任何业务事务 // 的最后一行代码。 }
NHibernate用户注意事项
如果你拥有NHibernate背景,那么可以告诉你的是EF将变化持久化到数据库的方式是它与NHibernate的最大不同。
在NHibernate中,Session操作默认情况下处于AutoFlush模式。在这种模式下,Session将在执行任何‘select’操作之前自动将所有变化持久化到数据库——确保已持久化到数据库的实体和它们在Session中的内存状态保持一致。对NHibernate来说,EF的默认行为相当于将Session.FlushMode设置为Never。
EF的这个行为可能会导致一些微妙的bug——查询意外的返回过时的或者不正确的数据。默认情况下NHibernate是绝不可能出现这种情况的。但从另外一方面来说,这却又极大的简化了数据库事务管理的问题。
在NHibernate中最棘手的问题之一就是正确的管理数据库事务。由于NHibernate的Session可以在它的整个生命周期中的任何时间点自动地将未持久化的变化持久化到数据库,并且可能在一个业务事务里面持久化多次——这儿没有一个定义良好的点或者方法来开启数据库事务以确保所有的修改以原子的行为提交或者回滚。
在NHibernate中正确管理数据库事务的唯一可靠方法就是将你的所有服务方法打包在一个显式数据库事务中。这就是大部分基于NHibernate的应用程序的处理方式。
这种方式的负面效应就是它要求打开一个数据库连接和事务的时间比实际需要的要更长——因此增加了数据库锁的竞争和数据库死锁发生的可能性。开发者也很容易不经意的执行一个长时间计算或者一个远程服务方法的调用而没有意识到甚至根本就不知道他们是在一个数据库事务打开的上下文中。
EF的方式——只有SaveChanges()方法必须被打包在一个显式数据库事务中(当然使用一个REPEATABLE READ 或者SERIALIZABLE隔离级别的情况例外),保证了数据库连接和事务保持尽可能的短暂。
使用自动提交事务(AutoCommit transaction)来执行读取操作
DbContext不支持打开一个显式事务来执行读取操作。它依赖于SQL Server的自动提交事务(Autocommit Transaction) (或者 隐式事务(Implicit Transaction)——如果你启用了它们的话,但那相对来说不是常见的操作)。自动提交事务(或者隐式事务)将会使用数据库引擎被配置的默认事务隔离级别(对SQL Server来说就是READ COMMITTED)。
如果你已经工作有一段时间,尤其是如果你以前使用过NHibernate,那么你可能听说过“自动提交事务(或者隐式事务)是糟糕的”。实际上,依赖于自动提交事务的写操作可能在性能上产生灾难性影响。
但对于读操作来说情况就大不一样了。你可以跑下面的SQL脚本亲自去看看。对select语句来说,自动提交事务或者隐式事务都不会有任何明显的性能影响。
/* * 用自动提交事务,隐式事务,显式事务分部执行10000 * 次select查询. * * 这些脚本假定数据库包含一张Users表,它有一个列名为Id * 类型为INT的列。 * * 如果你在SQL Server Management Studio里面运行的话 * 右键查询窗口,进入查询选项 -> 点击结果并勾选 * “执行后放弃结果”。否则你的测试结果将会被网格的 * 刷新验证影响 */ --------------------------------------------------- -- 自动提交事务 -- 6 秒 DECLARE @i INT SET @i = 0 WHILE @i < 100000 BEGIN SELECT Id FROM dbo.Users WHERE Id = @i SET @i = @i + 1 END --------------------------------------------------- -- 隐式提交事务 -- 6 秒 SET IMPLICIT_TRANSACTIONS ON DECLARE @i INT SET @i = 0 WHILE @i < 100000 BEGIN SELECT Id FROM dbo.Users WHERE Id = @i SET @i = @i + 1 END COMMIT; SET IMPLICIT_TRANSACTIONS OFF ---------------------------------------------------- -- 显示事务 -- 6 秒 DECLARE @i INT SET @i = 0 BEGIN TRAN WHILE @i < 100000 BEGIN SELECT Id FROM dbo.Users WHERE Id = @i SET @i = @i + 1 END COMMIT TRAN
很显然,如果你需要用一个比默认READ COMMITTED更高的隔离级别的话,那么所有读操作都将是显式数据库事务的一部分。在那种情况下,你需要自己开启事务——EF将不会为你做这个。但这通常只会为指定的业务事务做特别处理。EF的默认设置能适合大部分业务事务。
使用显式事务来执行写操作
EF通过DbContext.SaveChanges()方法自动地将所有操作打包在一个显式数据库事务里面——以确保应用在context的所有修改要么完全提交要么完全回滚。
EF写操作使用数据库引擎配置的默认事务隔离级别(对SQL Server来说就是READ COMMITTED)。
NHibernate用户注意事项
这是EF和NHibernate之间的另一个很大的不同点。在NHibernate中,数据库事务完全掌握在开发者手中。NHibernate的Session永远不会自动地打开一个显式数据库事务。
你可以重写EF的默认行为并控制数据库事务范围和隔离级别
using (var context = new MyDbContext(ConnectionString)) { using (var transaction =context.BeginTransaction(IsolationLevel.RepeatableRead)) { [...] context.SaveChanges(); transaction.Commit(); } }
手动控制数据库事务范围的一个非常明显的副作用就是你必须在整个事务范围中让数据库连接和事务保持打开。
你应当尽可能的让这个事务范围生命周期短暂。打开一个数据库事务运行太长时间可能会对应用程序的性能和可扩展性有非常巨大的影响。特别指出的是,尽量不要再一个显示事务范围内调用其它的服务方法——它们可能执行长时间运行的操作而没有意识到它们是在一个打开的数据库事务内被调用。
EF没有内建的方式来重写用作自动提交事务和自动显式事务的默认隔离级别
就像上面提到的,EF依赖自动提交事务来执行读操作并且当调用SaveChanges()方法的时候自动以数据库配置的默认隔离级别开启一个显式事务。
很不幸的是没有内建的方式来重写这些隔离级别,如果你想用另一个隔离级别,你必须自己开启和管理数据库事务。
通过DbContext打开的数据库连接自动加入一个周围环境的TransactionScope
另外,你也可以用TransactionScope来控制事务范围和隔离级别。EF打开的数据库连接自动加入周围环境的TransactionScope。
在EF6之前,使用TransactionScope是唯一可靠的方式来控制数据库事务范围和隔离级别。
在实践中,除非你真的需要一个分布式事务,否则尽量避免使用TransactionScope。TransactionScope,通常指分布式事务,对大部分应用程序来说都是不必要的。并且它们通常会带来比它们解决的问题都要更多的问题。如果你真的需要一个分布式事务的话,可以查看EF文档章节——在EF中使用TransactionScope。
DbContext实例应当被释放掉(但是如果没有释放掉,也可能没事)
DbContext实现了IDisposable接口,因此一旦它们不需要了就应当尽快释放。
然而在实践中,除非你选择显式控制DbContext使用的数据库连接或者事务,否则不调用DbContext.Dispose()方法也不会引起任何问题——就像Diego Vega,一个EF团队成员解释的那样。
这是一个好消息——因为你会发现很多代码不能正确地释放DbContext实例。尤其是那些尝试用DI容器来管理DbContext实例生命周期的情况——实际情况比听起来要棘手得多。
一个DI容器,比如说StructureMap,它不支持释放它创建的组件。因此,如果你依赖StructureMap来创建DbContext实例,那么它们将不会被释放掉——不管你为它们设置的什么生命周期方式。使用像这样的DI容器来管理可释放组件的唯一正确方式就是复杂你的DI配置并且使用一个嵌套依赖注入容器——就像Jeremy Miller描述的那样。