如何处理Entity Framework / Entity Framework Core中的DbUpdateConcurrencyException异常(转载)

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异常,避免错误提交。

顺便说下,如果在实体类的属性上不用[ConcurrencyCheck]标签,在EF Core中上面这种情况是不会抛出异常的。不加[ConcurrencyCheck]标签时,EF Core中发生DbUpdateConcurrencyException异常的条件是,使用DbContext.SaveChanges方法时,生成的Update或Delete语句根据实体的Key属性值在数据库表中找不到对应行。

不过实测EF Core的实体类属性加上[ConcurrencyCheck]标签后,也会出现上面说的问题。

2. 如何正确处理DbUpdateConcurrencyException异常


2.1 放弃更新或删除数据库中已经不存在的数据

原理就是,发生DbUpdateConcurrencyException异常时将实体的EntityState设置为Detached,放弃更新或删除抛出DbUpdateConcurrencyException异常的实体,只更新或删除不抛出异常的实体:

using (TestDBContext dbContext = new TestDBContext())
{
    var jim = dbContext.Person.Where(p => p.Name == "Jim").First();
    var sam = dbContext.Person.Where(p => p.Name == "Sam").First();
    var jack = dbContext.Person.Where(p => p.Name == "Jack").First();

    jim.Age = jim.Age + 10;
    sam.Age = sam.Age + 10;
    jack.Age = jack.Age + 10;

    bool saveFailed;
    do
    {
        saveFailed = false;
        try
        {
            dbContext.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            saveFailed = true;
            // 获取抛出DbUpdateConcurrencyException异常的实体
            var entry = ex.Entries.Single();

            // 设置实体的EntityState为Detached,放弃更新或删除抛出异常的实体
            entry.State = EntityState.Detached;

        }
    } while (saveFailed);
}

也可以重写DbContext的SaveChanges方法,加入上面的逻辑来安全使用DbContext.SaveChanges方法,当Entity Framework的实体类属性上不用[ConcurrencyCheck]标签时,这种方式就很适合。

public override int SaveChanges()
{
    bool saveFailed;
    do
    {
        saveFailed = false;
        try
        {
            return base.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            saveFailed = true;
            // 获取抛出DbUpdateConcurrencyException异常的实体
            var entry = ex.Entries.Single();

            // 设置实体的EntityState为Detached,放弃更新或删除抛出异常的实体
            entry.State = EntityState.Detached;

        }
    } while (saveFailed);

    return 0;
}

2.2 数据库优先方式

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

using (TestDBContext dbContext = new TestDBContext())
{
    var jim = dbContext.Person.Where(p => p.Name == "Jim").First();
    var sam = dbContext.Person.Where(p => p.Name == "Sam").First();
    var jack = dbContext.Person.Where(p => p.Name == "Jack").First();

    jim.Age = jim.Age + 10;
    sam.Age = sam.Age + 10;
    jack.Age = jack.Age + 10;

    bool saveFailed;
    do
    {
        saveFailed = false;
        try
        {
            dbContext.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.3 客户端优先方式

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

using (TestDBContext dbContext = new TestDBContext())
{
    var jim = dbContext.Person.Where(p => p.Name == "Jim").First();
    var sam = dbContext.Person.Where(p => p.Name == "Sam").First();
    var jack = dbContext.Person.Where(p => p.Name == "Jack").First();

    jim.Age = jim.Age + 10;
    sam.Age = sam.Age + 10;
    jack.Age = jack.Age + 10;

    bool saveFailed;
    do
    {
        saveFailed = false;
        try
        {
            dbContext.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            saveFailed = true;
            // Update original values from the database
            var entry = ex.Entries.Single();
            entry.OriginalValues.SetValues(entry.GetDatabaseValues());
        }
    } while (saveFailed);
}

2.4 根据情况判断做相应的处理
综合前面几种方式的处理方法,根据抛出DbUpdateConcurrencyException异常实体的EntityState值做相应的处理

using (TestDBContext dbContext = new TestDBContext())
{
    var jim = dbContext.Person.Where(p => p.Name == "Jim").First();
    var sam = dbContext.Person.Where(p => p.Name == "Sam").First();
    var jack = dbContext.Person.Where(p => p.Name == "Jack").First();

    jim.Age = jim.Age + 10;
    sam.Age = sam.Age + 10;
    jack.Age = jack.Age + 10;

    bool saveFailed;
    do
    {
        saveFailed = false;
        try
        {
            dbContext.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            saveFailed = true;
            var entry = ex.Entries.Single();
            //The MSDN examples use Single so I think there will be only one
            //but if you prefer - do it for all entries
            //foreach(var entry in ex.Entries)
            //{
            if (entry.State == EntityState.Deleted)
                //When EF deletes an item its state is set to Detached
                //http://msdn.microsoft.com/en-us/data/jj592676.aspx
                entry.State = EntityState.Detached;
            else
                entry.OriginalValues.SetValues(entry.GetDatabaseValues());
            //throw; //You may prefer not to resolve when updating
            //}
        }
    } while (saveFailed);
}

不过这种方式可能会造成死循环。。。还需要改进,原因如下:

well according to msdn.microsoft.com/en-us/library/… ex.Entries contains all the entities that could not be saved. The only problem i have with the while loop is that it will spin forever if there is an entity that could not be saved for a reason other than entry.State == EntityState.Deleted. 

来自链接

参考文件

如何处理Entity Framework中的DbUpdateConcurrencyException异常

猜你喜欢

转载自www.cnblogs.com/OpenCoder/p/9847517.html
今日推荐