2.1 Entity Framework簡介
Ado.net Entity Framework 是Microsoft推出的ORM框架。
2.1.1 什么是ORM
對象關系映射(Object Relational Mapping,簡稱ORM)模式是一種為了解決面向對象與關系數據庫存在的互不匹配的現象的技術。它的作用是在關系型數據庫和對象之間作一個映射,這樣,我們在具體的操作數據庫的時候,就不需要再去和復雜的SQL語句打交道,只要像平時操作對象一樣操作它就可以了 。
ORM三個字母分別代表如下圖所示:

圖2-1 ORM關系圖
2.1.2 Entity Framework 體系結構
Entity Framework的體系結構如圖2-2所示,EMD是整個框架實現ORM最核心的部分。

圖2-2 Entity Framework 體系結構
EDM(Entity Data Model) 即實體數據模型,它能夠將關系數據庫模型映射為實體數據模型。由以下三種模型和具有相應文件擴展名的映射文件進行定義。
- CSDL:概念層,定義概念模型,用實體來來表示數據庫中的對象。(O)
- SSDL:存儲層,定義存儲模型,描述了表、列、關系、主鍵、索引等數據庫中的概念。(R)
- MSL: 對應層,定義存儲模型與概念模型之間的映射(M)
EF包括三種模式:DBFirst、CodeFist、ModelFirst 。EF可以調用SQL語句、可以使用Linq查詢、可以使用Lambda查詢,EF還有很多高級屬性,比如延遲加載、緩存屬性等等。
2.2 從數據庫到對象模型(Database First)
2.2.1 添加實體數據模型。
在項目中添加"ADO.NET實體數據模型",選擇"來自數據庫的EF設計器",按照提示配置數據庫相關信息。選擇相關的數據庫表。如圖2-3、圖2-4、圖2-5所示。

圖2-4 項目中添加"ADO.NET實體數據模型"

圖2-4 選擇模型內容

圖2-5 選擇數據庫對象
完成以上步驟后,就可以在項目中看到生成的文件,如圖2-6所示。

圖2-6 項目結構
2.2.2 認識EDMX文件
雙擊打開.emdx文件,可以看到如圖2-6所示的數據庫模型圖。改圖完整的體現了數據庫的表字段和表間關系。
圖2-7 數據庫模型圖
將.emdx文件以XML的形式查看,可以看到完整的XML定義,如圖2-8所示。

