(译者注:使用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描述的那样

在EntityFramework6中管理DbContext的正确方式——2DbContext的默认行为(外文翻译)的更多相关文章

  1. 在EntityFramework6中管理DbContext的正确方式——1考虑的关键点(外文翻译)

    (译者注:使用EF开发应用程序的一个难点就在于对其DbContext的生命周期管理,你的管理策略是否能很好的支持上层服务 使用独立事务,使用嵌套事务,并行执行,异步执行等需求? Mehdi El Gu ...

  2. 在EntityFramework6中管理DbContext的正确方式——4DbContextScope:一个简单的,正确的并且灵活的管理DbContext实例的方式(外文翻译)

    (译者注:使用EF开发应用程序的一个难点就在于对其DbContext的生命周期管理,你的管理策略是否能很好的支持上层服务 使用独立事务,使用嵌套事务,并行执行,异步执行等需求? Mehdi El Gu ...

  3. 在EntityFramework6中管理DbContext的正确方式——3环境上下文DbContext vs 显式DbContext vs 注入DbContext(外文翻译)

    (译者注:使用EF开发应用程序的一个难点就在于对其DbContext的生命周期管理,你的管理策略是否能很好的支持上层服务 使用独立事务,使用嵌套事务,并行执行,异步执行等需求? Mehdi El Gu ...

  4. 在ANGULAR6中使用Echarts的正确方式之一

    这里的正确指的是不会在运行过程中报错,不会再prod模式下编译报错,不会再AOT模式下编译报错 个人环境说明: { "name": "angular-for-echart ...

  5. 在Linux中配置DNS的正确方式

    链接:http://ccl.cse.nd.edu/operations/condor/hostname.shtml Common Hostname Problem on Linux Newly ins ...

  6. Android笔记之Fragment中创建ViewModel的正确方式

    之前一直都是这么写的 pageViewModel = ViewModelProviders.of(this).get(PageViewModel.class); //参数this是当前fragment ...

  7. 前端项目引入Echarts中的dataTool的正确方式

    使用echarts画箱线图时调用echarts.dataTool.prepareBoxplotData() 报错:"echarts.dataTool.prepareBoxplotData i ...

  8. Entity Framework中DbContext结合TransactionScope提交事务的正确方式

    问: I would like know what is the best possible way to implement transactions with DBContext. In part ...

  9. Swift中编写单例的正确方式

    在之前的帖子里聊过状态管理有多痛苦,有时这是不可避免的.一个状态管理的例子大家都很熟悉,那就是单例.使用Swift时,有许多方法实现单例,这是个麻烦事,因为我们不知道哪个最合适.这里我们来回顾一下单例 ...

随机推荐

  1. git ——本地项目上传到git

    1.(先进入项目文件夹)通过命令 git init 把这个目录变成git可以管理的仓库 git init 2.把文件添加到版本库中,使用命令 git add .添加到暂存区里面去,不要忘记后面的小数点 ...

  2. caffe细节

    1.BN层参数设置 在训练时所有BN层要设置use_global_stats: false(也可以不写,caffe默认是false) 在测试时所有BN层要设置use_global_stats: tru ...

  3. MySQL学习笔记:调用存储过程或函数报1418错误

    问题 MySQL开启bin-log后,调用存储过程或者函数以及触发器时,会出现错误号为1418的错误: ERROR 1418 (HY000): This function has none of DE ...

  4. Visual Studio Code 常用插件整理

    常用插件说明: 一.HTML Snippets 超级使用且初级的H5代码片段以及提示 二.HTML CSS Support  让HTML标签上写class智能提示当前项目所支持的样式 三.Debugg ...

  5. Spark(十一)Spark分区

    一.分区的概念 分区是RDD内部并行计算的一个计算单元,RDD的数据集在逻辑上被划分为多个分片,每一个分片称为分区,分区的格式决定了并行计算的粒度,而每个分区的数值计算都是在一个任务中进行的,因此任务 ...

  6. Loadrunner11在win7下录制脚本,ie打不开

    Loadrunner11在win7下录制脚本,ie打不开 使用loadrunner11录制脚本时试了很多办法都无法打开ie浏览器,最后终于解决了 1.ie浏览器去掉启用第三方浏览器扩展 2.loadr ...

  7. MySQL 20个经典面试题

    1.MySQL的复制原理以及流程 基本原理流程,3个线程以及之间的关联: 1. 主:binlog线程——记录下所有改变了数据库数据的语句,放进master上的binlog中: 2. 从:io线程——在 ...

  8. Bootstrap进阶五:Web开发中很实用的交互效果积累

    1.页面切换效果 我们已经在示例中罗列了一组动画,可以被应用到页面切换过程中,创造出很有趣的导航效果.  2.视差滚动(parallax-slider) 视差滚动(parallax-slider)已 ...

  9. 搭建 Android 集成开发环境

    在搭建 Android 集成开发环境之前,我想说的是,我们学习的目标是同时掌握移动开发三种方式:iOS开发.Android开发和Html5手机网页开发.由于iOS的开发工具是采用苹果官方的XCode, ...

  10. js缓存加密

    1.访问A链接就以A链接的特定部分为密码盐,生成一个js跳转配置文件名 aojoweiojoiwjeiof2.PHP在生成js跳转文件名的时候,也是根据数据库中的跳转起始链接特定部分作为盐,生成的文件 ...