zoukankan      html  css  js  c++  java
  • 实体框架自定义代码优先约定(EF6以后)

    仅限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列组成。如果我们在设计器中查看模型,它将如下所示:

    compositeKey

    属性约定的另一个示例是在我的模型中配置所有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提供了一种跨团队或项目分组和共享约定的便捷方式。例如,您可以拥有一个类库,其中包含所有组织项目使用的一组通用约定。

    自定义属性

     约定的另一个重要用途是在配置模型时启用新属性。为了说明这一点,让我们创建一个属性,我们可以使用该属性将String属性标记为非Unicode。
     
    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));

    上面的代码将映射到如下所示的表结构:

    tptExample

    您可以通过以下几种方式避免这种情况,并维护默认的TPH映射:

    1. 使用层次结构中每种类型的相同表名调用ToTable。
    2. 仅在层次结构的基类上调用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     }
  • 相关阅读:
    比较两个DataTable数据(结构相同),返回新增的,删除的,修改前的,修改后的 DataTable
    通用jig类,用到委托
    网站性能优化之HTTP请求过程简述!
    常见JS效果实现实现之图片减速度滚动
    Firefox中实现的outerHTML
    电子商务网站上的常用的放大镜效果
    div背景半透明,覆盖整个可视区域的遮罩层效果
    Javascript类定义语法,私有成员、受保护成员、静态成员等
    HTML/CSS控制div垂直&&水平居中屏幕
    CSS团队协作开发方式的思考
  • 原文地址:https://www.cnblogs.com/yanglang/p/9406017.html
Copyright © 2011-2022 走看看