Code First 数据批注

注意

仅限 EF4.1 及更高版本 - 此页面中讨论的功能、API 等已引入 Entity Framework 4.1。 如果使用的是早期版本,则部分或全部信息不适用。

此页面上的内容改编自最初由 Julie Lerman (<http://thedatafarm.com>) 撰写的一篇文章。

利用实体框架 Code First,可以使用自己的域类来表示 EF 执行查询、更改跟踪和更新功能所依赖的模型。 Code First 利用称为“约定优于配置”的编程模式。 Code First 将假设你的类遵循实体框架的约定,在这种情况下,将自动确定如何执行其工作。 但是,如果你的类不遵循这些约定,可以向类添加配置以向 EF 提供必要的信息。

Code First 提供了两种将这些配置添加到类的方法。 一种方法是使用称为 DataAnnotations 的简单属性,第二种方法是使用 Code First 的 Fluent API,它提供了一种在代码中强制描述配置的方法。

本文将重点介绍使用 DataAnnotations(在 System.ComponentModel.DataAnnotations 命名空间中)来配置类,并重点介绍最常用的配置。 许多 .NET 应用程序也能理解 DataAnnotations,例如 ASP.NET MVC,它允许这些应用程序利用相同的注释进行客户端验证。

模型

我将使用一对简单的类来演示 Code First DataAnnotations:Blog 和 Post。

    public class Blog
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string BloggerName { get; set;}
        public virtual ICollection<Post> Posts { get; set; }
    }

    public class Post
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public DateTime DateCreated { get; set; }
        public string Content { get; set; }
        public int BlogId { get; set; }
        public ICollection<Comment> Comments { get; set; }
    }

事实上,Blog 和 Post 类方便地遵循 Code First 约定,不需要进行任何调整来实现 EF 兼容性。 但是,也可以使用注释向 EF 提供有关这些类和它们映射到的数据库的更多信息。

 

实体框架依赖于每个具有用于实体跟踪的键值的实体。 Code First 的一个约定是隐式键属性;Code First 将查找名为“Id”的属性,或类名和“Id”的组合,例如“BlogId”。 此属性将映射到数据库中的主键列。

Blog 和 Post 类都遵循这个约定。 如果没有,会怎样? 如果 Blog 使用名称 PrimaryTrackingKey,甚至 foo,会怎样? 如果 Code First 没有找到与此约定匹配的属性,则会引发异常,因为实体框架要求必须具有键属性。 可以使用键注释来指定将哪个属性用作 EntityKey。

    public class Blog
    {
        [Key]
        public int PrimaryTrackingKey { get; set; }
        public string Title { get; set; }
        public string BloggerName { get; set;}
        public virtual ICollection<Post> Posts { get; set; }
    }

如果使用的是 Code First 的数据库生成功能,则 Blog 表将有一个名为 PrimaryTrackingKey 的主键列,此列默认情况下也定义为标识。

带有主键的 Blog 表

组合键

实体框架支持组合键,即由多个属性组成的主键。 例如,可以有一个 Passport 类,其主键是 PassportNumber 和 IssuingCountry 的组合。

    public class Passport
    {
        [Key]
        public int PassportNumber { get; set; }
        [Key]
        public string IssuingCountry { get; set; }
        public DateTime Issued { get; set; }
        public DateTime Expires { get; set; }
    }

尝试在 EF 模型中使用上述类会导致 InvalidOperationException

无法确定“Passport”类型的组合主键顺序。 使用 ColumnAttribute 或 HasKey 方法来指定组合主键的顺序。

若要使用组合键,实体框架要求定义键属性的顺序。 为此,可以使用 Column 注释指定顺序。

注意

顺序值是相对的(而不是基于索引的),因此可以使用任意值。 例如,可以使用 100 和 200 代替 1 和 2。

    public class Passport
    {
        [Key]
        [Column(Order=1)]
        public int PassportNumber { get; set; }
        [Key]
        [Column(Order = 2)]
        public string IssuingCountry { get; set; }
        public DateTime Issued { get; set; }
        public DateTime Expires { get; set; }
    }

如果实体具有组合外键,则必须指定用于相应主键属性的相同列顺序。

