ASP.NET MVC - 数据注解



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

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。

让我们看一下包含 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 资源管理器中查看数据库时,您将看到 Students 表中的主键现在是 StdntID。

Primary Key StdntID

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 注解指定顺序。

Composite Keys

Timestamp

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

更常见的是使用 rowversion 或 timestamp 字段进行并发检查。但是,与其使用 ConcurrencyCheck 注解,只要属性的类型是字节数组,就可以使用更具体的 TimeStamp 注解。在一个给定的类中只能有一个 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 表中创建一个 timestamp 列 TStamp。

ConcurrencyCheck

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

让我们来看一个简单的例子,看看 ConcurrencyCheck 如何工作,方法是将其添加到 Course 类中的 Title 属性。

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 类,其中 Required id 添加到 FirstMidName 属性。Required 属性将强制 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 列,如下面的屏幕截图所示。

Students Table

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) 列 Title,如下面的屏幕截图所示。

Column Title Coursed Table

现在,当用户设置包含超过 24 个字符的 Title 时,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 属性只能应用于域类的字符串类型属性。

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。

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,如下面的屏幕截图所示。

Table Name 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; }
}

在上面的示例中,表使用 admin 模式指定。现在 Code First 将在 Admin 模式下创建 StudentsInfo 表,如下面的屏幕截图所示。

StudentsInfo Table in 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,如下面的屏幕截图所示。

Column Name FirstName

Index

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

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

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

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

您可以使用 Index 属性向数据库添加索引,并覆盖默认的 Unique 和 Clustered 设置以获得最适合您方案的索引。默认情况下,索引将命名为 IX_

让我们看一下下面的代码,其中在 Course 类中为 Credits 添加了 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 属性。现在,当生成表时,您将在 Indexes 中看到 IX_Credits。

IX_Credits in Indexes

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

FatherName Column Created

对于没有 getter 或 setter 的属性,Code First 不会创建列。

InverseProperty

当类之间存在多个关系时,使用 `InverseProperty`。在 `Enrollment` 类中,您可能希望跟踪谁注册了 `Current Course` 和谁注册了 `Previous Course`。

让我们为 `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 会创建 `{类名}_{主键}` 外键列,如上述类所示。生成数据库时,您会看到许多外键,如下面的屏幕截图所示。

Number of ForeignKeys

如您所见,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; }
}

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

ForeignKey Enrollments Table

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

广告