本篇目錄
本篇的源碼下載:點擊下載
先附上codeplex上EF的源碼:entityframework.codeplex.com,此外,本人的實驗環境是VS 2013 Update 5,windows 10,MSSQL Server 2008。
上一篇《第一個Code First應用》簡單介紹了如何使用EF的Code First方式創建一個項目,也介紹了如何進行簡單的CRUD以及數據庫模式的改變。這一篇,我們會深入學習領域建模需要注意的地方以及實體之間關系的管理。
理解Code First及其約定和配置
深入理解Code First
傳統設計應用的方式都是由下而上的,即我們習慣優先考慮數據庫,然后使用這個以數據為中心的方法來在數據之上構建應用程序。這種方法非常適合於數據密集的應用或者數據庫很可能包含多個應用使用的業務邏輯的應用。對於這種應用,如果要使用EF的話,我們必須使用Database First方式。
設計應用的另一種方法就是以領域為中心的方式(領域驅動設計DDD)。DDD是一種由上而下的方式,我們通過從實現應用所需要的領域模型和實體的角度思考,從而開始設計應用。數據庫很少用來用於領域模型數據的持久化。使用DDD意味着我們要根據每個應用的需求來設計模型和實體,而且模型和實體是數據庫可忽略的,即可使用任何數據庫技術實現保存。在這些情景中,我們應該使用EF的Code First方式,因為它允許我們創建POCOs(Plain Old CLR Objects)作為持久化可忽略的領域模型。
使用EF Code First的優勢在於:
- 支持DDD
- 可以早早地着手開發,因為我們不必等待數據庫的創建
- 持久化層(底層的數據庫)的改變不會對現有的模型有任何影響
理解Code First的約定和配置
我們需要搞清楚的第一件事就是約定大於配置的概念。Code First方式期望模型類遵守一些約定,這樣的話數據庫持久化邏輯就可以從模型中提取出來。比如,如果我們給一個模型定義了一個Id屬性,那么它就會映射到數據庫中該類所對應的那張表的主鍵。這種基於約定的方式的好處在於,如果我們遵守了這些約定,那么我們就不必寫額外的代碼來管理數據庫持久邏輯。缺點在於,如果沒有遵守某個約定,那么EF就不會從模型中提取到需要的信息,運行時會拋異常。
在這種沒有遵守約定又要持久化數據的情況下,我們需要使用Code First的配置項提供關於模型一些額外的信息。比如,如果我們的模型類中沒有Id屬性作為主鍵,那么我們需要在想要的屬性上加上[Key]
特性,這樣它就會被當作主鍵了。
EF使用模型類的復數的約定來創建數據表名,創建的列名和該類的屬性名是一樣的。
創建數據表結構
.Net類型和SQL類型之間的映射
首先,我們第一篇就說了,EF這個ORM工具就是用來解決.NET 類型和SQL Server列類型之間的阻抗失配的問題。比如,假設你在.net中定義了一個int類型的屬性,那么你就可以認為EF已經安全地處理這個列的定義並使用了合適的類型與之對應。記住一些.Net類型和SQL Server列類型之間的映射是很有必要的,下面是一些最常用的映射關系:
完整的映射列表可以參考MSDN SQL Server數據類型映射,如果你使用的其他類型的數據庫,你可以在網上自行查找,比如,如果是Oracle,那么你可以在這里查看:Oracle數據類型映射。
配置原始屬性
就以.Net中的string類型的屬性開始討論吧。SQL Server中的很多類型都會映射到.Net中的string類型,其他主流的RDBMS也是一樣的。因此,決定如何存儲字符串類型的信息是很重要的,很多關系數據庫管理引擎都有多個字符存儲類型,他們通常都有以字母N打頭的字符類型,這個字母表示要存在這些列中的數據是Unicode數據,基於每個字符以2個字節的格式存儲。因此,如果你的數據庫中的列存儲的是英文的話,就可以使用varchar或者char(可能會加速查詢),如果使用的是中文的話,就要使用nvarchar或者nchar。此外,還可以使用帶有var的字符類型來指定列的長度是可變的,不使用var的話,字符長度是不可變的。
在EF中有以下幾種配置數據庫結構的方式,分別是:
- 特性,也叫數據注解
DbModelBuilder
API- 配置伙伴類
特性【數據注解】
這些特性類都是.Net的一部分,位於System.ComponentModel.DataAnnotaions
命名空間。下面我們修改之前的代碼:
[Table("Donator")]
public class Donator
{
[Key]
[Column("Id")]
public int DonatorId { get; set; }
[StringLength(10,MinimumLength = 2)]
public string Name { get; set; }
public decimal Amount { get; set; }
public DateTime DonateDate { get; set; }
}
[Table("PayWay")]
public class PayWay
{
public int Id { get; set; }
[MaxLength(8,ErrorMessage = "支付方式的名稱長度不能大於8")]
public string Name { get; set; }
}
修改代碼之后,我們將表的名字使用Table特性全部重新命名成了單數,將Donator的主鍵通過Colum特性更改為了Id,Key特性指定它是主鍵,還通過StringLength指定了Donator的名字最長為10個字符,最少為2個字符,下面對比一下默認約定生成的數據庫和手動修改之后產生的數據庫:
第一張圖片是默認約定生成的數據庫,第二張是修改代碼后生成的數據庫。
下面是常用的用於重寫默認的約定的特性,使用這些特性可以更改數據庫模式:
- Table:指定該類要映射到數據庫中的表名
- Column:指定類的屬性要映射到數據表中的列名
- Key:指定該屬性是否以主鍵對待
- TimeStamp:將該屬性標記為數據庫中的時間戳列
- ForeignKey:指定一個導航屬性的外鍵屬性
- NotMapped:指定該屬性不應該映射到數據庫中的任何列
- DatabaseGenerated:指定屬性應該映射到數據表中計算的列。也可以用於映射到自動增長的數據庫表。
此外,數據注解也用作驗證特性。如果持久化數據時,模型對象的屬性值和數據注解所標記的不一致,就會拋異常。例如上面的PayWay類的Name屬性,ErrorMessage的值就是發生異常時拋出的信息。
fluent API
DbContext類有一個OnModelCreating
方法,它用於流利地配置領域類到數據庫模式的映射。下面我們以fluent API的方式來定義映射。
首先,先將Donator類注釋掉,重新編寫該類:
public class Donator
{
public int DonatorId { get; set; }
public string Name { get; set; }
public decimal Amount { get; set; }
public DateTime DonateDate { get; set; }
}
然后在數據庫上下文中的OnModelCreating
方法中使用Fluent API來定義Donator表的數據庫模式:
namespace FirstCodeFirstApp
{
public class Context:DbContext
{
public Context()
: base("name=FirstCodeFirstApp")
{
}
public DbSet<Donator> Donators { get; set; }
public DbSet<PayWay> PayWays { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Donator>().ToTable("Donators").HasKey(m => m.DonatorId);//映射到表Donators,DonatorId當作主鍵對待
modelBuilder.Entity<Donator>().Property(m => m.DonatorId).HasColumnName("Id");//映射到數據表中的主鍵名為Id而不是DonatorId
modelBuilder.Entity<Donator>().Property(m => m.Name)
.IsRequired()//設置Name是必須的,即不為null,默認是可為null的
.IsUnicode()//設置Name列為Unicode字符,實際上默認就是unicode,所以該方法可不寫
.HasMaxLength(10);//最大長度為10
base.OnModelCreating(modelBuilder);
}
}
}
modelBuilder.Entity<Donator>()
會得到EntityTypeConfiguration
類的一個實例。此外,使用fluent API的一個重要決定因素是我們是否使用了外部的POCO類,即實體模型類是否來自一個類庫。我們無法修改類庫中類的定義,所以不能通過數據注解來提供映射細節。這種情況,我們必須使用fluent API。
生成后的數據庫表如下(剛才兩張表名都是單數,現在又使用fluent API將Donator改為了復數):
每個實體類配置一個伙伴類
不知道你有沒有注意到一個問題?上面的OnModelCreating
方法中,我們只配置了一個類Donator,也許代碼不是很多,但也不算很少,如果我們有1000個類怎么辦?都寫在這一個方法中肯定不好維護!EF提供了另一種方式來解決這個問題,那就是為每個實體類單獨創建一個配置類。然后再在OnModelCreating
方法中調用這些配置伙伴類。
先創建Donator的配置伙伴類:
public class DonatorMap:EntityTypeConfiguration<Donator>
{
public DonatorMap()
{
ToTable("DonatorFromConfig");//為了區分之前的結果
Property(m => m.Name)
.IsRequired()//將Name設置為必須的
.HasColumnName("DonatorName");//為了區別之前的結果,將Name映射到數據表的DonatorName
}
}
接下來直接在數據庫上下文中調用就可以了:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new DonatorMap());
base.OnModelCreating(modelBuilder);
}
查看數據庫,可以看到符合我們的更改:
這種寫法和使用model builder是幾乎一樣的,只不過這種方法更好組織處理多個實體。你可以看到上面的語法和寫jQuery的鏈式編程一樣,這種方法的鏈式寫法就叫Fluent API。
處理可空(nullable)屬性
有些列是可空的,有些不可空。EF會通過約定來決定一列是否是nullable。比如,string類型允許null值,因此匹配的基於字符的列就是nullable。另一方面,datetime和int變量在.Net中是不能為null的,所以這些列是non-nullable。如果我們想讓這些列是nullable或者想使得字符串存儲列強制有值,該怎么辦?
一是直接使用可空類型對實體類的屬性進行定義,這是目前最簡單的方法。例如,如果上面的打賞日期允許空值的話,那么應該這樣定義:public DateTime? DonateDate { get; set; }
。
另一方面,如果Donator的名字不可為空,那么我們可以像上面的配置類中那樣寫,使用IsRequired()方法。相對應地,IsOptional()方法就是允許為空值。
需要格外注意的是,如果你使用的是其他的數據庫,.Net中的某些類型可能不能正確地映射到這些數據庫系統。解決方案就是在屬性配置類中使用HasColumnType
方法,然后指定你想要顯式使用的名字。如果你想支持多個數據庫引擎,那么只要寫一個helper類就可以解決了,該helper類會基於當前配置的數據庫引擎以字符串的形式返回正確的數據庫類型。所有的原始屬性配置類共享兩個方法,HasColumnName
和HasColumnOrder
。HasColumnName
允許我們可以創建不同於屬性名稱的列名,如果你想定義成一樣的,那么就不需要該方法了。HasColumnOrder
可以讓我們精確地控制列在表中的排列位置。
管理實體關系
我們現在已經知道如何使用Code First來定義簡單的領域類,並且如何使用DbContext類來執行數據庫操作。現在我們來看下數據庫理論中的多樣性關系,我們會使用Code First實現下面的幾種關系:
- 一對多關系
- 一對一關系
- 多對多關系
首先要明確關系的概念。關系就是定義兩個或多個對象之間是如何關聯的。它是由關系兩端的多樣性值識別的,比如,一對多意味着在關系的一端,只有一個實體,我們有時稱為父母;在關系的另一端,可能有多個實體,有時稱為孩子。EF API將那些端分別稱為主體和依賴。一對多關系也叫做一或零對多(One-or-Zero-to-Many),這意味着一個孩子可能有或可能沒有父母。一對一關系也稍微有些變化,就是關系的兩端都是可選的。
一對多關系
要在數據庫中配置一對多關系,我們可以依賴EF約定,或者可以使用數據注解/fluent API來顯式創建關系。接下來還是使用捐贈者Donator和支付方法PayWay這兩個類來舉例子,這里的一對多關系是:一個人可以通過多種支付方式贊助我。
支付方式PayWay類采用數據注解的方式定義如下:
[Table("PayWay")]
public class PayWay
{
public int Id { get; set; }
[MaxLength(8,ErrorMessage = "支付方式的名稱長度不能大於8")]
public string Name { get; set; }
}
因為一個贊助者可以通過多種支付方式贊助我,這句話就表明了Donator對象應該有一個PayWay的集合,因此,我們要給Donator類新加入一個屬性,Donator類采用配置伙伴類的方式定義如下:
public class Donator
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Amount { get; set; }
public DateTime DonateDate { get; set; }
public virtual ICollection <PayWay> PayWays { get; set; }
}
public class DonatorMap:EntityTypeConfiguration<Donator>
{
public DonatorMap()
{
ToTable("Donators");
Property(m => m.Name)
.IsRequired(); //將Name設置為必須的
}
}
為了避免潛在的null引用異常可能性,當Donator對象創建時,我們使用HashSet的T集合類型實例創建一個新的集合實例,如下所示:
public Donator()
{
PayWays=new HashSet<PayWay>();
}
你會注意到當我定義PayWays屬性時使用了virtual
關鍵字,當為一個贊助者查詢他的支付方式時,該關鍵字允許我們使用懶加載(lazy loading),也就是說當你嘗試訪問Donator的PayWays屬性時,EF會動態地從數據庫加載PayWays對象到該集合中。懶加載,顧名思義,就是首次不會執行查詢來填充PayWays屬性,而是在請求它時才會加載數據。還有另一加載相關數據的方式叫做預先加載(eager loading)。通過預先加載,在訪問PayWays屬性之前,PayWays就會主動加載。現在我們假設要充分使用懶加載功能,所以這里使用了virtual
關鍵字。這里有意思的是,在支付方法PayWay類中並沒有包含Donator的Id屬性,這是作為數據庫開發者必須要做的一些事,但在EF的世界中,我們有很大的靈活性來忽略這個屬性,由於當我們看支付方式的時候可能沒有合理的業務原因來知道該贊助者的Id,所以我們可以忽略該屬性。這個例子中,我們只想在Donator的上下文中了解他的支付方式,並不把它們分離開作為獨立對象。現在我們假設這能正常運行,然后添加一個網名叫做“鍵盤里的鼠標”的贊助者,因為他支付寶和微信都打賞過了。代碼如下:
#region 6.0 一對多關系
var donator = new Donator
{
Amount = 6,
Name = "鍵盤里的鼠標",
DonateDate =DateTime.Parse("2016-4-13"),
};
donator.PayWays.Add(new PayWay{Name = "支付寶"});
donator.PayWays.Add(new PayWay{Name = "微信"});
context.Donators.Add(donator);
context.SaveChanges();
#endregion
上面的代碼中,我們添加了一個贊助者,然后給該對象的PayWays屬性追加兩個元素,最后批量保存它們。注釋掉初始化器中的種子數據,然后運行應用,生成的結果如下:
我們只編寫了OOP代碼:創建了一個類的實例,然后將PayWay類的實例添加到一個集合。EF的很多默認約定可以幫我們創建正確的數據庫結構,包括將對象操作轉成數據庫查詢。從上面的截圖來看,在PayWays表中,EF使用默認約定幫我們自動創建了一個Donator_Id
的列作為外鍵,當然,你完全可以通過代碼手動修改這個外鍵的名字。比如要將Donator_Id
修改為DonatorId
,只需要在PayWays類中添加一個屬性DonatorId(該屬性名稱是我們想要的列名)。然后,我們需要配置Donator對象,告訴它有多個支付方式,每一個支付方式都會通過DonatorId屬性鏈接到一個Donator。給Donator配置伙伴類DonatorMap追加代碼如下:
HasMany(d => d.PayWays)
.WithRequired()
.HasForeignKey(p => p.DonatorId);
上面的代碼對於關系的定義很經典。HasMany
方法告訴EF在Donator
和Payway
類之間有一個一對多的關系。WithRequired
方法表明鏈接在PayWays
屬性上的Donator
是必須的,換言之,Payway對象不是獨立的對象,必須要鏈接到一個Donator。HasForeignKey
方法會識別哪一個屬性會作為鏈接。
更改默認的外鍵列名結果:
另一個用例
接下來在看一個例子,這個用例出現在當主要實體上有一個查詢屬性,且該屬性指向另一個實體時。查詢屬性指向一個完整的子實體的父親,當操作或檢查一個子記錄需要訪問父信息時,這些屬性很有用。比如,我們再創建一個類DonatorType(該類用來表示贊助者的類型,比如有博客園園友和非博客園園友),然后給Donator類添加DonatorType屬性。本質上,這個例子還是一個一對多的關系,但是方法有些不同。這種情況下,我們一般會在主實體的編輯頁面使用一個包含查詢父表值的下拉控件。我們的查詢父表很簡單,只有Id和Name列,我們將使該關系為可選的,以描述如何添加可空的外鍵。因此,Donator類中的DonatorTypeId必須是可空的。Donator類的定義如下:
public class Donator
{
public Donator()
{
PayWays=new HashSet<PayWay>();
}
public int Id { get; set; }
public string Name { get; set; }
public decimal Amount { get; set; }
public DateTime DonateDate { get; set; }
public virtual ICollection<PayWay> PayWays { get; set; }
public int? DonatorTypeId { get; set; }
public virtual DonatorType DonatorType { get; set; }
}
反過來,我們要在DonatorType類中添加一個集合屬性,表示每種贊助者類型有很多贊助者,代碼如下:
public class DonatorType
{
public int Id { set; get; }
public string Name { set; get; }
public virtual ICollection<Donator> Donators { get; set; }
}
提到關系,我們可以在關系的任何一端(主體端或依賴端)進行配置。下面,我們創建一個新的DonatorTypeMap
伙伴類以達到目的,代碼如下:
public class DonatorTypeMap:EntityTypeConfiguration<DonatorType>
{
public DonatorTypeMap()
{
HasMany(dt=>dt.Donators)
.WithOptional(d=>d.DonatorType)
.HasForeignKey(d=>d.DonatorTypeId)
.WillCascadeOnDelete(false);
}
}
WithOptional
方法表示外鍵約束可以為空,使用WillCascadeOnDelete
方法可以指定約束的刪除規則。對於外鍵關系約束,大多數數據庫引擎都支持刪除規則的多操作,這些規則指定了當一個父親刪除之后會發生什么。將外鍵列設置為null之后,如果孩子不存在或者刪除了所有相關的依賴就會報錯。EF允許開發者要么刪除所有的孩子行,要么啥也別做。一些數據庫管理員反對級聯刪除,因為一些數據庫引擎沒有提供級聯刪除時的充足的日志信息。
不要忘了在Context類中添加DonatorType的DbSet,還有在model builder上添加DonatorTypeMap的配置類。調用WillCascadeOnDelete
的另一種選擇是,從 model builder中移除全局的約定,在數據庫上下文的OnModelCreating
方法中關閉整個數據庫模型的級聯刪除規則,如下設置:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new DonatorMap());
modelBuilder.Configurations.Add(new DonatorTypeMap());
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
base.OnModelCreating(modelBuilder);
}
運行程序,生成的數據庫結構如下:
創建一對多關系的代碼:
#region 6.1 一對多關系 例子2
var donatorType = new DonatorType
{
Name = "博客園園友",
Donators = new List<Donator>
{
new Donator
{
Amount =6,Name = "鍵盤里的鼠標",DonateDate =DateTime.Parse("2016-4-13"),
PayWays = new List<PayWay>{new PayWay{Name = "支付寶"},new PayWay{Name = "微信"}}
}
}
};
var donatorType2 = new DonatorType
{
Name = "非博客園園友",
Donators = new List<Donator>
{
new Donator
{
Amount =10,Name = "待贊助",DonateDate =DateTime.Parse("2016-4-27"),
PayWays = new List<PayWay>{new PayWay{Name = "支付寶"},new PayWay{Name = "微信"}}
}
}
};
context.DonatorTypes.Add(donatorType);
context.DonatorTypes.Add(donatorType2);
context.SaveChanges();
#endregion
運行程序,執行結果如下:
可以看到,網友“鍵盤里的鼠標”的DonatorTypeId是1,即對應的DonatorType表中的第一行;第二條為測試數據。
一對一關系
一對一關系並不常用,但是偶爾也會出現。如果一個實體有一些可選的數據,那么你可以選擇這種設計。下圖中的兩張表的主鍵是一一對應的。
比如,創建兩個實體類Person和Student,一個人可以是個具有注冊日期的大學生,對於那些不是大學生的人來說,大學和注冊日期就是可空的。因此,我們會將這些數據組合到一個新的實體Student中,如下所示:
public class Person
{
public int PersonId { get; set; }
public string Name { get; set; }
public bool IsActive { get; set; }
public virtual Student Student { get; set; }
}
public class Student
{
public int PersonId { get; set; }
public virtual Person Person { get; set; }
public string CollegeName { get; set; }
public DateTime EnrollmentDate { get; set; }
}
注意這里我們為了啟用懶加載又用了virtual關鍵字,Student的配置伙伴類你應該已經很熟悉了,如下:
public class StudentMap:EntityTypeConfiguration<Student>
{
public StudentMap()
{
HasRequired(s=>s.Person)
.WithOptional(p=>p.Student);
HasKey(s => s.PersonId);
Property(s => s.CollegeName)
.HasMaxLength(50)
.IsRequired();
}
}
這里使用了HasKey
方法,指定了一個表的主鍵,換言之,這是一個允許我們找到一個實體的獨一無二的值。之前我們沒有用這個方法是因為我們要么用了Key特性或者遵守了EF的默認約定(如果屬性名是由類名加上"Id"后綴或者只是"Id"組成,那么EF會計算出該主鍵)。因為我們現在使用了PersonId
作為主鍵,所以我們現在需要給運行時提供額外的提示,這就是HasKey
派生用場的地方。最后子表中的主鍵會成為父表中的外鍵。
因為該關系是可選的,所以它也稱為一或零對一(One-or-Zero-to-One)。關系的兩端都是必須要存在的關系稱為一對一。比如,每個人必須要有一個單獨的login,這是強制性的。你也可以使用WithRequiredDepentent
或者WithRequiredPrincipal
方法來代替WithOptional
方法。
我們可以總是從該關系的主體端或者依賴端來配置關系。我們總是需要配置一對一關系的兩端(即兩個實體),使用
Has
和With
方法確保一對一關系的創建。
創建數據的代碼
#region 7 一對一關系
var student = new Student
{
CollegeName = "XX大學",
EnrollmentDate = DateTime.Parse("2011-11-11"),
Person = new Person
{
Name = "Farb",
}
};
context.Students.Add(student);
context.SaveChanges();
#endregion
運行程序,結果如下:
多對多關系
當關系的兩端都有多個實體時,我們就該考慮使用多對多(Many-to-Many)關系了。比如,每個人可以為多個公司干活,每個公司也可以雇佣多個人。在數據庫層,這種關系是通過所謂的連接表(junction table)定義的,有時也叫交叉引用表,這個表會包含該關系兩端表的主鍵列。這種類型的關系有兩種用例對我們來說很重要,一個連接表可以沒有額外的數據或者列,或者它可以有額外的數據。如果連接表沒有其他的數據,那么從技術上講,我們根本不需要創建表示這個連接表的模型。
下面就讓我們對這種情況進行編碼。創建新的模型類Company,Person類是在之前的基礎上添加屬性,然后修改PersonMap伙伴類來表示兩個實體間的關系。就像一對多關系一樣,我們會在Person和Company類中添加相關類的集合。代碼如下:
public class Company
{
public Company()
{
Persons = new HashSet<Person>();
}
public int CompanyId { get; set; }
public string CompanyName { get; set; }
public virtual ICollection <Person> Persons { get; set; }
}
public class Person
{
public Person()
{
Companies=new HashSet<Company>();
}
public int PersonId { get; set; }
public string Name { get; set; }
public bool IsActive { get; set; }
public virtual Student Student { get; set; }
public virtual ICollection<Company> Companies { get; set; }
}
public class PersonMap:EntityTypeConfiguration<Person>
{
public PersonMap()
{
HasMany(p => p.Companies)
.WithMany(c => c.Persons)
.Map(m =>
{
m.MapLeftKey("PersonId");
m.MapRightKey("CompanyId");
});
}
}
然后在數據庫上下文中添加DbSet的屬性和在OnModelCreating方法中添加PersonMap的配置引用。
public DbSet<Company> Companies { get; set; }
modelBuilder.Configurations.Add(new PersonMap());
技術上講,如果你對EF生成列名的約定沒問題的話,那么該配置是可省略的,意思就是說,EF實際上會根據該關系兩端定義的類和屬性獨立地創建一個連接表,因為相關的實體有集合屬性。如果我們想要不同於默認創建的表名或列名,就可以在連接表中顯式指定表名或列名。
現在我們添加兩個人(比爾蓋茨和喬布斯)和一個公司(微軟),代碼如下:
#region 8 多對多關系
var person = new Person
{
Name = "比爾蓋茨",
};
var person2 = new Person
{
Name = "喬布斯",
};
context.People.Add(person);
context.People.Add(person2);
var company = new Company
{
CompanyName = "微軟"
};
company.Persons.Add(person);
context.Companies.Add(company);
context.SaveChanges();
#endregion
運行程序,查看數據庫結構及填充的數據:
可以看到,EF自動把幫我們生成了連接表PersonCompanies,當然我們也可以在PersonMap伙伴類中自定義,只需要添加m.ToTable("PersonCompany");
即可。
如果我們連接表需要保存更多的數據怎么辦?比如當每個人開始為公司干活時,我們想為他們添加雇佣日期。這樣的話,實際上我們需要創建一個類來模型化該連接表,我們暫且稱為PersonCompany吧。它仍然具有兩個的主鍵屬性,PersonId和CompanyId,它還有Person和Company的屬性以及雇佣日期的屬性。此外,Person和Company類分別都有PersonCompanies的集合屬性而不是單獨的Person和Company集合屬性。
三種繼承模式
到現在為止,我們已經學會了如何使用EF的Code First將領域實體類映射到數據庫表。我們也學會了如何創建具有多樣性關系的實體,以及如何使用EF將這些關系映射到數據庫表之間的關系。
現在我們看一下領域實體間的繼承關系,以及使用EF將這些數據映射到單獨的表中。接下來會介紹下面的三種繼承類型:
- Table per Type(TPT)繼承
- Table per Class Hierarchy(TPH)繼承
- Table per Concrete Class(TPC)繼承
TPT繼承
當領域實體類有繼承關系時,TPT繼承很有用,我們想把這些實體類模型到數據庫中,這樣,每個領域實體都會映射到單獨一張表中。這些表會使用一對一關系相互關聯,數據庫會通過一個共享的主鍵維護這個關系。
假設有這么個場景:一個組織維護了在一個部門工作的所有人的數據庫,這些人有些事拿着固定工資的員工,一些是按小時付費的供應商,要模型化這個情景,我們要創建三個領域實體,Person,Employee和Vendor
。Person類是基類,另外兩個類會從它繼承。在VS中畫的類圖如下:
在TPT繼承中,我們想為每個領域實體類創建單獨的一張表,這些表共享一個主鍵。因此生成的數據庫就像下面這樣:
現在我新創建一個控制台項目(不明白的可下載源碼),然后創建實體類:
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string PhoneNumber { get; set; }
}
[Table("Employees")]
public class Employee : Person
{
public decimal Salary { get; set; }
}
[Table("Vendors")]
public class Vendor : Person
{
public decimal HourlyRate { get; set; }
}
對於Person類,我們使用EF的默認約定來映射到數據庫,而對Employee和Vendor類,我們使用了數據注解,將它們映射為我們想要的表名。
然后我們需要創建自己的數據庫上下文類:
public class Context:DbContext
{
public virtual DbSet<Person> People { get; set; }
}
上面的上下文中,我們只添加了實體Person的DbSet。因為其它的兩個領域模型都是從這個模型派生的,所以我們也就相當於將其它兩個類添加到了DbSet集合中了,這樣EF會使用多 態性來使用實際的領域模型。當然,你也可以使用fluent API和實體伙伴類來配置映射細節信息,這里不再多說。
現在,我們使用這些領域實體來創建一個Employee和Vendor類型:
#region 1.0 TPT繼承
var employee = new Employee
{
Name = "farb",
Email = "farbguo@qq.com",
PhoneNumber = "12345678",
Salary = 1234m
};
var vendor = new Vendor
{
Name = "tkb至簡",
Email = "farbguo@outlook.com",
PhoneNumber = "78956131",
HourlyRate = 4567m
};
context.People.Add(employee);
context.People.Add(vendor);
context.SaveChanges();
#endregion
運行程序,數據庫結構及數據填充情況如下:
我們可以看到每個表都包含單獨的數據,這些表之間都有一個共享的主鍵。因而這些表之間都是一對一關系。
TPH繼承
當領域實體有繼承關系,但是我們想將來自所有的實體類的數據保存到單獨的一張表中時,TPH繼承很有用。從領域實體的角度,我們的模型類的繼承關系仍然像上面的截圖一樣:
但是從數據庫的角度,應該只有一張表存儲數據。因此,最終生成的數據庫的樣子應該是下面這樣的:
在這種情況下,無論何時我們創建了一個worker類型,公共的字段都會填充。如果該worker類型是Employee類型,那么除了公共字段外,Salary還會包含值,但是HourlyRate字段就會是null;如果該worker是Vendor類型,那么HourlyRate會包含值,Salary就會為null。
從數據庫的角度來看,這種模式很不優雅,因為我們將無關的數據保存到了單張表中,我們的表示不標准的。如果我們使用這種方法,那么總會存在一些包含null值的冗余列。
現在我們創建實體類來實現該繼承,注意,這次創建的三個實體類和之前創建的只是沒有了類上的數據注解,這樣它們就會映射到數據庫的單張表中(EF會默認使用父類的DbSet屬性名或其復數形式作為表名,並且將派生類的屬性映射到那張表中):
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string PhoneNumber { get; set; }
}
public class Employee : Person
{
public decimal Salary { get; set; }
}
public class Vendor : Person
{
public decimal HourlyRate { get; set; }
}
對這些實體執行操作的數據庫上下文的配置如下:
public class Context:DbContext
{
public Context():base("ThreeInheritance")
{
}
public virtual DbSet<Person> Person { get; set; }
}
現在,我們使用這些領域類來創建一個Employee和一個Vendor類型:
#region 2.0 TPH 繼承
var employee = new Employee
{
Name = "farb",
Email = "farbguo@qq.com",
PhoneNumber = "12345678",
Salary = 1234m
};
var vendor = new Vendor
{
Name = "tkb至簡",
Email = "farbguo@outlook.com",
PhoneNumber = "78956131",
HourlyRate = 4567m
};
context.Person.Add(employee);
context.Person.Add(vendor);
context.SaveChanges();
#endregion
運行程序,發現數據庫中只有一張表,而且三個類的所有字段都在這張表中了,如下圖:
如果你細心,你會發現生成的表中多了個字段Descriminator,它是用來找到記錄的實際類型,即從Person表中找到Employee或者Vendor。
因此,如果我們沒有在具有繼承關系的實體之間提供確切的配置,那么EF會默認將其對待成TPH繼承,並把數據放到單張表中。
TPC繼承
當多個領域實體派生自一個基實體,並且我們想將所有具體類的數據分別保存在各自的表中,以及抽象基類實體在數據庫中沒有對應的表時,使用TPC繼承。
從領域實體的角度看,我們仍然想要模型維護該繼承關系。因此,實體模型和之前的一樣:
然而,從數據庫的角度看,只有所有具體類所對應的表,而沒有抽象類對應的表。生成的數據庫樣子如下圖:
這種數據庫設計的最大問題之一是數據表中列的重復問題,從數據庫標准的角度這是不推薦的。
現在,創建領域實體類,這里Person基類應該是抽象的,其他的地方都和上面的一樣:
public abstract class Person
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string PhoneNumber { get; set; }
}
public class Vendor : Person
{
public decimal HourlyRate { get; set; }
}
public class Employee : Person
{
public decimal Salary { get; set; }
}
接下來就是應該配置數據庫上下文了,如果我們只在數據庫上下文中添加了Person的DbSet泛型集合屬性,那么EF會當作TPH繼承處理,如果我們需要實現TPC繼承,那么還需要使用fluent API來配置映射(當然也可以使用配置伙伴類):
public virtual DbSet<Person> People { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Employee>().Map(m =>
{
m.MapInheritedProperties();
m.ToTable("Employees");
});
modelBuilder.Entity<Vendor>().Map(m =>
{
m.MapInheritedProperties();
m.ToTable("Vendors");
});
base.OnModelCreating(modelBuilder);
}
上面的代碼中,MapInheritedProperties
方法將繼承的屬性映射到表中,然后我們根據不同的對象類型映射到不同的表中。
然后我們創建一個Employee和一個Vendor,代碼和上面的一樣,這里就不重復貼代碼了。
運行程序,在VS中查看數據庫如下:
雖然數據是插入到數據庫了,但是運行程序時也出現了異常,見下圖。出現該異常的原因是EF嘗試去訪問抽象類中的值,它會找到兩個具有相同Id的記錄,然而Id列被識別為主鍵,因而具有相同主鍵的兩條記錄就會產生問題。這個異常清楚地表明了存儲或者數據庫生成的Id列對TPC繼承無效。
如果我們想使用TPC繼承,那么要么使用基於GUID的Id,要么從應用程序中傳入Id,或者使用能夠維護對多張表自動生成的列的唯一性的某些數據庫機制。
本章小結
這篇博客中,我們看到了如何使用EF Code First方法在應用程序中使用領域實體,以及如何持久化數據到數據庫,也看了如何管理實體間的多樣性關系,以及使用EF將這些關系映射到數據庫。最后我們看了如何使用EF來管理涉及繼承關系的實體,看到了三種繼承關系對應三種不同的數據庫模式。
自我測試
-
你可以使用哪種類型來定義存儲整數的列,該整數不是必須的?
- Decimal
- Decimal?
- Int
- Int?
-
如果你想使得姓名列Name在數據庫中是不可空的,那么你可以依賴EF的默認約定,對嗎?
-
你不能重寫EF預加載的約定,比如默認的外鍵約束級聯刪除,對嗎?
-
下面哪一個不是關系?
- One-to-Many
- Many-to-Many
- One-or-Zero-to-Many
- Many-to-Default
-
給所有的實體類配置所有屬性的最佳方法是在上下文的
OnModelCreating
方法中一個一個地列舉它們,對嗎? -
如果沒有為字符串屬性配置一些額外的信息,那么SQL Server數據庫默認會使用什么類型?
- NVARCHAR(4000)
- NVARCHAR(MAX)
- VARBINARY(MAX)
- VARCHAR(MAX)
-
下面哪一個不是一個關系的第一個端點的合適名稱?
- Principal
- Parent
- Domain
-
如果你想使用一個伙伴類配置一個實體,那么你應該繼承哪個類?
- EntityTypeConfiguration
(of T) - PrimitivePropertyConfiguration
(of T) - ComplexTypeConfiguration
(of T) - EntityConfiguration
(of T)
- EntityTypeConfiguration
9.EF中的三種繼承模式指的是哪三種?
如果您覺得這篇文章對您有價值或者有所收獲,請點擊右下方的店長推薦,然后查看答案,謝謝!
參考書籍:
《Mastering Entity Framework》
《Code-First Development with Entity Framework》
《Programming Entity Framework Code First》