只有外键属性内的相对顺序需要相同,分配给 Order 的确切值不需要匹配。 例如,在下面的类中,可以使用 3 和 4 代替 1 和 2。

    public class PassportStamp
    {
        [Key]
        public int StampId { get; set; }
        public DateTime Stamped { get; set; }
        public string StampingCountry { get; set; }

        [ForeignKey("Passport")]
        [Column(Order = 1)]
        public int PassportNumber { get; set; }

        [ForeignKey("Passport")]
        [Column(Order = 2)]
        public string IssuingCountry { get; set; }

        public Passport Passport { get; set; }
    }

必须

Required 注释告诉 EF 需要特定属性。

向 Title 属性添加 Required 将强制 EF(和 MVC)确保该属性中有数据。

    [Required]
    public string Title { get; set; }

由于应用程序中没有额外的代码或标记更改,MVC 应用程序将执行客户端验证,甚至使用属性和注释名称动态生成消息。

带有“Title 为必须项”错误的创建页

Required 属性还将通过使映射属性不可为 null 来影响生成的数据库。 请注意,Title 字段已更改为“非 null”。

注意

在某些情况下,即使此属性是必需的,数据库中的该列也可能为 null。 例如,使用 TPH 继承策略时,多种类型的数据存储在单个表中。 如果派生的类型包含所需的属性,则该列可能为 null,因为层次结构中的所有类型并非都具有此属性。

 

Blog 表

 

MaxLength 和 MinLength

MaxLengthMinLength 特性使你可以指定额外的属性验证,就像对 Required 执行的操作一样。

下面是具有长度要求的 BloggerName。 该示例还演示了如何合并特性。

    [MaxLength(10),MinLength(5)]
    public string BloggerName { get; set; }

MaxLength 注释将通过将属性的长度设置为 10 来影响数据库。

Blog 表显示 BloggerName 列的最大长度

MVC 客户端注释和 EF 4.1 服务器端注释都将遵守此验证,并再次动态生成错误消息:“字段 BloggerName 必须是最大长度为“10”的字符串或数组类型。”这条消息有点长。 使用许多注释都可以使用 ErrorMessage 特性指定错误消息。

    [MaxLength(10, ErrorMessage="BloggerName must be 10 characters or less"),MinLength(5)]
    public string BloggerName { get; set; }

还可以在 Required 注释中指定 ErrorMessage。

带有自定义错误消息的创建页

 

NotMapped

Code First 约定规定,支持的数据类型的每个属性都在数据库中表示。 但在应用程序中并非总是如此。 例如,Blog 类中可能有一个属性,该属性根据 Title 和 BloggerName 字段创建代码。 该属性可以动态创建,不需要存储。 可以使用 NotMapped 注释标记未映射到数据库的任何属性,例如此 BlogCode 属性。

    [NotMapped]
    public string BlogCode
    {
        get
        {
            return Title.Substring(0, 1) + ":" + BloggerName.Substring(0, 1);
        }
    }

 

ComplexType

跨一组类描述域实体,然后将这些类分层以描述完整实体的情况并不少见。 例如,可以向模型中添加一个名为 BlogDetails 的类。

    public class BlogDetails
    {
        public DateTime? DateCreated { get; set; }

        [MaxLength(250)]
        public string Description { get; set; }
    }

请注意,BlogDetails 没有任何类型的键属性。 在域驱动设计中,BlogDetails 被称为值对象。 实体框架将值对象称为复杂类型。  不能单独跟踪复杂类型。

但是,作为 Blog 类中的属性,BlogDetails 将作为 Blog 对象的一部分进行跟踪。 为了让 Code First 识别此项,必须将 BlogDetails 类标记为 ComplexType

    [ComplexType]
    public class BlogDetails
    {
        public DateTime? DateCreated { get; set; }

        [MaxLength(250)]
        public string Description { get; set; }
    }

现在可以在 Blog 类中添加一个属性来表示该博客的 BlogDetails

        public BlogDetails BlogDetail { get; set; }

在数据库中,Blog 表将包含博客的所有属性,包括其 BlogDetail 属性中包含的属性。 默认情况下,每个名称前面都有复杂类型“BlogDetail”的名称。

具有复杂类型的 Blog 表

ConcurrencyCheck

使用 ConcurrencyCheck 注释,可以标记一个或多个属性,以便在用户编辑或删除实体时,将该属性用于数据库中的并发检查。 如果使用的是 EF 设计器,这与将属性的 ConcurrencyMode 设置为 Fixed 一致。

让我们通过将 ConcurrencyCheck 添加到 BloggerName 属性来看看它是如何工作的。

    [ConcurrencyCheck, MaxLength(10, ErrorMessage="BloggerName must be 10 characters or less"),MinLength(5)]
    public string BloggerName { get; set; }

