8. 冲突的处理

如果多个用户修改同一个记录,然后保存状态,会发生什么?最后谁的变更会保存下来?

如果访问同一个数据库的多个用户处理不同的记录,就没有冲突。所有用户都可以保存她们的数据,而不干扰其他用户编辑的数据。但是,如果多个用户处理同一记录,就需要考虑如何解决冲突。有不同的方法来处理冲突。最简单的一个方法是最后一个用户获胜。最后保存数据的用户覆盖以前用户执行的变更。

EF Core还提供了一种方式,使第一个保存数据的用户获胜,采用这一选项,保存记录时,需要验证最初读取的数据是否仍在数据库中。如果是,就继续保存数据,因为读写操作之间没有发生变化。然而,如果数据发生了变化,就需要解决冲突。

下面看看这些不同的选项。

1. 最后一个更改获胜

默认情况下,最后一个保存的更改获胜。为了查看对数据库的多个访问,扩展BooksSample应用程序。

为了简单地模拟两个用户,方法ConfictHandling调用两次方法PrepareUpdate,对两个引用相同记录的Book对象进行不同的改变,病调用Update方法两次。最后,把书的ID传递给CheckUpdate方法,它显示了数据库中书的实际状态:

        private static async Task ConflictHandling()
        {
            //user 1
            var tuple1 = await PrepareUpdate();
            tuple1.book.Title = "updated from user 1";

            //user 2
            var tuple2 = await PrepareUpdate();
            tuple2.book.Title = "updated from user 2";

            //user 1
            await Update(tuple1.context, "user 1");

            //user 2
            await Update(tuple2.context, "user 2");

            tuple1.context.Dispose();
            tuple2.context.Dispose();

            await CheckUpdate(tuple1.book.BookId);
        }
    }

PrepareUpdate方法打开一个BooksContext,并在元祖中返回上下文和图书。记住,该方法调用两次,返回与不同context对象相关的不同Book对象:

        private static async Task<(BooksContext context, Book book)> PrepareUpdate()
        {
            var context = new BooksContext();
            Book book = await context.Books
                .Where(b => b.Title == "JavaScript for Kids")
                .FirstOrDefaultAsync();
            return (context, book);
        }

Update方法接收打开的BooksContext,把这本书保存到数据库中。记住,该方法调用两次:

        private static async Task Update(BooksContext context, string user)
        {
            int records = await context.SaveChangesAsync();
            Console.WriteLine($"{user}: {records} record try to updated form {user}");
        }

CheckUpdate方法把指定id的图书写到控制台:

        private static async Task CheckUpdate(int id)
        {
            using (var context = new BooksContext())
            {
                Book book = await context.Books.FindAsync(id);
                Console.WriteLine($"updated : {book.Title}");
            }
        }

运行应用程序时,会发生什么呢?第一个更新会成功,第二个更新也会成功。更新一条记录时,不验证读取记录后是否发生变化,这个示例应用程序就是这样。第二个更新会覆盖第一个更新的数据,如应用程序的输出所示:

user 1: 1 record try to updated form user 1
user 2: 1 record try to updated form user 2
updated : updated from user 2

2. 第一个更改获胜

如果需要不同的行为,如第一个用户的更改保存到记录中,就需要做一些改变。示例项目ConflictHandlingSamples像以前一样使用Book和BooksContext对象,但它处理第一个更改获胜的场景。

为了解决冲突,需要指定属性,如果在读取和更新之间发生了什么,就应使用并发性令牌验证该属性,基于指定的属性,修改SQL UPDATE语句,不仅验证主键,还验证用并发性令牌标记的所有属性。给实体类型添加许多并发性令牌,会在UPDATE语句中创建一个巨大的WHERE子句,这不是非常有效。相反,可以添加一个属性,在SQL Server中用每个UPDATE语句更新——这就是Book类完成的工作。属性TimeStamp在SQL Server中定义为timeStamp:

    public class Book
    {
        public int BookId { get; set; }
        public string Title { get; set; }
        public string Publisher { get; set; }
        public byte[] TimeStamp { get; set; }
    }

在SQL Server中将TimeStamp属性定义为timestamp类型,要使用Fluent API。SQL数据类型使用HasColumeType方法定义。方法ValueGeneratedOnAddOrUpdate通知上下文,在每一个SQL INSERT或UPDATE语句中,可以改变TimeStamp属性,这些操作后,它需要用上下文设置。IsConcurrencyToken方法将这个属性标记为必要,检查它在读取操作完成后是否没有改变:

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Entity<Book>(m => 
            {
                m.HasKey(b => b.BookId);
                m.Property(b => b.Title).HasMaxLength(120).IsRequired();
                m.Property(b => b.Publisher).HasMaxLength(50);
                m.Property(b => b.TimeStamp)
                .HasColumnType("timestamp")
                .ValueGeneratedOnAddOrUpdate()
                .IsConcurrencyToken();
            });
        }

注意

不使用IsConcurrencyToken方法与Fluent API,也可以给应检查并发性的属性应用ConcurrencyCheck特性。

检查冲突处理的过程类似于之前的操作。用户1和用户2都调用PrepareUpdate方法,改变了书名,并调用Update方法修改数据库:

        private static async Task ConflictHandling()
        {
            //user 1
            var tuple1 = await PrepareUpdate();
            tuple1.book.Title = "user 1 wins";

            //user 2
            var tuple2 = await PrepareUpdate();
            tuple2.book.Title = "user 2 wins";

            //user 1
            await Update(tuple1.context, "user 1");

            //user 2
            await Update(tuple2.context, "user 2");

            tuple1.context.Dispose();
            tuple2.context.Dispose();

            await CheckUpdate(tuple1.book.BookId);
        }

这里不重复PrepareUpdate方法,因为该方法的实现方式与前面的示例相同。Update方法则截然不同。为了查看更新前和更新后不同的时间戳,实现自己数组的自定义扩展方法StringOutput,将字节数组以可读的形式写到控制台。接着,调用ShowChanges辅助方法,显示对Book对象的更改。调用SaveChanges方法,把所有更新写到数据库中。如果更新失败,并抛出DbUpdateConcurrencyException异常,就把失败信息写入控制台:

        private static async Task Update(BooksContext context,Book book, string user)
        {
            //int records = await context.SaveChangesAsync();
            //Console.WriteLine($"{user}: {records} record try to updated form {user}");

            try
            {
                Console.WriteLine($"{user}: updating id {book.BookId}, timestamp: {book.TimeStamp.StringOutput()}");
                ShowChanges(book.BookId, context.Entry(book));
                int records = await context.SaveChangesAsync();
                Console.WriteLine($">>>{user}: updated {book.TimeStamp.StringOutput()}>>>");
                Console.WriteLine($"{user}: {records} record(s) updated while updating {book.Title}");
            }
            catch (DbUpdateConcurrencyException ex)
            {
                Console.WriteLine($"{user}: update failed with {book.Title}");
                Console.WriteLine($"error: {ex.Message}");
                foreach (var entry in ex.Entries)
                {
                    if (entry.Entity is Book b)
                    {
                        Console.WriteLine($"{b.Title} {b.TimeStamp.StringOutput()}");
                        ShowChanges(book.BookId,context.Entry(book));
                    }                   
                }
            }
        }

对于上下文相关的对象,使用PropertyEntry对象可以访问原始值和当前值。从数据库中读取对象时获取的原始值,可以用OriginalValue属性访问,其当前值可以用CurrentValue属性访问。在ShowChanges和ShowChange方法中,PropertyEntry对象可以用EntityEntry的属性方法访问,如下所示:

        private static void ShowChanges(int id,EntityEntry entity)
        {
            void ShowChange(PropertyEntry propertyEntry)=>
                Console.WriteLine($"id: {id}, current: {propertyEntry.CurrentValue}, "+
                $"original: {propertyEntry.OriginalValue}, "+
                $"modified: {propertyEntry.IsModified}");
            ShowChange(entity.Property("Title"));
            ShowChange(entity.Property("Publisher"));
        }

为了转换SQL Server中更新的TimeStamp属性的字节数组,以可视化输出,定义了扩展方法StringOutput:

   public static class ByteArrayExtension
    {
        public static string StringOutput(this byte[] data)
        {
            var str = new StringBuilder();
            foreach (var byteValue in data)
            {
                str.Append($"{byteValue}.");
            }
            return str.ToString();
        }
    }

当运行应用程序时,可以看到如下输出。时间戳值在每次运行时都不同。第一个用户把书的原标题 JavaScript for Kids 更新为新标题 user 1 wins 。IsModified属性给Title属性返回true,但给Publisher属性返回false。因为只有标题改变了。原来的时间戳是0.0.0.0.0.0.54.177. 。更新到数据库后,时间戳改为 updated 0.0.0.0.0.0.54.178. 。与此同时,用户2打开相同的记录;该书的时间戳仍然是 0.0.0.0.0.0.54.177. 。用户2更新该书,但这里更新失败了,因为该书的时间戳不匹配数据库中的时间戳。这里会抛出一个DbUpdateConcurrencyException异常。在异常处理过程中,异常的原因写入控制台,如程序的输出所示:

user 1: updating id 3, timestamp: 0.0.0.0.0.0.54.177.
id: 3, current: user 1 wins, original: JavaScript for Kids, modified: True
id: 3, current: Wrox Press, original: Wrox Press, modified: False
>>>user 1: updated 0.0.0.0.0.0.54.178.>>>
user 1: 1 record(s) updated while updating user 1 wins
user 2: updating id 3, timestamp: 0.0.0.0.0.0.54.177.
id: 3, current: user 2 wins, original: JavaScript for Kids, modified: True
id: 3, current: Wrox Press, original: Wrox Press, modified: False
user 2: update failed with user 2 wins
error: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
user 2 wins 0.0.0.0.0.0.54.177.
id: 3, current: user 2 wins, original: JavaScript for Kids, modified: True
id: 3, current: Wrox Press, original: Wrox Press, modified: False
updated : user 1 wins

当使用并发性令牌和处理DbUpdateConcurrencyException时,可以根据需要处理并发冲突。例如,可以自动解决并发问题。如果改变了不同的属性,可以检索更改的记录并合并更改。如果改变的属性是一个数字,要执行一些计算,例如点系统,就可以在两个更新中递增或递减值,如果达到极限,就抛出一个异常。也可以给用户提供数据库中目前的信息,询问他要进行什么修改,要求用户解决并发性问题。不要要求用户提供太多的信息。用户可能只是想摆脱这个很少显示的对话框,这意味着他可能会单击OK或Cancle,而不阅读其内容。对于罕见的冲突,也可以编写日志,通知系统管理员,需要解决一个问题。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值