• EF里Guid类型数据的自增长、时间戳和复杂类型的用法


    EF里Guid类型数据的自增长、时间戳和复杂类型的用法

    通过前两章Lodging和Destination类的学习,我们基本已经知道EF是怎么玩的了。
    文章的开头提示下:下载源码也要跟着文章的思路走。我写demo为了后面演示效果,前面代码有些是注释了的,请按照文章讲解的顺序先后释放注释。

    I.EF里Guid类型数据的自增长

    现在新添加一个Trip旅行类:

    复制代码
        /// <summary>
        /// 旅行类
        /// </summary>
        public class Trip
        {
            public Guid Identifier { get; set; }
            public DateTime StartDate { get; set; }   //开始时间
            public DateTime EndDate { get; set; }   //结束时间
            public decimal CostUSD { get; set; }     //花费
        }
    复制代码

    当然,还需要在BreakAwayContext类中添加上让上下文能识别Trip类:

    public DbSet<CodeFirst.Model.Trip> Trips { get; set; }

    跟以往的实体类不同的地方:Trip类的第一个属性不是类名+id,也不是int类型的;
    EF的默认约定就是第一个属性如果是类名+id,并且是int类型的,那么直接设置第一个属性为主键,同时设置自增长。显然两条都不符合,如果直接跑程序,那么会报一个ModelValidationException错:
    One or more validation errors were detected during model generation:
    System.Data.Edm.EdmEntityType: : EntityType 'Trip' has no key defined. Define the key for this EntityType.
    System.Data.Edm.EdmEntitySet: EntityType: EntitySet ?Trips? is based on type ?Trip? that has no keys defined.

    很明显是没有主键的原因。我们可以使用上一节说过的Data Annation的方式设置主键:直接在Identifier属性上加注[key]
    个人还是喜欢Fluent API的方式,按照之前说的,方便以后修改,我们在DataAccess类库下新建一个实现EntityTypeConfiguration接口的TripMap类,把所有的Fluent API配置都写在此类的构造函数里:

            public TripMap()
            {
                this.HasKey(t => t.Identifier);   //主键
            }

    同样,这个也要添加到BreakAwayContext类的OnModelCreating方法里:
    modelBuilder.Configurations.Add(new TripMap());

    这时我们再跑下程序,主键就正常的生成了。但是由于是guid类型的,EF一样不会为我们设置自增长,不设自增长会有什么坏处呢,往下看。
    我们添加一个新方法,插入一条数据到Trip表里:

    复制代码
            private static void InsertTrip()
            {
                var trip = new CodeFirst.Model.Trip
                {
                    CostUSD = 800,
                    StartDate = new DateTime(2011, 9, 1),
                    EndDate = new DateTime(2011, 9, 14)
                };
                using (var context = new CodeFirst.DataAccess.BreakAwayContext())
                {
                    context.Trips.Add(trip);
                    context.SaveChanges();
                }
            }
    复制代码

    在Main方法里调用下InsertTrip方法,再跑下程序,再去查看下数据库,结果:

    可见,没有设置自增长并且程序中没有向此字段注入guid类型的数据,数据库会自动补充一堆0,下次添加还是全部0,自然会报错(主键不允许重复)

    脑补:Guid就是全局统一标识符的意思,随机的一串字母,重复概率记录为0,所以适合用来当主键。可以向控制台打印Guid试试,每次执行都不一样:

    我们可以通过注解和api的方式为guid类型的数据设置自增长,分别是:

    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    this.Property(t => t.Identifier).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);  //Guid类型主键自增长

    再跑下程序,guid列就有值了。

    有心的朋友肯定注意到了:DatabaseGeneratedOption还有另外两个枚举值:None(空)、Computed(计算)
    None还是挺有用的,这里演示下None的用法,新建一个Person类:

    复制代码
        /// <summary>
        /// 人类
        /// </summary>
        public class Person
        {
            public int SocialSecurityNumber { get; set; }   //社保号
            public string FirstName { get; set; }
            public string LastName { get; set; }
        }
    复制代码

    没有PersonId属性,那么我们需要手动配置主键。前面说过很多次了,这里不再赘述。直接使用Fluent API配置(不会请下载源码)
    配置好关系后,我们在Program.cs里再加一个方法,向Person表里插入一条数据:

    复制代码
            private static void InsertPerson()
            {
                var person = new CodeFirst.Model.Person
                {
                    FirstName = "Rowan",
                    LastName = "Miller",
                    SocialSecurityNumber = 12345678
                };
                using (var context = new CodeFirst.DataAccess.BreakAwayContext())
                {
                    context.People.Add(person);
                    context.SaveChanges();
                }
            }
    复制代码

    运行后如图,id为1,并不是我们插入的12345678。因为int类型的设置为主键后,会自动设置标识增量1、标识种子1。那么就从1开始自增长了,它会忽略这行插入的任何数据。

    我们可以通过配置None来解决这个问题,很明显了设置为None就是不自增长了。

    this.Property(p => p.SocialSecurityNumber).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);

    再跑下程序,id列就是我们设置的12345678了。
    注意:每次重新配置实体类和映射关系重新生成数据库的过程都需要【断开数据库连接】,否则提示“数据库正在使用,无法删除重新生成”

    II.EF里时间戳的用法

    我们在Trip类和Person类里同时添加一个byte[]字节数组的属性(时间戳必须是byte[]类型的):

    public byte[] RowVersion { get; set; }

    直接在属性上标注:[Timestamp],或者使用Fluent API配置:

    this.Property(p => p.RowVersion).IsRowVersion();  //时间戳

    重新跑下程序:

    可以看出RowVersion列的类型是timestamp类型的,时间戳可以防并发。并发分为乐观和悲观并发
    悲观并发:一个用户访问一条数据时,则把这个数据变为只读属性 。把该数据变为独占,只有该用户释放了这条数据,其他用户才能修改。
    乐观并发:用户读取数据时不锁定数据。当一个用户更新数据时,系统将进行检查,查看该用户读取数据后其他用户是否又更改了该数据。如果其他用户更新了数据,将产生一个错误。
    摘自这里。EF里的并发都是乐观并发。

    重新执行下之前的InsertTrip方法,同时打开sql profiler跟踪下发到数据库的sql:
    注意:如果不改变实体直接执行,那么不会重新生成数据库,再执行下InsertTrip方法,那么Trip表加上之前的数据就有两条了。如果想不管实体发生不发生变化都重新生成数据库,那么直接在main方法里初始化数据库之后加上:

                using (var context = new CodeFirst.DataAccess.BreakAwayContext())
                {
                    context.Database.Initialize(true);
                }

    有时间戳的表,虽然是插入语句 但是仍然执行了查询,每次都返回了RowVersion:

    复制代码
    exec sp_executesql N'declare @generated_keys table([Identifier] uniqueidentifier)
    insert [dbo].[Trips]([StartDate], [EndDate], [CostUSD])
    output inserted.[Identifier] into @generated_keys
    values (@0, @1, @2)
    select t.[Identifier], t.[RowVersion]
    from @generated_keys as g join [dbo].[Trips] as t on g.[Identifier] = t.[Identifier]
    where @@ROWCOUNT > 0',N'@0 datetime2(7),@1 datetime2(7),@2 decimal(18,2)',@0='2011-09-01 00:00:00',@1='2011-09-14 00:00:00',@2=800.00
    复制代码

    比较乱,重点看这一句:select t.[Identifier], t.[RowVersion] from...

    继续演示,添加一个更新的方法:

    复制代码
            private static void UpdateTrip()
            {
                using (var context = new CodeFirst.DataAccess.BreakAwayContext())
                {
                    var trip = context.Trips.FirstOrDefault();
                    trip.CostUSD = 750;
                    context.SaveChanges();
                }
            }
    复制代码

    把价格从800改成750
    注意:不要重复执行该方法,数据库的CostUSD价格是750的话,再执行此方法把价格改成750没效果,EF监测到修改之前之后没区别的话不会发sql到数据库,可自行测试下。
    注意:必须同时执行InsertTrip方法和UpdateTrip方法,否则重新生成数据库后找不到要修改的数据。
    sql profiler监测到update的sql:

    复制代码
    exec sp_executesql N'update [dbo].[Trips]
    set [CostUSD] = @0
    where (([Identifier] = @1) and ([RowVersion] = @2))
    select [RowVersion]
    from [dbo].[Trips]
    where @@ROWCOUNT > 0 and [Identifier] = @1',N'@0 decimal(18,2),@1 uniqueidentifier,@2 binary(8)',@0=750.00,@1='1D424880-CCC2-4F34-8D6E-3838E5FC72EF',@2=0x00000000000007D1
    复制代码

    重点看这一句:where (([Identifier] = @1) and ([RowVersion] = @2))
    更新的where同时俩条件:一个是主键值,一个就是时间戳。如果有人修改了数据,那么时间戳RowVersion的值就不一样了,就不符合where条件了,自然无法更新,程序会抛错。同样,如果更新成功,那么同时也会更新时间戳的值。
    为何sql profiler没监控到更新时间戳的sql呢?看这里

    ToHexString是一个二进制数据转成16进制字符串的方法。注意看修改前和修改后的时间戳不一样了。
    用心的朋友肯定看到了,为何sql profiler监控的时间戳的值是0x00000000000007D1,而程序里拿到的是00000000000007D1
    这就是计算机16进制的问题了。一般都会在开头加上0x表示16进制的数。其他不够位数的都补0。看这里

    这就是ef的乐观并发。为了更好的验证,我们新加一个方法:

    复制代码
            private static void UpdateTrip2()
            {
                var firstContext = new CodeFirst.DataAccess.BreakAwayContext();
                var trip1 = firstContext.Trips.FirstOrDefault();  //第一个用户取出第一条记录
                trip1.CostUSD = 750;  //修改但是还没来得及保存
    
                using (var secondContext = new CodeFirst.DataAccess.BreakAwayContext())
                {
                    var trip2 = secondContext.Trips.FirstOrDefault();  //第二个用户进来同样取出第一条记录
                    trip2.CostUSD = 900;
                    secondContext.SaveChanges();  //修改并保存(保存的操作不仅修改了CostUSD为900,同时修改了RowVersion)
                }
    
                try
                {
                    firstContext.SaveChanges();  //此时第一个用户想保存,但是RowVersion已经改变了
                    Console.WriteLine("保存成功!");
                }
                catch (DbUpdateConcurrencyException ex)
                {
                    Console.WriteLine(ex.Entries.First().Entity.GetType().Name + " 保存失败");
                }
                finally
                {
                    firstContext.Dispose();
                }
            }
    复制代码

    刚才我们演示了通过设置列为timestamp类型达到控制并发的效果,但是好多数据库甚至没有timestamp类型,所以控制并发再介绍一个:ConcurrencyCheck
    社保号SocialSecurityNumber一般是唯一的,它是int类型,并不是timestamp类型。我们使用控制并发,直接标注ConcurrencyCheck或者使用Fluent API配置(具体见源码)再添加一个更新的方法:

    复制代码
            private static void UpdatePerson()
            {
                using (var context = new CodeFirst.DataAccess.BreakAwayContext())
                {
                    var person = context.People.FirstOrDefault();
                    person.FirstName = "Rowena";
                    context.SaveChanges();
                }
            }
    复制代码

    和之前的InsertPerson方法一起执行,否则找不到对象,sql profiler监控到的sql语句:

    exec sp_executesql N'update [dbo].[People]
    set [FirstName] = @0
    where ([SocialSecurityNumber] = @1)
    ',N'@0 nvarchar(max) ,@1 int',@0=N'Rowena',@1=12345678

    修改的时候有双条件,这样也达到了控制并发的效果,和timestamp差不多。

    III.EF中的复杂类型
    如果给Person类添加更详细的信息,类似:StreetAddress、City、State、ZipCode等属性,我们可以抽象出一个Address类,然后把Address类作为Person类的属性,这样更符合面向对象的思维,也方便理解。如果直接把Address类作为Person的属性,那么就如前面第一章的例子,自动映射成主外键关系了。我们要的自然不是,可以通过标注复杂类型来得到我们想要的。先添加Address类:

    复制代码
        /// <summary>
        /// 地址类(复杂类型)
        /// </summary>
        public class Address
        {
            //public int AddressId { get; set; } //复杂类型不能有主键idpublic string StreetAddress { get; set; }
            public string City { get; set; }
            public string State { get; set; }  //public string ZipCode { get; set; }  //右边
        }
    复制代码

    同时在Person类里添加Address类的属性。把Main方法里所有的方法都注释了,直接重新生成数据库,我们可以看到Address的属性都在People表里了。
    当然一个类也可以包括多个复杂类型,具体请自行尝试。

    本文到此结束,谢谢阅读。本章源码

    系列文章导航:
    1. EF Code First 初体验
    2. EF的默认映射以及如何使用Data Annotations和Fluent API配置数据库的映射
    3. EF里Guid类型数据的自增长、时间戳和复杂类型的用法
     
     
  • 相关阅读:
    MySQL-安装mysql8
    MySQL-Prometheus
    MySQL-sysbench
    MySQL-客户端登录问题
    学习进度第十六周
    学习进度第十五周
    寻找最长单词链
    用户体验评价
    学习进度第十四周
    找水王问题
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/3245149.html
Copyright © 2020-2023  润新知