实体框架 - DataAnnotations

DataAnnotations 用于配置类,以突出显示最常用的配置。许多 .NET 应用程序(例如 ASP.NET MVC)也能理解 DataAnnotations,这使得这些应用程序能够利用相同的注释进行客户端验证。 DataAnnotation 属性会覆盖默认的 CodeFirst 约定。

System.ComponentModel.DataAnnotations 包含以下会影响列的可空性或大小的属性。

  • Key
  • Timestamp
  • ConcurrencyCheck
  • Required
  • MinLength
  • MaxLength
  • StringLength

System.ComponentModel.DataAnnotations.Schema 命名空间包含以下会影响数据库架构的属性。

  • Table
  • Column
  • Index
  • ForeignKey
  • NotMapped
  • InverseProperty

Key

Entity Framework 依赖于每个实体都有一个键值,用于跟踪实体。Code First 所依赖的约定之一是它如何暗示哪个属性是每个 Code First 类中的键。

  • 约定是查找名为"Id"的属性或结合了类名和"Id"的属性,例如"StudentId"。

  • 该属性将映射到数据库中的主键列。

  • Student、Course 和 Enrollment 类遵循此约定。

现在让我们假设 Student 类使用名称 StdntID 而不是 ID。当 Code First 找不到符合此约定的属性时,它将引发异常,因为 Entity Framework 要求您必须具有一个键属性。您可以使用 key 注释来指定要用作 EntityKey 的属性。

让我们看一下以下 Student 类的代码,该类包含 StdntID,但它不遵循默认的 Code First 约定。因此,为了处理这个问题,添加了一个 Key 属性,使其成为主键。

public class Student {

   [Key]
   public int StdntID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

当您运行应用程序并在 SQL Server Explorer 中查看数据库时,您将看到主键现在是 Students 表中的 StdntID。

Primary Key

Entity Framework 还支持复合键。复合键也是由多个属性组成的主键。例如,您有一个 DrivingLicense 类,其主键是 LicenseNumber 和 IssuingCountry 的组合。

public class DrivingLicense {

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

当您有复合键时,Entity Framework 要求您定义键属性的顺序。您可以使用 Column 注释来指定顺序。

Column Annotation

Timestamp

Code First 将 Timestamp 属性视为与 ConcurrencyCheck 属性相同,但它还将确保 Code First 生成的数据库字段不可为空。

  • 使用 rowversion 或 timestamp 字段进行并发检查更为常见。

  • 只要属性的类型是字节数组,您就可以使用更具体的 TimeStamp 注释,而不是使用 ConcurrencyCheck 注释。

  • 给定类中只能有一个 timestamp 属性。

让我们看一个简单的示例,将 TimeStamp 属性添加到 Course 类 −

public class Course {

