仅限EF6仅向前 - 此页面中讨论的功能,API等在实体框架6中引入。如果您使用的是早期版本,则部分或全部信息不适用。
使用Code First时,您的模型是使用一组约定从您的类计算的。默认的Code First Conventions确定哪些属性成为实体的主键,实体映射到的表的名称,以及默认情况下十进制列具有的精度和比例。
有时,这些默认约定对于您的模型并不理想,您必须通过使用Data Annotations或Fluent API配置许多单个实体来解决这些问题。自定义代码优先约定允许您定义自己的约定,为您的模型提供配置默认值。在本演练中,我们将探讨不同类型的自定义约定以及如何创建它们。
此页面介绍了用于自定义约定的DbModelBuilder API。此API应足以创作大多数自定义约定。但是,还可以创建基于模型的约定 - 一旦创建后操纵最终模型的约定 - 来处理高级场景。有关更多信息,请参阅基于模型的约定(EF6以上版本)。
让我们从定义一个可以与我们的约定一起使用的简单模型开始。将以下类添加到项目中。
1 using System; 2 using System.Collections.Generic; 3 using System.Data.Entity; 4 using System.Linq; 5 6 public class ProductContext : DbContext 7 { 8 static ProductContext() 9 { 10 Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>()); 11 } 12 13 public DbSet<Product> Products { get; set; } 14 } 15 16 public class Product 17 { 18 public int Key { get; set; } 19 public string Name { get; set; } 20 public decimal? Price { get; set; } 21 public DateTime? ReleaseDate { get; set; } 22 public ProductCategory Category { get; set; } 23 } 24 25 public class ProductCategory 26 { 27 public int Key { get; set; } 28 public string Name { get; set; } 29 public List<Product> Products { get; set; } 30 }
让我们编写一个约定,将任何名为Key的属性配置为其实体类型的主键。
在模型构建器上启用约定,可以通过在上下文中重写OnModelCreating来访问这些约定。更新ProductContext类,如下所示:
1 public class ProductContext : DbContext 2 { 3 static ProductContext() 4 { 5 Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>()); 6 } 7 8 public DbSet<Product> Products { get; set; } 9 10 protected override void OnModelCreating(DbModelBuilder modelBuilder) 11 { 12 modelBuilder.Properties() 13 .Where(p => p.Name == "Key") 14 .Configure(p => p.IsKey()); 15 } 16 }
现在,名为Key的模型中的任何属性都将被配置为其所属实体的主键。
我们还可以通过过滤我们要配置的属性类型来使我们的约定更具体:
1 modelBuilder.Properties<int>() 2 .Where(p => p.Name == "Key") 3 .Configure(p => p.IsKey());
这会将名为Key的所有属性配置为其实体的主键,但前提是它们是整数。
IsKey方法的一个有趣特征是它是附加的。这意味着如果您在多个属性上调用IsKey,它们都将成为复合键的一部分。需要注意的是,当您为键指定多个属性时,还必须为这些属性指定顺序。您可以通过调用HasColumnOrder方法执行此操作,如下所示:
1 modelBuilder.Properties<int>() 2 .Where(x => x.Name == "Key") 3 .Configure(x => x.IsKey().HasColumnOrder(1)); 4 5 modelBuilder.Properties() 6 .Where(x => x.Name == "Name") 7 .Configure(x => x.IsKey().HasColumnOrder(2));
此代码将配置模型中的类型,以使组合键由int Key列和字符串Name列组成。如果我们在设计器中查看模型,它将如下所示:
属性约定的另一个示例是在我的模型中配置所有DateTime属性以映射到SQL Server中的datetime2类型而不是datetime。您可以通过以下方式实现此目的:
1 modelBuilder.Properties<DateTime>() 2 .Configure(c => c.HasColumnType("datetime2"));
定义约定的另一种方法是使用约定类来封装您的约定。使用Convention类时,您将创建一个继承System.Data.Entity.ModelConfiguration.Conventions命名空间中的Convention类的类型。
我们可以通过执行以下操作来创建具有我们之前显示的datetime2约定的Convention类:
1 public class DateTime2Convention : Convention 2 { 3 public DateTime2Convention() 4 { 5 this.Properties<DateTime>() 6 .Configure(c => c.HasColumnType("datetime2")); 7 } 8 }
要告诉EF使用此约定,请将其添加到OnModelCreating中的Conventions集合中,如果您一直按照演练进行操作,则它将如下所示:
1 protected override void OnModelCreating(DbModelBuilder modelBuilder) 2 { 3 modelBuilder.Properties<int>() 4 .Where(p => p.Name.EndsWith("Key")) 5 .Configure(p => p.IsKey()); 6 7 modelBuilder.Conventions.Add(new DateTime2Convention()); 8 }
如您所见,我们将约定的实例添加到约定集合中。继承自Convention提供了一种跨团队或项目分组和共享约定的便捷方式。例如,您可以拥有一个类库,其中包含所有组织项目使用的一组通用约定。
自定义属性
1 [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] 2 public class NonUnicode : Attribute 3 { 4 }
现在,让我们创建一个约定来将此属性应用于我们的模型:
1 modelBuilder.Properties() 2 .Where(x => x.GetCustomAttributes(false).OfType<NonUnicode>().Any()) 3 .Configure(c => c.IsUnicode(false));
使用此约定,我们可以将NonUnicode属性添加到任何字符串属性,这意味着数据库中的列将存储为varchar而不是nvarchar。
有关此约定的一点需要注意的是,如果将NonUnicode属性放在字符串属性以外的任何内容上,则会抛出异常。这样做是因为您无法在字符串以外的任何类型上配置IsUnicode。如果发生这种情况,那么您可以使您的约定更具体,以便过滤掉任何不是字符串的内容。
虽然上述约定适用于定义自定义属性,但还有另一个API可以更容易使用,尤其是当您想要使用属性类中的属性时。
对于此示例,我们将更新我们的属性并将其更改为IsUnicode属性,因此它看起来像这样:
1 [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] 2 internal class IsUnicode : Attribute 3 { 4 public bool Unicode { get; set; } 5 6 public IsUnicode(bool isUnicode) 7 { 8 Unicode = isUnicode; 9 } 10 }
一旦我们有了这个,我们可以在我们的属性上设置一个bool来告诉约定一个属性是否应该是Unicode。我们可以通过访问配置类的ClrProperty来实现这一点,如下所示:
1 modelBuilder.Properties() 2 .Where(x => x.GetCustomAttributes(false).OfType<IsUnicode>().Any()) 3 .Configure(c => c.IsUnicode(c.ClrPropertyInfo.GetCustomAttribute<IsUnicode>().Unicode));
这很容易,但通过使用convention API的Having方法,有一种更简洁的方法来实现这一点。Having方法有一个类型为Func <PropertyInfo,T>的参数,它接受PropertyInfo与Where方法相同,但是应该返回一个对象。如果返回的对象为null,则不会配置该属性,这意味着您可以像使用Where一样过滤掉属性,但它的不同之处在于它还将捕获返回的对象并将其传递给Configure方法。这类似于以下内容:
1 modelBuilder.Properties() 2 .Having(x =>x.GetCustomAttributes(false).OfType<IsUnicode>().FirstOrDefault()) 3 .Configure((config, att) => config.IsUnicode(att.Unicode));
自定义属性不是使用Having方法的唯一原因,在配置类型或属性时,您需要在任何地方推断您正在过滤的内容。
到目前为止,我们所有的约定都是针对属性的,但是还有另一个约定API区域用于配置模型中的类型。这种体验类似于我们目前看到的约定,但configure中的选项将在实体而不是属性级别。
类型级约定可以真正有用的一件事是更改表命名约定,要么映射到不同于EF默认值的现有模式,要么创建具有不同命名约定的新数据库。为此,我们首先需要一个方法,它可以接受模型中类型的TypeInfo,并返回该类型的表名应该是什么:
1 private string GetTableName(Type type) 2 { 3 var result = Regex.Replace(type.Name, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]); 4 5 return result.ToLower(); 6 }
此方法接受一个类型并返回一个字符串,该字符串使用带有下划线的小写而不是CamelCase。在我们的模型中,这意味着ProductCategory类将映射到名为product_category而不是ProductCategories的表。
一旦我们有了这个方法,我们就可以在这样的约定中调用它:
1 modelBuilder.Types() 2 .Configure(c => c.ToTable(GetTableName(c.ClrType)));
此约定将我们模型中的每个类型配置为映射到从GetTableName方法返回的表名。此约定相当于使用Fluent API为模型中的每个实体调用ToTable方法。
有一点需要注意的是,当你调用ToTable时,EF将把你提供的字符串作为确切的表名,而不是在确定表名时通常会做的任何复数。这就是为什么我们的约定中的表名是product_category而不是product_categories。我们可以通过自己打电话给多元化服务来解决这个问题。
在下面的代码中,我们将使用EF6中添加的依赖项解析功能来检索EF将使用的复数化服务并复数我们的表名。
1 private string GetTableName(Type type) 2 { 3 var pluralizationService = DbConfiguration.DependencyResolver.GetService<IPluralizationService>(); 4 5 var result = pluralizationService.Pluralize(type.Name); 6 7 result = Regex.Replace(result, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]); 8 9 return result.ToLower(); 10 }
注意:GetService的通用版本是System.Data.Entity.Infrastructure.DependencyResolution命名空间中的扩展方法,您需要在上下文中添加using语句才能使用它。
ToTable和继承
ToTable的另一个重要方面是,如果您将类型显式映射到给定表,那么您可以更改EF将使用的映射策略。如果为继承层次结构中的每种类型调用ToTable,将类型名称作为表的名称传递,就像我们上面所做的那样,那么您将默认的Table-Per-Hierarchy(TPH)映射策略更改为Table-Per-Type( TPT)。描述这个的最好方法是一个具体的例子:
1 public class Employee 2 { 3 public int Id { get; set; } 4 public string Name { get; set; } 5 } 6 7 public class Manager : Employee 8 { 9 public string SectionManaged { get; set; } 10 }
默认情况下,employee和manager都映射到数据库中的同一个表(Employees)。该表将包含一个带有鉴别器列的员工和经理,该列将告诉您每行中存储的实例类型。这是TPH映射,因为层次结构有一个表。但是,如果在两个classe上调用ToTable,则每个类型将被映射到其自己的表,也称为TPT,因为每个类型都有自己的表。
1 modelBuilder.Types() 2 .Configure(c=>c.ToTable(c.ClrType.Name));
上面的代码将映射到如下所示的表结构:
您可以通过以下几种方式避免这种情况,并维护默认的TPH映射:
- 使用层次结构中每种类型的相同表名调用ToTable。
- 仅在层次结构的基类上调用ToTable,在我们的示例中将是employee。
约定以最后的方式运行,与Fluent API相同。这意味着如果你编写两个约定来配置相同属性的相同选项,那么最后一个执行获胜。例如,在下面的代码中,所有字符串的最大长度都设置为500,但我们将模型中名为Name的所有属性配置为最大长度为250。
modelBuilder.Properties<string>() .Configure(c => c.HasMaxLength(500)); modelBuilder.Properties<string>() .Where(x => x.Name == "Name") .Configure(c => c.HasMaxLength(250));
因为将max length设置为250的约定是在将所有字符串设置为500的约定之后,所以我们模型中名为Name的所有属性将具有250的MaxLength,而任何其他字符串(例如描述)将为500。这种方式意味着您可以为模型中的类型或属性提供一般约定,然后将它们覆盖在不同的子集上。
Fluent API和Data Annotations也可用于在特定情况下覆盖约定。在上面的示例中,如果我们使用Fluent API来设置属性的最大长度,那么我们可以在约定之前或之后放置它,因为更具体的Fluent API将胜过更一般的配置约定。
由于自定义约定可能受默认的Code First约定的影响,因此添加约定以在另一个约定之前或之后运行会很有用。为此,您可以在派生的DbContext上使用Conventions集合的AddBefore和AddAfter方法。以下代码将添加我们之前创建的约定类,以便它将在内置键发现约定之前运行。
1 modelBuilder.Conventions.AddBefore<IdKeyDiscoveryConvention>(new DateTime2Convention());
在添加需要在内置约定之前或之后运行的约定时,这将是最有用的,可以在此处找到内置约定的列表:System.Data.Entity.ModelConfiguration.Conventions Namespace。
您还可以删除不希望应用于模型的约定。要删除约定,请使用Remove方法。以下是删除PluralizingTableNameConvention的示例。
1 protected override void OnModelCreating(DbModelBuilder modelBuilder) 2 { 3 modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); 4 }