数据库的映射指的就是对数据库进行配置,包括生成表名,架构名,列名。这些内容前面的笔记已经涉及到了,还包括的复杂类型的设置,这里就不在赘述。
本次主要学习和掌握如何将单个类映射到多个表中,多个类如何映射到一个通用表中和各种类继承架构的配置。
让多个实体映射到同一个表: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 三种种方式的 其他数据操作比如查询,删除,修改均没有试验,需要待考察!