通过前两章Lodging和Destination类的演示,大家肯定基本了解Code First是怎么玩的了,本章继续演示一些很实用的东西。
文章的开头提示下:提供的demo为了后面演示效果,前面代码有些是注释了的,请按照文章讲解的顺序先后释放注释并运行查看效果。

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

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

  1. /// <summary>
  2. /// 旅行类
  3. /// </summary>
  4. public class Trip
  5. {
  6. public Guid Identifier { get; set; }
  7. public DateTime StartDate { get; set; } //开始时间
  8. public DateTime EndDate { get; set; } //结束时间
  9. public decimal CostUSD { get; set; } //花费
  10. }

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

  1. 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配置都写在此类的构造函数里:

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

同样,这个也要添加到BreakAwayContext类的OnModelCreating方法里:

  1. modelBuilder.Configurations.Add(new TripMap());

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

  1. private static void InsertTrip()
  2. {
  3. var trip = new CodeFirst.Model.Trip
  4. {
  5. CostUSD = ,
  6. StartDate = new DateTime(, , ),
  7. EndDate = new DateTime(, , )
  8. };
  9. using (var context = new CodeFirst.DataAccess.BreakAwayContext())
  10. {
  11. context.Trips.Add(trip);
  12. context.SaveChanges();
  13. }
  14. }

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

可见,没有设置自增长并且程序中没有向此字段注入guid类型的数据,数据库会自动补充一堆0,下次再添加还是全部0,自然会报错(主键不允许重复)
脑补:Guid就是全局统一标识符的意思,随机的一串字母。几乎不可能重复,所以适合用来当主键。可以向mssql的控制台打印Guid试试,每次执行都不一样:

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

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

再跑下程序,guid列就有值了。
到这里就有疑问了,自增长不是像int类型一样每次加1么,这里的guid无法加1啊,其实就是自动生成。我本以为大家对“自增长”理解太狭隘啊:+1才是自增长,其实类似guid自动生成的形式也属于自增长。但是查阅了资料,也没有什么结果。大家在文章下面的留言都挺有道理。这里姑且算自动生成的吧,也更好理解。留个坑,以后学习遇到了再来修改,当然如果有高手愿意分享,期待你的回复。

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

  1. /// <summary>
  2. /// 人类
  3. /// </summary>
  4. public class Person
  5. {
  6. public int SocialSecurityNumber { get; set; } //社保号
  7. public string FirstName { get; set; }
  8. public string LastName { get; set; }
  9. }

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

  1. private static void InsertPerson()
  2. {
  3. var person = new CodeFirst.Model.Person
  4. {
  5. FirstName = "Rowan",
  6. LastName = "Miller",
  7. SocialSecurityNumber =
  8. };
  9. using (var context = new CodeFirst.DataAccess.BreakAwayContext())
  10. {
  11. context.People.Add(person);
  12. context.SaveChanges();
  13. }
  14. }

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

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

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

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

II.EF里时间戳的用法

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

  1. public byte[] RowVersion { get; set; }

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

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

重新跑下程序:

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

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

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

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

  1. exec sp_executesql N'declare @generated_keys table([Identifier] uniqueidentifier)
  2. insert [dbo].[Trips]([StartDate], [EndDate], [CostUSD])
  3. output inserted.[Identifier] into @generated_keys
  4. values (@0, @1, @2)
  5. select t.[Identifier], t.[RowVersion]
  6. from @generated_keys as g join [dbo].[Trips] as t on g.[Identifier] = t.[Identifier]
  7. 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...
继续演示,添加一个更新的方法,把价格从800修改成750:

  1. private static void UpdateTrip()
  2. {
  3. using (var context = new CodeFirst.DataAccess.BreakAwayContext())
  4. {
  5. var trip = context.Trips.FirstOrDefault();
  6. trip.CostUSD = ;
  7. context.SaveChanges();
  8. }
  9. }

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

  1. exec sp_executesql N'update [dbo].[Trips]
  2. set [CostUSD] = @0
  3. where (([Identifier] = @1) and ([RowVersion] = @2))
  4. select [RowVersion]
  5. from [dbo].[Trips]
  6. 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条件了,自然无法更新,同时程序会抛错。同样,如果更新成功,那么同时也会更新时间戳的值。下图展示了修改数据前后时间戳的值:

ToHexString是一个二进制数据转成16进制字符串的方法。注意看修改前和修改后的时间戳不一样了。但是为何sql profiler监控的时间戳的值是0x00000000000007D1,而程序里拿到的是00000000000007D1?这就是计算机16进制的问题了。一般都会在开头加上0x表示16进制的数。其他不够位数的都补0。看这里 为了说明这个,再举一例:打开windows 7自带的计算器,选择查看 - 程序员 - 选择左侧16进制 - 输入上面的7D1:

然后点击左侧的十进制,看十六进制的7D1转换成了十进制的2001了:

转到程序里试试,先定义一个2001的int类型变量,然后调试看看它的16进制是多少(右键 - 以16进制查看)。的确如我们所料:0x开头,不够位数的都补0了:

