關於DbUpdateConcurrencyException

Building an MVC 4 App with Database First and Entity Framework

最近遇到的專案,是已經有大量的資料在資料庫裡。對於這種情況,就不適合使用
比較熟悉的code first的方式來開發,所以將採用DB First的方式來進行。當我們用
ADO.NET Entity Data Model,visual studio 將會為我們先建立好相關的類別、模
型。

在這個時候問題來了,採用這種方式的話,我們在code first中,我們會直接把
表單驗證的條件寫在model中,但是db first的方式,我們取得的model是經由範本
產生的,所以當我們資料庫有做了異動,或是我們的model有做更新的話,我們把
驗證的條件寫在自動產生的model中,將會全被清掉。

在自動產生的類別可以看到這幾行的註解。


//------------------------------------------------------------------------------
// <auto-generated>
//    This code was generated from a template.
//
//    Manual changes to this file may cause unexpected behavior in your application.
//    Manual changes to this file will be overwritten if the code is regenerated.
// </auto-generated>
//----------------------------

所以這時候我們將要用到Buddy Class的幫忙。

例如一開始我們建立的model如下 : 

namespace MvcLeaning.Models
{
    using System;
    using System.Collections.Generic;
    
    public partial class Blog
    {
        public Blog()
        {
            this.Posts = new HashSet<Post>();
        }
    
        public int Id { get; set; }
        public string Title { get; set; }
        public string BloggerName { get; set; }
    
        public virtual ICollection<Post> Posts { get; set; }
    }
}

我們再另外建立一個新的類別。這裡就先取一個BuddyClass.cs
如下。

namespace MvcLeaning.Models
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;//using這個lib做資料驗證
    using System.Web.Mvc;

    [MetadataType(typeof(BlogMetaData))]//在這加入這一行,告訴系統說我們要用這個類別來做資料驗證。
    public partial class Blog //因這個類別是partial,所以我們直接在這個類別中加入我們要的方法。
    {
       
      // [Bind(Exclude = "Name")]//Exclude是用來要排除某個欄位不進行驗證。
        public class BlogMetaData
        {
           
            [ConcurrencyCheck]//需加上這個,避免寫入錯誤。
            public int Id { get; set; }
          
            [Required]
            public string Title { get; set; }

            [Required]
            public string BloggerName { get; set; }
        }

    }
}

在這個專案中我自已有遇到一個情況,DbUpdateConcurrencyException。解決方法參考以下的文章。

http://www.cnblogs.com/JustRun1983/archive/2012/10/10/2717891.html
作者為justrun


1. Concurrency的作用

场景

有个修改用户的页面功能,我们有一条数据User, ID是1的这个User的年龄是20, 性别是female(数据库中的原始数据)

正确的该User的年龄是25, 性别是male



这个时候A发现User的年龄不对, 就给改成25, 那么在Entity Framework中,我们会这样做。

var user = dbConext.User.Find(1);

//B用户在这里完成修改了User的性别

user.age = 25;

dbContext.SaveChanges();

但是加入在上面注释处,有个B用户发现性别不对,完成了对用户性别的修改,改成male. 会出现什么结果呢。

var user = dbConext.User.Find(1);

当A执行这个代码的时候,获取的性别是female

user.age = 25;

当A执行这个代码的时候, 不知道B已经修改了这个记录的性别,这个时候A的user的性别还是female

dbContext.SaveChanges();

保存修改的时候,会把female覆盖回去,这样B用户的修改就作废了。

 

但这不是A的本意,A其实只是想修改年龄而已。

Entity Framework使用[ConcurrencyCheck] 来解决这种问题, 当标记为[ConcurrencyCheck] 的Entity属性,如果发现在从数据库中取下来和提交的时候不一致,就会出现DbUpdateConcurrencyException异常,避免错误提交。

2. 如何正确处理DbUpdateConcurrencyException异常

2.1 数据库优先方式

原理是在出现异常的时候,重新加载数据库中的数据,覆盖Context本地数据



