ASP.NET MVC - 数据注解

DataAnnotations 用于配置模型类,它将突出显示最常用的配置。 许多 .NET 应用程序(例如 ASP.NET MVC)也可以理解 DataAnnotations,这允许这些应用程序利用相同的注解进行客户端验证。 DataAnnotation 属性会覆盖默认的 Code-First 约定。

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

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

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

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

Key 键

实体框架依赖于每个实体都有一个用于跟踪实体的 Key 键值。 Code First 代码优先模式所依赖的约定之一是它如何暗示哪个属性是每个 Code First 代码优先模式类中的键。

约定是查找名为"Id"的属性或组合类名和"Id"的属性,例如"StudentId"。 该属性将映射到数据库中的主键列。 学生、课程和注册课程遵循此约定。

现在假设 Student 类使用名称 StdntID 而不是 ID。 当 Code First 代码优先模式找不到与此约定匹配的属性时,它将抛出异常,因为实体框架要求您必须拥有关键属性。

您可以使用 key 注解来指定将哪个属性用作 EntityKey。

让我们看一下包含 StdntID 的 Student 类。 它不遵循默认的 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。

主键 StdntID

实体框架还支持组合键。 复合键是由多个属性组成的主键。 例如,您有一个 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; }
}

当您有复合键时,实体框架要求您定义键属性的顺序。 您可以使用 Column 注解来指定顺序来执行此操作。

复合键

Timestamp 时间戳

Code First 代码优先模式会将 Timestamp 属性视为与 ConcurrencyCheck 属性相同,但它还将确保 Code First 代码优先模式生成的数据库字段不可为 null。

更常见的是使用 rowversion 或时间戳字段进行并发检查。 但是,只要属性的类型是字节数组,您就可以使用更具体的 TimeStamp 注解,而不是使用 ConcurrencyCheck 注解。 给定类中只能有一个时间戳属性。

让我们看一个简单的示例,将 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 设计器,这与将属性的 ConcurrencyMode 设置为"固定"一致。

让我们看一个简单的示例,通过将其添加到 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; }
}

您可以在上面的 Student 类示例中看到,Required 属性应用于 FirstMidName 和 LastName。 因此,Code First 代码优先模式将在 Students 表中创建 NOT NULL FirstMidName 和 LastName 列,如以下屏幕截图所示。

学生表

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 将在 Coursed 表中创建一个 nvarchar(24) 列标题,如以下屏幕截图所示。

Column Title Coursed Table

现在,当用户设置的 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; }
}

实体框架还验证 StringLength 属性的属性值。 现在,如果用户设置的标题包含超过 24 个字符,则 EF 将抛出 EntityValidationError。

Table

默认的 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 表,如以下屏幕截图所示。

管理架构中的 StudentInfo 表

Column

它也与Table属性相同,但是Table属性覆盖表行为,而Column属性覆盖列行为。 默认 Code First 代码优先模式约定创建与属性名称相同的列名称。

如果您让 Code First 代码优先模式创建数据库,并且您还想更改表中列的名称。 列属性会覆盖此默认约定。 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 在创建数据库时在数据库中创建相应的索引。

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

Index 是实体框架中的新功能,您可以通过减少从数据库查询数据所需的时间来提高 Code First 代码优先模式应用程序的性能。

您可以使用"索引"属性向数据库添加索引,并覆盖默认的"唯一"和"聚集"设置以获得最适合您的场景的索引。 默认情况下,索引将命名为 IX_<property name>

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

public class Cours{
   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 属性。 现在,当生成表时,您将在索引中看到 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 属性

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 类中。

FatherName 列已创建

Code First 代码优先模式不会为没有 getter 或 setter 的属性创建列。

InverseProperty

当类之间有多个关系时,可以使用 InverseProperty。 在注册课程中,您可能想要跟踪谁注册了当前课程以及谁注册了先前课程。

让我们为 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} 外键列。 生成数据库后,您将看到许多外键,如下面的屏幕截图所示。

外键数量

正如您所看到的,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 表中仅创建两个外键列,如以下屏幕截图所示。

ForeignKey 注册表

我们建议您执行上面的示例以便更好地理解。