简单的进制转换问题。有时间戳的数据修改后,时间戳列的值都会改变,但是注意看更新的sql,为何并没有看到更新时间戳列的sql?看这里
这就是ef的乐观并发。为了更好的验证,新加一个新的方法:

  1. private static void UpdateTrip2()
  2. {
  3. var firstContext = new CodeFirst.DataAccess.BreakAwayContext();
  4. var trip1 = firstContext.Trips.FirstOrDefault(); //第一个用户取出第一条记录
  5. trip1.CostUSD = ; //修改但是还没来得及保存
  6. using (var secondContext = new CodeFirst.DataAccess.BreakAwayContext())
  7. {
  8. var trip2 = secondContext.Trips.FirstOrDefault(); //第二个用户进来同样取出第一条记录
  9. trip2.CostUSD = ;
  10. secondContext.SaveChanges(); //修改并保存(保存的操作不仅修改了CostUSD为900,同时修改了RowVersion)
  11. }
  12. try
  13. {
  14. firstContext.SaveChanges(); //此时第一个用户想保存,但是RowVersion已经改变了
  15. Console.WriteLine("保存成功!");
  16. }
  17. catch (DbUpdateConcurrencyException ex)
  18. {
  19. Console.WriteLine(ex.Entries.First().Entity.GetType().Name + " 保存失败");
  20. }
  21. finally
  22. {
  23. firstContext.Dispose();
  24. }
  25. }

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

  1. private static void UpdatePerson()
  2. {
  3. using (var context = new CodeFirst.DataAccess.BreakAwayContext())
  4. {
  5. var person = context.People.FirstOrDefault();
  6. person.FirstName = "Rowena";
  7. context.SaveChanges();
  8. }
  9. }

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

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

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

III.EF中的复杂类型
如果给Person类添加更详细的信息,类似:StreetAddress、City、State、ZipCode等属性,直接添加感觉Person类越来越臃肿。可以抽象出一个Address类,然后把Address类作为Person类的属性,这样更符合面向对象的思维,也方便管理和重用。如果直接把Address类作为Person的属性,那么就如前面第一章的例子,自动映射成主外键关系了。可以通过标注复杂类型(ComplexType)来实现。先添加Address类:

  1. /// <summary>
  2. /// 地址类(复杂类型)
  3. /// </summary>
  4. public class Address
  5. {
  6. //public int AddressId { get; set; } //复杂类型不能有主键id
    public string StreetAddress { get; set; }
    public string City { get; set; }
  7. public string State { get; set; } //州
  8. public string ZipCode { get; set; } //邮编
  9. }

然后可以直接在Address类上标注[ComplexType],这是Data Annotations的方式。自然也可以用Fluent API的形式配置,同样提供两种方法,一个直接写、一个单独写成类,然后添加进去,方便管理:

  1. modelBuilder.ComplexType<CodeFirst.Model.Address>(); //这个直接写在OnModelCreating方法里
  1. public class AddressConfiguration :ComplexTypeConfiguration<Address>
  2. {
  3. public AddressConfiguration()
  4. {
  5. Property(a => a.StreetAddress).HasMaxLength();
  6. }
  7. }
  1. modelBuilder.Configurations.Add(new AddressConfiguration());

最后在Person类里添加Address类的属性。把Main方法里所有的方法都注释了,重新生成下数据库,可以看到Address的属性都在People表里了。
使用复杂类型需要注意:

  1. 没有主键列;
  2. 实例不能重复(Person类里只能有一个Address实例);
  3. 只能是单实例,不能是一个集合(Person类里是Address单实例,不能是List<Address>)。

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

EF Code First 系列文章导航

  1. EF Code First 初体验
  2. EF里的默认映射以及如何使用Data Annotations和Fluent API配置数据库的映射  本节源码
  3. EF里Guid类型数据的自增长、时间戳和复杂类型的用法  本节源码
  4. EF里一对一、一对多、多对多关系的配置和级联删除  本节源码
  5. EF里的继承映射关系TPH、TPT和TPC的讲解以及一些具体的例子  本节源码

