一、概述
利用实体框架Code First,可以使用自己的域类来表示EF执行查询,更改跟踪和更新功能所依赖的模型。Code First利用称为“约定优于配置”的编程模式。Code First将假设你的类遵循实体框架的约定,在这种情况下,将自动确定如何执行其工作。如果需要你的类不遵循这些约定,可以向类添加配置以向EF提供必要的信息。
Code First 提供了两种将这些配置添加到类的方法。
- 一种方法是使用称为 DataAnnotations 的简单属性
- 第二种方法是使用 Code First 的 Fluent API,它提供了一种在代码中强制描述配置的方法
本文将重点介绍使用DataAnnotations(System.ComponentModel.DataAnnotations命名空间中)来配置类。并重点介绍最常用的配置。许多.NET应用程序也能理解DataAnnotations。
二、模型
我将使用一对简单的类来演示 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 提供有关这些类和它们映射到的数据库的更多信息。
二、键Key
实体框架依赖于每个具有用于实体跟踪的键值的实体。Code First的一个约定是隐式键属性;Code First将查找名为"Id"的属性,或类名和"Id"的组合,例如“BlogId”,此属性将映射到数据库中的主键列。
Blog和Post类都遵循这个约定。如果没有,会引发异常。这个是因为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; } }
三、组合键
实体框架支持组合键,既由多个属性组合的主键。如:可以有一个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注释指定顺序。
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; } }
如果实体具有组合外键,则必须指定用于相应主键属性的相同列顺序
四、外键-ForeigKey
所有一对一和一对多关系均由依赖端上的外键所定义,用于引用主体端上的主键或备用键。为了方便起见,此主键或备用键称为关系的"主键"。多对多关系由两个一对多关系组成,每个关系本身由引用主键的外键所定义。
第一种:指定导航属性,会自动生成外键,命名规则为:“对象名称_主键名“
public class TUsers { [Key] public int UserId { get; set; } public string Account { get; set; } public string Password { get; set; } public DateTime CreateDate { get; set; } public List<TUsersRoles> TUsersRolesList { get; set; } } public class TUsersExtInfo { [Key] public int id { get; set; } //生产的外键名称是Users_UserId,格式为"对象名称_主角名称" public virtual TUsers Users { get; set; } }
第二种:默认情况下与导航属性的主键名称相同的字段会自动被标记为外键
public class TUsers { [Key] public int UserId { get; set; } public string Account { get; set; } public string Password { get; set; } public DateTime CreateDate { get; set; } } public class TUsersExtInfo { [Key] public int id { get; set; } public int UserID { get; set; } //如果没有声明TUsers对象,则UserID是一个普通的字段,没有外键关系 public virtual TUsers Users { get; set; } }
第三种方法:可指定生成的数据库中的列名。
public class TUsers { [Key] public int UserId { get; set; } public string Account { get; set; } public string Password { get; set; } public DateTime CreateDate { get; set; } } public class TUsersExtInfo { [Key] public int id { get; set; } public int TUsers_Id { get; set; } [ForeignKey("TUsers_Id")] public virtual TUsers Users { get; set; } }
第四种方法:可指定生成的数据库中的列名。
方式2的升级版,与导航属性的主键名称相同的字段会自动被标记为外键,然后指定字段对应的数据库中的列名
public class TUsers { [Key] public int UserId { get; set; } public string Account { get; set; } public string Password { get; set; } public DateTime CreateDate { get; set; } } public class TUsersExtInfo { [Key] public int id { get; set; } [Column("TUsers_Id")] public int UserId { get; set; } public virtual TUsers Users { get; set; } }
五、验证-Required
Required
注释告诉 EF 需要特定属性。Required 属性还将通过使映射属性不可为 null 来影响生成的数据库。 请注意,Title 字段已更改为“非 null”。
[Required] public string Title { get; set; }
在某些情况下,即使此属性是必需的,数据库中的该列也可能为 null。 例如,使用 TPH 继承策略时,多种类型的数据存储在单个表中。 如果派生的类型包含所需的属性,则该列可能为 null,因为层次结构中的所有类型并非都具有此属性。
六、MaxLength 和 MinLength
MaxLength
和 MinLength
特性使你可以指定额外的属性验证,就像对 Required
执行的操作一样。
[MaxLength(10),MinLength(5)] public string BloggerName { get; set; }
七、NotMapped
Code First 约定规定,支持的数据类型的每个属性都在数据库中表示。 但在应用程序中并非总是如此。 如:Blog类中可能有一个属性,该属性根据Title和BloggerName字段创建代码,该属性可以动态创建,不需要存储。可以使用NotMapped注释标记未映射到数据库的任何属性。
[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; } }
九、ComplexType
使用 ConcurrencyCheck
注释,可以标记一个或多个属性,以便在用户编辑或删除实体时,将该属性用于数据库中的并发检查。 如果使用的是 EF 设计器,这与将属性的 ConcurrencyMode
设置为 Fixed
一致。
让我们通过将 ConcurrencyCheck
添加到 BloggerName
属性来看看它是如何工作的。
[ConcurrencyCheck, MaxLength(10, ErrorMessage="BloggerName must be 10 characters or less"),MinLength(5)] public string BloggerName { get; set; }
十、表和列
如果允许 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;}
十一、DatabaseGenerated
一个重要的数据库特性是具有计算属性的能力。 如果要将 Code First 类映射到包含计算列的表,则不希望实体框架尝试更新这些列。 但是,在插入或更新数据后,你确实需要 EF 从数据库返回这些值。 可以使用 DatabaseGenerated 注释与 Computed 枚举一起标记类中的这些属性。 其他枚举为 None 和 Identity。
[DatabaseGenerated(DatabaseGeneratedOption.Computed)] public DateTime DateCreated { get; set; }
当 Code First 生成数据库时,可以使用在字节或时间戳列上生成的数据库,否则只应在指向现有数据库时使用此数据库,因为 Code First 将无法确定计算列的公式。
如上所述,在默认情况下,整数的键属性将成为数据库中的标识键。 这与将 DatabaseGenerated 设置为 DatabaseGeneratedOption.Identity 相同。 如果不希望它成为标识键,则可以将该值设置为 DatabaseGeneratedOption.None。
十二、索引
可以使用 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; }
十三、多列索引
通过在给定表的多个索引注释中使用相同的名称来指定跨越多个列的索引。 创建多列索引时,需要为索引中的列指定顺序。 例如,以下代码在 Rating 和 BlogId 上创建了一个名为 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 约定将处理模型中最常见的关系,但在某些情况下需要帮助。
更改 Blog
类中键属性的名称会导致其与 Post
的关系出现问题。
生成数据库时,Code First 看到 Post 类中的 BlogId 属性,并按照约定将其识别为与类名加 Id 相匹配的 Blog 类的外键。 但是 Blog 类中没有 BlogId 属性。 对此的解决方案是在 Post 中创建导航属性,并使用 ForeignKey DataAnnotation 帮助 Code First 了解如何生成两个类之间的关系(使用 Post.BlogId 属性),以及如何指定数据库中的约束。
[InverseProperty("CreatedBy")] public List<Post> PostsWritten { get; set; } [InverseProperty("UpdatedBy")] public List<Post> PostsUpdated { get; set; }
十五、总结
DataAnnotations 不仅使你能够在 Code First 类中描述客户端和服务器端验证,而且还可以增强甚至更正 Code First 将根据其约定对类做出的假设。 使用 DataAnnotations,不仅可以驱动数据库模式生成,还可以将 Code First 类映射到预先存在的数据库。