EF里Guid类型数据的自增长、时间戳和复杂类型的用法
通过前两章Lodging和Destination类的演示,大家肯定基本了解Code First是怎么玩的了,本章继续演示一些很实用的东西。
文章的开头提示下:提供的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 = ,
- StartDate = new DateTime(, , ),
- EndDate = new DateTime(, , )
- };
- using (var context = new CodeFirst.DataAccess.BreakAwayContext())
- {
- context.Trips.Add(trip);
- context.SaveChanges();
- }
- }
在Main方法里调用下InsertTrip方法,再跑下程序,再去查看下数据库,结果:
可见,没有设置自增长并且程序中没有向此字段注入guid类型的数据,数据库会自动补充一堆0,下次再添加还是全部0,自然会报错(主键不允许重复)
脑补:Guid就是全局统一标识符的意思,随机的一串字母。几乎不可能重复,所以适合用来当主键。可以向mssql的控制台打印Guid试试,每次执行都不一样:
可以通过注解或者api的方式为guid类型的数据设置自增长,分别是:
- [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- this.Property(t => t.Identifier).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); //Guid类型主键自增长
再跑下程序,guid列就有值了。
到这里就有疑问了,自增长不是像int类型一样每次加1么,这里的guid无法加1啊,其实就是自动生成。我本以为大家对“自增长”理解太狭隘啊:+1才是自增长,其实类似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 =
- };
- 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里的并发都是乐观并发(optimistic concurrency)。
重新执行下之前的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...
继续演示,添加一个更新的方法,把价格从800修改成750:
- private static void UpdateTrip()
- {
- using (var context = new CodeFirst.DataAccess.BreakAwayContext())
- {
- var trip = context.Trips.FirstOrDefault();
- trip.CostUSD = ;
- context.SaveChanges();
- }
- }
注:不要重复执行该方法,数据库的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条件了,自然无法更新,同时程序会抛错。同样,如果更新成功,那么同时也会更新时间戳的值。下图展示了修改数据前后时间戳的值:
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的乐观并发。为了更好的验证,新加一个新的方法:
- private static void UpdateTrip2()
- {
- var firstContext = new CodeFirst.DataAccess.BreakAwayContext();
- var trip1 = firstContext.Trips.FirstOrDefault(); //第一个用户取出第一条记录
- trip1.CostUSD = ; //修改但是还没来得及保存
- using (var secondContext = new CodeFirst.DataAccess.BreakAwayContext())
- {
- var trip2 = secondContext.Trips.FirstOrDefault(); //第二个用户进来同样取出第一条记录
- trip2.CostUSD = ;
- 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等属性,直接添加感觉Person类越来越臃肿。可以抽象出一个Address类,然后把Address类作为Person类的属性,这样更符合面向对象的思维,也方便管理和重用。如果直接把Address类作为Person的属性,那么就如前面第一章的例子,自动映射成主外键关系了。可以通过标注复杂类型(ComplexType)来实现。先添加Address类:
- /// <summary>
- /// 地址类(复杂类型)
- /// </summary>
- public class Address
- {
- //public int AddressId { get; set; } //复杂类型不能有主键id
public string StreetAddress { get; set; }
public string City { get; set; }- public string State { get; set; } //州
- public string ZipCode { get; set; } //邮编
- }
然后可以直接在Address类上标注[ComplexType],这是Data Annotations的方式。自然也可以用Fluent API的形式配置,同样提供两种方法,一个直接写、一个单独写成类,然后添加进去,方便管理:
- modelBuilder.ComplexType<CodeFirst.Model.Address>(); //这个直接写在OnModelCreating方法里
- public class AddressConfiguration :ComplexTypeConfiguration<Address>
- {
- public AddressConfiguration()
- {
- Property(a => a.StreetAddress).HasMaxLength();
- }
- }
- modelBuilder.Configurations.Add(new AddressConfiguration());
最后在Person类里添加Address类的属性。把Main方法里所有的方法都注释了,重新生成下数据库,可以看到Address的属性都在People表里了。
使用复杂类型需要注意:
- 没有主键列;
- 实例不能重复(Person类里只能有一个Address实例);
- 只能是单实例,不能是一个集合(Person类里是Address单实例,不能是List<Address>)。
本文到此结束,谢谢阅读。本章源码
EF里Guid类型数据的自增长、时间戳和复杂类型的用法的更多相关文章
- EF——Guid类型数据的自增长、时间戳和复杂类型的用法 03 (转)
EF里Guid类型数据的自增长.时间戳和复杂类型的用法 通过前两章Lodging和Destination类的演示,大家肯定基本了解Code First是怎么玩的了,本章继续演示一些很实用的东西.文 ...
- 1.1两个char类型数据相加后,转化为int类型
#include<stdio.h> main() { char a = 127; char i=0; char ai=0; ai= a+i; printf("size short ...
- EF里的默认映射以及如何使用Data Annotations和Fluent API配置数据库的映射
I.EF里的默认映射 上篇文章演示的通过定义实体类就可以自动生成数据库,并且EF自动设置了数据库的主键.外键以及表名和字段的类型等,这就是EF里的默认映射.具体分为: 数据库映射:Code First ...
- EF里一对一、一对多、多对多关系的配置和级联删除
本章节开始了解EF的各种关系.如果你对EF里实体间的各种关系还不是很熟悉,可以看看我的思路,能帮你更快的理解. I.实体间一对一的关系 添加一个PersonPhoto类,表示用户照片类 /// < ...
- EF里的继承映射关系TPH、TPT和TPC的讲解以及一些具体的例子
本章节讲解EF里的继承映射关系,分为TPH.TPT.TPC.具体: 1.TPH:Table Per Hierarchy 这是EF的默认的继承映射关系:一张表存放基类和子类的所有列,自动生成的discr ...
- StackExchange.Redis帮助类解决方案RedisRepository封装(字符串类型数据操作)
本文版权归博客园和作者本人共同所有,转载和爬虫请注明原文链接 http://www.cnblogs.com/tdws/tag/NoSql/ 目录 一.基础配置封装 二.String字符串类型数据操作封 ...
- SpringMVC 处理Date类型数据@InitBinder @DateTimeFormat 注解 的使用
使用SpringMVC的时候,需要将表单中的日期字符串转换成对应JavaBean的Date类型,而SpringMVC默认不支持这个格式的转换,解决方法有两种,如下: 方法一 . 在需要日期转换的Con ...
- JAVA包装类介绍(一)(包装类、基本类型数据)
1. 包装类把基本类型数据转换为对象 1.1每个基本类型在java.lang包中都有一个相应的包装类 2.包装类有何作用 2.1 提供了一系列实用的方法 2.2集合不允许存放基本数据类型 ...
- Json数据的序列化与反序列化的三种经常用法介绍
下面内容是本作者从官网中看对应的教程后所做的demo.其体现了作者对相关知识点的个人理解..作者才疏学浅,难免会有理解不到位的地方.. 还请各位读者批判性对待... 本文主要介绍在Json数据的序列化 ...
随机推荐
- Syscan360会议胸牌破解揭秘
Syscan360会议胸牌破解揭秘 背景 有幸参加今年11月份的上海Syscan360安全会议,会议期间有一个亮点就是360的独角兽团队设计了一款电子badge(胸牌)供参加人员进行破解尝试,类似于美 ...
- 获取Canvas当前坐标系矩阵
前言 在我的另一篇博文 Canvas坐标系转换 中,我们知道了所有的平移缩放旋转操作都会影响到画布坐标系.那在我们对画布进行了一系列操作之后,怎么再知道当前矩阵数据状态呢. 具体代码 首先请看下面的一 ...
- iOS的ATS配置 - 2017年前ATS规定的适配
苹果规定 从2017年1月1日起,新提交的 app 不允许使用NSAllowsArbitraryLoads来绕过ATS(全称:App Transport Security)的限制. 以前为了能兼容ht ...
- Android -- 真正的 高仿微信 打开网页的进度条效果
(本博客为原创,http://www.cnblogs.com/linguanh/) 目录: 一,为什么说是真正的高仿? 二,为什么要搞缓慢效果? 三,我的实现思路 四,代码,内含注释 五,使用方法与截 ...
- css3圆形百分比进度条的实现原理
原文地址:css3圆形百分比进度条的实现原理 今天早上起来在查看jquery插件机制的时候,一不小心点进了css3圆形百分比进度条的相关文章,于是一发不可收拾,开始折腾了... 关于圆形圈的实现,想必 ...
- C#日志
参考页面: http://www.yuanjiaocheng.net/Entity/first.html http://www.yuanjiaocheng.net/Entity/jieshao.htm ...
- Win10 UWP开发系列——开源控件库:UWPCommunityToolkit
在开发应用的过程中,不可避免的会使用第三方类库.之前用过一个WinRTXamlToolkit.UWP,现在微软官方发布了一个新的开源控件库—— UWPCommunityToolkit 项目代码托管在G ...
- .Net中的AOP系列之《拦截位置》
返回<.Net中的AOP>系列学习总目录 本篇目录 位置拦截 .Net中的字段和属性 PostSharp位置拦截 真实案例--懒加载 .Net中的懒加载 使用AOP实现懒加载 如何懒加载字 ...
- 【初码干货】在Window Server 2016中使用Web Deploy方式发布.NET Web应用的重新梳理
在学习和工作的过程中,发现很多同事.朋友,在做.NET Web应用发布的时候,依然在走 生成-复制到服务器 这样的方式,稍微高级一点的,就是先发布到本地,再上传到服务器 这种方式不仅效率低下,而且不易 ...
- BCL中String.Join的实现
在开发中,有时候会遇到需要把一个List对象中的某个字段用一个分隔符拼成一个字符串的情况.比如在SQL语句的in条件中,我们通常需要把List<int>这样的对象转换为“1,2,3”这样的 ...