EF里Guid类型数据的自增长、时间戳和复杂类型的用法的更多相关文章

  1. EF——Guid类型数据的自增长、时间戳和复杂类型的用法 03 (转)

    EF里Guid类型数据的自增长.时间戳和复杂类型的用法   通过前两章Lodging和Destination类的演示,大家肯定基本了解Code First是怎么玩的了,本章继续演示一些很实用的东西.文 ...

  2. 1.1两个char类型数据相加后,转化为int类型

    #include<stdio.h> main() { char a = 127; char i=0; char ai=0; ai= a+i; printf("size short ...

  3. EF里的默认映射以及如何使用Data Annotations和Fluent API配置数据库的映射

    I.EF里的默认映射 上篇文章演示的通过定义实体类就可以自动生成数据库,并且EF自动设置了数据库的主键.外键以及表名和字段的类型等,这就是EF里的默认映射.具体分为: 数据库映射:Code First ...

  4. EF里一对一、一对多、多对多关系的配置和级联删除

    本章节开始了解EF的各种关系.如果你对EF里实体间的各种关系还不是很熟悉,可以看看我的思路,能帮你更快的理解. I.实体间一对一的关系 添加一个PersonPhoto类,表示用户照片类 /// < ...

  5. EF里的继承映射关系TPH、TPT和TPC的讲解以及一些具体的例子

    本章节讲解EF里的继承映射关系,分为TPH.TPT.TPC.具体: 1.TPH:Table Per Hierarchy 这是EF的默认的继承映射关系:一张表存放基类和子类的所有列,自动生成的discr ...

  6. StackExchange.Redis帮助类解决方案RedisRepository封装(字符串类型数据操作)

    本文版权归博客园和作者本人共同所有,转载和爬虫请注明原文链接 http://www.cnblogs.com/tdws/tag/NoSql/ 目录 一.基础配置封装 二.String字符串类型数据操作封 ...

  7. SpringMVC 处理Date类型数据@InitBinder @DateTimeFormat 注解 的使用

    使用SpringMVC的时候,需要将表单中的日期字符串转换成对应JavaBean的Date类型,而SpringMVC默认不支持这个格式的转换,解决方法有两种,如下: 方法一 . 在需要日期转换的Con ...

  8. JAVA包装类介绍(一)(包装类、基本类型数据)

     1. 包装类把基本类型数据转换为对象      1.1每个基本类型在java.lang包中都有一个相应的包装类  2.包装类有何作用 2.1 提供了一系列实用的方法 2.2集合不允许存放基本数据类型 ...

  9. Json数据的序列化与反序列化的三种经常用法介绍

    下面内容是本作者从官网中看对应的教程后所做的demo.其体现了作者对相关知识点的个人理解..作者才疏学浅,难免会有理解不到位的地方.. 还请各位读者批判性对待... 本文主要介绍在Json数据的序列化 ...

随机推荐

  1. Syscan360会议胸牌破解揭秘

    Syscan360会议胸牌破解揭秘 背景 有幸参加今年11月份的上海Syscan360安全会议,会议期间有一个亮点就是360的独角兽团队设计了一款电子badge(胸牌)供参加人员进行破解尝试,类似于美 ...

  2. 获取Canvas当前坐标系矩阵

    前言 在我的另一篇博文 Canvas坐标系转换 中,我们知道了所有的平移缩放旋转操作都会影响到画布坐标系.那在我们对画布进行了一系列操作之后,怎么再知道当前矩阵数据状态呢. 具体代码 首先请看下面的一 ...

  3. iOS的ATS配置 - 2017年前ATS规定的适配

    苹果规定 从2017年1月1日起,新提交的 app 不允许使用NSAllowsArbitraryLoads来绕过ATS(全称:App Transport Security)的限制. 以前为了能兼容ht ...

  4. Android -- 真正的 高仿微信 打开网页的进度条效果

    (本博客为原创,http://www.cnblogs.com/linguanh/) 目录: 一,为什么说是真正的高仿? 二,为什么要搞缓慢效果? 三,我的实现思路 四,代码,内含注释 五,使用方法与截 ...

  5. css3圆形百分比进度条的实现原理

    原文地址:css3圆形百分比进度条的实现原理 今天早上起来在查看jquery插件机制的时候,一不小心点进了css3圆形百分比进度条的相关文章,于是一发不可收拾,开始折腾了... 关于圆形圈的实现,想必 ...

  6. C#日志

    参考页面: http://www.yuanjiaocheng.net/Entity/first.html http://www.yuanjiaocheng.net/Entity/jieshao.htm ...

  7. Win10 UWP开发系列——开源控件库:UWPCommunityToolkit

    在开发应用的过程中,不可避免的会使用第三方类库.之前用过一个WinRTXamlToolkit.UWP,现在微软官方发布了一个新的开源控件库—— UWPCommunityToolkit 项目代码托管在G ...

  8. .Net中的AOP系列之《拦截位置》

    返回<.Net中的AOP>系列学习总目录 本篇目录 位置拦截 .Net中的字段和属性 PostSharp位置拦截 真实案例--懒加载 .Net中的懒加载 使用AOP实现懒加载 如何懒加载字 ...

  9. 【初码干货】在Window Server 2016中使用Web Deploy方式发布.NET Web应用的重新梳理

    在学习和工作的过程中,发现很多同事.朋友,在做.NET Web应用发布的时候,依然在走 生成-复制到服务器 这样的方式,稍微高级一点的,就是先发布到本地,再上传到服务器 这种方式不仅效率低下,而且不易 ...

  10. BCL中String.Join的实现

    在开发中,有时候会遇到需要把一个List对象中的某个字段用一个分隔符拼成一个字符串的情况.比如在SQL语句的in条件中,我们通常需要把List<int>这样的对象转换为“1,2,3”这样的 ...