1. 程式人生 > >EntityFramework Core 2.1重新梳理系列屬性對映

EntityFramework Core 2.1重新梳理系列屬性對映

  滿血復活啦,大概有三個月的時間沒更新部落格了,關於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 =www.leyou1178.cn> p.Id).ValueGeneratedOnAdd();      b.Property(P =www.dfgj157.com> P.Id).ValueGeneratedOnAddOrUpdate(www.fengshen157.com/);      b.Property(p =www.tianzunyule178.com> 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)www.444814.cn;      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方法實現即可