圖2-8 .emdx的XML的形式
可以看到,edmx中包含SSDL、CSDL和C-S mapping三種類型的節點定義,分別對應EDM中的存儲層、概念層、對應層。這也是EF實現ORM的關鍵配置文件。
2.2.3 實體文件
通過圖2-7,生成的實體模型中是通過"導航屬性"來描述數據庫表之間關系的。對應生成的實體類代碼如下:
Grade.cs文件
| public virtual ICollection<Student> Student { get; set; } public virtual ICollection<Subject> Subject { get; set; } |
Student.cs文件
| public virtual Grade Grade { get; set; } public virtual ICollection<Result> Result { get; set; } |
在Student實體類中,Grade屬性表示一個學生對應一個年級,集合類型的Results表示一個學生對應多個成績,並且使用virtual關鍵字修飾,表示可以延遲加載(后面會詳細介紹)。
2.2.4 數據上下文
這個類文件的擴展名為".Context.cs",維護了受業務影響的對象列表,負責管理對象的增、刪、改、查操作,以及相應的事務及並發問題。簡單地說,它相當於一個對象數據庫的管理者。.Context.cs類代碼如示例1所示。
示例1
| public partial class MySchoolEntities : DbContext { public MySchoolEntities() : base("name=MySchoolEntities") { }
protected override void OnModelCreating(DbModelBuilder modelBuilder) { throw new UnintentionalCodeFirstException(); }
public virtual DbSet<Grade> Grade { get; set; } public virtual DbSet<Result> Result { get; set; } public virtual DbSet<Student> Student { get; set; } public virtual DbSet<Subject> Subject { get; set; } } |
從示例1可以看出,MySchoolEntities類中包含了多個對象的集合,相對於包含了多個表的數據庫,MySchoolEntities繼承的基類為DbContext,該類提供了以對象形式查詢和使用實體數據的功能,屬於Entity Framework體系結構中的Object Services部分。
2.2.5 操作數據
下面我們通過EF來對數據進行CRUD操作。
- 查詢數據
示例2:
| using (MySchoolEntities entities = new MySchoolEntities()) { //按名稱查詢年級編號 Grade grade = entities.Grade.SingleOrDefault(g => g.GradeName == "S2"); if (grade!=null) Console.WriteLine("S2年級的編號為{0}",grade.GradeId); //用where()方法查詢符合條件的數據 IQueryable<Student> query = entities.Student.Where( s => s.GradeId == grade.GradeId); Console.WriteLine("學號\t姓名"); foreach (var stu in query) { Console.WriteLine("{0}\t{1}",stu.StudentNo,stu.StudentName); } } |
- 添加數據
示例3
| using (MySchool1Entities entities = new MySchool1Entities()) { Student stu=new Student() { StudentNo="S10001", StudentName="王翱翔", GradeId=2, Sex="男" }; entities.Student.Add(stu); //向實體集添加數據 if (entities.SaveChanges()>0) //將添加的數據保存到數據庫中 Console.WriteLine("添加數據成功!"); } |
- 修改數據
示例4
| using (MySchool1Entities entities = new MySchool1Entities()) { //先查詢要修改的數據 Student stu = entities.Student.FirstOrDefault(s => s.StudentName == "王翱翔"); if (stu!=null) { stu.LoginPwd = "123456"; //修改對象屬性值 } if (entities.SaveChanges()>0) //將修改的對象保存到數據庫中 { Console.WriteLine("修改數據成功!"); } } |
- 刪除數據
示例5
| using (MySchool1Entities entities = new MySchool1Entities()) { //先查詢要刪除的數據 Student stu = entities.Student.FirstOrDefault(s => s.StudentName == "王翱翔"); if (stu!=null) { entities.Student.Remove(stu); //從數據集中移除數據 } if (entities.SaveChanges()>0) { Console.WriteLine("刪除數據成功!"); } } |
從以上代碼可以看出,所有對數據庫的操作,都可以通過對強類型對象的操作間接完成。需要額外做的兩件事是創建數據上下文對象和調用其SaveChange()方法實現數據的增、刪、改。
2.3 從代碼到數據庫(Code First)
在開發過程中,經常會先設計數據模型,再根據數據模型創建相應的數據庫表。EF的Code First模式支持這種方式。
2.3.1 Entity Framework Code First 的配置步驟
-
安裝Entity Framework package
通過VS的Nuget的包管理器添加對Entity Framework的引用。

圖2-9 按照EnityFramework包
-
創建實體類
創建年級和學生兩個實體類,代碼如下。
| public class Grade { public int GradeId { get; set; } public string GradeName { get; set; } }
public class Student { public string StudentId { get; set; } public string StudentName { get; set; } public int Age { get; set; } public int GradeId { get; set; } } |
-
創建DbContext類
首先在配置文件中配置數據庫連接字符串。
| <configuration> <connectionStrings> <add name="MySchool" providerName="System.Data.SqlClient" connectionString="Data Source=.;Initial Catalog=Test;Persist Security Info=True;User ID=sa;Password=sa"/> </connectionStrings> </configuration> |
注意:<connectionStrings>中必須指定"providerName"屬性。
接下來創建MySchoolContext類,繼承自DbContext類(命名空間:System.Data.Entity)。代碼如下:
| using System.Data.Entity; public class MySchoolContext:DbContext { //構造函數,會讀取配置文件中的連接字符串 public MySchoolContext() :base("MySchool") //.config文件數據庫連接字符串名稱 { } //對象集合 public DbSet<Grade> Grade { get; set; } public DbSet<Student> Student { get; set; }
//重新父類中的方法(當模型初始化后被調用) protected override void OnModelCreating(DbModelBuilder modelBuilder) { //指定單數形式的表名(否則數據庫的表名會是復數形式) //需要命名空間:using System.Data.Entity.ModelConfiguration.Conventions; modelBuilder.Conventions .Remove<PluralizingTableNameConvention>(); } } |
當程序運行后,當第一次使用EF訪問數據時,數據庫會被創建。可在SQL Server中查看生成的數據庫及數據表。
2.4 EF Code First中的約定
通過查看生成的數據庫表,發現生成的表存在很多不滿足實際的地方,如主鍵的指定,數據長度、是否可空等等。這是由EF的默認約定來確定的。
2.4.1 EF中的默認約定
- 關於表和字段名的約定
Code First 約定表名使用英語語法的類名復數形式來命名表名,屬性映射的列使用與類中屬性一致的名字命名。
-
關於主鍵的約定
Code First默認約定將命名為 Id 或 [類名]Id 的屬性視為類的鍵,如果是int類型同時會設為標識列。
-
字符串屬性的約定
字符串約定為映射到不限長度的非空列中,默認數據類型為nvarchar(max),且允許為空。
-
布爾值的約定
Code Frirst將bool屬性映射為 bit 類型,且不允許為空。
4. 關系
- 1對1關系
一個學生對應一個或零個地址,一個地址只能對應一個學生。
實現方式:
Student表中添加StudentAddress屬性,StudentAddress表中添加Student屬性,然后給StudentAddress中的主鍵加上[ForeignKey("stu")] (stu為地址表的屬性)。
- 1對多關系
一個年級有多個學生,一個學生只能在一個年級。
實現方式:
Student表中添加Grade屬性,Grade表中添加 ICollection<Student>屬性。生成的數據庫Student表中,會增加了一個新的字段grade_gradeId作為外鍵。
通常情況,我們會自己指定外鍵,方法如下:在Student類中增一個 GradeId屬性,並指定為外鍵。
[ForeignKey("stu")]
public int GradeId { get; set; }
- 多對多關系
一個學生有多門課,一門課有多個學生上。
實現方式:
Student表中添加 ICollection<Course>屬性,Course表中添加ICollection<Student>屬性。生成的數據庫中多了一張表:StudentCourse,里面有兩個外鍵。
2.4.2 使用配置來覆寫約定
Code First允許你覆寫約定,方法是添加配置.可以選擇使用Data Annotation的特性標記也可以使用強類型的Fluent API來配置。
-
使用Data Annotation特性
Data Annotations是最簡單的配置方式,直接應用到你的類和類屬性上。
-
System.ComponentModel.DataAnnotations命名空間下的特性是表中列的屬性的。包括:Key、Required、MinLength和MaxLength、StringLength、
Timestamp、ConcurrencyCheck。
- System.ComponentModel.DataAnnotations.Schema命名空間下的特性是控制數據庫結構的。包括:Table、Column、ForeignKey、NotMapped。
表2-1 EF中常用特性
| 特性 |
說明 |
| Key |
聲明主鍵 |
| Required |
非空聲明 |
| MinLength/MaxLength |
設置string類型的最大長度和最小長度,數據庫的對應nvarchar |
| StringLength |
設置string類型的長度,數據庫對應nvarchar |
| Timestamp |
將byte[]類型設置為timestamp類型 |
| ConcurrencyCheck |
並發檢查,執行update操作時,會檢查並發性(樂觀鎖) |
| Table |
給代碼中的類換一個名來映射數據庫中的表名.(還可以設置表的架構名稱 [Table("myAddress", Schema = "Admin")] ) |
| Column |
給代碼中類的屬性換一個名來映射數據庫中表的列名. (還可以設置列的類型、列在表中的顯示順序 [Column("myAddressName2", Order = 1, TypeName = "varchar")]) |
| ForeignKey |
設置外鍵 |
| NotMapped |
類中的列名不在數據庫表中映射生成. (還可以只設置get屬性或者只設置set屬性,在數據庫中也不映射) |
示例6
| public class Student { [Key] //主鍵聲明 public string studentKey { get; set; }
[Required] //非空聲明 public string stuName { get; set; }
[MaxLength(10)] //最大長度 public string stuTxt1 { get; set; }
[MaxLength(10), MinLength(2)] //最大長度和最小長度 public string stuTxt2 { get; set; }
[ConcurrencyCheck] //並發檢查 public string stuTxt3 { get; set; }
public virtual Address stuAddress { get; set; } }
[ Table("myAddress")] //設置類映射的數據庫表名 public class Address { [ForeignKey("stu")] //設置外鍵(對應下面聲明的 stu) //這里符合 類名+id(忽略大小寫)的規則,所以自動生成主鍵 public string AddressId { get; set; }
[Column("myAddressName")] //設置映射數據庫中表的列名 public string AddressName { get; set; }
[Column("myAddressName2", Order = 1, TypeName = "varchar")] //設置映射數據庫中表的列名、順序、類型 public string AddrssName2 { get; set; }
[NotMapped]//不映射數據 public string addressNum { get; set; }
public virtual Student stu { get; set; } } |
-
使用Fluent API
Fluent API 形式,可以將一個類映射成多個數據庫表,還可以將配置寫成多個文件,方便控制。
(1)優先級:Fluent API > data annotations > default conventions.
(2)所有的Fluent API配置都要在 OnModelCreating這個重寫方法中進行
(3)常見的配置:
① 獲取表對應的配置根: var stu =modelBuilder.Entity<XXX>();
② 設置主鍵:HasKey<string>(s => s.studentKey);
③ 獲取屬性:stu.Property(p => p.stuName)
④ 設置可空或非空:IsRequired和IsOptional
⑤ 設置最大值:HasMaxLength
⑥ 修改屬性名→修改屬性的次序→修改屬性對應的數據庫類型:
HasColumnName→HasColumnOrder→HasColumnType
⑦ 修改表名:ToTable
(4)可以建立多個Fluent API的配置文件,然后通過
modelBuilder.Configurations.Add(new XXX());添加到一起。
示例7
| public class Student { //主鍵聲明 public string studentKey { get; set; } //非空聲明 public string stuName { get; set; } //最大長度 public string stuTxt1 { get; set; } //最大長度和最小長度 public string stuTxt2 { get; set; } //並發檢查 public string stuTxt3 { get; set; } } public class Address { //既是主鍵、又是外鍵 public string AddressId { get; set; } //設置映射數據庫中表的列名 public string AddressName { get; set; } //設置映射數據庫中表的列名、順序、類型 public string AddrssName2 { get; set; } //不映射數據 public string addressNum { get; set; } }
/// <summary> /// Game實體,與其它兩個沒有什么直接關系,單純的為了演示, Fluent API的配置,可以根據實體進行拆分 /// 文件來配置,方便管理 /// </summary> public class Game { public int GameId { get; set; } public string GameName { get; set; } }
/// <summary> /// Game實體的配置文件 /// </summary> public class GameConfiguration : EntityTypeConfiguration<Game> { public GameConfiguration() { this.HasKey(p => p.GameId); this.Property(p => p.GameName).HasMaxLength(10).IsRequired(); } }
public class dbContext : DbContext { public dbContext() : base("name=MySchool") {
}
public DbSet<Student> Student { get; set; }
public DbSet<Address> Address { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder) { //所有的FluentAPI均在方法中進行重寫 //一. 屬性層次上的設置 var stu = modelBuilder.Entity<Student>(); var stuAddress = modelBuilder.Entity<StudentAddress>();
//1. 設置主鍵 stu.HasKey<string>(s => s.studentKey); stuAddress.HasKey<string>(s => s.AddressId);
//2. 設置非空 (擴展:IsOptional 設置可空) stu.Property(p => p.stuName).IsRequired( );
//3. 設置最大值(不能設置最小值) stu.Property(p => p.stuTxt1).HasMaxLength(10);
//4. 修改列的名稱、排序、類型 stuAddress.Property(p => p.stuAddrssName2).HasColumnName("myAddress2").HasColumnOrder(1).HasColumnType("varchar");
//5.修改表名 stu.Map<Student>(c => { c.ToTable("MyStudent"); });
//6.將一個實體映射成多張表,並分別給其命名 //stuAddress.Map<StudentAddress>(c => //{ // c.Properties(p => new // { // p.AddressId, // p.AddressName // }); // c.ToTable("MyStuAddress1"); //}).Map<StudentAddress5>(c => //{ // c.Properties(p => new // { // p.stuAddressId, // p.stuAddrssName2 // }); // c.ToTable("MyStuAddress2"); //});
//三. 將Game實體的配置添加進來 modelBuilder.Configurations.Add(new GameConfiguration());
base.OnModelCreating(modelBuilder); } }
|
2.5 EF CodeFirst 數據庫初始化策略和數據遷移
無論是DataAnnotation還是Fluent API,都會發現一個現象,當數據庫結構發生變化的時候,就會拋出異常,不得不把數據庫刪除,重新生成,這樣就會導致原數據庫中的數據丟失,在實際開發中,顯然是不可取的。怎么解決這個數據丟失的問題呢?
2.5.1 數據庫初始化策略
EF的CodeFirst模式下數據庫的初始化有四種策略來決定結構方式改變時如何處理:
- CreateDatabaseIfNotExists:EF的默認策略,數據庫不存在,生成數據庫;一旦model發生變化,拋異常,提示走數據遷移
- DropCreateDatabaseIfModelChanges:一旦model發生變化,刪除數據庫重新生成
- DropCreateDatabaseAlways:數據庫每次都重新生成
- 自定義初始化(繼承上面的三種策略中任何一種,然后追加自己的業務)
也可以關閉數據庫初始化策略:
Database.SetInitializer<dbContext6>(null);
關閉后改變實體類,不會報錯,不會丟失數據,但也無法映射改變數據庫結構。
示例8
| public class MySchoolEntities : DbContext {
public MySchoolEntities () : base("name=MySchool") { //在這里可以改變生成數據庫的初始化策略 //1. CreateDatabaseIfNotExists (EF的默認策略,數據庫不存在,生成數據庫;一旦model發生變化,拋異常,提示走數據遷移) //Database.SetInitializer<MySchoolEntities>(new CreateDatabaseIfNotExists<MySchoolEntities >());
//2. DropCreateDatabaseIfModelChanges (一旦model發生變化,刪除數據庫重新生成) //Database.SetInitializer<MySchoolEntities >(new DropCreateDatabaseIfModelChanges<MySchoolEntities >());
//3.DropCreateDatabaseAlways (數據庫每次都重新生成) //Database.SetInitializer<MySchoolEntities >(new DropCreateDatabaseAlways<MySchoolEntities >());
//4. 自定義初始化(繼承上面的三種策略中任何一種,然后追加自己的業務) //Database.SetInitializer<MySchoolEntities >(new MySpecialIntializer());
//5. 禁用數據庫策略(不會報錯,不會丟失數據,但是改變不了數據庫的結構了) //Database.SetInitializer<MySchoolEntities >(null); }
public DbSet<Animal> Animal { get; set; }
public DbSet<AnimalKind> AnimalKind { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); }
/// <summary> /// 自定義一個初始化策略,每次初始化時追加10條數據 /// </summary> public class MySpecialIntializer : DropCreateDatabaseAlways<MySchoolEntities > { public override void InitializeDatabase(MySchoolEntities context) { base.InitializeDatabase(context); }
//重寫seed,追加初始數據 protected override void Seed(MySchoolEntities context) { for (int i = 0; i < 10; i++) { context.Animal.Add(new Animal() { id = "animal" + i, animalName = "mru" + i, animalKind = "mruru" + i, }); } context.SaveChanges(); base.Seed(context); } } } |
2.5.2 數據遷移
數據遷移能很好的解決修改表結構,數據丟失的問題。
方式一:自動遷移
步驟:
(1)新建Configuration.cs類,在其構造函數中進行相關配置。
(2)改變數據庫的初始化策略為,MigrateDatabaseToLatestVersion ,並在Configuration類中配置開啟自動遷移:AutomaticMigrationsEnabled = true;
(3)顯式開啟允許修改表結構:AutomaticMigrationDataLossAllowed = true; (默認是關閉的)
示例9
| internal sealed class Configuration : DbMigrationsConfiguration<MySchoolEntities > { public Configuration() { AutomaticMigrationsEnabled = true; //啟用自動遷移 AutomaticMigrationDataLossAllowed = true; //更改數據庫中結構(增加、刪除列、修改列、改變列的屬性、增加、刪除、修改表),需要顯示開啟。 } }
public class MySchoolEntities : DbContext { public MySchoolEntities () : base("name=MySchool") { //數據庫遷移配置
//數據庫遷移的初始化方式 Database.SetInitializer(new MigrateDatabaseToLatestVersion<MySchoolEntities , Configuration>("MySchoolEntities")); }
public DbSet<Animal> Animal { get; set; } public DbSet<AnimalKind> AnimalKind { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); } }
|
進行測試:
(1)增加一列:改變Animal類,增加一個字段,刷新數據庫,發現數據多了一列 (無需特別的配置),但后面再次增加,還是需要配置AutomaticMigrationDataLossAllowed設置為true
(2)刪除一列、修改列、改變列的屬性:報錯,提示需要將AutomaticMigrationDataLossAllowed設置為true, 設置后,修改或刪除成功。
(3)增加一張表:增加一個AnimalKind類,運行代碼,發現數據庫多了一張表。
(4) 刪除表:注釋掉:public DbSet<AnimalKind> AnimalKind { get; set; } ,運行代碼,數據庫中AnimalKind表被刪除。
方式二: 手動遷移
步驟:
在程序包管理控制台中輸入:
- Enable-Migrations:在項目中啟用數據庫遷移,然后會創建一個Configuration類
2. Add-Migration:創建了一個遷移類,其中指定了Up和Down方法。 
3. Update-Database:執行Add_migration指令中創建的遷移,將改變應用到數據庫中。
在使用Add-Migration命令之后,你需要更新數據庫。通過執行Update-Database命令,來提交修改到數據庫中,還可以在后面加上–verbose 就可以看到生成的SQL腳本
到這個時候,數據庫就被創建或更新了,現在不管什么時候,模型發生改變的時候,執行Add-Migration 帶上參數名,就創建一個新的遷移文件,然后執行Update-Database命令,就將修改提交到數據庫了。
遷移回退
假設你想要回退到之前的任何一個狀態,那么你可以執行update-database后面跟着–TargetMigration,指定你想要回退的版本。例如,假設SchoolDB數據庫有很多遷移記錄,但是你想回退到第一個版本,那么你可以執行下面的代碼:
PM> update-database -TargetMigration:SchoolDB-v1
2.6 小結
EF Code First 配置步驟
- 安裝Entity Framework Package
- 創建實體類並設置約定
- 創建DbContext類,為每個實體類創建一個DbSet
- 在配置文件中配置數據庫連接字符串
- 創建Initializer類, 指定EF初始化數據庫策列
- 創建Configuration 類啟用數據遷移