using (var context = new BloggingContext())
{
  var blog = context.Blogs.Find(1);
  blog.Name = "The New ADO.NET Blog";  
  bool saveFailed;
  do
  {
    saveFailed = false;
    try
    {
      context.SaveChanges();
    }
    catch (DbUpdateConcurrencyException ex)
    {
      saveFailed = true;
      // Update the values of the entity that failed to save from the store
      ex.Entries.Single().Reload();
    }
  } while (saveFailed);
}

2.2 客户端优先方式

以Context保存的客户端数据为主,覆盖数据库中的数据

using (var context = new BloggingContext())
{
  var blog = context.Blogs.Find(1);
  blog.Name = "The New ADO.NET Blog";
  bool saveFailed;
  do
  {
    saveFailed = false;
    try
    {
      context.SaveChanges();
    }
    catch (DbUpdateConcurrencyException ex)
    {
      saveFailed = true;
      // Update original values from the database
      var entry = ex.Entries.Single();
      entry.OriginalValues.SetValues(entry.GetDatabaseValues());
    }
  } while (saveFailed);

3.3 综合方式

有时候,不是非A即B的关系,我们希望综合数据库中的数据和context中修改的数据,再保存到数据库中

使用下面的CurrentValues, GetDatabaseValues(), 得到Context数据和数据库数据,重新构建一个正确的Entity,再更新到数据库中

using (var context = new BloggingContext())
{
  var blog = context.Blogs.Find(1);
  blog.Name = "The New ADO.NET Blog";
  bool saveFailed;
  do
  {
    saveFailed = false;
    try
    {
      context.SaveChanges();
    }
    catch (DbUpdateConcurrencyException ex)
    {
      saveFailed = true;
      // Get the current entity values and the values in the database
      var entry = ex.Entries.Single();
      var currentValues = entry.CurrentValues;
      var databaseValues = entry.GetDatabaseValues();
      // Choose an initial set of resolved values. In this case we
      // make the default be the values currently in the database.
      var resolvedValues = databaseValues.Clone();
      // Have the user choose what the resolved values should be
      HaveUserResolveConcurrency(currentValues, databaseValues, resolvedValues);
      // Update the original values with the database values and
      // the current values with whatever the user choose.
      entry.OriginalValues.SetValues(databaseValues);
      entry.CurrentValues.SetValues(resolvedValues);
    }
  } while (saveFailed);
}

public void HaveUserResolveConcurrency(DbPropertyValues currentValues,
DbPropertyValues databaseValues,
DbPropertyValues resolvedValues)
{
  // Show the current, database, and resolved values to the user and have
  // them edit the resolved values to get the correct resolution.
}

对上面方法的优化

使用DbPropertyValues总是别扭,使用Enttiy对象就会方便很多,下面就是转换成Entity对象操作的方法

using (var context = new BloggingContext())
{
  var blog = context.Blogs.Find(1);
  blog.Name = "The New ADO.NET Blog";
  bool saveFailed;
  do
  {
    saveFailed = false;
    try
    {
      context.SaveChanges();
    }
    catch (DbUpdateConcurrencyException ex)
    {
      saveFailed = true;
      // Get the current entity values and the values in the database
      // as instances of the entity type
      var entry = ex.Entries.Single();
      var databaseValues = entry.GetDatabaseValues();
      var databaseValuesAsBlog = (Blog)databaseValues.ToObject();
      // Choose an initial set of resolved values. In this case we
      // make the default be the values currently in the database.
      var resolvedValuesAsBlog = (Blog)databaseValues.ToObject();
      // Have the user choose what the resolved values should be
      HaveUserResolveConcurrency((Blog)entry.Entity,
        databaseValuesAsBlog,
        resolvedValuesAsBlog);
      // Update the original values with the database values and
      // the current values with whatever the user choose.
      entry.OriginalValues.SetValues(databaseValues);
      entry.CurrentValues.SetValues(resolvedValuesAsBlog);
    }
  } while (saveFailed);
}

public void HaveUserResolveConcurrency(Blog entity,
  Blog databaseValues,
  Blog resolvedValues)
{
// Show the current, database, and resolved values to the user and have
// them update the resolved values to get the correct resolution.
}







留言

這個網誌中的熱門文章

[心得] 圖解 微分、積分生活中的微積分-第一章

Objective-C的數學運算函式

c# 把List中重復的資料去掉