I.EF里的默認映射
上篇文章演示的通過定義實體類就可以自動生成數據庫,並且EF自動設置了數據庫的主鍵、外鍵以及表名和字段的類型等,這就是EF里的默認映射。具體分為:
- 數據庫映射:Code First 默認會在本地的SQL Expression數據庫中建立一個和DbContext的子類的全名相同的數據庫,全名指的是命名空間加上類名;
- 表映射:Code First 默認會按照類型名復數建立數據表,比如說Destination類對應的表名就叫Destinations;
- 列映射:Code First 默認會按照類中的屬性名建立column,它還有默認的數據類型映射習慣,int會映射為interger,string會映射為 nvarchar(max),decimal會映射為decimal(18,2);
- 主鍵映射:Code First 默認會在類的屬性中需找名字為Id或類型名稱+Id的int類型的屬性作為主鍵,並且是自增字段。
摘自這里
默認的映射一般只是簡單映射,方便使用罷了。當然這些都是可以進行修改的,請往下看。
II.使用Data Annotations和Fluent API配置數據庫的映射
Data Annotations翻譯過來就是數據注解,是通過直接在實體類的屬性上加注類似標簽的東西達到對數據庫的映射;
Fluent API翻譯過來就是流利的API,Fluent API是在DbContext中定義數據庫配置的一種方式。要使用Fluent API 就必須在你自定義的繼承自DbContext的類中重載OnModelCreating這個方法。注意:
- Data Annotations和Fluent API任選其一就可以了,不需要同時配置;
- 使用Data Annotations需添加引用:using System.ComponentModel.DataAnnotations; 更具體的引用請參考本章結尾提供的源碼。
實戰:
1.Data Annotations:
設置Destination表的Name不為null:
[Required] public string Name { get; set; }
很簡單,直接在屬性上加[Required]標注即可;
2.Fluent API:
用Fluent api必須重寫OnModelCreating方法,我們在上下文類里重寫下OnModelCreating方法並添加不為空的配置:
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<CodeFirst.Model.Destination>().Property(d => d.Name).IsRequired(); }
方法分析:先找到需要配置的實體類,然后點Property就是點出屬性,=>是lambda表達式的寫法,找到Name屬性,然后調用IsRequired方法設置不為null。初見這東西肯定不好理解,多寫寫就熟悉了。
兩種配置方式個人更喜歡Fluent API的方式,故放出的demo中Data Annotations也是有的,不過都被注釋了。隨着開發的深入,有些東西還是必須用Fluent API的方式才能配置出來的。
注意:我的類庫都修改了默認命名空間(右鍵類庫 - 屬性),都加了個CodeFirst. 方便區分,其他類庫中調用,我也是習慣用完整的命名空間.類庫再.實體類來調用。
思考:使用Fluent API方式配置,每次都需要在OnModelCreating方法里寫上一行配置,這樣一個實體類如果有3個屬性需要配置,10個實體類就需要配置30個,那么就得在OnModelCreating方法里寫30行,很麻煩且不易維護。
解決辦法:注意返回值可以看出modelBuilder的Entity<>泛型方法的返回值是EntityTypeConfiguration<>泛型類。我們可以定義一個繼承自EntityTypeConfiguration<>泛型類的類來定義domain中每個類的數據庫配置。
ok,我們在DataAccess類庫下新建一個繼承自EntityTypeConfiguration<>泛型類的DestinationMap類,在構造函數里寫上配置:
public class DestinationMap : EntityTypeConfiguration<CodeFirst.Model.Destination> { public DestinationMap() { Property(d => d.Name).IsRequired(); } }
需添加引用:using System.Data.Entity.ModelConfiguration;
這樣,以后Destination需要配置的都來這里配置,然后添加到上下文中的OnModelCreating方法即可:
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new DestinationMap()); }
其他類的配置也是這樣,先添加一個類名+Map的方法(命名隨意),然后添加到OnModelCreating方法。
隨意配置一些屬性,然后跑下程序看生成的數據庫:
可見:Name列已經不可空,Description也設置了長度不超過500等等。
注意:如果重新跑程序生成數據庫報這個錯:
The model backing the 'BreakAwayContext' context has changed since the database was created. Either manually delete/update the database, or call Database.SetInitializer with an IDatabaseInitializer instance. For example, the DropCreateDatabaseIfModelChanges strategy will automatically delete and recreate the database, and optionally seed it with new data.
意思就是,當前數據庫正在使用中,無法刪除再重新生成。這個時候斷開數據庫連接,再跑下程序就可以了。以后每次配置實體類再重新跑程序都需要斷開數據庫連接。
不知道大家還記不記得第一篇文章講的EdmMetadata這個表,EF會自動生成這個表。這個表是監控實體類變化的,其實是個詬病,每次操作數據庫都會發訪問這張表的sql到數據庫,用sql Profiler跟蹤下即可發現如下sql:
2013.08.07補充:感謝園長dudu在回復中(4樓)的糾正,不是每次操作數據庫都會發送sql到數據庫,只是在EF初始化的時候會發送訪問EdmMetadata表的sql到數據庫。另推薦dudu的兩篇文章供大家閱讀:讓Entity Framework不再私闖sys.databases 揭開Entity Framework LINQ查詢的一點面紗
SELECT TOP (1) [Extent1].[Id] AS [Id], [Extent1].[ModelHash] AS [ModelHash] FROM [dbo].[EdmMetadata] AS [Extent1] ORDER BY [Extent1].[Id] DESC
注:sql Profiler是一個監控發送到數據庫sql語句的工具。sql server 2008自帶,操作簡單,如果不會請自行搜索相關資料。如果沒有這個工具,那么是數據庫版本不對或者數據庫裝出了問題。sql Profiler這工具后面講一對一、一對多、多對多的各種操作時會經常使用到,也是我們調試EF語句性能的好幫手:
擴展:可以在OnModelCreating方法里添加:
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();//移除復數表名的契約 modelBuilder.Conventions.Remove<IncludeMetadataConvention>();//防止黑幕交易 要不然每次都要訪問
需引入命名空間:
using System.Data.Entity.ModelConfiguration.Conventions; using System.Data.Entity.Infrastructure;
第一句是移除復數表名的契約,就是EF默認生成的表名都是實體類的復數形式,有這句,表名就是實體類的名字了,不會再加個s了,當然你也可以通過強大的Fluent API配置這個。
第二句就是移除對EdmMetadata表的訪問的,下次再操作數據庫,就不會有訪問EdmMetadata表的sql了。
注意:如果之前已經有數據庫了,那么再加上移除對EdmMetadata表訪問的配置再跑程序會報一個NotSupportedException的錯:
Model compatibility cannot be checked because the EdmMetadata type was not included in the model. Ensure that IncludeMetadataConvention has been added to the DbModelBuilder conventions.
這個時候,先把BreakAwayConfigFile數據庫分離,然后在本地刪除掉數據庫文件,再跑程序就可以了。如果不知道sql server的mdf和ldf文件的默認路徑,請自行搜索。
結果跑了下程序,的確沒有產生對EdmMetadata表訪問的sql了,但是sql Profiler監控到了更多的sql語句被發送到了數據庫。當初為了少一個訪問EdmMetadata表的sql,結果多了更多的sql,並且如果再次生成數據庫還必須要手動分離現有數據庫並刪除硬盤上的數據庫文件。這里使不使用還是有待商量的,暫時注釋掉:
//modelBuilder.Conventions.Remove<IncludeMetadataConvention>();
思考:EF小組設計EdmMetadata表肯定有它的道理,移除對其的訪問也不一定是科學的。這里僅做學習演示,真正的項目中可能也很少用Code First的方式生成數據庫的。至少4.1版本下的EF使用Code First是有缺陷的,每次都要干掉所有數據,這個肯定不合適。后續版本的EF有數據遷徙的功能能解決這個,目前只是學習4.1這個經典的EF版本。
補充:實際開發中還是database First的方式比較主流。
上面只演示了簡單的Data Annotations和Fluent API的一些簡單配置,其實還有很多,下面列出部分常用,初學者應該都試着敲敲,然后對照着生成的數據庫好好學習下如何配置:

//【主鍵】 //Data Annotations: [Key] public int DestinationId { get; set; } //Fluent API: public class BreakAwayContext : DbContext { protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<Destination>().HasKey(d => d.DestinationId); } } //【外鍵】 //Data Annotations: public int DestinationId { get; set; } [ForeignKey("DestinationId")] public Destination Destination { get; set; } //Fluent API: modelBuilder.Entity<Lodging>().HasRequired(p => p.Destination).WithMany(p=>p.Lodgings).HasForeignKey(p => p.DestinationId); //【長度】 //Data Annotations:通過StringLength(長度),MinLength(最小長度),MaxLength(最大長度)來設置數據庫中字段的長度 [MinLength(10),MaxLength(30)] public string Name { get; set; } [StringLength(30)] public string Country { get; set; } //Fluent API:沒有設置最小長度這個方法 modelBuilder.Entity<Destination>().Property(p => p.Name).HasMaxLength(30); modelBuilder.Entity<Destination>().Property(p => p.Country).HasMaxLength(30); //【非空】 //Data Annotations: [Required(ErrorMessage="請輸入描述")] public string Description { get; set; } //Fluent API: modelBuilder.Entity<Destination>().Property(p => p.Country).IsRequired(); //【數據類型】 Data Annotations: 將string映射成ntext,默認為nvarchar(max) [Column(TypeName = "ntext")] public string Owner { get; set; } //Fluent API: modelBuilder.Entity<Lodging>().Property(p => p.Owner).HasColumnType("ntext"); //【表名】 //Data Annotations: [Table("MyLodging")] public class Lodging { } //Fluent API modelBuilder.Entity<Lodging>().ToTable("MyLodging"); //【列名】 //Data Annotations: [Column("MyName")] public string Name { get; set; } //Fluent API: modelBuilder.Entity<Lodging>().Property(p => p.Name).HasColumnName("MyName"); //【自增長】 //Data Annotations [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] Guid類型的主鍵、自增長 public Guid SocialId { get; set; } //Fluent API: modelBuilder.Entity<Person>().Property(p => p.SocialId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); //【忽略列映射】 //Data Annotations: [NotMapped] public string Name { get { return FirstName + " " + LastName; } } //Fluent API: modelBuilder.Entity<Person>().Ignore(p => p.Name); //【忽略表映射】 //Data Annotations: [NotMapped] public class Person { } //Fluent API: modelBuilder.Ignore<Person>(); //【時間戳】 //Data Annotations:Timestamp [Timestamp] public Byte[] TimeStamp { get; set; } 只能是byte類型 //Fluent API: modelBuilder.Entity<Lodging>().Property(p => p.TimeStamp).IsRowVersion(); //【復雜類型】 //Data Annotations: [ComplexType] public class Address { public string Country { get; set; } public string City { get; set; } } //Fluent API: modelBuilder.ComplexType<Address>();
部分摘自這里
目前階段看這個的確稍顯復雜,僅做了解和方便后期查閱。后續講一對一、一對多和多對多的關系時反復的手寫Fluent API的時候就會很好理解了。
源碼:點擊下載
EF Code First 系列文章導航: