前言
滿血復活啦,大概有三個月的時間沒更新博客了,關於EF Core最新進展這三個月也沒怎么去看,不知現階段有何變化沒,本文將以EF Core 2.1穩定版本作為重新梳理系列,希望對看本文的你有所幫助,歡迎一起探討。(請不要嫌棄啰嗦哈,我習慣於將來龍去脈給大家梳理清楚,各種我能想到的場景給大家講解明白)。
屬性映射探討
當我們利用Code First映射屬性時,此時本身沒有什么太大問題,但是當我們初始化表或者獲取數據時等等,通過日志會發現打印出一些需要我們注意的地方,推薦我們使用最佳方式,對於屬性探討我們將着眼於進一步探討日志中所打印的信息。我們依然利用兩個類Blog和Post來探討,大家也好對照着看。
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。這些都是最基礎的東西,在我寫的書中也有詳細介紹,就不再啰嗦了。我們從以下幾點開始探討。
主鍵映射並添加數據探討
數據庫主鍵列無外乎就是INT、BIGINT、GUID、VARCHAR(36)這幾種常見類型,接下來我們一一探討。對於INT或者BIGINT整數類型大多數情況下,我們的主鍵都是數據庫自動生成即自增長,所以此時我們進行如下操作萬無一失。
var blog = new Blog() { CreatedTime = Convert.ToDateTime("2018-10-20"), Deleted = false, Status = 1, Name = "EFCore" }; _context.Blogs.Add(blog); var result = _context.SaveChanges();
上述我們也說過我們並未設置主鍵是否自增長,如果不進行手動配置,這個根據默認約定而配置。當然對於主鍵若是客戶端自動生成,我們只需進行如下映射即可。
b.HasKey(k => k.Id); b.Property(p => p.Id).ValueGeneratedNever();
我個人比較習慣對於主鍵也手動通過HasKey進行配置。在添加數據時根據約定主鍵自動增長,如上述。當然我們也可以手動配置在添加還是更新時自增長,如下:
b.Property(p => p.Id).ValueGeneratedOnAdd(); b.Property(P => P.Id).ValueGeneratedOnAddOrUpdate(); b.Property(p => p.Id).ValueGeneratedOnUpdate();
是不是就這么完了呢?其實遠沒有這么簡單,我們只看上述第一個即 ValueGeneratedOnAdd ,我們去看其方法解釋。
// 摘要: // 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插入數據不同,請參看此鏈接:https://www.cnblogs.com/CreateMyself/p/9017296.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/),追蹤圖中可獲取到對象所有狀態和主鍵是否已經設置,所以最終解決方案如下:
_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位字符串,將會覆蓋數據庫自動生成。
初始化默認值探討(什么時候用HasDefaultValue和HasDefaultValueSql)
默認值最常見類型屬於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,后續也會發布到騰訊課堂。我們下節再會,感謝您的耐心閱讀。