   public int CourseID { get; set; }
   public string Title { get; set; }
   public int Credits { get; set; }
   [Timestamp]
   public byte[] TStamp { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

如您在上例中看到的,Timestamp 属性应用于 Course 类的 Byte[] 属性。因此,Code First 将在 Courses 表中创建一个时间戳列 TStamp

ConcurrencyCheck

ConcurrencyCheck 注释允许您在用户编辑或删除实体时标记一个或多个用于数据库中并发检查的属性。如果您一直在使用 EF Designer,这与将属性的 ConcurrencyMode 设置为 Fixed 一致。

让我们通过将其添加到 Course 类中的 Title 属性来查看 ConcurrencyCheck 的工作原理的简单示例。

public class Course {

   public int CourseID { get; set; }
   [ConcurrencyCheck]
   public string Title { get; set; }
   public int Credits { get; set; }
   [Timestamp, DataType("timestamp")]
   public byte[] TimeStamp { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

在上述 Course 类中,ConcurrencyCheck 特性应用于现有的 Title 属性。现在,Code First 将在更新命令中包含 Title 列以检查乐观并发,如以下代码所示。

exec sp_executesql N'UPDATE [dbo].[Courses]
   SET [Title] = @0
   WHERE (([CourseID] = @1) AND ([Title] = @2))
   ',N'@0 nvarchar(max) ,@1 int,@2 nvarchar(max) ',@0=N'Maths',@1=1,@2=N'Calculus'
go

Required 注释

Required 注释告诉 EF 需要某个特定属性。 我们来看看下面的 Student 类,其中将必需的 id 添加到 FirstMidName 属性中。 必需的属性将强制 EF 确保属性中包含数据。

public class Student {

   [Key]
   public int StdntID { get; set; }

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

   [Required]
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

如上例所示,Required 属性应用于 FirstMidName 和 LastName。因此,Code First 将在 Students 表中创建 NOT NULL FirstMidName 和 LastName 列,如下图所示。

Not Null

MaxLength

MaxLength 属性允许您指定其他属性验证。它可以应用于域类的字符串或数组类型属性。EF Code First 将设置 MaxLength 属性中指定的列大小。

让我们看一下以下 Course 类,其中 MaxLength(24) 属性应用于 Title 属性。

public class Course {

   public int CourseID { get; set; }
   [ConcurrencyCheck]
   [MaxLength(24)]
   public string Title { get; set; }
   public int Credits { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

运行上述应用程序时,Code First 将在 CourseId 表中创建一个 nvarchar(24) 列 Title,如下图所示。

nvarchar Column

当用户设置的 Title 包含超过 24 个字符时,EF 将抛出 EntityValidationError。

MinLength

MinLength 属性还允许您指定其他属性验证,就像您对 MaxLength 所做的那样。MinLength 属性也可以与 MaxLength 属性一起使用,如以下代码所示。

public class Course {

   public int CourseID { get; set; }
   [ConcurrencyCheck]
   [MaxLength(24) , MinLength(5)]
   public string Title { get; set; }
   public int Credits { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

如果您设置的 Title 属性值小于 MinLength 属性中指定的长度或大于 MaxLength 属性中指定的长度,EF 将抛出 EntityValidationError。

StringLength

StringLength 还允许您指定其他属性验证,如 MaxLength。唯一的区别是 StringLength 属性只能应用于 Domain 类的字符串类型属性。

public class Course {

   public int CourseID { get; set; }
   [StringLength (24)]
   public string Title { get; set; }
   public int Credits { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

Entity Framework 还验证 StringLength 属性的值。如果用户设置的 Title 包含超过 24 个字符,则 EF 将抛出 EntityValidationError。

默认 Code First 约定会创建一个与类名类似的表名。如果您让 Code First 创建数据库,并且还想更改它正在创建的表的名称。那么 −

  • 您可以将 Code First 与现有数据库一起使用。但类的名称并不总是与数据库中表的名称相匹配。

  • Table 属性会覆盖此默认约定。

  • EF Code First 将在 Table 属性中为给定的域类创建一个具有指定名称的表。

让我们看一下以下示例,其中类名为 Student,按照惯例,Code First 假定这将映射到名为 Students 的表。如果不是这种情况,您可以使用 Table 属性指定表的名称,如以下代码所示。

[Table("StudentsInfo")]
public class Student {

   [Key]
   public int StdntID { get; set; }
   [Required]
   public string LastName { get; set; }
   [Required]
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

现在您可以看到 Table 属性将表指定为 StudentsInfo。生成表后,您将看到表名 StudentsInfo,如下图所示。

StudentsInfo

您不仅可以指定表名,还可以使用 Table 属性为表指定架构,如以下代码所示。

[Table("StudentsInfo", Schema = "Admin")] 
public class Student {

   [Key]
   public int StdntID { get; set; }
   [Required]
   public string LastName { get; set; }
   [Required]
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

您可以在上面的示例中看到,该表是用管理架构指定的。现在,Code First 将在管理架构中创建 StudentsInfo 表,如下图所示。

Admin Schema

Column

它也与 Table 属性相同,但 Table 属性会覆盖表行为,而 Column 属性会覆盖列行为。默认的 Code First 约定会创建一个类似于属性名称的列名。如果您让 Code First 创建数据库,并且还想更改表中列的名称。然后 −

  • Column 属性会覆盖默认约定。

  • EF Code First 将在给定属性的 Column 属性中创建一个具有指定名称的列。

让我们看一下以下示例,其中属性名为 FirstMidName,按照惯例,Code First 假定这将映射到名为 FirstMidName 的列。

如果不是这种情况,您可以使用 Column 属性指定列的名称,如以下代码所示。

public class Student {

   public int ID { get; set; }
   public string LastName { get; set; }
   [Column("FirstName")]
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

您可以看到 Column 属性将列指定为 FirstName。生成表时,您将看到列名 FirstName,如下图所示。

FirstName

Index

Index 属性是在 Entity Framework 6.1 中引入的。如果您使用的是早期版本,则本节中的信息不适用。

  • 您可以使用 IndexAttribute 在一个或多个列上创建索引。

  • 将属性添加到一个或多个属性将导致 EF 在创建数据库时在数据库中创建相应的索引。

  • 在大多数情况下,索引使数据检索更快、更高效。但是,使用索引重载表或视图可能会对其他操作(如插入或更新)的性能产生不利影响。

  • 索引是 Entity Framework 中的新功能,您可以通过减少从数据库查询数据所需的时间来提高 Code First 应用程序的性能。

  • 您可以使用 Index 属性向数据库添加索引,并覆盖默认的 Unique 和 Clustered 设置以获取最适合您方案的索引。

  • 默认情况下,索引将被命名为 IX_<property name>

让我们看一下以下代码,其中在 Course 类中添加了 Credits 的 Index 属性。

public class Course {
   public int CourseID { get; set; }
   public string Title { get; set; }
   [Index]
   public int Credits { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

您可以看到 Index 属性已应用于 Credits 属性。生成表后,您将在 Indexes 中看到 IX_Credits。

IX Credits

默认情况下,索引是非唯一的,但您可以使用 IsUnique 命名参数来指定索引应该是唯一的。以下示例引入了一个唯一索引,如以下代码所示。

public class Course {
   public int CourseID { get; set; }
   [Index(IsUnique = true)]
	
   public string Title { get; set; }
   [Index]
	
   public int Credits { get; set; }
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

ForeignKey

Code First 约定将处理模型中最常见的关系,但在某些情况下需要帮助。例如,通过更改 Student 类中键属性的名称,会导致其与 Enrollment 类的关系出现问题。

public class Enrollment {
   public int EnrollmentID { get; set; }
   public int CourseID { get; set; }
   public int StudentID { get; set; }
   public Grade? Grade { get; set; }
	
   public virtual Course Course { get; set; }
   public virtual Student Student { get; set; }
}

public class Student {
   [Key]
   public int StdntID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

在生成数据库时,Code First 会看到 Enrollment 类中的 StudentID 属性,并根据其与类名加"ID"匹配的惯例将其识别为 Student 类的外键。但是,Student 类中没有 StudentID 属性,但 Student 类中有 StdntID 属性。

此问题的解决方案是在 Enrollment 中创建一个导航属性,并使用 ForeignKey DataAnnotation 帮助 Code First 了解如何在两个类之间建立关系,如以下代码所示。

public class Enrollment {
   public int EnrollmentID { get; set; }
   public int CourseID { get; set; }
   public int StudentID { get; set; }
	
   public Grade? Grade { get; set; }
   public virtual Course Course { get; set; }
   [ForeignKey("StudentID")]
	
   public virtual Student Student { get; set; }
}

现在您可以看到 ForeignKey 属性已应用于导航属性。

ForeignKey Attribute

NotMapped

根据 Code First 的默认约定,每个具有受支持数据类型且包含 getter 和 setter 的属性都会在数据库中表示。但您的应用程序中并非总是如此。NotMapped 属性会覆盖此默认约定。例如,您可能在 Student 类中有一个属性,例如 FatherName,但不需要存储它。您可以将 NotMapped 属性应用于您不想在数据库中创建其列的 FatherName 属性,如以下代码所示。

public class Student {
   [Key]
   public int StdntID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
	
   public DateTime EnrollmentDate { get; set; }
   [NotMapped]

   public int FatherName { get; set; }
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

您可以看到 NotMapped 属性已应用于 FatherName 属性。生成表时,您将看到 FatherName 列不会在数据库中创建,但它存在于 Student 类中。

NotMapped Attribute

Code First 不会为没有 getter 或 setter 的属性创建列,如以下 Student 类的 Address 和 Age 属性示例所示。

InverseProperty

当类之间存在多个关系时,将使用 InverseProperty。在 Enrollment 类中,您可能希望跟踪谁注册了当前课程和上一门课程。让我们为 Enrollment 类添加两个导航属性。

public class Enrollment {
   public int EnrollmentID { get; set; }
   public int CourseID { get; set; }
   public int StudentID { get; set; }
   public Grade? Grade { get; set; }
	
   public virtual Course CurrCourse { get; set; }
   public virtual Course PrevCourse { get; set; }
   public virtual Student Student { get; set; }
}

同样,您还需要添加这些属性引用的 Course 类。Course 类具有导航回 Enrollment 类的属性,其中包含所有当前和以前的注册。

public class Course {

   public int CourseID { get; set; }
   public string Title { get; set; }
   [Index]

   public int Credits { get; set; }
   public virtual ICollection<Enrollment> CurrEnrollments { get; set; }
   public virtual ICollection<Enrollment> PrevEnrollments { get; set; }
}

如果外键属性未包含在特定类中(如上图所示),Code First 将创建 {Class Name}_{Primary Key} 外键列。生成数据库时,您将看到以下外键。

Foreign Keys

如您所见,Code first 无法自行匹配两个类中的属性。 Enrollments 的数据库表应该有一个用于 CurrCourse 的外键和一个用于 PrevCourse 的外键,但 Code First 将创建四个外键属性,即

  • CurrCourse _CourseID
  • PrevCourse _CourseID
  • Course_CourseID,和
  • Course_CourseID1

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

public class Course {

   public int CourseID { get; set; }
   public string Title { get; set; }
   [Index]

   public int Credits { get; set; }
   [InverseProperty("CurrCourse")]

   public virtual ICollection<Enrollment> CurrEnrollments { get; set; }
   [InverseProperty("PrevCourse")]

   public virtual ICollection<Enrollment> PrevEnrollments { get; set; }
}

如您所见,InverseProperty 属性通过指定它属于 Enrollment 类的哪个引用属性应用于上述 Course 类。现在,Code First 将生成一个数据库并在 Enrollments 表中仅创建两个外键列,如下图所示。

外键列

我们建议您逐步执行上述示例,以便更好地理解。