第五章
对数据库映射使用默认规则与配置
到目前为止我们已经领略了Code First的默认规则与配置对属性、类间关系的影响。在这两个领域内,Code First不仅影响模型也影响数据库。在这一章,你将让默认规则与配置的目光聚焦在类映射到数据库上而不影响概念模型。
我们从简单的映射开始,设法指定数据库的表名,构架与属性。在此你将掌握如何让多个类映射到一个通用表中,或将单个类映射到多个表中。最后,带您漫步各种继承架构的配置。
将类名映射到数据库表名和构架名
EF框架使用模型的类名的复数形式来生成数据库表名—Destination变成Destinations,Person变成People等。默认的类命名规则可能与您设定的命名规则不一致,例如需要指定非复数形式的表名(例如PersonPhoto类不要映射到PersonPhotoes表上去),或者你想映射到一些现有的表,其表名恰好与Code First的规则不同。
可以使用Data Annotations 来确保Code First将你的类映射到正确的表名上。使用Table标记,就可改变数据库构架的表名。
数据库表命名对其他映射至关重要,正如你将在本章看到的,包括实体分割,继承层次结构,甚至多对多映射。
默认规则里Code First复数化类名,然后使用复数化后的结果作为类映射的表名。另外,默认情况下所有的表都被安置到dbo构架里。
使用Data Annotations 配置表和构架名
Table标记允许你改变类映射的表名。在上一章,PersonPhoto被不恰当地复数化了,表名为PersonPhotoes。尽管这样仍可以正常工作,但直接使用PersonPhoto也是可行的:
[Table("PersonPhotos")] public class PersonPhoto
另一个例子是原始的BrakAway数据库,在Programming Entity Framework的第一,二版都使用过.包含相关目的地信息的表名为Locations。如果你映射到此表,就必须指定表名:
[Table("Locations")] public class Destination
Table标记还有一个参数可以指定构架名.这里是一个与表名共同指定的例子:
[Table("Locations", Schema="baga")] public class Destination
记住VB的语法有些不同:
<Table("Locations", Schema:="baga")> Public Class Destination
图5-1显示了baga构架下的Locations表:
第二个参数不是必须的,使用Table("Locations") 标记是完全合法的.但是,如果你只想指定构架不想指定表名,仍然需要提供第一个参数,这个参数不能是empty,空格或null,因此你必须提供表名.
使用Fluent API配置表和构架名
Fluent API有一个ToTable方法用于指定表名称构架名.需要两个参数,第一个是表名,第二个是构架名.如同Data Annotations 一样,你可以提供表名参数而不提供构架名,但不能在指定构架时不包含表名:
映射属性名到数据库列
不仅可以重新映射表名,也可修改预设的数据库列名。根据默认规则,Code First使用属性名作为映射的列名,但这并不总是符合需要的。例如,在原有的BreakAway数据库中,不仅包含有目的地信息的表名区别于默认设置,而且主键字段也被称为LocationID,而不是DestinationId.并且字段中还包含了一个称为LocationName的字段。
使用Data Annotations 修改默认的列名
你可能会回想起使用Column标记去修改列的数据类型。同样的标记也可以用来对列名进行修改,如代码5-1
Example 5-1. Specifying column names for properties using Data Annotations
[Column("LocationID")] public int DestinationId { get; set; } [Required, Column("LocationName")] public string Name { get; set; }
使用Fluent API修改默认列名
HasColumnName是一个用于指定属性在数据库中列名的Fluent方法,如代码5-2所示,效果与图5-1所示的相同。bgga.Locations表配置结果合适地映射了Destination类。注意HasColumnName可以附加到现有的属性配置上。
Example 5-2. Specifying column names for properties using the Fluent API
public class DestinationConfiguration : EntityTypeConfiguration<Destination> { public DestinationConfiguration() { Property(d => d.Nam .IsRequired().HasColumnName("LocationName"); Property(d => d.DestinationId).HasColumnName("LocationID");
对复杂类型的列名施加影响
第3章,我们已经创建了一个来自于Address类的复杂类型,将在Person类中添加了一个Address属性。你可能会想起Code First对Person.Address映射列的命名方式为Address_StreetAddress 或 Address_State等。
你可以使用前面对Destination类类似的配置方法来调整Adderss复杂类型在数据库中的列名。
Example 5-3. Configuring column names to be used in the table of any class that hosts the complex type
[ComplexType] public class Address { public int AddressId { get; set; } [MaxLength(150)] [Column("StreetAddress")] public string StreetAddress { get; set; } [Column("City")] public string City { get; set; } [Column("State")] public string State { get; set; } [Column("ZipCode")] public string ZipCode { get; set; } }
现在任何在Address复杂类型中的属性都可以使用这些Data Annotations 标记来指定列名了。
如果你需要流畅地配置列名,你可以在两种方式内选择:1)任何以Address作为属性的类中统一进行命名;2)显示地在每个“宿主”类中进行单独配置;
代码5-4和5-5显示Fluent API配置应用于Address复杂类型上的情况。这与使用Data Annotations 进行配置时,效果相同,所有使用Address的类都会使用这些列名。
Example 5-4. Configuring a complex type column name from the modelBuilder
modelBuilder.ComplexType<Address>() .Property(p => p.StreetAddress).HasColumnName("StreetAddress");
Example 5-5. Configuring the complex type column name from a configuration class
public class AddressConfiguration : ComplexTypeConfiguration<Address> { public AddressConfiguration() { Property(a => a.StreetAddress).HasColumnName("StreetAddress"); } }
这种配置的效果使得Person表中的Address_StreetAddress简化为SteetAddress.
代码5-6显示了一个配置Person实体的导航属性来控制StreetAddress列名的例子。首先,配置是分别在OnModelCreating方法和configuration类中进行暴露。在Person类中的配置将会只影响People表的列名。如果在另一个类中也有Address属性,它的表不会使用这种列名。
Example 5-6. Configuring the StreetAddress column name to be used when Address is a property of Person
modelBuilder.Entity<Person>() .Property(p => p.Address.StreetAddress) .HasColumnName("StreetAddress"); public class PersonConfiguration : EntityTypeConfiguration<Person> { public PersonConfiguration() { Property(p => p.Address.StreetAddress) .HasColumnName("StreetAddress"); } }
让多个实体映射到同一个表:aka表切分
通常一个数据库表中虽然有很多列,但在很多场景只需要使用其中的一部分,其他的只是一些附加的数据。当我们映射一个实体到这样的表以后,你会发现要浪费资源来处理一些无用的数据。表切分技术可以解决这个问题,这种技术允许在一个单独表中访问多个实体。下面就介绍如何使用Code First来配置表切分功能。
在映射到一个现存的数据库时你可能更想使用表切分技术,因为这种情况下你会发现场景与上面描述的很相似,在这种情况下你甚至都想让Code First来会创建一个新的数据库。
我们更想person的photo和caption直接储存在People表中而不是存在单独的PersonPhotos表中。由于我们访问person的姓名与个人信息频率要远高于photo,让photo放在一个单独的类中会工作得很好。代码5-7回顾了PersonPhoto的类,也回顾了Person类中的Photo属性。我们来配置Photo属性为一个image列,正如我们在第2章对Destination.Photo属性所做的一样。
Example 5-7. The PersonPhoto class with Data Annotations
[Table("PersonPhotos")] 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; } }
使用默认规则,Code First会将PersonPhoto映射到它自已的表上,表名依照我们的配置命名为PersonPhotos.
为了将多个实体映射到一个通用的表中,实体必须遵循如下规则:
- 实体必须是一对一关系
- 实体必须共享一个通用键
Person和PersonPhoto类符合这些条件;(原文中PersonTable应有误,在此更改为PersonPhoto,译者注)
使用Data Annotations 映射到通用表
设置这种映射需要使用Data Annotations 的Table标记。因为我们知道Person实体映射到People表,你可以配置PersonPhoto类也映射到这个表中。但是你需要对所有相关类指定表名,否则,EF框架就会使用另一个默认规则来避免表名重复。你可以在图5-2看到这种错误的结果。因为Table标记的配置,PersonPhoto表被命名为People,因此当Code First尝试自动为Person类命名表名时,发现People已经使用过了,只好命名为People1.
因此我们必须对两个类作同一命名:
[Table("People")] public class Person [Table("People")] public class PersonPhoto
随着这种对模型的调整,Code First会再次重建数据库。People表现在有了Photo和Caption字段(见图5-3),不再有PersonPhotos表。
如果你回到InsertPerson和UpdatePerson方法,你会发现不需要任何修改方法仍然能够工作。类仍然是单独的。EF框架可以识别表的映射,对数据库执行正确的命令。
更有意思的是当你查询一个实体时不用再浪费资源从其他实体中抽取数据了。
再次执行对Person类的查询,如context.People.ToList(),EF框架项目只将这些列映射到Person类而没有映射到PersonPhoto的任何字段。(代码5-8)。
Example 5-8. SQL Query retrieving subset of table columns
SELECT [Extent1].[PersonId] AS [PersonId], [Extent1].[SocialSecurityNumber] AS [SocialSecurityNumber], [Extent1].[FirstName] AS [FirstName], [Extent1].[LastName] AS [LastName], [Extent1].[Info_Weight_Reading] AS [Info_Weight_Reading], [Extent1].[Info_Weight_Units] AS [Info_Weight_Units], [Extent1].[Info_Height_Reading] AS [Info_Height_Reading], [Extent1].[Info_Height_Units] AS [Info_Height_Units], [Extent1].[Info_DietryRestrictions] AS [Info_DietryRestrictions], [Extent1].[StreetAddress] AS [StreetAddress], [Extent1].[City] AS [City], [Extent1].[State] AS [State], [Extent1].[ZipCode] AS [ZipCode] FROM [dbo].[People] AS [Extent1]
感谢在Person和PersonPhoto类之间建立的关系,你可以轻松地加载photo数据,例如使用context.People.Include(“Photo”)或者显示地在需要时加载,或者使用延迟加载。这些都是不能使用标量属性来实现的功能。
延迟加载分割表数据
虽然DbContext默认情况下启用 延迟加载,但我们目前尚未讨论如何使你的代码类利用延迟加载。事实上,由于在EF4引入了POCO的支持,任何EF框架中的简单类都可具有这项功能。任何使用virturl关键字(在Visual Basic为Overridable)的导航属性都会在第一次数据库检索时自动应用延迟加载。
例如,你可以改变Photo的属性,以便它可以延迟加载:
[Required] public virtual PersonPhoto Photo { get; set; }
如下的代码示例显示了Photo的延迟加载。一个查询返回了数据库中所有的Person数据,但相关的Photo数据没有在查询中使用。然后代码会对某个Person执行一些任务。在最后一行,代码显示Person的图片Caption被导航到了Person.Photo.Caption.由于此时Photo在内存中还没有加载,此调用将触发实体框架运行后台的查询,并从数据库中检索数据。就代码而言,Photo就在那里。如果Photo属性不是虚拟的,或延迟加载被上下文显示地禁用,最后一行代码将抛出一个异常,因为照片的属性将会为null:
var people = context.People.ToList(); var firstPerson = people[0]; SomeCustomMethodToDisplay(firstPerson.Photo.Caption);
使用Fluent API切分表
正如Data Annotations 一样,你只需要为类指定表名即可,但是必须要为所有包含的类指定同样的表名。如下代码显示了使用Fluent API配置切分表的方法,直接使用了OnModelCreating方法中的modelBuilder实例,你也可以使用相应的EntityTypeConfiguration类来配置:
modelBuilder.Entity<Person>().ToTable("People"); modelBuilder.Entity<PersonPhoto>().ToTable("People");
映射一个单独的实体到多个表
现在,我们将完全翻转最后一个场景。我们不需要经常看到照片,因为我们只需要看名字就可以。另一个比较常见的情况是当我们检索目的地时希望能够随时看到目的地的照片。如果你映射到一个现有的数据库,有可能照片已经被存储在一个数据库单独的表中,原因可能是为了规范化,性能,或其他什么原因。但您的域模型表示所有的数据都是在一个单独的类中。这就需要将单个的Destination类分配到两个数据库的表中,以获取所有的详细信息。这种映射称为实体分割。
这个映射的关键是Code First配置一系列属性映射到一个特定的表的能力。实现这个功能不能使用Data Annotations ,因为Data Annotations 没有子属性的概念。
Fluent API有一个Map方法可以使用你分配一个属性的列表到一个表名。我们将使用这个方法映射部分Destination的属性到Locations表和其他一些属性映射到名为LocationPhotos的表。务必不要跳过任何属性!
关于在Destinaion类中进行了一系列的配置,代码5-9展示了在DestinationConfiguration类中的配置方法:
Example5-9. DestinationConfiguration with Entity Splitting mapping at the end
public class DestinationConfiguration : EntityTypeConfiguration<Destination> { public DestinationConfiguration() { Property(d => d.Name) .IsRequired().HasColumnName("LocationName"); Property(d => d.DestinationId).HasColumnName("LocationID"); Property(d => d.Description).HasMaxLength(500); Property(d => d.Photo).HasColumnType("image"); // ToTable("Locations", "baga"); Map(m => { m.Properties(d => new {d.Name, d.Country, d.Description }); m.ToTable("Locations"); }); Map(m => { m.Properties(d => new { d.Photo }); m.ToTable("LocationPhotos"); }); } }
Map配置中的Lambda表达式是多行表达式语句,你看到里面的Lambda表达式有分号。多行的Lambda语句VB也是支持的,相同的配置类在VB中如下所示:
Example 5-10. DestinationConfiguration using Visual Basic syntax
Public Class DestinationConfiguration Inherits EntityTypeConfiguration(Of Destination) Public Sub New() Me.Property(Function(d) d.Name) .IsRequired().HasColumnName("LocationName") Me.Property(Function(d) d.DestinationId) .HasColumnName("LocationID") Me.Property(Function(d) d.Description).HasMaxLength(500) Me.Property(Function(d) d.Photo).HasColumnType("image") Me.Ignore(Function(d) d.TodayForecast) ' Me.ToTable("Locations") REM replaced by table mapping below Me.Map(Sub(m) m.Properties(Function(d) New With {Key d.Name, Key d.Country, Key d.Description}) m.ToTable("Locations") End Sub) Map(Sub(m) m.Properties(Function(d) New With {Key d.Photo}) m.ToTable("LocationPhotos") End Sub) End Sub End Class
最后,我们可以看到在图5-4数据库的效果。
请注意即使我们只有Photo属性映射到LocationPhotos表,Code First也会共享该表的主键和外键。这也创建了Location和LocationPhotos之间的PK / FK约束。有趣的是,没有为LocationPhotos表中定义级联删除。但是,EF框架知道,如果你删除一个目的地,它必须建立一个跨越两个表的删除命令。让我们来看看EF框架在对Destination对象的各种CRUD操作生成的SQL。
代码5-12显示了我们在第2章中插入一个Destination对象方法,然后调用了SaveChanges方法保存数据到数据库。
Example 5-12. Insert a single object that maps to two database tables
private static void InsertDestination() { var destination = new Destination { Country = "Indonesia", Description = "EcoTourism at its best in exquisite Bali", Name = "Bali" }; using (var context = new BreakAwayContext()) { context.Destinations.Add(destination); context.SaveChanges(); } }
exec sp_executesql N'insert [dbo].[Locations]([LocationName], [Country], [Description]) values (@0, @1, @2) select [LocationID] from [dbo].[Locations] where @@ROWCOUNT > 0 and [LocationID] = scope_identity()', N'@0 nvarchar(max) ,@1 nvarchar(max) ,@2 nvarchar(500)', @0=N'Bali',@1=N'Indonesia',@2=N'EcoTourism at its best in exquisite Bali' exec sp_executesql N'insert [dbo].[LocationPhotos]([LocationID], [Photo]) values (@0, null)', N'@0 int',@0=1
第一个SQL语句向Locations表中插入数据,此语句中的代码返回了新生成的LocationID的值。第二个SQL语句,插入一个新行到LocationPhotos,包含了从第一个命令返回LocationID。由于该方法没有提供任何照片信息,照片字段的值插入到该表是空的。
例5-13显示了代码来查询,更新和删除Destination数据。你可以看到通过SQL,EF框架如何在响应SaveChanges方法时处理多个表的关系的。
Example 5-13. Query, update and delete a Destination
using (var context = new BreakAwayContext()) { var destinations = context.Destinations.ToList(); var destination = destinations[0]; destination.Description += "Trust us, you'll love it!"; context.SaveChanges(); context.Destinations.Remove(destination); context.SaveChanges(); }
—-RESPONSE TO QUERY SELECT [Extent1].[LocationID] AS [LocationID], [Extent2].[LocationName] AS [LocationName], [Extent2].[Country] AS [Country], [Extent2].[Description] AS [Description], [Extent1].[Photo] AS [Photo] FROM [dbo].[LocationPhotos] AS [Extent1] INNER JOIN [dbo].[Locations] AS [Extent2] ON [Extent1].[LocationID] = [Extent2]. [LocationID] —RESPONSE TO UPDATE exec sp_executesql N'update [dbo].[Locations] set [Description] = @0 where ([LocationID] = @1)', N'@0 nvarchar(500),@1 int', @0='Trust us, you''ll love it!',@1=1 --RESPONSE TO DELETE exec sp_executesql N'delete [dbo].[LocationPhotos] where ([LocationID] = @0)',N'@0 int',@0=1 exec sp_executesql N'delete [dbo].[Locations] where ([LocationID] = @0)',N'@0 int',@0=1
现在,我们来看看实体分割的结果,把所有的Destination数据再存到一个表中。如果您已经在Visual Studio中,删除了我们刚才添加的实体分割配置,恢复单个ToTable调用映射,从Destination映射到Locations表:
ToTable("Locations", "baga");
控制到映射到数据库中数据类型
每次你添加一个类模型,你也得添加到一个DbSet到BreakAwayContext 。 DbSet提供两个功能。首先,它返回一个特定类型的可查询数据集。其次可以让DbModelBuilder知道,每个数据集中引用的类型应包括在模型中。
但是确保一个类型成为你的模型的一部分,这不是唯一的途径。包括一个类型到模型中有三种方式:
1。在上下文暴露类型的DbSet。
2。另一个已经映射到类型中被引用(即,该类型可以通过另一种类型与模型接触)。
3。从任何Fluent API调用的DbModelBuilder引用一个类型。
你已经见过第一种方式。下面介绍其他两种方式。
我们将添加一个新的类到模型中:Reservation。
Example 5-14. The new Reservation class
namespace Model { public class Reservation { public int ReservationId { get; set; } public DateTime DateTimeMade { get; set; } public Person Traveler { get; set; } public Trip Trip { get; set; } public DateTime PaidInFull { get; set; } } }
如果你运行程序,DbModelBuilder并不知道这个类的存在。因为即没有DbSet<Reservation>也没有其他两种形式的配置。因此,模型必须进行更改,数据库才会重新创建。如果你现在看数据库,里面不会有Reservation表。现在我们进入Person类,添加一个属性以便我们可以看到所有由Person设置的Resvervations。
public List<Reservation> Reservations { get; set; }
再次运行应用程序,现在你会看到数据库中有了一个Reservations表,如图5-5所示:
根据默认规则,由于Person是模型中的类,而Person找到了Reservationod ,Code First就会将Reservation类放进模型中。
现在来看看第三种默认规则:通过提供一个配置将类包括在模型中。
首先我们将Person类中的Reservation属性注释掉:
// public List<Reservation> Reservations { get; set; }
为reservation添加Fluent API配置。将配置封装到一个EntityTypeConfiguration类中和直接调用modelBuilder.Configurations的OnModelCreating方法效果是一样。只是为了让事情井井有条,我们将创建一个单独的配置类,示例5-15中所示。
Example 5-15. An empty configuration class for Reservation
public class ReservationConfiguration : EntityTypeConfiguration<Reservation> { }
注意我们除了声明这个类以外什么都没做。里面还没有代码。这足以让你向DbModelBuilder配置中添加类,确保Reservation包含进模型中将映射到数据库Reservations表。
modelBuilder.Configurations.Add(new ReservationConfiguration());
现在你已经看到Code First包含一个类到模型的默认方式,现在我们来学习如何配置模型来排降一个类。
避免类型包含进模型中
您可能在应用程序中为了某些目的定义了一些类,但并不要求它些在数据库被持久化。即使你不为它们定义一个DbSet或任何配置,它们也可能的被另一种已经映射的类型所访问而被拉入模型,在查询或更新数据库时被非预期创建在数据库中。
但是,您可以明确地告诉Code First忽略一个类,不要使其成为模型的一部分。
使用Data Annotations 来忽略类型
NotMapped特性标记可以用来指导Code First从模型中排队类型:
[NotMapped] public class MyInMemoryOnlyClass
使用Fluent API配置来忽略类型
使用Fluent API,你需要使用Ignore方法来避免类型被扯入模型中。如果你想忽略一个类,你需要直接从DbModelBuilder来实现,不能在EntityTypeConfigraiton内进行:
modelBuilder.Ignore<MyInMemoryOnlyClass>();
理解属性映射和可访问性
有各种因素可以影响你的类是否能被Code First所识别和映射。下面是一些在定义类时需要注意的规则,可以来了解默认规则需要什么,如何通过配置来改变默认映射。
标量属性映射
如果属性性可以转化为EDM支持的类型称为标量属性,映射是唯一的。
合法的EDM类型有:Binary, Boolean, Byte, DateTime, DateTimeOffset, Decimal, Double, Guid, Int16, Int32, Int64, SByte, Single, String, Time.
标量属性不能映射到一个被忽略的EDMonton类型(如枚举类型或无符号整型数);
属性的可访问性,Getters和Setters
1.public属性将会被Code First自动映射。
2.Set访问器可以用更严格的访问规则界定,但get访问器必须保持public才能被自动映射;
3.非公开的属性必须使用Fluent API配置才能被Code First所映射;
对于非公开属性,这意味只有执行配置的位置才能访问该属性。
例如,如果Person类有一个internal Name的属性,使用与PersonContext类相同的程序集,你可以在Person context的OnModelCreating方法中调用modelBuilder.Entity<Person>().Property(p => p.Name)。这就会使用该属性包含进你的方法。但是如果Person和PersonContext定义在单独的程序集中,你就需要添加一个PersonConfiguration类(EntityConfiguration<Person>)到Person的同一程序集中,以执行配置类内的配置。这要求包含有域类的程序集必须添加对EntityFramework.dll的引用。PersonCOnfig配置类可以在PersonContext的OnModelCreating方法中被注册。
类似的方法可以用于受保护的和私有的属性。但是,配置类必须嵌套在类内部,成为模型的一部分,这样才能访问私有或受保护的属性。下面是一个这样的例子,使用private隐藏了Name属性,但是允许外部代码使用CreatePerson方法对Name属性进行设置。嵌套的PersonConfig类可以访问本地复制的Name属性。
public class Person { public int PersonId { get; set; } private string Name { get; set; } public class PersonConfig : EntityTypeConfiguration<Person> { public PersonConfig() { Property(b => b.Name); } } public string GetName() { return this.Name; } public static Person CreatePerson(string name) { return new Person { Name = name }; } }
当我们配置类为为嵌套类时,可以使用下列代码:
modelBuilder.Configurations.Add(new Person.PersonConfig());
一个常见的场景是为了避免开发者在代码中修改某个特定的属性(如PersonId),使用set访问器来将属性设置为private或internal.这种场景的实现归功于上述所列的第二个规则:Set访问器可以用更严格的访问规则界定,但get访问器必须保持public才能被自动映射;EF框架必须使用反射才能访问非公开的set访问器,但当运行于中等信任的模式时这并不提供支持。除了中等信任的情况以外,这意味着当真实对象作为查询或插入的结果时,上下文将能够填充受限的属性。上下文也能够以查询或插入的数据为属性设置值--即使上下文和域类处于单独的程序集或名称空间里。这即可以工作在有键值的情况,也可以工作在没有键值的情况下。
避免属性被包含在模型中
默认规则里,所有同时拥有get和set访问器的公开属性都会包含进模型中。
Code First使用使用相同的配置方法—Data Annotations 的NotMapped标记或Fluent API的Ignore配置方法---来排除类中的属性。
一个典型的不想储存在数据库中的属性例子是在类中使用其他属性计算出来的属性。例如,你可能想很容易地访问一个人的full name,这是根据First Name和Last Name合并计算得到的。类可以计算它而没有必要将其存进数据库。
如果一个属性吸有get或set访问器,也不会被包含进模型中。
如果你在Person类中有FullName属性,可以只设置get访问器而不设置set访问器来实现排除在映射之外。
public string FullName { get { return String.Format("{0} {1}", FirstName.Trim(), LastName); } }
但是,你可以还会有一些同时拥有get和set访问器的属性不想持久化到数据库。例如,Destination类可能会有一个字符串包含有当前的天气预报。但你可能想在程序的什么地方弹出预报内容,而不想对预报本身作出什么探究。
private string _todayForecast; public string TodayForecast { get { return _todayForecast; } set { _todayForecast = value; } }
这种情况下你不想持久化天气预报信息到数据库。EF框架在执行映射到Destination的查询或修改数据库表的时候不能包含这个属性。
使用Data Annotations 忽略属性
使用Data Annotations ,必须应用NotMapped特性标记:
[NotMapped] public string TodayForecast
使用Fluent API忽略属性
在Fluent API,你需要配置实体忽略一个属性。下列是一个在DestinationConfiguration类中使用Ignore方法的例子:
Ignore(d => d.TodayForecast);
注意:在使用NotMapped或Ignore对private属性进行设置时有一个已知的bug。你可以MSDN找到关于此bug的描述。2011-8-18,微软评论声称:此bug已修复,并且会在下一个主要版本的Code First中发布。
映射到继承层次结构
EF框架支持各种模型中的继承层次结构。无论你使用Code First,Model First还是Database First来定义模型都不用担心继承的类型问题,也不用考虑EF框架如何使用这些类型进行查询,跟踪变更和更新数据。
使用Code First的默认继承:每层次结构映射表(TPH)
TPH描述了映射继承类关系到独立数据库表的方法,这种方法使用鉴别列来识别是否为子类型。这是Code First默认规则使用的表映射方法。为了观察这种行为,我们对模型做两处修改。首先我们从Lodging类里移除IsResort属性,然后创建一个单独的Resort类继承自Lodging类。代码5-16显示了这些类:
Example 5-16. Modified Lodging class and a new Resort class that derives from Lodging
public class Lodging { public int LodgingId { get; set; } [Required] [MaxLength(200)] [MinLength(10)] public string Name { get; set; } public string Owner { get; set; } // public bool IsResort { get; set; } public decimal MilesFromNearestAirport { get; set; } [InverseProperty("PrimaryContactFor")] public Person PrimaryContact { get; set; } [InverseProperty("SecondaryContactFor")] public Person SecondaryContact { get; set; } public int DestinationId { get; set; } public Destination Destination { get; set; } public List<InternetSpecial> InternetSpecials { get; set; } } public class Resort : Lodging { public string Entertainment { get; set; } public string Activities { get; set; } }
图5-6显示了数据库使用默认规则后的变化:
Resort信息储存在Lodgings表中,Code First创建了一个列命名为Didcriminator。注意这是一个非可空列,类型为nvarchar(128)。默认情况下,Code First会使用每个类型在继承层次中的类名作为discrimnator列的存储值。例如,如果你添加和运行InsertLogdging方法(代码5-17),由EF框架生成的INSERT语句会将字符串“Lodging”放进新加入行的Discriminator列中。
Example 5-17. Code to insert a new Lodging type
private static void InsertLodging() { var lodging = new Lodging { Name = "Rainy Day Motel", Destination=new Destination { Name="Seattle, Washington", Country="USA" } }; using (var context = new BreakAwayContext()) { context.Lodgings.Add(lodging); context.SaveChanges(); } }
作为可选方案,代码5-18显示了一个新的Resort类型的实例:
Example 5-18. Code to insert a new Resort type
private static void InsertResort() { var resort = new Resort { Name = "Top Notch Resort and Spa", MilesFromNearestAirport=30, Activities="Spa, Hiking, Skiing, Ballooning", Destination=new Destination{ Name="Stowe, Vermont", Country="USA"} }; using (var context = new BreakAwayContext()) { context.Lodgings.Add(resort); context.SaveChanges(); } }
这一次,EF框架将会在Discriminator列中插入字符串“Resort”.
这种默认规则的行为基于你可能会加入更多的衍生Lodging类型。如果Lodging类只有有Resort和没有这两种情况,此时discrimainator列就可指定为Boolen型,同时也就没有扩展层次的空间了。这种灵活性在默认规则中工作得很好。
使用Fluent API定制TPH区分符字段
你可以通过指定配置来定制discriminator列的类型和命名,方法是使用Fluent API(Data Annotations 没有标记可用于定制TPH)。
使用Map配置方法来实现,前已述及,Map可用于实体分割。这个方法需要包含几个配置:
Example 5-19. Configuring the discriminator column name and possible values
Map(m => { m.ToTable("Lodgings"); m.Requires("LodgingType").HasValue("Standard"); }) .Map<Resort>(m => { m.Requires("LodgingType").HasValue("Resort"); });
注意我们看到了几个新的配置方法-Requires和HasValue.Requires是一个配置,在此用于指定一个discriminator列。HasValue也用于指定配置discriminator.你可以使用HasValue来指定在一个特定类型中使用什么样的值。我们告诉Code First使用LodgingType作为discriminator列的列名而不是使用默认的名称:Discriminator.默认规则规定,Code First使用类名作为Discriminator值(例如:“Lodging”).我们将其调整为“Standard”(Lodging基类适用)和“Resort”(派生类适用)。
你可能知道Loding的唯一派生类就是Resort,因此决定使用Boolean型数据,如用IsResort表示,这也是可以的。在这种情况下,值将是Boolean型。你不需要告知Code First这个事实,只需要在提供期望的值即可,Code First将正确地识别discriminator列的类型。
修改为布尔型的discriminator的代码见代码5-20.
Example 5-20. Configuring a discriminator column to be a boolean
Map(m => { m.ToTable("Lodging"); m.Requires("IsResort").HasValue(false); }) .Map<Resort>(m => { m.Requires("IsResort").HasValue(true); });
结果见图5-7:
配置每个类型映射表(TPT)
TPH将所有层次的类都放在了一个表里,而TPT在一个单独的表中储存来自基类的属性。在派生类定义的附加属性储存在另一个表里,并使用外键与主表相连接。如果你的数据库构架使用单独的表表达层次关系,你需要对派生类进行显示的配置。下面是一个简单配置的例子,你需要指定派生类的表名,可以使用Data Annotations 也可以使用Fluent API来完成这项工作。
下面是对Resort类型的配置:
[Table("Resorts")] public class Resort : Lodging { public string Entertainment { get; set; } public string Activities { get; set; } }
联合使用继承和Data Annotation 的Table特性标记将告知Code First要为Resort类型创建一个新表,由于该表继承自Lodging,它将继承Lodging的键属性。
图5-8显示了Lodgings和新建的Resorts表。注意:Lodgings不再包含discriminator也没有Resort的字段(Entertainment and Activities).
新增的Resorts表中有一个LodgingId类,这是一个主键也是一个外键,后者的名字被区分为:Resort_TypeConstraint_From_Lodging_To_Resorts.
值得好奇的是,在Resort_Type Constraint_From_Lodging_To_Resorts 键上没有定义级联删除。EF框架将在必要的时候来维护删除的数据。
当你添加一个新的Resort并调用 SaveChanges方法时,就触发EF框架首先在Lodging表添加以合适的值,然后返回一个新LodgingId值,然后使用这个LodgingId值在Resorts表中插入一个新行。
使用Fluent API你可以使用ToTable方法来获得TPT映射。你只需要指定派生实体的表名,Resort,这样Code First就会创建附加表和约束,如您在较长5-8所见。代码5-21显示了只接通过modelBuilder实例调用的例子。
Example 5-21. In-line ToTable mapping used for TPT inheritance
modelBuilder.Entity<Resort>().ToTable("Resorts");
你也可以从基类开始配置,使用Map方法得到Resort类型:
Example 5-22. Mapping ToTable within the Map method
modelBuilder.Entity<Lodging>() .Map<Resort>(m => { m.ToTable("Resorts"); } );
如果你想显示操作,你可以指定每个层级的表名。在这种情况下,Lodgings表已经被默认规则所指定,但是为了更明确的配置,我们使用了更为清晰的代码,如代码5-23:
Example 5-23. Mapping ToTable for a TPT inheritance from base entity
modelBuilder.Entity<Lodging>().Map(m => { m.ToTable("Lodgings"); }).Map<Resort>(m => { m.ToTable("Resorts"); });
这最后的变化很有趣,你可以从它的基类配置映射的派生类。但无法从实体<Resort>开始,然后添加<Lodging>的映射。
创建这个映射的所有三个变化达到同样的目的。如果需要,你可能想作出选择以更好地适应您的编码风格。
Configuring for Table Per Concrete Type (TPC) Inheritance
配置每概念层级类型一个表(TPC)
TPC类似于TPT,除了每个类型的所有属性都储存在单独表里以外。在层级中没有一个核心表包含通用于所有类型的数据。这就使用需要映射一个继承结构层次到层叠(通用)的字段上。这对你将历史数据存存储在备用表时特别有用。可我们会将Lodgings和Resorts映射到一个resort表里,这个表里也包含Name,Ownert MilesFromNearestAirport字段。你可以使用Fluent API来配置你的结构层次映射到这样的表中。(Data Annotations 不支持TPC)。
下面对Lodging/Resort结构层次再做些改变。
TPC使用MapInheritedProperties方法来进行配置,只能在Map方法里才能访问。既然我们需要为派生类提供单独的表(这会复制继承的属性),我们需要联合设置Table和MapInheritedProperties方法来进行配置。
注意你必须使用ToTable方法包含到基类实体的映射。虽然这在TPT不是必须的,但TPC是:
modelBuilder.Entity<Lodging>() .Map(m => { m.ToTable("Lodgings"); }) .Map<Resort>(m => { m.ToTable("Resorts"); m.MapInheritedProperties(); });
MapInheritedProperties方法告知Code First它想要重映射所有继承自基类的属性到派生类表的新列里。
避免TPC里的映射异常
as a reminder in Example 5-24.
如果你尝试运行当前程序来检查配置,你会得到一个异常,描述存在映射冲突,因为DbModelBuilder 尝试创建新模型。这个冲突来自于Lodging类。
TPC要求任何TPC层级内的类通过一个显示定义的外键属性来建立关系。观察Lodging类,如代码5-24所示:
Example 5-24. A reminder of the Lodging class
public class Lodging { public int LodgingId { get; set; } public string Name { get; set; } public string Owner { get; set; } public decimal MilesFromNearestAirport { get; set; } public List<InternetSpecial> InternetSpecials { get; set; } public Person PrimaryContact { get; set; } public Person SecondaryContact { get; set; } public int DestinationId { get; set; } public Destination Destination { get; set; } }
While the navigation to Destination is complemented by the DestinationId property, there are two navigation reference properties that do not have a foreign key property: PrimaryContact and SecondaryContact. Code First leverages the database foreign key fields to take care of persisting the relationship. If you’ve been using Entity Framework since the first version, you may recognize this as independent associations, which were the only option for building relationships in Visual Studio 2008. Foreign Key associations, where we can have a foreign key property such as DestinationId in the class, were introduced to Entity Framework in Visual Studio 2010 and .NET 4. TPC can’t work with classes that have independent associations in them.
虽然到Destinaition的导航,是由DestinationId属性补充的,虽然有两个导航引用属性,但是没有外键属性:PrimaryContact和SecondaryContact。Code First利用数据库外键字段来维护关系的持久化。如果您使用过EF框架的第一个版本,您可以将此视为独立的关系,是Visual Studio 2008中建立关系的唯一选项。而外键关系,在这里我们可以使用外键属性如类中的DestinationId属性,被引入到Visual Studio 2010和.NET4下的EF框架中。 TPC不能与带有独立关联的类一同工作。(译者注:此处不知所云,请高手赐教!)
为了解决这个问题,你必须给Lodging添加外键属性。对某些开发人员而言,这是一个被迫吞下的痛苦的药丸---为了让类加入而被迫遵守EF框架的约定。不幸的是,正如您可能已经关注到的,实现Code First的很多映射就需要有外键可用。
请记住,在我们的域中,可能是一个Lodging既没有PrimaryContact,也没有SecondaryContact。当我们在第4章增加PrimaryContact和SecondaryContact导航属性时,Code First通过约定推断它们是可空类型(Optional)。新的外键的属性是整数,默认情况下,非可空。这样冲突就出现了,因为如果在外键中有一个值,Contact就不是可选的。因此,我们将创建新的具有可空性的外键属性。注意使用Nullable<T>泛型来创建PrimaryContactId和SecondaryContactId两个新属性,如代码5-25所示。
Example 5-25. Lodging class with nullable foreign keys
abstract public class Lodging { public int LodgingId { get; set; } public string Name { get; set; } public string Owner { get; set; } public decimal MilesFromNearestAirport { get; set; } public List<InternetSpecial> InternetSpecials { get; set; } public Nullable<int> PrimaryContactId { get; set; } public Person PrimaryContact { get; set; } public Nullable<int> SecondaryContactId { get; set; } public Person SecondaryContact { get; set; } public int DestinationId { get; set; } public Destination Destination { get; set; } }
我们还没有完成。记住Code First无法在没有协助的情况下识别出非常规的外键属性。这些新的属性不匹配任何三种Code First检测外键的可能模式(例如,PersonId)。所以你需要使用HasForeignKey来进行映射,如同例4-3。
代码5-26显示了对这些属性在LodgingConfiguration类的两个现有配置。我们通过加入HasForeignKey映射对其进行了修改。
Example 5-26. Fixing up the model for unconventional foreign key properties
HasOptional(l => l.PrimaryContact) .WithMany(p => p.PrimaryContactFor) .HasForeignKey(p=>p.PrimaryContactId); HasOptional(l => l.SecondaryContact) .WithMany(p => p.SecondaryContactFor) .HasForeignKey(p => p.SecondaryContactId);
最后所有的片断都安置在TPC所需的正确位置。模型将会对其进行验证,如图5-9所示。事实上,所有自Lodging类的继承字段现在都在Resorts表中。由我们协助Code First识别外键,这些外键也在Resorts表中进行了合适的配置。
使用抽象基类
所有我们前面使用的继承都是基类为实体类的情况,我们也可以使用抽象类来进行工作。在使用Code First来构建抽象基类的模型之前我们先作一个快速的了解。
我们将Lodging类修改成为抽象基类。这意味着我们不再能直接使用Lodging类。它不能被实例化。我们只能使用派生于它的类。见例5-27,我们添加了第二个派生类:Hostel.
代码5-27列出了所有的三个类。
Example 5-27. The abstract base class, Lodging, with its derived classes, Resort and Hostel
abstract public class Lodging { public int LodgingId { get; set; } public string Name { get; set; } public string Owner { get; set; } public decimal MilesFromNearestAirport { get; set; } public List<InternetSpecial> InternetSpecials { get; set; } public Nullable<int> PrimaryContactId { get; set; } public Person PrimaryContact { get; set; } public Nullable<int> SecondaryContactId { get; set; } public Person SecondaryContact { get; set; } public int DestinationId { get; set; } publicDestination Destination { get; set; }
} public class Resort : Lodging { public string Entertainment { get; set; } public string Activities { get; set; } } public class Hostel: Lodging { public int MaxPersonsPerRoom { get; set; } public bool PrivateRoomsAvailable { get; set; } }
当你将Lodging类变成抽象类,这意味着不能在程序中实例化Lodging类。如果控制台程序中有这样的代码将会导致编译错误。因此你应该将有关代码注释掉。一个好的方法是将方法使用#if/#endif包括起来,例如:
#if false private static void InsertLodging() { var lodging = new Lodging { Name = "Rainy Day Motel", Destination = new Destination { Name = "Seattle, Washington", Country = "USA" } }; using (var context = new BreakAwayContext()) { context.Lodgings.Add(lodging); context.SaveChanges(); } } #endif
为了重新使用代码,可以将其改为#if true
我们已经移除了TPC的配置使模型和数据库表完全基于Code First的默认配置,这表示继承结构重新成为TPH。所有派生类的字段都会包含在Lodgings表中。在图5-10中可以看到尽管Lodging类是一个抽象类,对数据库的构建与其不是抽象类没有什么影响。但是,既然我们有了另一个派生类,还是有一些新的属性包含了进去,这些属性是Hostel类型提供的。
代码5-28显示了一系列插入新的Resort和新的Hostel的方法,然后从数据库中查出所有Lodgings来观察。
Example 5-28. Code to insert a Resort, then insert a Hostel, and finally to query Lodgings
private static void InsertResort() { var resort = new Resort { Name = "Top Notch Resort and Spa", MilesFromNearestAirport = 30, Activities = "Spa, Hiking, Skiing, Ballooning", Destination = new Destination { Name = "Stowe, Vermont", Country = "USA" } }; using (var context = new BreakAwayContext()) { context.Lodgings.Add(resort); context.SaveChanges(); } } private static void InsertHostel() { var hostel = new Hostel { Name = "AAA Budget Youth Hostel", MilesFromNearestAirport = 25, PrivateRoomsAvailable=false, Destination = new Destination { Name = "Hanksville, Vermont", Country = "USA" } }; using (var context = new BreakAwayContext()) { context.Lodgings.Add(hostel); context.SaveChanges(); } } private static void GetAllLodgings() { var context = new BreakAwayContext(); var lodgings = context.Lodgings.ToList(); foreach (var lodging in lodgings) { Console.WriteLine("Name: {0} Type: {1}", lodging.Name, lodging.GetType().ToString()); } Console.ReadKey(); }
当EF框架发送INSERT命令到数据库,它使用“Resort”和“Hostel”分别填充了Discriminator列的数据。当调出所Lodgings的数据时,它过滤了Resort和Hostel识别标记,如代码5-29所示的SQL语句:
Example 5-29. SQL to retrieve all of the known types that derive from Lodging
SELECT [Extent1].[Discriminator] AS [Discriminator], [Extent1].[LodgingId] AS [LodgingId], [Extent1].[Name] AS [Name], [Extent1].[Owner] AS [Owner], [Extent1].[MilesFromNearestAirport] AS [MilesFromNearestAirport], [Extent1].[PrimaryContactId] AS [PrimaryContactId], [Extent1].[SecondaryContactId] AS [SecondaryContactId], [Extent1].[DestinationId] AS [DestinationId], [Extent1].[Entertainment] AS [Entertainment], [Extent1].[Activities] AS [Activities], [Extent1].[MaxPersonsPerRoom] AS [MaxPersonsPerRoom], [Extent1].[PrivateRoomsAvailable] AS [PrivateRoomsAvailable] FROM [dbo].[Lodgings] AS [Extent1] WHERE [Extent1].[Discriminator] IN ('Resort','Hostel')
为什么要使用discriminators而不是简单地返回所有的Lodging数据?这覆盖了其他类型在数据库中不是模型的一部分的场景。
图5-11显示了前两个方法插入数据后调用GetAllLodgings方法的控制台输出结果。
可以修改映射变成TPT或TPC。例如,如果你同时给Resort和Hostel类指定表明而Lodging类是抽象的,你就会得到三个数据库表,Resorts,Hosetls和Lodgings。代码5-28在没有任何修改的情况下可以正常工作。SQL命令将在必要时组合内联有关表,正如Lodging不是抽象的一样。所有模型中围绕着抽象基类的行为都简单遵从了EF框架第一版以来的行为。唯一的区别就是以新的方式定义了模型。
现在我们已经探索了抽象基类,现在将将abstract关键字从Lodging类移走,以便我们可以再次创建Lodging类的实例。也可将前面已经注释掉的任何方法重新启用:
public class Lodging
映射关系
到目前为止我们已经掌握了如何控制类或其属性的映射方法;最后我们来看看关系如何来映射.这包括控制外键列的命名以及多对对关系内联表的命名.第4章对关系的默认规则和配置进行了全面介绍.你应该对映射已经很熟悉了,这一部分将提供一些方式去控制映射的一些细节部分.
控制包含在类中的外键
你已经看到两个类的关系通过添加导航属性得到了创建.可以选择性地在一个单独类中设置外键.默认情况,Code First将会使用属性名作为列名.在第4章我们了解到,当添加DestinationId到Lodging类,Code First将添加了一个DestinationId列到数据库里并将其配置为一个外键 .
将外键属性的列名进行更改类似于更改其他属性的列名.改变外键属性名不会影响Code First检查是否为外键的能力.外键检测只针对属性名而不是属性映射到数据库的列名.
假如想把列名更为destination_id.你可以使用Column的Data Annotations 标记直接将外键属性进行更改.
[Column("destination_id")] public int DestinationId { get; set; }
当然也可使用Fluent API来配置LodgingConfiguration类达成同样的目的:
Property(l => l.DestinationId).HasColumnName("destination_id");
控制由Code First生产的外键
如果类中没有包含外键属性Code First就会自动创建一下.例Lodging类,一个Destination_DestinationId列就被加到数据库中,就是一个自动添加的外键列.我们从Lodging类中移走DestinationId外键属性,让Code First自动创建一下.(如代码3-10).
Example 5-30. Foreign key property commented out
public class Lodging { public int LodgingId { get; set; } public string Name { get; set; } public string Owner { get; set; } public decimal MilesFromNearestAirport { get; set; } //public int DestinationId { get; set; } public Destination Destination { get; set; } public List<InternetSpecial> InternetSpecials { get; set; } public Nullable<int> PrimaryContactId { get; set; } public Person PrimaryContact { get; set; } public Nullable<int> SecondaryContactId { get; set; } public Person SecondaryContact { get; set; } }
变更外键列名只能通过Fluent API.你可以使用Map方法来控制的映射,也可使用Map方法来控制关系的映射.
代码5-31显示了如何添加Map方法到关系来给外键名进行指定.
Example 5-31. Generated foreign key column configured
HasRequired(l => l.Destination) .WithMany(d => d.Lodgings) .Map(c => c.MapKey("destination_id"));
现在你就已经观察到这些行为.将所有注释掉的代码恢复过来,移除上述Fluent API配置.你也可以取消DeleteDestinationInMemoryAndDbCascade方法的注释:
public int DestinationId { get; set; } public Destination Destination { get; set; }
控制使用实体切分生成的外键
前面已经学习过有关实体拆分的技术。可以让一个类分配其属性到多个数据库表中。在此可以按意图对表中添加的外键列进行控制。
默认情况,外键将添加到你在实体分割配置中指定的第一个表中。你可以通过附加在生成外键的ToTable方法后来变更这种情况。例如,将Lodging实体划分为Lodgings表和LodgingInfo表。如果想要将外键放在LodgingInfo表的相关destination上,你将应按如下方式添加配置:
Example 5-32. Generated foreign key column configured
HasRequired(l => l.Destination) .WithMany(d => d.Lodgings) .Map(c => c.MapKey("destination_id").ToTable("LodgingInfo"));
控制多对多关系中的内联表
前面我们已经在Acitvity和Trip之间引入了多对多关系,最终在数据库中生成ActivityTrips内联表(见图4-10)。
但是,在我们的域中将表名命名为TripActivites可能含义更明了。幸运的是可以使用Map方法配置这个表名。添加的配置设置见代码5-33:
Example 5-33. Many-to-many join table name changed
HasMany(t => t.Activities) .WithMany(a => a.Trips) .Map(c => c.ToTable("TripActivities"));
映射首先使用HasMany和WithMany方法来识别要配置的关系。一旦关系得到识别,就可以使用Map方法来指定映射。在映射关系中,你可使用ToTable方法来指定表名。我们来看看Map方法最终得到的结果。内联表更新后如图5-12所示:
你可以想要外键名叫做TripIdentifier和ActivityId.可以通过Map方法来实现列名的指定。
Example 5-34. Changing the many-to-many column names
HasMany(t => t.Activities) .WithMany(a => a.Trips) .Map(c => { c.ToTable("TripActivities"); c.MapLeftKey("TripIdentifier"); c.MapRightKey("ActivityId"); });
注意MapLeftKey和MapRightKey方法用来指定列名。MapLeftKey影响指向配置的类的外键。你可以添加配置到TripConfiguration类,因此Trips是被配置的实体。这样Trip被视为左实体,Activity视为右实体。图5-13显示了更改列名后的效果:
小结
本章学习了Code First控制类与属性映射到数据库的技术。通过学习,您应掌握控制列,表乃至构架的方法。还学习了如何配置类映射到多个表以及如何将多个类指向单一的表的技术。然后花了很多时间研究了如何配置继承层次关系 ,很多配置只能使用Fluent API 来进行,最后你还学到如何调整到关系的映射。
总之,本章所学已经能够使Code First实现使用EDMX进行设计所实现的几乎全部功能 。通过Code Fi,你有能力将域类插入到EF框架中,而不使用设计器或任何附加模型。