EF学习笔记(十) 处理并发
总目录:ASP.NET MVC5 及 EF6 学习笔记 - (目录整理)
本篇原文链接:Handling Concurrency
Concurrency Conflicts 并发冲突
发生并发冲突很简单,一个用户点开一条数据进行编辑,另外一个用户同时也点开这条数据进行编辑,那么如果不处理并发的话,谁后提交修改保存的,谁的数据就会被记录,而前一个就被覆盖了;
如果在一些特定的应用中,这种并发冲突可以被接受的话,那么就不用花力气去特意处理并发;毕竟处理并发肯定会对性能有所影响。
Pessimistic Concurrency (Locking) 保守并发处理(锁)
如果应用需要预防在并发过程中数据丢失,那么一种方式就是采用数据库锁;这种方式称为保守并发处理。
这种就是原有的处理方式,要修改数据前,先给数据库表或者行加上锁,然后在这个事务处理完之前,不会释放这个锁,等处理完了再释放这个锁。
但这种方式应该是对一些特殊数据登记才会使用的,比如取流水号,多个用户都在取流水号,用一个表来登记当前流水号,那么取流水号过程肯定要锁住表,不然同时两个用户取到一样的流水号就出异常了。
而且有的数据库都没有提供这种处理机制。EF并没有提供这种方式的处理,所以本篇就不会讲这种处理方式。
Optimistic Concurrency 开放式并发处理
替代保守并发处理的方式就是开放式并发处理,开放式并发处理运行并发冲突发生,但是由用户选择适当的方式来继续;(是继续保存数据还是取消)
比如在出现以下情况:John打开网页编辑一个Department,修改预算为0, 而在点保存之前,Jone也打开网页编辑这个Department,把开始日期做了调整,然后John先点了保存,Jone之后点了保存;
在这种情况下,有以下几种选择:
1、跟踪用户具体修改了哪个属性,只对属性进行更新;当时也会出现,两个用户同时修改一个属性的问题;EF是否实现这种,需要看自己怎么写更新部分的代码;在Web应用中,这种方式不是很合适,需要保持大量状态数据,维护大量状态数据会影响程序性能,因为状态数据要么需要服务器资源,要么需要包含在页面本身(隐藏字段)或Cookie中;
2、如果不做任何并发处理,那么后保存的就直接覆盖前一个保存的数据,叫做: Client Wins or Last in Wins
3、最后一种就是,在后一个人点保存的时候,提示相应错误,告知其当前数据的状态,由其确认是否继续进行数据更新,这叫做:Store Wins(数据存储值优先于客户端提交的值),此方法确保没有在没有通知用户正在发生的更改的情况下覆盖任何更改。
Detecting Concurrency Conflicts 检测并发冲突
要想通过解决EF抛出的OptimisticConcurrencyException来处理并发冲突,必须先知道什么时候会抛出这个异常,EF必须能够检测到冲突。因此必须对数据模型进行适当的调整。
有两种选择:
1、在数据库表中增加一列用来记录什么时候这行记录被更新的,然后就可以配置EF的Update或者Delete命令中的Where部分把这列加上;
一般这个跟踪记录列的类型为 rowversion ,一般是一个连续增长的值。在Update或者Delete命令中的Where部分包含上该列的原本值;
如果原有记录被其他人更新,那么这个值就会变化,那么Update或者Delete命令就会找不到原本数据行;这个时候,EF就会认为出现了并发冲突。
2、通过配置EF,在所有的Update或者Delete命令中的Where部分把所有数据列都包含上;和第1种方式一样,如果其中有一列数据被其他人改变了,那么Update或者Delete命令就不会找到原本数据行,这个时候,EF就会认为出现了并发冲突。
这个方式唯一问题就是where后面要拖很长很长的尾巴,而且以前版本中,如果where后面太长会引发性能问题,所以这个方式不被推荐,后面也不会再讲。
如果确定要采用这个方案,则必须为每一个非主键的Properites都加上ConcurrencyCheck属性定义,这个会让EF的update的WHERE加上所有的列;
Add an Optimistic Concurrency Property to the Department Entity
给Modles/Department 加上一个跟踪属性:RowVersion
public class Department
{
public int DepartmentID { get; set; } [StringLength(, MinimumLength = )]
public string Name { get; set; } [DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; } [DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; } [Display(Name = "Administrator")]
public int? InstructorID { get; set; } [Timestamp]
public byte[] RowVersion { get; set; } public virtual Instructor Administrator { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
Timestamp 时间戳属性定义表示在Update或者Delete的时候一定要加在Where语句里;
叫做Timestamp的原因是SQL Server以前的版本使用timestamp 数据类型,后来用SQL rowversion取代了 timestamp 。
在.NET里 rowversion 类型为byte数组。
当然,如果喜欢用fluent API,你可以用IsConcurrencyToken方法来定义一个跟踪列:
modelBuilder.Entity<Department>()
.Property(p => p.RowVersion).IsConcurrencyToken();
记得变更属性后,要更新数据库,在PMC中进行数据库更新:
Add-Migration RowVersion
Update-Database
修改Department 控制器
先增加一个声明:
using System.Data.Entity.Infrastructure;
然后把控制器里4个事件里的SelectList里的 LastName 改为 FullName ,这样下拉选择框里就看到的是全名;显示全名比仅仅显示Last Name要友好一些。
下面就是对Edit做大的调整:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(int? id, byte[] rowVersion)
{
string[] fieldsToBind = new string[] { "Name", "Budget", "StartDate", "InstructorID", "RowVersion" }; if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
} var departmentToUpdate = await db.Departments.FindAsync(id);
if (departmentToUpdate == null)
{
Department deletedDepartment = new Department();
TryUpdateModel(deletedDepartment, fieldsToBind);
ModelState.AddModelError(string.Empty,
"Unable to save changes. The department was deleted by another user.");
ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
return View(deletedDepartment);
} if (TryUpdateModel(departmentToUpdate, fieldsToBind))
{
try
{
db.Entry(departmentToUpdate).OriginalValues["RowVersion"] = rowVersion;
await db.SaveChangesAsync(); return RedirectToAction("Index");
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var clientValues = (Department)entry.Entity;
var databaseEntry = entry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty,
"Unable to save changes. The department was deleted by another user.");
}
else
{
var databaseValues = (Department)databaseEntry.ToObject(); if (databaseValues.Name != clientValues.Name)
ModelState.AddModelError("Name", "Current value: "
+ databaseValues.Name);
if (databaseValues.Budget != clientValues.Budget)
ModelState.AddModelError("Budget", "Current value: "
+ String.Format("{0:c}", databaseValues.Budget));
if (databaseValues.StartDate != clientValues.StartDate)
ModelState.AddModelError("StartDate", "Current value: "
+ String.Format("{0:d}", databaseValues.StartDate));
if (databaseValues.InstructorID != clientValues.InstructorID)
ModelState.AddModelError("InstructorID", "Current value: "
+ db.Instructors.Find(databaseValues.InstructorID).FullName);
ModelState.AddModelError(string.Empty, "The record you attempted to edit "
+ "was modified by another user after you got the original value. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again. Otherwise click the Back to List hyperlink.");
departmentToUpdate.RowVersion = databaseValues.RowVersion;
}
}
catch (RetryLimitExceededException /* dex */)
{
//Log the error (uncomment dex variable name and add a line here to write a log.
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
}
ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
return View(departmentToUpdate);
}
可以看到,修改主要分为以下几个部分:
1、先通过ID查询一下数据库,如果不存在了,则直接提示错误,已经被其他用户删除了;
2、通过 db.Entry(departmentToUpdate).OriginalValues["RowVersion"] = rowVersion; 这个语句把原版本号给赋值进来;
3、EF在执行SaveChange的时候自动生成的Update语句会在where后面加上版本号的部分,如果语句执行结果没有影响到任何数据行,则说明出现了并发冲突;EF会自动抛出DbUpdateConcurrencyException异常,在这个异常里进行处理显示已被更新过的数据,比如告知用户那个属性字段被其他用户变更了,变更后的值是多少;
var clientValues = (Department)entry.Entity; //取的是客户端传进来的值
var databaseEntry = entry.GetDatabaseValues(); //取的是数据库里现有的值 ,如果取来又是null,则表示已被其他用户删除
这里有人会觉得,不是已经在前面处理过被删除的情况,这里又加上出现null的情况处理,是不是多余,应该是考虑其他异步操作的问题,就是在第1次异步查询到最后SaveChange之间也可能被删除。。。(个人觉得第1次异步查询有点多余。。也许是为了性能考虑吧)
最后就是写一堆提示信息给用户,告诉用户哪个值已经给其他用户更新了,是否还继续确认本次操作等等。
对于Edit的视图也需要更新一下,加上版本号这个隐藏字段:
@model ContosoUniversity.Models.Department @{
ViewBag.Title = "Edit";
} <h2>Edit</h2> @using (Html.BeginForm())
{
@Html.AntiForgeryToken() <div class="form-horizontal">
<h4>Department</h4>
<hr />
@Html.ValidationSummary(true)
@Html.HiddenFor(model => model.DepartmentID)
@Html.HiddenFor(model => model.RowVersion)
最后测试一下效果:
打开2个网页,同时编辑一个Department:
第一个网页先改预算为 0 ,然后点保存;
第2个网页改日期为新的日期,然后点保存,就出现以下情况:
这个时候如果继续点Save ,则会用最后一次数据更新到数据库:
忽然又有个想法,如果在第2次点Save之前,又有人更新了这个数据呢?会怎么样?
打开2个网页,分别都编辑一个Department ;
然后第1个网页把预算变更为 0 ;点保存;
第2个网页把时间调整下,点保存,这时候提示错误,不点Save ;
在第1个网页里,再编辑该Department ,把预算变更为 1 ,点保存;
回到第2个网页,点Save , 这时 EF会自动再次提示错误;
下面对Delete 处理进行调整,要求一样,就是删除的时候要检查是不是原数据,有没有被其他用户变更过,如果变更过,则提示用户,并等待用户是否确认继续删除;
把Delete Get请求修改一下,适应两种情况,一种就是有错误的情况:
public async Task<ActionResult> Delete(int? id, bool? concurrencyError)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Department department = await db.Departments.FindAsync(id);
if (department == null)
{
if (concurrencyError.GetValueOrDefault())
{
return RedirectToAction("Index");
}
return HttpNotFound();
} if (concurrencyError.GetValueOrDefault())
{
ViewBag.ConcurrencyErrorMessage = "The record you attempted to delete "
+ "was modified by another user after you got the original values. "
+ "The delete operation was canceled and the current values in the "
+ "database have been displayed. If you still want to delete this "
+ "record, click the Delete button again. Otherwise "
+ "click the Back to List hyperlink.";
} return View(department);
}
把Delete Post请求修改下,在删除过程中,处理并发冲突异常:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Delete(Department department)
{
try
{
db.Entry(department).State = EntityState.Deleted;
await db.SaveChangesAsync();
return RedirectToAction("Index");
}
catch (DbUpdateConcurrencyException)
{
return RedirectToAction("Delete", new { concurrencyError = true, id=department.DepartmentID });
}
catch (DataException /* dex */)
{
//Log the error (uncomment dex variable name after DataException and add a line here to write a log.
ModelState.AddModelError(string.Empty, "Unable to delete. Try again, and if the problem persists contact your system administrator.");
return View(department);
}
}
最后要修改下Delete的视图,把错误信息显示给用户,并且在视图里加上DepartmentID和当前数据版本号的隐藏字段:
@model ContosoUniversity.Models.Department @{
ViewBag.Title = "Delete";
} <h2>Delete</h2> <p class="error">@ViewBag.ConcurrencyErrorMessage</p> <h3>Are you sure you want to delete this?</h3>
<div>
<h4>Department</h4>
<hr />
<dl class="dl-horizontal">
<dt>
Administrator
</dt> <dd>
@Html.DisplayFor(model => model.Administrator.FullName)
</dd> <dt>
@Html.DisplayNameFor(model => model.Name)
</dt> <dd>
@Html.DisplayFor(model => model.Name)
</dd> <dt>
@Html.DisplayNameFor(model => model.Budget)
</dt> <dd>
@Html.DisplayFor(model => model.Budget)
</dd> <dt>
@Html.DisplayNameFor(model => model.StartDate)
</dt> <dd>
@Html.DisplayFor(model => model.StartDate)
</dd> </dl> @using (Html.BeginForm()) {
@Html.AntiForgeryToken()
@Html.HiddenFor(model => model.DepartmentID)
@Html.HiddenFor(model => model.RowVersion) <div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-default" /> |
@Html.ActionLink("Back to List", "Index")
</div>
}
</div>
最后看看效果:
打开2个网页进入Department Index页面,第1个页面点击一个Department的Edit ,第2个页面点击该 Department的Delete;
然后第一个页面把预算改为100,点击Save.
第2个页面点击Delete 确认删除,会提示错误:
EF学习笔记(十) 处理并发的更多相关文章
- EF学习笔记(十二):EF高级应用场景
学习总目录:ASP.NET MVC5 及 EF6 学习笔记 - (目录整理) 上篇链接:EF学习笔记(十一):实施继承 本篇原文链接:Advanced Entity Framework Scenari ...
- EF学习笔记(十一):实施继承
学习总目录:ASP.NET MVC5 及 EF6 学习笔记 - (目录整理) 上篇链接:EF学习笔记(十) 处理并发 本篇原文链接:Implementing Inheritance 面向对象的世界里, ...
- python3.4学习笔记(十四) 网络爬虫实例代码,抓取新浪爱彩双色球开奖数据实例
python3.4学习笔记(十四) 网络爬虫实例代码,抓取新浪爱彩双色球开奖数据实例 新浪爱彩双色球开奖数据URL:http://zst.aicai.com/ssq/openInfo/ 最终输出结果格 ...
- Learning ROS for Robotics Programming Second Edition学习笔记(十) indigo Gazebo rviz slam navigation
中文译著已经出版,详情请参考:http://blog.csdn.net/ZhangRelay/article/category/6506865 moveit是书的最后一章,由于对机械臂完全不知,看不懂 ...
- EF学习笔记(九):异步处理和存储过程
总目录:ASP.NET MVC5 及 EF6 学习笔记 - (目录整理) 上一篇:EF学习笔记(八):更新关联数据 本篇原文:Async and Stored Procedures 为何要采用异步? ...
- EF学习笔记(八):更新关联数据
学习笔记主目录链接:ASP.NET MVC5 及 EF6 学习笔记 - (目录整理) 上一篇链接:EF学习笔记(七):读取关联数据 本篇原文链接:Updating Related Data 本篇主要考 ...
- python3.4学习笔记(十八) pycharm 安装使用、注册码、显示行号和字体大小等常用设置
python3.4学习笔记(十八) pycharm 安装使用.注册码.显示行号和字体大小等常用设置Download JetBrains Python IDE :: PyCharmhttp://www. ...
- python3.4学习笔记(十九) 同一台机器同时安装 python2.7 和 python3.4的解决方法
python3.4学习笔记(十九) 同一台机器同时安装 python2.7 和 python3.4的解决方法 同一台机器同时安装 python2.7 和 python3.4不会冲突.安装在不同目录,然 ...
- python3.4学习笔记(十六) windows下面安装easy_install和pip教程
python3.4学习笔记(十六) windows下面安装easy_install和pip教程 easy_install和pip都是用来下载安装Python一个公共资源库PyPI的相关资源包的 首先安 ...
随机推荐
- Linux下使用ps命令查看某个进程文件的启动位置
ps -ef|grep shutdown ls -al /proc/4170
- 解决访问HTTPS,抛出的异常javax.net.ssl.SSLHandshakeException
本地测试没问题,http换成https抛出异常javax.net.ssl.SSLHandshakeException,网上有说是服务器证书,有说要启动SSL3协议的,反正没有找到有用的. 在GET和P ...
- AVL树实现记录
https://github.com/xieqing/avl-tree An AVL Tree Implementation In C There are several choices when i ...
- JDBC 心得
还记得jdbc的及个步骤, 一是class出对象 2 链接数据库 3 SQL pre开头的 4 允许SQL,result,exeupdate, 在这里想写的通过反射得到对象, Hibernate有 ...
- 小强学渲染之Unity Shader编程HelloWorld
第一个简单的顶点vert/片元frag着色器 1)打开Unity 5.6编辑器,新建一个场景后ctrl+s保存命名为Scene_5.默认创建的场景是包含了一摄像机,一平行光,且场景背景是一天空盒而 ...
- yum与rpm常用命令
1 yum常用命令 2 rpm常用命令 1 yum常用命令 (1)列出所有可更新的软件清单命令:yum check-update (2)更新所有软件命令:yum update (4)仅安装指定的 ...
- HDU - 4858 项目管理
N个点,M条无向边.现在有Q组操作,一种是给 i号点增加能量,一种是询问 i号点相邻点的能量和(点间有多条边就算两次). 据说暴力能过,但还是用这题学习了一下 点分块 . 度数不超过 sqrt(M) ...
- ARM指令集详解
一.跳转指令 B: 跳转指令 BL: 带返回的跳转指令 BLX: 带返回和状态切换的跳转指令 BX: 带状态切换的跳转指令 二.数据处理指令 1.MOV:数据传送指令 MOV{条件}{S} 目的 ...
- Kubernetes集群升级(kubeadm升级方式)
1.升级前的版本确认(相同的大版本号下的小版本升级还是跨版本升级) 例如:从1.12.0升级到1.12.7 或者 从1.12.7升级到1.13.0 2.配置kubernetes安装源(已配置kuber ...
- Windows10开机pin界面循环重启解决办法
昨天电脑在开机时,进入pin界面,输入pin码之后系统没反应,也不显示登陆成功,大概一分钟之后自动重启,遂百度答案:大部分建议都是在开机显示win图标时强制关机,强制关机两次即自动进入疑难解答页面,以 ...