满血复活啦,大概有三个月的时间没更新博客了,关于EF Core最新进展这三个月也没怎么去看,不知现阶段有何变化没,本文将以EF Core 2.1稳定版本做为从新梳理系列,但愿对看本文的你有所帮助,欢迎一块儿探讨。(请不要嫌弃啰嗦哈,我习惯于未来龙去脉给你们梳理清楚,各类我能想到的场景给你们讲解明白)。html
当咱们利用Code First映射属性时,此时自己没有什么太大问题,可是当咱们初始化表或者获取数据时等等,经过日志会发现打印出一些须要咱们注意的地方,推荐咱们使用最佳方式,对于属性探讨咱们将着眼于进一步探讨日志中所打印的信息。咱们依然利用两个类Blog和Post来探讨,你们也好对照着看。node
public class Blog { public int Id { get; set; } public string Name { get; set; } public byte Status { get; set; } public bool Deleted { get; set; } public DateTime CreatedTime { get; set; } public ICollection<Post> Posts { get; set; } } public class Post { public int Id { get; set; } public int BlogId { get; set; } public string Title { get; set; } public string Content { get; set; } public Blog Blog { get; set; } }
首先咱们在映射时,不给定属性默认值以及映射列类型等,直接看看迁移时生成的列类型是怎样,而后咱们再来进一步深刻。对于关系映射仍是建议手动配置一下,虽然EF Core也会经过约定来自动进行配置,可是手动配置便于理解,以下:数据库
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>(b => { b.ToTable("Blogs"); b.HasMany(m => m.Posts) .WithOne(o => o.Blog) .HasForeignKey(k => k.BlogId); }); modelBuilder.Entity<Post>(b => { b.ToTable("Posts"); }); }
以上迁移是EF Core默认根据约定生成列类型以及约束和级联删除的状况。属性Id做为主键且自增加,对于字符串默认建立为NVARCHAR且长度为max,同时可空。日期类型默认为DATETIME2。这些都是最基础的东西,在我写的书中也有详细介绍,就再也不啰嗦了。咱们从如下几点开始探讨。less
数据库主键列无外乎就是INT、BIGINT、GUID、VARCHAR(36)这几种常见类型,接下来咱们一一探讨。对于INT或者BIGINT整数类型大多数状况下,咱们的主键都是数据库自动生成即自增加,因此此时咱们进行以下操做万无一失。ide
var blog = new Blog() { CreatedTime = Convert.ToDateTime("2018-10-20"), Deleted = false, Status = 1, Name = "EFCore" }; _context.Blogs.Add(blog); var result = _context.SaveChanges();
上述咱们也说过咱们并未设置主键是否自增加,若是不进行手动配置,这个根据默认约定而配置。固然对于主键如果客户端自动生成,咱们只需进行以下映射便可。ui
b.HasKey(k => k.Id); b.Property(p => p.Id).ValueGeneratedNever();
我我的比较习惯对于主键也手动经过HasKey进行配置。在添加数据时根据约定主键自动增加,如上述。固然咱们也能够手动配置在添加仍是更新时自增加,以下:spa
b.Property(p => p.Id).ValueGeneratedOnAdd(); b.Property(P => P.Id).ValueGeneratedOnAddOrUpdate(); b.Property(p => p.Id).ValueGeneratedOnUpdate();
是否是就这么完了呢?其实远没有这么简单,咱们只看上述第一个即 ValueGeneratedOnAdd ,咱们去看其方法解释。3d
// 摘要: // Configures a property to have a value generated only when saving a new entity, // unless a non-null, non-temporary value has been set, in which case the set value // will be saved instead. The value may be generated by a client-side value generator // or may be generated by the database as part of saving the entity.
此方法解释若是主键为非空或者没有临时值未被设置的话,数据库将自动生成主键。同时请注意,它也代表主键值在保存时可经过客户端或者数据库自动生成。咱们刚才演示了在数据库自动生成,既然解释在客户端也可自动生成,那咱们再来添加一条数据试试呢。日志
var blog = new Blog() { Id = 2, CreatedTime = Convert.ToDateTime("2018-10-20"), Deleted = false, Status = 1, Name = "EFCore" }; _context.Blogs.Add(blog); var result = _context.SaveChanges();
有关EF 6.x和EF Core插入数据不一样,请参看此连接:http://www.javashuo.com/article/p-uerkgmpc-gc.html。该方法明确说好的在客户端也可自动生成的啊,难道解释有误?在EF 6.x中默认打开了IDENTITY_INSERT,而在EF Core中将IDENTITY_INSERT给关闭了,因此咱们要想始终使用自增加值即便客户端给定了值且不抛出异常,那么须要在数据库中将IDENTITY_INSERT给打开才行。在EF Core会遇到将主键设置成临时值的状况,可是若是咱们又不想显式打开IDENTITY_INSERT,同时须要始终使用自增加值,那么该如何作呢?在EF 6.x中能够经过 HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity) 进行设置,在EF Core则不能进行全局设置。这个问题的出现仍是dudu老大所提出的,最终我所给出的答案则是利用追踪图来解决(https://q.cnblogs.com/q/110205/),追踪图中可获取到对象全部状态和主键是否已经设置,因此最终解决方案以下:code
_context.ChangeTracker.TrackGraph(blog, node => { if (node.Entry.IsKeySet) { blog.Id = 0; _context.Blogs.Add(blog); } });
到了这里,还有一个方法咱们一直不曾讲解到,不知你是否在项目中应用过,在EF Core还有一个 UseSqlServerIdentityColumn 方法,其名为使用SQL Server标识列,该方法只能针对Key进行设置,最终则会调用 ValueGeneratedOnAdd 方法,如此而已。
对于主键咱们只是讲解了INT类型,对于BIGINT和INT同样都是整数,那咱们看看带小数点的,好比主键为decimal类型。下面咱们来改变一下上述Blog表主键Id的数据类型。修改成decimal,同时Post类中的外键BlogId也修改成decimal,且手动配置添加 ValueGeneratedOnAdd 方法,以下:
public decimal Id { get; set; }
此时咱们再来从新迁移一下。
经过上述错误咱们知道主键列的数据类型,同时呢也知道 ValueGeneratedOnAdd 方法手动配置只是针对于整数类型,对于小数类型,则不能应用其方法,咱们去掉该方法再看看。
由于其带小数,因此此时EF Core会自动发现主键且非自增加,这和咱们在数据库正常设置主键和是否自增加一致。下面咱们再来看看主键为GUID的状况,将主键修改成GUID以下:
public Guid Id { get; set; }
若是主键是GUID,那么对于数据库列类型就是 uniqueidentifier 。此时也就涉及到两种状况,一是客户端自动生成,二是数据库自动生成。默认状况下会在数据库自动生成GUID。若是咱们手动设置了GUID,那么将以手动设置的GUID为准,以下:
var blog = new Blog() { Id = Guid.Parse("13D375A1-8AE7-4B84-B220-6BAB72FA2454"), CreatedTime = Convert.ToDateTime("2018-10-20"), Deleted = false, Status = 1, Name = "EFCore" }; _context.Blogs.Add(blog); var result = _context.SaveChanges();
如果咱们同时配置了添加时自动生成和数据库自动生成,此时在添加时倒是给定了其值,此时依然是添加的为准,以下:
b.Property(p => p.Id).HasDefaultValueSql("NEWID()"); b.Property(p => p.Id).ValueGeneratedOnAdd();
讲完主键为GUID类型,咱们再来说讲主键列类型为VARCHAR(36),咱们要使其在添加时自动生成,进行以下设置便可,EF Core会自动发现并生成36位的字符串,无需配置 ValueGeneratedOnAdd 方法。
b.Property(p => p.Id).HasColumnType("VARCHAR(36)");
虽然咱们一直说对于字符串类型映射,默认映射为可空,可是主键不一样,主键原本就不可空,因此上述咱们设置主键为VARCHAR(36),无需画蛇添足设置 IsRequired ,可是须要注意的是此时对于外键BlogId,依据关系是否必须,若是必须必定要设置 IsRequired ,不然为可空类型。若进行以下设置数据库自动生成,同时客户端手动指定了主键,则以手动指定为准。
b.Property(p => p.Id).HasDefaultValueSql("NEWID()");
以上讲了这么多,咱们来对主键映射作一个完整的总结。
(1)对于Int、Int64等整数,默认状况下自增加即数据库自动生成,添加数据时若是主键为空或者为0,数据库将自动生成,不然抛出异常。而对于decimal小数,主键Id由客户端指定生成。
(2) 对于Guid类型,默认状况下数据库自动生成,无需显式调用ValueGeneratedOnAdd方法或者HasDefaultValueSql("NEWID()"),若显式指定Guid,将会覆盖数据库自动生成。
(3)对于VARCHAR(36)类型,默认状况下自动生成,无需显式调用ValueGeneratedOnAdd方法或者HasDefaultValueSql("NEWID()"),若显式指定36位字符串,将会覆盖数据库自动生成。
默认值最多见类型属于bool、byte、datetime等等。同时提供默认咱们可经过HasDefaultValue和HasDefaultValueSql两个方法来进行,那么是否是两者使用没有什么异同呢?下面咱们一一来探讨,在Blog类中有Deleted属性,咱们映射其默认值为false,以下:
b.Property(p => p.Deleted).HasDefaultValue(false); //或者 b.Property(p => p.Deleted).HasDefaultValueSql("0");
当咱们迁移时会发现以下日志:
布尔类型默认值原本就是false,因此上述彻底不用显式去配置,如此配置画蛇添足并且在日志中还会打印一条warning消息建议使用可空类型布尔值。对于布尔类型,不用映射时给定默认值,若是该类型为可空,将其属性设为可空便可。那么要是咱们设置默认值不为false而是true呢?下面咱们再来看看。
b.Property(p => p.Deleted).HasDefaultValue(true);
此时迁移时依然会打印上述警示信息,在应用程序日志中也会显示这些警示信息,这个警示我认为有点让人疑惑,应该改善提示信息,可是咱们真的不想根据其建议设置为可空类型,没什么太大意义。此时咱们应该怎么办呢?咱们能够进行以下映射配置从而警示信息也不会再有:
b.Property(p => p.Deleted).HasDefaultValue(true).ValueGeneratedNever();
上述经过ValueGeneratedNever方法配置意在代表:
当迁移时对数据库中已存在的行使用其配置的默认值,而对新增的数据行彻底不使用其默认值,换句话说,告知EF Core即便配置了默认值,在EF Core运行时也不该该使用其默认值。
下面咱们再来看看byte映射为列tinyint状况。对于上述Status属性,咱们进行以下映射。
b.Property(k => k.Status).HasDefaultValue(0);
接下来咱们修改成HasDefaultValueSql,以下:
b.Property(k => k.Status).HasDefaultValueSql("0");
咱们看到在使用HasDefaultValue和HasDefaultValueSql的区别所在,对于byte映射时利用HasDefaultValue会存在转换的状况(注意:在EF Core 2.0中利用HasDefaultValue不存在转换问题和HasDefaultValueSql配置一致),因此此时只能利用HasDefaultValueSql来映射默认值。对于日期列类型大部分状况都是DateTime,此时咱们也只能经过HasDefaultValueSql来指定默认值,以下:
b.Property(p => p.CreatedTime).HasColumnType("DATETIME").HasDefaultValueSql("GETDATE()");
上述咱们对Status和DateTime指定了默认值,咱们在添加数据时,依然指定Status默认值为0,CreatedTime也指定时间看看。
var blog = new Blog() { Status = 0, CreatedTime = Convert.ToDateTime("2018-10-30"), Name = "EFCore", }; _context.Blogs.Add(blog); var result = _context.SaveChanges();
在此咱们对属性指定默认值作一个完整的总结:
(1)若是指定默认值和CLR Type默认值一致,此时数据库列默认值即CLR type默认值,同时咱们无需经过HasDefaultValue和HasDefaultValueSql方法显式配置默认值,无疑是画蛇添足且会在日志打印warning信息。不然须要经过HasDefaultValue和HasDefaultValueSql显式指定默认值。
(2)对于HasDefaultValue和HasDefaultValueSql方法显式配置默认值时需注意值类型是否和数据库列类型一致,若是一致用HasDefaultValue方法,不然请用HasDefaultValueSql方法。如若否则会存在默认值显式转换的状况。
(3)指定默认值为CLR Type默认值后,在添加数据时,若显式指定了CLR Type默认值,那么此时将会在数据库自动生成(由上添加数据生成的SQL没有Status列可知)。
(4)若日期指定默认值为数据库自动生成,但添加时显式指定日期,此时将覆盖数据库自动生成的默认日期。
对于字符串默认映射类型为NVARCHAR且长度为MAX,同时为可空,是否可空经过IsRequired来修正。若是映射为VARCHAR类且长度为50,咱们可经过HasColumnType方法来指定类型,以下:
b.Property(p => p.Name).HasColumnType("VARCHAR(50)");
在EF Core 2.1以前,咱们经过HasColumnType方法指定类型,而经过HasMaxLength指定长度迁移会抛异常,那么如今是否能够呢?,答案是:迁移不会抛异常,但结果长度不正确,以下:
b.Property(p => p.Name).HasColumnType("VARCHAR").HasMaxLength(50);
在EF 6.x中对于char、nchar等须要经过HasMaxLength和IsFixedLength方法类修正实现,在EF Core中都统一经过HasColumnType方法实现便可。
本文比较详细的介绍了主键映射和属性映射以及映射默认值问题,下一节咱们详细讲解关系映射。三 个月没写博客主要是私下去录制了ASP.NET Core MVC基础进阶视频,涉及到一些细节和基本原理,感兴趣的童鞋能够了解下,若您有任何疑问可私信于我,连接地址:https://study.163.com/course/courseMain.htm?courseId=1005573012&share=2&shareId=400000000517056,后续也会发布到腾讯课堂。咱们下节再会,感谢您的耐心阅读。