數據庫的映射指的就是對數據庫進行配置,包括生成表名,架構名,列名。這些內容前面的筆記已經涉及到了,還包括的復雜類型的設置,這里就不在贅述。
本次主要學習和掌握如何將單個類映射到多個表中,多個類如何映射到一個通用表中和各種類繼承架構的配置。
讓多個實體映射到同一個表:AKA表切分
通常一個數據庫表中雖然有很多列,但在很多場景只需要使用其中的一部分,其他的只是一些附加的數據。當我們映射一個實體到這樣的表以后,你會發現要浪費資源來處理一些無用的數據。表切分技術可以解決這個問題,這種技術允許在一個單獨表中訪問多個實體。
為了將多個實體映射到一個通用的表中,實體必須遵循如下規則:
- 實體必須是一對一關系
- 實體必須共享一個通用鍵
看一下書中的例子:
[Table("People")] public class Person { //共享了通用鍵 [Key, ForeignKey("Photo")] public int PersonId { get; set; } public int SocialSecurityNumber { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public PersonPhoto Photo { get; set; } } [Table("People")] public class PersonPhoto { //共享通用鍵 [Key, ForeignKey("PhotoOf")] public int PersonId { get; set; } [Column(TypeName="image")] public byte[] Photo { get; set; } public string Caption { get; set; } public Person PhotoOf { get; set; } }
兩類的確滿足我們上面的關系,我們在程序運行的時候插入一條數據:
static void Main(string[] args) { Database.SetInitializer(new DropCreateDatabaseIfModelChanges<LxContext>()); InsertPerson(); } private static void InsertPerson() { PersonPhoto ph = new PersonPhoto() { Caption = "個人照片", Photo = new byte[8] }; //可以插入成功 Person p1 = new Person() { FirstName = "Jhon", LastName = "Micheal", SocialSecurityNumber = 123, Photo = ph }; using (var context = new LxContext()) { context.Persons.Add(p1); context.SaveChanges(); } }
數據庫生成后如下圖:

注意:當我們這樣做的時候,當我們插入數據的時候,兩個類必須都擁有數據。否則會產生“遇到了無效數據。缺少必要的關系” 的異常!
將一個單獨的實體映射到多個表
翻轉一下上面的例子,我們可以把 一個單獨的實體映射的到多個表中,這種稱之為實體分割。實現這個功能 不能使用Data Annotations ,因為Data Annotations 沒有子屬性的概念。
我們再增加一個類:
//將PersonInfo 分割成兩個表 public class PersonInfo { [Key] public int PersonId { get; set; } public int SocialSecurityNumber { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public byte[] Photo { get; set; } public string Caption { get; set; } }
然后用FluntAPI進行配置:
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<PersonInfo>().Map(m => { m.ToTable("t_PersonInfo"); m.Properties(p => p.FirstName); m.Properties(p => p.LastName); m.Properties(p => p.SocialSecurityNumber); }).Map(m => { m.ToTable("t_PersonPhoto"); m.Properties(p => p.Photo); m.Properties(p => p.Caption); }); }
結果如下圖:

我們發現PersonInfo這個類的確被我們拆分成了兩個表,t_PersonInfo 和 t_PersonPhoto,而且還進行了外鍵關聯。
但這里有幾個需要注意的地方:
1、雖然生成了兩表,但是只有t_PersonInfo 的主鍵是自增的,t_PersonPhoto 的主鍵 PersonId 是非自增的,並且是外鍵關聯到了 t_PersonInfo的主鍵
2、雖然建立了外鍵但是不會建立級聯刪除。
3、在使用FluntAPI 進行實體分割的時候,務必不要跳過任何屬性。否則Code First還會自動的創建第三張表,保存那些你遺漏的屬性。
雖然沒有建立級聯刪除,但是 EF框架知道,如果對t_PersonInfo 操作,它必須建立一個跨越兩個表的執行命令,我們來實驗一下:
增加一個Person:
var p2 = new PersonInfo { FirstName = "Monroe", LastName = "Marilyn", SocialSecurityNumber = 456 }; using (var context = new LxContext()) { context.PersonInfos.Add(p2); context.SaveChanges(); }
我們利用Sql Profiler監視,會發現數據庫執行了兩條SQL分別是:
exec sp_executesql N'insert [dbo].[t_PersonInfo]([SocialSecurityNumber], [FirstName], [LastName]) values (@0, @1, @2) select [PersonId] from [dbo].[t_PersonInfo] where @@ROWCOUNT > 0 and [PersonId] = scope_identity()',N'@0 int,@1 nvarchar(max) ,@2 nvarchar(max) ',@0=456,@1=N'Monroe',@2=N'Marilyn' exec sp_executesql N'insert [dbo].[t_PersonPhoto]([PersonId], [Photo], [Caption]) values (@0, null, null) ',N'@0 int',@0=1
修改一個Person:
using (var context = new LxContext()) { var PersonList = context.PersonInfos.ToList(); var p = PersonList[0]; p.Caption = "my photo"; p.Photo = new byte[8]; context.SaveChanges(); }
監視執行的SQL 為:
--查詢
SELECT [Extent1].[PersonId] AS [PersonId], [Extent2].[SocialSecurityNumber] AS [SocialSecurityNumber], [Extent2].[FirstName] AS [FirstName], [Extent2].[LastName] AS [LastName], [Extent1].[Photo] AS [Photo], [Extent1].[Caption] AS [Caption] FROM [dbo].[t_PersonPhoto] AS [Extent1] INNER JOIN [dbo].[t_PersonInfo] AS [Extent2] ON [Extent1].[PersonId] = [Extent2].[PersonId] --修改 exec sp_executesql N'update [dbo].[t_PersonPhoto] set [Photo] = @0, [Caption] = @1 where ([PersonId] = @2) ',N'@0 varbinary(max) ,@1 nvarchar(max) ,@2 int',@0=0x0000000000000000,@1=N'my photo',@2=1
刪除Person:
using (var context = new LxContext()) { var PersonList = context.PersonInfos.ToList(); var p = PersonList[0]; context.PersonInfos.Remove(p); context.SaveChanges(); }
監視生成的SQL為:
--查詢 SELECT [Extent1].[PersonId] AS [PersonId], [Extent2].[SocialSecurityNumber] AS [SocialSecurityNumber], [Extent2].[FirstName] AS [FirstName], [Extent2].[LastName] AS [LastName], [Extent1].[Photo] AS [Photo], [Extent1].[Caption] AS [Caption] FROM [dbo].[t_PersonPhoto] AS [Extent1] INNER JOIN [dbo].[t_PersonInfo] AS [Extent2] ON [Extent1].[PersonId] = [Extent2].[PersonId] --刪除 exec sp_executesql N'delete [dbo].[t_PersonPhoto] where ([PersonId] = @0)',N'@0 int',@0=1 exec sp_executesql N'delete [dbo].[t_PersonInfo] where ([PersonId] = @0)',N'@0 int',@0=1
通過這些操作,我們已經知道雖然沒有生成級聯刪除或者修改,但是EF框架會知道如何執行,不用我們去手動管理。
類繼承的映射與配置:
EF框架支持各種模型中的繼承層次結構。無論你使用Code First,Model First還是Database First來定義模型都不用擔心繼承的類型問題,也不用考慮EF框架如何使用這些類型進行查詢,跟蹤變更和更新數據。
TPH(Table Per Hierarchy):
TPH:基類和派生類都映射到同一張表中,通過使用鑒別列來識別是否為子類型。這是Code First默認規則使用的表映射方法。
public class Pet { public int PetId { get; set; } public string PetName { get; set; } } public class Dog : Pet { public int Age { get; set; } public string Des { get; set; } }
然后插入一些數據:
var pet = new Pet { PetName = "兔寶寶", }; var dog = new Dog { PetName="狗寶寶", Age=1, Des="1歲狗寶寶" };
using (var context = new LxContext()) { context.Pets.Add(pet); context.Pets.Add(dog); context.SaveChanges(); }
運行程序會看到生成的表結構和插入的數據:


我們會發現,生成了一個表,其列名就是父類和子類屬性的和,還有一鑒別列 Discriminator,看數據我們就能知道,鑒別列的數值是通過類名來進行區分的,Pet :為父類數據,Dog:為子類數據。
以上是默認的配置規則,當然也可以利用FluntAPI方式進行配置,修改鑒別列的列名和值,EF 會自動識別值的類型。
modelBuilder.Entity<Pet>().Map(m => { m.ToTable("t_Pet"); m.Requires("IsFather").HasValue(true); }).Map<Dog>(m => { m.Requires("IsFather").HasValue(false); });


TPT(Table Per Type):
TPH將所有層次的類都放在了一個表里,而TPT在一個單獨的表中儲存來自基類的屬性,在派生類定義的附加屬性儲存在另一個表里,並使用外鍵與主表相連接。
我們只需要為派生類指定表名即可,可以使用Data Annotations 也可以使用Fluent API來完成這項工作。
DataAnnotation方式:
[Table("t_Pet")] public class Pet { public int PetId { get; set; } public string PetName { get; set; } } [Table("t_Dog")] public class Dog : Pet { public int Age { get; set; } public string Des { get; set; } }
結果如下:


FluntAPI 方式:
modelBuilder.Entity<Pet>().ToTable("t_Pet"); modelBuilder.Entity<Dog>().ToTable("t_Dog");
也可以更具體指定關系:
modelBuilder.Entity<Pet>().Map(m => { m.ToTable("t_Pet"); }).Map<Dog>(m => { m.ToTable("t_Dog"); });
TPC(Table Per Concrete Type)
TPC類似TPT,基類與派生類都映射在不同的表中,不同的是派生類中還包括了基類的字段。TPC只能用Fluent API來配置。
modelBuilder.Entity<Pet>() .Map(m => { m.ToTable("t_Pet"); }) .Map<Dog>(m => { m.ToTable("t_Dog"); m.MapInheritedProperties(); });
生成表結構如下:

雖然生成了表,但是兩個表的主鍵PetId 均不是自動增長,我們插入數據的時候,需要指定PetId:
var pet = new Pet { PetId=1, PetName = "兔寶寶", }; //雖然生成了兩個表,但是PetId 不能=1,否則出現重復鍵異常 var dog = new Dog { PetId=2, PetName="狗寶寶", Age=1, Des="1歲狗寶寶" }; using (var context = new LxContext()) { context.Pets.Add(pet); context.Pets.Add(dog); context.SaveChanges(); }
結果如下:

注意:TPH 、TPT、TPC 三種種方式的 其他數據操作比如查詢,刪除,修改均沒有試驗,需要待考察!