调用 SaveChanges 时,由于 BloggerName 字段上的 ConcurrencyCheck 注释,将在更新中使用该属性的原始值。 该命令将尝试通过不仅筛选键值而且筛选 BloggerName 的原始值来定位正确的行。  以下是发送到数据库的 UPDATE 命令的关键部分,可以在其中看到该命令将更新 PrimaryTrackingKey 为 1 且 BloggerName 为“Julie”的行,这是从数据库中检索到该博客时的原始值。

    where (([PrimaryTrackingKey] = @4) and ([BloggerName] = @5))
    @4=1,@5=N'Julie'

如果在此期间有人更改了该博客的博主名称,则此更新将失败,你将收到需要处理的 DbUpdateConcurrencyException

 

时间戳

更常见的情况是使用 rowversion 或时间戳字段进行并发检查。 但是,可以使用更具体的 TimeStamp 注释,而不是使用 ConcurrencyCheck 注释,只要属性的类型是字节数组即可。 Code First 会将 Timestamp 属性视为与 ConcurrencyCheck 属性相同,但它还会确保 Code First 生成的数据库字段不可为 null。 给定的类中只能有一个时间戳属性。

将以下属性添加到 Blog 类:

    [Timestamp]
    public Byte[] TimeStamp { get; set; }

导致 Code First 在数据库表中创建一个不可为 null 的时间戳列。

具有时间戳列的 Blog 表

 

表和列

如果允许 Code First 创建数据库,则可能需要更改其要创建的表和列的名称。 还可以将 Code First 与现有数据库一起使用。 但是,域中的类和属性的名称并不总是与数据库中的表和列的名称相匹配。

我的类名为 Blog,按照惯例,Code First 会假定这将映射到名为 Blogs 的表。 如果不是这种情况,则可以使用 Table 属性指定表的名称。 例如,这里的注释指定表名称为 InternalBlogs

    [Table("InternalBlogs")]
    public class Blog

Column 注释更擅长指定映射列的属性。 可以规定名称、数据类型甚至列在表中显示的顺序。 以下是 Column 属性的示例。

    [Column("BlogDescription", TypeName="ntext")]
    public String Description {get;set;}

不要将 Column 的 TypeName 属性与 DataType DataAnnotation 混淆。 DataType 是用于 UI 的注释,Code First 会忽略它。

以下是重新生成后的表。 表名已更改为 InternalBlogs,复杂类型的 Description 列现在为 BlogDescription。 由于名称是在注解中指定的,因此 Code First 不会使用以复杂类型名称作为列名开头的约定。

Blog 表和重命名后的列

 

DatabaseGenerated

一个重要的数据库特性是具有计算属性的能力。 如果要将 Code First 类映射到包含计算列的表,则不希望实体框架尝试更新这些列。 但是,在插入或更新数据后,你确实需要 EF 从数据库返回这些值。 可以使用 DatabaseGenerated 注释与 Computed 枚举一起标记类中的这些属性。 其他枚举为 NoneIdentity

    [DatabaseGenerated(DatabaseGeneratedOption.Computed)]
    public DateTime DateCreated { get; set; }

当 Code First 生成数据库时,可以使用在字节或时间戳列上生成的数据库,否则只应在指向现有数据库时使用此数据库,因为 Code First 将无法确定计算列的公式。

如上所述,在默认情况下,整数的键属性将成为数据库中的标识键。 这与将 DatabaseGenerated 设置为 DatabaseGeneratedOption.Identity 相同。 如果不希望它成为标识键,则可以将该值设置为 DatabaseGeneratedOption.None

 

索引

注意

仅 EF6.1 以上版本 - Index 特性已引入实体框架 6.1 中。 如果使用的是早期版本,则本部分中的此信息不适用。

可以使用 IndexAttribute 在一个或多个列上创建索引。 将属性添加到一个或多个属性时,将导致 EF 在创建数据库时在数据库中创建相应的索引,或者如果使用的是 Code First Migrations,则将为相应的 CreateIndex 调用基架

例如,以下代码将导致在数据库中 Posts 表的 Rating 列上创建索引。

    public class Post
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        [Index]
        public int Rating { get; set; }
        public int BlogId { get; set; }
    }

默认情况下,索引将命名为 IX_<属性名称>(上例中为 IX_Rating)。 也可以为索引指定一个名称。 以下示例指定索引应命名为 PostRatingIndex

    [Index("PostRatingIndex")]
    public int Rating { get; set; }

