在 Asp.net MVC 2 中解决页面提交数据并发问题
通过本篇,你能了解到 Asp.net MVC 模型绑定处理过程,一种解决并发颗粒度到一条数据的方法.
* 如何解决互联网中某条数据的并发问题
在一个页面提交数据库前把从数据库取出的数据和提交时数据库中的数据比较,不同则给出提示.
在和其他童鞋的讨论中,都明确的指出了采用HashCode的方案,HashCode作为区分对象不同的一种方案,确实可行,但也有反对的声音.采用HashCode难免会涉及到对于实体对象的HashCode生成的问题,而实体对象的Hash又是根据自身属性生成,而属性又包含了其他关系,这种连带作用十分可观,也想过采用对于所有值属性采用Hash生成,对于关系对象着获取ID后再生成,但表数量也不少,如果考虑反射则每次提交都要遍历所有属性,性能客观,这还仅仅只是一次修改呢,折腾再三有了这样的方法.
在每张表中加入一个新的DateTime字段,每次修改赋最新值.
对,就是这么简单,每次读取时获得DateTime对象,返回到客户端页面,提交时再和数据库中对象比较.DateTime对象精确到毫秒,这种程度已经足够应付需求中所定义的并发.
通过Ticks构造的DateTime对象
*如何在Asp.net MVC中使用DateTime对象作为验证数据是否已过期依据
Asp.net MVC 的模型肩负着承载数据的责任.同时又很容易配置,使用.下面这张图介绍了Asp.net MVC如何处理实体对象模型的(截自Pro Asp.net MVC 2 Framework.
同样的,作为客户端,以单纯的字符串是无法直接在.net中使用的,又需要进行模型绑定.才能转换为.net对象,继而使用他.
知道了整个模型的整个流程后,我们有了几个对策,思考再三决定传递DateTime对象的Ticks作为依据,因为直接输出DateTime最多也就精确到秒,不能和数据库中的原始数据DateTime匹配.所以获得其Ticks,作为字符串传输,然后返回的时候再解析.有了解决思路后就好做了.
这是我们的模型,注意其中的HashTime可空属性. 注意他标上了一个UIHint Attribute,能够绑定指定的用户控件,继而单独的渲染该属性.
2 {
3 [HiddenInput]
4 [DisplayName("用户编号")]
5 [Required]
6 public int UserId { get; set; }
7
8 [Required(ErrorMessage="名称必须不为空")]
9 [DisplayName("用户名")]
10 [DataType(System.ComponentModel.DataAnnotations.DataType.Text)]
11 public string UserName { get; set; }
12 [Required(ErrorMessage="Email必须存在")]
13 [DisplayName("电子邮件")]
14 [DataType(System.ComponentModel.DataAnnotations.DataType.EmailAddress)]
15 public string Email { get; set; }
16
17 [HiddenInput(DisplayValue = false)]
18 [UIHint("HiddenDateTime")]
19 [Required(ErrorMessage="HashTime必须存在")]
20 public DateTime? HashTime { get; set; }
21
22
23
24 public ModelSimpleChangeUser() { }
25 public ModelSimpleChangeUser(ES.DAL.User u)
26 {
27 this.UserId = u.UserId;
28 this.Email = u.Email;
29 this.HashTime = u.HashTime;
30 this.UserName = u.UserName;
31 }
32
33 public ES.DAL.User ChangeTo(ES.DAL.User user)
34 {
35 if(UserId == user.UserId && user.HashTime.HasValue)
36 {
37 user.UserName = UserName;
38 user.Email = Email;
39 }
40 return user;
41 }
42 }
整个编辑视图也就一行起作用的代码.之所以能够这样是因为Asp.net MVC 2强大的模型特性标记功能,他能够在模型中声明如何显示,这就使得视图和模型完全分离,在视图里我都没有内联ViewPage的泛型类.使得页面可以编写完全独立的效果,而不必纠缠于和模型属性配对的问题中.
在HiddenDateTime控件实现了一个简单的从当前实体模型对象读取Ticks的功能,由于是可空DateTime,所以处理了为null情况(标记字段在测试的时候有些是Null,所以这里给处理了一下,如果在真实的环境中这是不存在的,因为在建立和修改时都已经自动赋值了)
2
3 <%
4 Dictionary<string, object> others = new Dictionary<string, object>();
5 //others.Add("Oth", "ccc");
6 %>
7 <%= Html.Hidden("", Model.HasValue ? Model.Value.Ticks : -1, others)%>
效果如下:
然后是绑定从客户端发来的数据问题.在这个点上费了很久,后来还是在书上找到了解决办法.
首先要知道Asp.net MVC是从那些地方以怎样的顺序获取了数据,下面是获取的顺序.
2. RouteData.Values
3. Request.QueryString
4. Request.Files
5. null
前台输出的是DateTime的Ticks而不是默认的DateTime输出,所以还需要自己解析下.这里有一段ValueProviderFactroy的代码是截自书中的,在Global.asax的Appcation_Start事件注册后,就能截获所有参数的传递了.
2 {
3 AreaRegistration.RegisterAllAreas();
4
5 RegisterRoutes(RouteTable.Routes);
6
7 ValueProviderFactories.Factories.Insert(0, new ES.WEB.Models.HiddentTimeValueProviderFactory());
8 }
下面是那个解析工厂
{
public override IValueProvider GetValueProvider(ControllerContext ctx)
{
return new HiddentTimeValueProvider(ctx.HttpContext.Request);
}
private class HiddentTimeValueProvider : IValueProvider
{
private HttpRequestBase request;
public HiddentTimeValueProvider(HttpRequestBase Request)
{
request = Request;
}
public bool ContainsPrefix(string prefix)
{
return "HashTime".Equals(prefix, StringComparison.OrdinalIgnoreCase);
}
public ValueProviderResult GetValue(string key)
{
var result = ContainsPrefix(key)
? new ValueProviderResult(new DateTime(long.Parse(request["HashTime"])), null, CultureInfo.CurrentCulture)
: null;
return result;
}
}
}
自此我们的代码已经完全可以跑起来了,在控制器里对传过来的数据直接分析,看Ticks是否相等就能判断有并发情况了.当然在返回错误的视图中我们也做了一些改善用户体验的处理.
下面是我们的控制器:
2 {
3 return DoSafe(() =>
4 {
5 var u = UserManager.FindById(users.UserId);
6 if (u.HashTime != null)
7 if(users.HashTime != null &&
8 users.HashTime.Value.Ticks == u.HashTime.Value.Ticks)
9 {
10 u.UserName = users.UserName;
11 u.Email = users.Email;
12 UserManager.Update(u);
13 return RedirectToAction("Index") as ActionResult;
14 }
15 ViewData["lastAction"] = "Edit";
16 ViewData["lastController"] = "User";
17 return View(RetiredPage, users);
18 });
19 }
注意15,16行,他将当前处理的Controller和Action的名称都保存了起来,以备在视图中使用,同样将用户输入的数据返回了出去.
下面是视图页面:
2
3 <h2>对不起,您访问的页面已过期!</h2>
4 <p>
5 您所操作的原始数据可能被更改,请您返回获取最新数据.
6 </p>
7 <%= Html.ActionLink("返回",
8 ViewData[BaseController.LastAction].ToString(),
9 ViewData[BaseController.LastController].ToString(),
10 Model, new { id = "PostBackLink"}) %>
11 <script type="text/javascript">
12 setTimeout(function () {
13 var g = document.getElementById("PostBackLink");
14 try { g.click(); } catch (ex) { }
15 }, 2000);
16 </script>
17 </asp:Content>
用HtmlHelper生成了一个超链接,使用了我们之前保存的控制器数据,从而得知是在哪里引发了错误,并且还保存了用户输入的数据模型,最后给这个超链接赋值了一个id属性,供我们在客户端脚本上使用.
在客户端脚本里我们触发了一个延迟脚本,两秒后触发返回超链接的click事件,由于在FireFox下没有该对象,所以只是简单的容错了一下,还有更多FireFox下的超链接的脚本触发请移步JavaScript模拟用户单击事件.
整个过程还是相当简单的,不过条条大路通罗马,不一定都要坐技术含量高的飞机嘛.
前些天分享了一个封装了EntityFramework的操作的类,这里又拓展了他,将HashTime自动写入,这里就没考虑泛型,因为数据的读写实在太频繁了,就将所有实体继承一个IHashTime接口,接口只有一个HashTime属性,然后封装了一个方法,在每次数据创建或者改写的时候调用.从而整个并发问题迎刃而解.
2 {
3 (o as IHashTime).HashTime = DateTime.Now;
4 }