ASP.NET MVC - 数据注释


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

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

  • 钥匙
  • 时间戳
  • 并发检查
  • 必需的
  • 最小长度
  • 最长长度
  • 字符串长度

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

  • 桌子
  • 柱子
  • 指数
  • 外键
  • 未映射
  • 逆向性质

钥匙

实体框架依赖于每个实体都有一个用于跟踪实体的键值。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。

主键标准ID

实体框架还支持组合键。复合键是由多个属性组成的主键。例如,您有一个 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 注释来指定顺序来执行此操作。

复合键

时间戳

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 注释允许您标记一个或多个属性,以便在用户编辑或删除实体时用于数据库中的并发检查。如果您一直在使用 EF 设计器,则这与将属性的 ConcurrencyMode 设置为“固定”一致。

让我们看一个简单的示例,通过将 ConcurrencyCheck 添加到 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 注释告诉 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 属性允许您指定其他属性验证。它可以应用于域类的字符串或数组类型属性。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) 列标题,如以下屏幕截图所示。

列标题 课程表

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

最小长度

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 还允许您指定其他属性验证,例如 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。

桌子

默认 Code First 约定创建与类名相同的表名。如果您让 Code First 创建数据库,您还可以更改它正在创建的表的名称。您可以将 Code First 与现有数据库结合使用。但类的名称并不总是与数据库中表的名称相匹配。

表属性会覆盖此默认约定。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 表,如以下屏幕截图所示。

管理架构中的 StudentsInfo 表

柱子

它也与 Table 属性相同,但 Table 属性覆盖表Behave,而 Column 属性覆盖列Behave。默认 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,如以下屏幕截图所示。

列名 名字

指数

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

您可以使用 IndexAttribute 在一列或多列上创建索引。将属性添加到一个或多个属性中将导致 EF 在创建数据库时在数据库中创建相应的索引。

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

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

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

让我们看一下下面的代码,其中在 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_索引中的学分

默认情况下,索引是非唯一的,但您可以使用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; }
}

外键

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 属性已应用于导航属性。

外键属性

未映射

按照 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。在注册课程中,您可能希望跟踪谁注册了当前课程以及谁注册了先前课程。

让我们为 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 将创建四个外键属性,即

  • 当前课程_课程ID
  • 上一课程_课程ID
  • 课程_课程ID
  • 课程_课程ID1

要解决这些问题,您可以使用 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 表中仅创建两个外键列,如下面的屏幕截图所示。

外键注册表

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