默认情况下,索引是非唯一的,但可以使用 IsUnique 命名参数来指定索引应该是唯一的。 以下示例对 User 的登录 ID 引入了唯一索引。

    public class User
    {
        public int UserId { get; set; }

        [Index(IsUnique = true)]
        [StringLength(200)]
        public string Username { get; set; }

        public string DisplayName { get; set; }
    }

多列索引

通过在给定表的多个索引注释中使用相同的名称来指定跨越多个列的索引。 创建多列索引时,需要为索引中的列指定顺序。 例如,以下代码在 RatingBlogId 上创建了一个名为 IX_BlogIdAndRating 的多列索引BlogId 是索引中的第一列,Rating 是第二列。

    public class Post
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        [Index("IX_BlogIdAndRating", 2)]
        public int Rating { get; set; }
        [Index("IX_BlogIdAndRating", 1)]
        public int BlogId { get; set; }
    }

 

关系属性:InverseProperty 和 ForeignKey

注意

本页提供有关使用数据注释在 Code First 模型中设置关系的信息。 有关 EF 中的关系以及如何使用关系访问和操作数据的一般信息,请参阅关系和导航属性。*

Code First 约定将处理模型中最常见的关系,但在某些情况下需要帮助。

更改 Blog 类中键属性的名称会导致其与 Post 的关系出现问题。 

生成数据库时,Code First 看到 Post 类中的 BlogId 属性,并按照约定将其识别为与类名加 Id 相匹配的 Blog 类的外键。 但是 Blog 类中没有 BlogId 属性。 对此的解决方案是在 Post 中创建导航属性,并使用 ForeignKey DataAnnotation 帮助 Code First 了解如何生成两个类之间的关系(使用 Post.BlogId 属性),以及如何指定数据库中的约束。

    public class Post
    {
            public int Id { get; set; }
            public string Title { get; set; }
            public DateTime DateCreated { get; set; }
            public string Content { get; set; }
            public int BlogId { get; set; }
            [ForeignKey("BlogId")]
            public Blog Blog { get; set; }
            public ICollection<Comment> Comments { get; set; }
    }

数据库中的约束显示了 InternalBlogs.PrimaryTrackingKeyPosts.BlogId 之间的关系。 

InternalBlogs.PrimaryTrackingKey 和 Posts.BlogId 之间的关系

当类之间有多个关系时使用 InverseProperty

Post 类中,你可能希望跟踪博客文章的作者和编辑者。 下面是 Post 类的两个新导航属性。

    public Person CreatedBy { get; set; }
    public Person UpdatedBy { get; set; }

还需要添加这些属性引用的 Person 类。 Person 类具有返回 Post 的导航属性,一个用于该人撰写的所有帖子,另一个用于该人更新的所有帖子。

    public class Person
    {
            public int Id { get; set; }
            public string Name { get; set; }
            public List<Post> PostsWritten { get; set; }
            public List<Post> PostsUpdated { get; set; }
    }

Code First 无法单独匹配两个类中的属性。 Posts 的数据库表应该有一个用于 CreatedBy 用户的外键和一个用于 UpdatedBy 用户的外键,但 Code First 将创建四个外键属性:Person_Id、Person_Id1、CreatedBy_Id 和 UpdatedBy_Id

具有额外外键的 Post 表

要解决这些问题,可以使用 InverseProperty 注释来指定属性的对齐方式。

    [InverseProperty("CreatedBy")]
    public List<Post> PostsWritten { get; set; }

    [InverseProperty("UpdatedBy")]
    public List<Post> PostsUpdated { get; set; }

因为 Person 中的 PostsWritten 属性知道这指的是 Post 类型,所以它将建立与 Post.CreatedBy 的关系。 同样,PostsUpdated 将连接到 Post.UpdatedBy。 并且 Code First 不会创建额外的外键。

不具有额外外键的 Post 表

 

总结

DataAnnotations 不仅使你能够在 Code First 类中描述客户端和服务器端验证,而且还可以增强甚至更正 Code First 将根据其约定对类做出的假设。 使用 DataAnnotations,不仅可以驱动数据库模式生成,还可以将 Code First 类映射到预先存在的数据库。

尽管它们非常灵活,但请记住,DataAnnotations 仅提供你可以对 Code First 类进行的最常用的配置更改。 若要为某些边缘情况配置类,应该查看备用配置机制,即 Code First 的 Fluent API。