共用方式為


第 5 部分,在 ASP.NET Core 中使用 EF Core 的 Razor 頁面 - 資料模型

作者:Tom DykstraJeremy LiknessJon P Smith

Contoso 大學 Web 應用程式將示範如何使用 EF Core 和 Visual Studio 來建立 Razor Pages Web 應用程式。 如需教學課程系列的資訊,請參閱第一個教學課程

如果您遇到無法解決的問題,請下載已完成的應用程式,並遵循本教學課程以將程式碼與您所建立的內容進行比較。

先前的教學課程建立了基本的資料模型,該模型由三個實體組成。 在本教學課程中:

  • 新增更多實體和關聯性。
  • 藉由指定格式、驗證和資料庫對應規則來自訂資料模型。

已完成的資料模型如下圖所示:

Entity diagram

下列資料庫圖表是使用 Dataedo 製作的:

Dataedo diagram

若要使用 Dataedo 建立資料庫圖表:

在上述 Dataedo 圖表中,CourseInstructor 是一個 Entity Framework 所建立的聯結資料表。 如需詳細資訊,請參閱多對多

Student 實體

以下列程式碼取代 Models/Student.cs 中的程式碼:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }
        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

上述程式碼會新增 FullName 屬性,並將下列屬性新增到現有屬性:

FullName 計算屬性

FullName 為一個計算屬性,會傳回藉由串連兩個其他屬性而建立的值。 您無法設定 FullName,因此它只會有 get 存取子。 資料庫中不會建立 FullName 資料行。

DataType 屬性

[DataType(DataType.Date)]

針對學生註冊日期,所有頁面目前都會同時顯示日期和一天當中的時間,雖然只要日期才是重要項目。 透過使用資料註解屬性,您便可以只透過單一程式碼變更來修正每個顯示資料頁面中的顯示格式。

DataType 屬性會指定一個比資料庫內建類型更明確的資料類型。 在此情況下,該欄位應該只顯示日期,而不會同時顯示日期和時間。 DataType 列舉提供了許多資料類型,例如 Date、Time、PhoneNumber、Currency、EmailAddress 等。DataType 屬性也可以讓應用程式自動提供限定於某些類型的功能。 例如:

  • DataType.EmailAddress 會自動建立 mailto: 連結。
  • DataType.Date 在大多數的瀏覽器中都會提供日期選取器。

DataType 屬性會發出 HTML5 data- (發音為 data dash) 屬性。 DataType 屬性不會提供驗證。

DisplayFormat 屬性

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

DataType.Date 未指定顯示日期的格式。 根據預設,日期欄位會依照根據伺服器的 CultureInfo 為基礎的預設格式顯示。

DisplayFormat 屬性會用來明確指定日期格式。 ApplyFormatInEditMode 設定會指定格式也應套用在編輯 UI。 某些欄位不應該使用 ApplyFormatInEditMode。 例如,貨幣符號通常不應顯示在編輯文字方塊中。

DisplayFormat 屬性可由自身使用。 通常使用 DataType 屬性搭配 DisplayFormat 屬性是一個不錯的做法。 DataType 屬性會將資料的語意以其在螢幕上呈現方式的相反方式傳遞。 DataType 屬性提供了下列優點,並且這些優點在 DisplayFormat 中無法使用:

  • 瀏覽器可以啟用 HTML5 功能。 例如,顯示日曆控制項、適合地區設定的貨幣符號、電子郵件連結和用戶端輸入驗證。
  • 根據預設,瀏覽器將根據地區設定,使用正確的格式呈現資料。

如需詳細資訊,請參閱 <input> 標籤協助程式文件

StringLength 屬性

[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]

您可使用屬性指定資料驗證規則和驗證錯誤訊息。 StringLength 屬性指定了在資料欄位中允許的最小及最大字元長度。 此處所顯示的程式碼會限制名稱不得超過 50 個字元。 設定字串長度下限的範例會在稍後顯示。

StringLength 屬性同時也提供了用戶端和伺服器端的驗證。 最小值對資料庫結構描述不會造成任何影響。

StringLength 屬性不會防止使用者在名稱中輸入空白字元。 RegularExpression 屬性可用來將限制套用到輸入。 例如,下列程式碼會要求第一個字元必須是大寫,其餘字元則必須是英文字母:

[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]

在 [SQL Server 物件總管] (SSOX) 中,按兩下 [Student] 資料表來開啟 Student 資料表設計工具。

Students table in SSOX before migrations

上述影像顯示了 Student 資料表的結構描述。 名稱欄位的型別為 nvarchar(MAX)。 在本教學課程稍後建立移轉並套用時,名稱欄位會因為字串長度屬性而變更為 nvarchar(50)

Column 屬性

[Column("FirstName")]
public string FirstMidName { get; set; }

可控制類別和屬性如何對應到資料庫的屬性。 在 Student 模型中,Column 屬性會用來將 FirstMidName 屬性名稱對應到資料庫中的 "FirstName"。

建立資料庫時,模型上的屬性名稱會用來作為資料行名稱 (使用 Column 屬性時除外)。 Student 模型針對名字欄位使用 FirstMidName,因為欄位中可能也會包含中間名。

透過 [Column] 屬性,資料模型中 Student.FirstMidName 會對應到 Student 資料表的 FirstName 資料行。 新增 Column 屬性會變更支援 SchoolContext 的模型。 支援 SchoolContext 的模型不再符合資料庫。 這種不一致會透過在本教學課程中稍後部分新增移轉來解決。

Required 屬性

[Required]

Required 屬性會讓名稱屬性成為必要欄位。 針對不可為 Null 型別的實值型別 (例如 DateTimeintdouble),Required 屬性並非必要項目。 不可為 Null 的類型會自動視為必要欄位。

Required 屬性必須搭配 MinimumLength 使用,才能強制執行 MinimumLength

[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }

MinimumLengthRequired 允許空白字元以滿足驗證。 使用 RegularExpression 屬性來完全控制字串。

Display 屬性

[Display(Name = "Last Name")]

Display 屬性指定了文字方塊的標題應為「名字」、「姓氏」、「全名」及「註冊日期」。預設標題中沒有使用空白分隔文字,例如「姓氏」。

建立移轉

執行應用程式並移至 Students 頁面。 擲回例外狀況。 [Column] 屬性會造成 EF 預期尋找名為 FirstName 的資料行,但資料庫中的資料行名稱仍是 FirstMidName

錯誤訊息會與下列範例相似:

SqlException: Invalid column name 'FirstName'.
There are pending model changes
Pending model changes are detected in the following:

SchoolContext
  • 在 PMC 中,輸入下列命令來建立新的移轉並更新資料庫:

    Add-Migration ColumnFirstName
    Update-Database
    
    

    這些命令中的第一個命令會產生下列警告訊息:

    An operation was scaffolded that may result in the loss of data.
    Please review the migration for accuracy.
    

    產生警告的理由是名稱欄位現在限制長度為 50 個字元。 若資料庫中的名稱超過 50 個字元,則第 51 到最後一個字元便會遺失。

  • 在 SSOX 中開啟 Student 資料表:

    Students table in SSOX after migrations

    在套用移轉前,名稱資料行的型別為 nvarchar(MAX)。 名稱資料行現在為 nvarchar(50)。 資料行的名稱已從 FirstMidName 變更為 FirstName

  • 執行應用程式並移至 Students 頁面。
  • 請注意時間並未輸入或和日期一同顯示。
  • 選取 [新建] 然後嘗試輸入超過 50 個字元的名稱。

注意

在下列各節中,在某些階段建置應用程式會產生編譯器錯誤。 指令會指定何時應建置應用程式。

Instructor 實體

以下列程式碼來建立 Models/Instructor.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor
    {
        public int ID { get; set; }

        [Required]
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

        public ICollection<Course> Courses { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}

在同一行上可以有多個屬性。 HireDate 屬性可以下列方式撰寫:

[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

CoursesOfficeAssignment 屬性為導覽屬性。

由於講師可以教授任何數量的課程,因此 Courses 已定義為一個集合。

public ICollection<Course> Courses { get; set; }

講師最多只能擁有一間辦公室,因此 OfficeAssignment 屬性會保存單一 OfficeAssignment 實體。 若沒有指派任何辦公室,則 OfficeAssignment 為 Null。

public OfficeAssignment OfficeAssignment { get; set; }

OfficeAssignment 實體

OfficeAssignment entity

以下列程式碼來建立 Models/OfficeAssignment.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public Instructor Instructor { get; set; }
    }
}

Key 屬性

[Key] 屬性用於將某個屬性名稱不是 classnameIDID 的屬性識別為主索引鍵 (PK)。

InstructorOfficeAssignment 實體之間有一對零或一關聯性。 辦公室指派只存在於與其受指派的講師關聯中。 OfficeAssignment PK 同時也是其連結到 Instructor 實體的外部索引鍵 (FK)。 當一個資料表中的 PK 同時是 PK 和另一個資料表中的 FK 時,就會發生一對零或一的關聯性。

EF Core 無法將 InstructorID 自動識別為 OfficeAssignment 的主索引鍵,因為 InstructorID 並未遵循識別碼或 classnameID 命名慣例。 因此,必須使用 Key 屬性將 InstructorID 識別為 PK:

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

根據預設,EF Core 會將索引鍵作為非資料庫產生的屬性處置,因為該資料行主要用於識別關聯性。 如需詳細資訊,請參閱 EF 索引鍵

Instructor 導覽屬性

Instructor.OfficeAssignment 導覽屬性可以是 Null,因為指定講師可能不會有 OfficeAssignment 資料列。 講師可能沒有辦公室指派。

OfficeAssignment.Instructor 導覽屬性一律會擁有一個講師實體,因為外部索引鍵 InstructorID 的型別是 int,其為不可為 Null 的實值型別。 辦公室指派無法獨立於講師之外存在。

Instructor 實體有相關的 OfficeAssignment 實體時,每一個實體都會在其導覽屬性中包含一個其他實體的參考。

Course 實體

以下列程式碼來更新 Models/Course.cs

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        [Display(Name = "Number")]
        public int CourseID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Title { get; set; }

        [Range(0, 5)]
        public int Credits { get; set; }

        public int DepartmentID { get; set; }

        public Department Department { get; set; }
        public ICollection<Enrollment> Enrollments { get; set; }
        public ICollection<Instructor> Instructors { get; set; }
    }
}

Course 實體具有外部索引鍵 (FK) 屬性DepartmentIDDepartmentID 會指向相關的 Department 實體。 Course 實體具有一個 Department 導覽屬性。

若模型針對相關實體已有導覽屬性,則 EF Core 針對資料模型便不需要外部索引鍵屬性。 EF Core 會自動在資料庫中需要的任何地方建立 FK。 EF Core 會為自動建立的 FK 建立陰影屬性。 但是,在資料模型中明確包含 FK (外部索引鍵) 可讓更新更簡單且更有效率。 例如,假設有一個模型,當中「不」包含 DepartmentID FK 屬性。 當擷取課程實體以進行編輯時:

  • 如果 Department 未明確載入屬性則會為 null
  • 若要更新課程實體,必須先擷取 Department 實體。

當 FK 屬性 DepartmentID 包含在資料模型中時,便不需要在更新前擷取 Department 實體。

DatabaseGenerated 屬性

[DatabaseGenerated(DatabaseGeneratedOption.None)] 屬性會指定 PK 是由應用程式提供的,而非資料庫產生的。

[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

預設情況下,EF Core 會假定 PK 值是由資料庫產生的。 由資料庫產生主索引鍵通常是最佳做法。 針對 Course 實體,使用者指定了 PK。 例如,課程號碼 1000 系列表示數學部門的課程,2000 系列則為英文部門的課程。

DatabaseGenerated 屬性也可用於產生預設值。 例如,資料庫可以自動產生日期欄位來記錄建立或更新資料列的日期。 如需詳細資訊,請參閱產生的屬性

外部索引鍵及導覽屬性

Course 實體中的外部索引鍵 (FK) 屬性和導覽屬性反映了下列關聯性:

由於一個課程已指派給了一個部門,因此當中具有 DepartmentID FK 和 Department 導覽屬性。

public int DepartmentID { get; set; }
public Department Department { get; set; }

由於課程可由任何數量的學生進行註冊,因此 Enrollments 導覽屬性為一個集合:

public ICollection<Enrollment> Enrollments { get; set; }

課程可由多個講師進行教授,因此 Instructors 導覽屬性為一個集合:

public ICollection<Instructor> Instructors { get; set; }

Department 實體

以下列程式碼來建立 Models/Department.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
                       ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}

Column 屬性

先前,Column 屬性主要用於變更資料行的名稱對應。 在 Department 實體的程式碼中,Column 屬性則用於變更 SQL 資料類型對應。 Budget 資料行在資料庫中是使用 SQL Server 的 money 型別來定義:

[Column(TypeName="money")]
public decimal Budget { get; set; }

通常您不需要資料行對應。 EF Core 會根據屬性的 CLR 型別來選擇適當 SQL Server 資料型別。 CLR decimal 類型會對應到 SQL Server 的 decimal 類型。 由於 Budget 是貨幣,因此金額資料類型會比較適合貨幣。

外部索引鍵及導覽屬性

FK 及導覽屬性反映了下列關聯性:

  • 部門可能有或可能沒有系統管理員。
  • 系統管理員一律為講師。 因此,InstructorID 已作為 FK 包含在 Instructor 實體中。

導覽屬性已命名為 Administrator,但其中保留了一個 Instructor 實體:

public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }

上述程式碼中的 ? 指定屬性可為 Null。

部門中可能包含許多課程,因此當中包含了一個 Course 導覽屬性:

public ICollection<Course> Courses { get; set; }

根據慣例,EF Core 會為不可為 Null 的 FK 和多對多關聯性啟用串聯刪除。 此預設行為可能會導致循環串聯刪除規則。 循環串聯刪除規則會在新增移轉時造成例外狀況。

例如,若 Department.InstructorID 屬性已定義成不可為 Null,EF Core 便會設定串聯刪除規則。 在這種情況下,若指派為部門管理員的講師遭到刪除,則會同時刪除部門。 在這種情況下,限制規則會更有意義。 下列 fluent API 會設定限制規則並停用串聯刪除。

modelBuilder.Entity<Department>()
   .HasOne(d => d.Administrator)
   .WithMany()
   .OnDelete(DeleteBehavior.Restrict)

Enrollment 外部索引鍵及導覽屬性

註冊記錄是某位學生參加的一門課程。

Enrollment entity

以下列程式碼來更新 Models/Enrollment.cs

using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}

FK 屬性及導覽屬性反映了下列關聯性:

註冊記錄乃針對一個課程,因此當中包含了一個 CourseID FK 屬性及一個 Course 導覽屬性:

public int CourseID { get; set; }
public Course Course { get; set; }

註冊記錄乃針對一位學生,因此當中包含了一個 StudentID FK 屬性及一個 Student 導覽屬性:

public int StudentID { get; set; }
public Student Student { get; set; }

多對多關聯性

StudentCourse 實體之間存在一個多對多關聯性。 Enrollment 實體的功能為資料庫中一個「具有承載」的多對多聯結資料表。 「具有承載」表示 Enrollment 資料表除了聯結資料表的 FK 之外,還包含了額外的資料。 在 Enrollment 實體中,FK 以外的其他資料是 PK 和 Grade

下列圖例展示了在實體圖表中這些關聯性的樣子。 (此圖表是使用 EF 6.x 的 EF Power Tools 產生的。建立圖表並不是此教學課程的一部分)。

Student-Course many to many relationship

每個關聯性線條都在其中一端有一個「1」,並在另外一端有一個「星號 (*)」,顯示其為一對多關聯性。

Enrollment 資料表並未包含成績資訊,則其便只需要包含兩個 FK:CourseIDStudentID。 沒有承載的多對多聯結資料表有時候也稱為「純聯結資料表 (PJT)」。

InstructorCourse 實體具有使用 PJT 的多對多關聯性。

更新資料庫內容

以下列程式碼來更新 Data/SchoolContext.cs

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable(nameof(Course))
                .HasMany(c => c.Instructors)
                .WithMany(i => i.Courses);
            modelBuilder.Entity<Student>().ToTable(nameof(Student));
            modelBuilder.Entity<Instructor>().ToTable(nameof(Instructor));
        }
    }
}

上述程式碼新增了新實體,並設定 InstructorCourse 實體之間的多對多關聯性。

屬性的 Fluent API 替代項目

上述程式碼中的 OnModelCreating 方法使用 fluent API 來設定 EF Core 行為。 此 API 稱為 "fluent" ,因為其常常會用於將一系列的方法呼叫串在一起,使其成為一個單一陳述式。 下列程式碼為 Fluent API 的其中一個範例:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
}

在本教學課程中,Fluent API 僅會用於無法使用屬性完成的資料庫對應。 然而,Fluent API 可指定大部分透過屬性可完成的格式、驗證及對應規則。

某些屬性 (例如 MinimumLength) 無法使用 Fluent API 來套用。 MinimumLength 不會變更結構描述。它只會套用一項最小長度驗證規則。

某些開發人員偏好單獨使用 Fluent API,使其實體類別保持整潔。 屬性和 Fluent API 可混合使用。 有一些組態只能使用 Fluent API 來完成 (例如,指定複合 PK)。 有一些設定只能透過屬性完成 (MinimumLength)。 使用 Fluent API 或屬性的建議做法為:

  • 從這兩種方法中選擇一項。
  • 持續且盡量使用您選擇的方法。

本教學課程中使用到的某些屬性主要用於:

  • 僅驗證 (例如,MinimumLength)。
  • 僅 EF Core 組態 (例如,HasKey)。
  • 驗證及 EF Core 組態 (例如,[StringLength(50)])。

如需屬性與 Fluent API 的詳細資訊,請參閱組態方法

植入資料庫

更新 Data/DbInitializer.cs 中的程式碼:

using ContosoUniversity.Models;
using System;
using System.Collections.Generic;
using System.Linq;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var alexander = new Student
            {
                FirstMidName = "Carson",
                LastName = "Alexander",
                EnrollmentDate = DateTime.Parse("2016-09-01")
            };

            var alonso = new Student
            {
                FirstMidName = "Meredith",
                LastName = "Alonso",
                EnrollmentDate = DateTime.Parse("2018-09-01")
            };

            var anand = new Student
            {
                FirstMidName = "Arturo",
                LastName = "Anand",
                EnrollmentDate = DateTime.Parse("2019-09-01")
            };

            var barzdukas = new Student
            {
                FirstMidName = "Gytis",
                LastName = "Barzdukas",
                EnrollmentDate = DateTime.Parse("2018-09-01")
            };

            var li = new Student
            {
                FirstMidName = "Yan",
                LastName = "Li",
                EnrollmentDate = DateTime.Parse("2018-09-01")
            };

            var justice = new Student
            {
                FirstMidName = "Peggy",
                LastName = "Justice",
                EnrollmentDate = DateTime.Parse("2017-09-01")
            };

            var norman = new Student
            {
                FirstMidName = "Laura",
                LastName = "Norman",
                EnrollmentDate = DateTime.Parse("2019-09-01")
            };

            var olivetto = new Student
            {
                FirstMidName = "Nino",
                LastName = "Olivetto",
                EnrollmentDate = DateTime.Parse("2011-09-01")
            };

            var students = new Student[]
            {
                alexander,
                alonso,
                anand,
                barzdukas,
                li,
                justice,
                norman,
                olivetto
            };

            context.AddRange(students);

            var abercrombie = new Instructor
            {
                FirstMidName = "Kim",
                LastName = "Abercrombie",
                HireDate = DateTime.Parse("1995-03-11")
            };

            var fakhouri = new Instructor
            {
                FirstMidName = "Fadi",
                LastName = "Fakhouri",
                HireDate = DateTime.Parse("2002-07-06")
            };

            var harui = new Instructor
            {
                FirstMidName = "Roger",
                LastName = "Harui",
                HireDate = DateTime.Parse("1998-07-01")
            };

            var kapoor = new Instructor
            {
                FirstMidName = "Candace",
                LastName = "Kapoor",
                HireDate = DateTime.Parse("2001-01-15")
            };

            var zheng = new Instructor
            {
                FirstMidName = "Roger",
                LastName = "Zheng",
                HireDate = DateTime.Parse("2004-02-12")
            };

            var instructors = new Instructor[]
            {
                abercrombie,
                fakhouri,
                harui,
                kapoor,
                zheng
            };

            context.AddRange(instructors);

            var officeAssignments = new OfficeAssignment[]
            {
                new OfficeAssignment {
                    Instructor = fakhouri,
                    Location = "Smith 17" },
                new OfficeAssignment {
                    Instructor = harui,
                    Location = "Gowan 27" },
                new OfficeAssignment {
                    Instructor = kapoor,
                    Location = "Thompson 304" }
            };

            context.AddRange(officeAssignments);

            var english = new Department
            {
                Name = "English",
                Budget = 350000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = abercrombie
            };

            var mathematics = new Department
            {
                Name = "Mathematics",
                Budget = 100000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = fakhouri
            };

            var engineering = new Department
            {
                Name = "Engineering",
                Budget = 350000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = harui
            };

            var economics = new Department
            {
                Name = "Economics",
                Budget = 100000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = kapoor
            };

            var departments = new Department[]
            {
                english,
                mathematics,
                engineering,
                economics
            };

            context.AddRange(departments);

            var chemistry = new Course
            {
                CourseID = 1050,
                Title = "Chemistry",
                Credits = 3,
                Department = engineering,
                Instructors = new List<Instructor> { kapoor, harui }
            };

            var microeconomics = new Course
            {
                CourseID = 4022,
                Title = "Microeconomics",
                Credits = 3,
                Department = economics,
                Instructors = new List<Instructor> { zheng }
            };

            var macroeconmics = new Course
            {
                CourseID = 4041,
                Title = "Macroeconomics",
                Credits = 3,
                Department = economics,
                Instructors = new List<Instructor> { zheng }
            };

            var calculus = new Course
            {
                CourseID = 1045,
                Title = "Calculus",
                Credits = 4,
                Department = mathematics,
                Instructors = new List<Instructor> { fakhouri }
            };

            var trigonometry = new Course
            {
                CourseID = 3141,
                Title = "Trigonometry",
                Credits = 4,
                Department = mathematics,
                Instructors = new List<Instructor> { harui }
            };

            var composition = new Course
            {
                CourseID = 2021,
                Title = "Composition",
                Credits = 3,
                Department = english,
                Instructors = new List<Instructor> { abercrombie }
            };

            var literature = new Course
            {
                CourseID = 2042,
                Title = "Literature",
                Credits = 4,
                Department = english,
                Instructors = new List<Instructor> { abercrombie }
            };

            var courses = new Course[]
            {
                chemistry,
                microeconomics,
                macroeconmics,
                calculus,
                trigonometry,
                composition,
                literature
            };

            context.AddRange(courses);

            var enrollments = new Enrollment[]
            {
                new Enrollment {
                    Student = alexander,
                    Course = chemistry,
                    Grade = Grade.A
                },
                new Enrollment {
                    Student = alexander,
                    Course = microeconomics,
                    Grade = Grade.C
                },
                new Enrollment {
                    Student = alexander,
                    Course = macroeconmics,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = alonso,
                    Course = calculus,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = alonso,
                    Course = trigonometry,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = alonso,
                    Course = composition,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = anand,
                    Course = chemistry
                },
                new Enrollment {
                    Student = anand,
                    Course = microeconomics,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = barzdukas,
                    Course = chemistry,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = li,
                    Course = composition,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = justice,
                    Course = literature,
                    Grade = Grade.B
                }
            };

            context.AddRange(enrollments);
            context.SaveChanges();
        }
    }
}

上述程式碼為新的實體提供了種子資料。 此程式碼中的大部分主要用於建立新的實體物件並載入範例資料。 範例資料主要用於測試。

套用移轉或卸除並重新建立

對於現有的資料庫,有兩種變更資料庫的方法:

這兩種選擇都適用於 SQL Server。 雖然套用移轉方法更複雜且耗時,但它是現實世界生產環境的慣用方法。

卸除並重新建立資料庫

強制 EF Core 建立新的資料庫、卸除並更新資料庫:

  • 刪除 Migrations 資料夾。
  • [套件管理員主控台] (PMC) 中,執行下列命令:
Drop-Database
Add-Migration InitialCreate
Update-Database

執行應用程式。 執行應用程式會執行 DbInitializer.Initialize 方法。 DbInitializer.Initialize 會填入新的資料庫。

在 SSOX 中開啟資料庫:

  • 若先前已開啟過 SSOX,按一下 [重新整理] 按鈕。
  • 展開 [資料表] 節點。 建立的資料表便會顯示。

下一步

接下來的兩個教學課程會示範如何讀取和更新相關資料。

先前的教學課程建立了基本的資料模型,該模型由三個實體組成。 在本教學課程中:

  • 新增更多實體和關聯性。
  • 藉由指定格式、驗證和資料庫對應規則來自訂資料模型。

已完成的資料模型如下圖所示:

Entity diagram

Student 實體

Student entity

以下列程式碼取代 Models/Student.cs 中的程式碼:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }
        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

上述程式碼會新增 FullName 屬性,並將下列屬性新增到現有屬性:

  • [DataType]
  • [DisplayFormat]
  • [StringLength]
  • [Column]
  • [Required]
  • [Display]

FullName 計算屬性

FullName 為一個計算屬性,會傳回藉由串連兩個其他屬性而建立的值。 您無法設定 FullName,因此它只會有 get 存取子。 資料庫中不會建立 FullName 資料行。

DataType 屬性

[DataType(DataType.Date)]

針對學生註冊日期,所有頁面目前都會同時顯示日期和一天當中的時間,雖然只要日期才是重要項目。 透過使用資料註解屬性,您便可以只透過單一程式碼變更來修正每個顯示資料頁面中的顯示格式。

DataType 屬性會指定一個比資料庫內建類型更明確的資料類型。 在此情況下,該欄位應該只顯示日期,而不會同時顯示日期和時間。 DataType 列舉提供了許多資料類型,例如 Date、Time、PhoneNumber、Currency、EmailAddress 等。DataType 屬性也可以讓應用程式自動提供限定於某些類型的功能。 例如:

  • DataType.EmailAddress 會自動建立 mailto: 連結。
  • DataType.Date 在大多數的瀏覽器中都會提供日期選取器。

DataType 屬性會發出 HTML5 data- (發音為 data dash) 屬性。 DataType 屬性不會提供驗證。

DisplayFormat 屬性

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

DataType.Date 未指定顯示日期的格式。 根據預設,日期欄位會依照根據伺服器的 CultureInfo 為基礎的預設格式顯示。

DisplayFormat 屬性會用來明確指定日期格式。 ApplyFormatInEditMode 設定會指定格式也應套用在編輯 UI。 某些欄位不應該使用 ApplyFormatInEditMode。 例如,貨幣符號通常不應顯示在編輯文字方塊中。

DisplayFormat 屬性可由自身使用。 通常使用 DataType 屬性搭配 DisplayFormat 屬性是一個不錯的做法。 DataType 屬性會將資料的語意以其在螢幕上呈現方式的相反方式傳遞。 DataType 屬性提供了下列優點,並且這些優點在 DisplayFormat 中無法使用:

  • 瀏覽器可以啟用 HTML5 功能。 例如,顯示日曆控制項、適合地區設定的貨幣符號、電子郵件連結和用戶端輸入驗證。
  • 根據預設,瀏覽器將根據地區設定,使用正確的格式呈現資料。

如需詳細資訊,請參閱 <input> 標籤協助程式文件

StringLength 屬性

[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]

您可使用屬性指定資料驗證規則和驗證錯誤訊息。 StringLength 屬性指定了在資料欄位中允許的最小及最大字元長度。 此處所顯示的程式碼會限制名稱不得超過 50 個字元。 設定字串長度下限的範例會在稍後顯示。

StringLength 屬性同時也提供了用戶端和伺服器端的驗證。 最小值對資料庫結構描述不會造成任何影響。

StringLength 屬性不會防止使用者在名稱中輸入空白字元。 RegularExpression 屬性可用來將限制套用到輸入。 例如,下列程式碼會要求第一個字元必須是大寫,其餘字元則必須是英文字母:

[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]

在 [SQL Server 物件總管] (SSOX) 中,按兩下 [Student] 資料表來開啟 Student 資料表設計工具。

Students table in SSOX before migrations

上述影像顯示了 Student 資料表的結構描述。 名稱欄位的型別為 nvarchar(MAX)。 在本教學課程稍後建立移轉並套用時,名稱欄位會因為字串長度屬性而變更為 nvarchar(50)

Column 屬性

[Column("FirstName")]
public string FirstMidName { get; set; }

可控制類別和屬性如何對應到資料庫的屬性。 在 Student 模型中,Column 屬性會用來將 FirstMidName 屬性名稱對應到資料庫中的 "FirstName"。

建立資料庫時,模型上的屬性名稱會用來作為資料行名稱 (使用 Column 屬性時除外)。 Student 模型針對名字欄位使用 FirstMidName,因為欄位中可能也會包含中間名。

透過 [Column] 屬性,資料模型中 Student.FirstMidName 會對應到 Student 資料表的 FirstName 資料行。 新增 Column 屬性會變更支援 SchoolContext 的模型。 支援 SchoolContext 的模型不再符合資料庫。 這種不一致會透過在本教學課程中稍後部分新增移轉來解決。

Required 屬性

[Required]

Required 屬性會讓名稱屬性成為必要欄位。 針對不可為 Null 型別的實值型別 (例如 DateTimeintdouble),Required 屬性並非必要項目。 不可為 Null 的類型會自動視為必要欄位。

Required 屬性必須搭配 MinimumLength 使用,才能強制執行 MinimumLength

[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }

MinimumLengthRequired 允許空白字元以滿足驗證。 使用 RegularExpression 屬性來完全控制字串。

Display 屬性

[Display(Name = "Last Name")]

Display 屬性指定了文字方塊的標題應為「名字」、「姓氏」、「全名」及「註冊日期」。預設標題中沒有使用空白分隔文字,例如「姓氏」。

建立移轉

執行應用程式並移至 Students 頁面。 擲回例外狀況。 [Column] 屬性會造成 EF 預期尋找名為 FirstName 的資料行,但資料庫中的資料行名稱仍是 FirstMidName

錯誤訊息會與下列範例相似:

SqlException: Invalid column name 'FirstName'.
  • 在 PMC 中,輸入下列命令來建立新的移轉並更新資料庫:

    Add-Migration ColumnFirstName
    Update-Database
    

    這些命令中的第一個命令會產生下列警告訊息:

    An operation was scaffolded that may result in the loss of data.
    Please review the migration for accuracy.
    

    產生警告的理由是名稱欄位現在限制長度為 50 個字元。 若資料庫中的名稱超過 50 個字元,則第 51 到最後一個字元便會遺失。

  • 在 SSOX 中開啟 Student 資料表:

    Students table in SSOX after migrations

    在套用移轉前,名稱資料行的型別為 nvarchar(MAX)。 名稱資料行現在為 nvarchar(50)。 資料行的名稱已從 FirstMidName 變更為 FirstName

  • 執行應用程式並移至 Students 頁面。
  • 請注意時間並未輸入或和日期一同顯示。
  • 選取 [新建] 然後嘗試輸入超過 50 個字元的名稱。

注意

在下列各節中,在某些階段建置應用程式會產生編譯器錯誤。 指令會指定何時應建置應用程式。

Instructor 實體

Instructor entity

以下列程式碼來建立 Models/Instructor.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor
    {
        public int ID { get; set; }

        [Required]
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

        public ICollection<CourseAssignment> CourseAssignments { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}

在同一行上可以有多個屬性。 HireDate 屬性可以下列方式撰寫:

[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

CourseAssignmentsOfficeAssignment 屬性為導覽屬性。

由於講師可以教授任何數量的課程,因此 CourseAssignments 已定義為一個集合。

public ICollection<CourseAssignment> CourseAssignments { get; set; }

講師最多只能擁有一間辦公室,因此 OfficeAssignment 屬性會保存單一 OfficeAssignment 實體。 若沒有指派任何辦公室,則 OfficeAssignment 為 Null。

public OfficeAssignment OfficeAssignment { get; set; }

OfficeAssignment 實體

OfficeAssignment entity

以下列程式碼來建立 Models/OfficeAssignment.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public Instructor Instructor { get; set; }
    }
}

Key 屬性

[Key] 屬性用於將某個屬性名稱不是 classnameID 或 ID 的屬性識別為主索引鍵 (PK)。

InstructorOfficeAssignment 實體之間有一對零或一關聯性。 辦公室指派只存在於與其受指派的講師關聯中。 OfficeAssignment PK 同時也是其連結到 Instructor 實體的外部索引鍵 (FK)。

EF Core 無法將 InstructorID 自動識別為 OfficeAssignment 的主索引鍵,因為 InstructorID 並未遵循識別碼或 classnameID 命名慣例。 因此,必須使用 Key 屬性將 InstructorID 識別為 PK:

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

根據預設,EF Core 會將索引鍵作為非資料庫產生的屬性處置,因為該資料行主要用於識別關聯性。

Instructor 導覽屬性

Instructor.OfficeAssignment 導覽屬性可以是 Null,因為指定講師可能不會有 OfficeAssignment 資料列。 講師可能沒有辦公室指派。

OfficeAssignment.Instructor 導覽屬性一律會擁有一個講師實體,因為外部索引鍵 InstructorID 的型別是 int,其為不可為 Null 的實值型別。 辦公室指派無法獨立於講師之外存在。

Instructor 實體有相關的 OfficeAssignment 實體時,每一個實體都會在其導覽屬性中包含一個其他實體的參考。

Course 實體

Course entity

以下列程式碼來更新 Models/Course.cs

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        [Display(Name = "Number")]
        public int CourseID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Title { get; set; }

        [Range(0, 5)]
        public int Credits { get; set; }

        public int DepartmentID { get; set; }

        public Department Department { get; set; }
        public ICollection<Enrollment> Enrollments { get; set; }
        public ICollection<CourseAssignment> CourseAssignments { get; set; }
    }
}

Course 實體具有外部索引鍵 (FK) 屬性DepartmentIDDepartmentID 會指向相關的 Department 實體。 Course 實體具有一個 Department 導覽屬性。

若模型針對相關實體已有導覽屬性,則 EF Core 針對資料模型便不需要外部索引鍵屬性。 EF Core 會自動在資料庫中需要的任何地方建立 FK。 EF Core 會為自動建立的 FK 建立陰影屬性。 但是,在資料模型中明確包含 FK (外部索引鍵) 可讓更新更簡單且更有效率。 例如,假設有一個模型,當中「不」包含 DepartmentID FK 屬性。 當擷取課程實體以進行編輯時:

  • Department 屬性未明確載入,則其為 Null。
  • 若要更新課程實體,必須先擷取 Department 實體。

當 FK 屬性 DepartmentID 包含在資料模型中時,便不需要在更新前擷取 Department 實體。

DatabaseGenerated 屬性

[DatabaseGenerated(DatabaseGeneratedOption.None)] 屬性會指定 PK 是由應用程式提供的,而非資料庫產生的。

[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

預設情況下,EF Core 會假定 PK 值是由資料庫產生的。 由資料庫產生主索引鍵通常是最佳做法。 針對 Course 實體,使用者指定了 PK。 例如,課程號碼 1000 系列表示數學部門的課程,2000 系列則為英文部門的課程。

DatabaseGenerated 屬性也可用於產生預設值。 例如,資料庫可以自動產生日期欄位來記錄建立或更新資料列的日期。 如需詳細資訊,請參閱產生的屬性

外部索引鍵及導覽屬性

Course 實體中的外部索引鍵 (FK) 屬性和導覽屬性反映了下列關聯性:

由於一個課程已指派給了一個部門,因此當中具有 DepartmentID FK 和 Department 導覽屬性。

public int DepartmentID { get; set; }
public Department Department { get; set; }

由於課程可由任何數量的學生進行註冊,因此 Enrollments 導覽屬性為一個集合:

public ICollection<Enrollment> Enrollments { get; set; }

課程可由多個講師進行教授,因此 CourseAssignments 導覽屬性為一個集合:

public ICollection<CourseAssignment> CourseAssignments { get; set; }

CourseAssignment 會在稍後進行解釋。

Department 實體

Department entity

以下列程式碼來建立 Models/Department.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}

Column 屬性

先前,Column 屬性主要用於變更資料行的名稱對應。 在 Department 實體的程式碼中,Column 屬性則用於變更 SQL 資料類型對應。 Budget 資料行在資料庫中是使用 SQL Server 的 money 型別來定義:

[Column(TypeName="money")]
public decimal Budget { get; set; }

通常您不需要資料行對應。 EF Core 會根據屬性的 CLR 型別來選擇適當 SQL Server 資料型別。 CLR decimal 類型會對應到 SQL Server 的 decimal 類型。 由於 Budget 是貨幣,因此金額資料類型會比較適合貨幣。

外部索引鍵及導覽屬性

FK 及導覽屬性反映了下列關聯性:

  • 部門可能有或可能沒有系統管理員。
  • 系統管理員一律為講師。 因此,InstructorID 已作為 FK 包含在 Instructor 實體中。

導覽屬性已命名為 Administrator,但其中保留了一個 Instructor 實體:

public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }

上述程式碼中的問號 (?) 表示屬性可為 Null。

部門中可能包含許多課程,因此當中包含了一個 Course 導覽屬性:

public ICollection<Course> Courses { get; set; }

根據慣例,EF Core 會為不可為 Null 的 FK 和多對多關聯性啟用串聯刪除。 此預設行為可能會導致循環串聯刪除規則。 循環串聯刪除規則會在新增移轉時造成例外狀況。

例如,若 Department.InstructorID 屬性已定義成不可為 Null,EF Core 便會設定串聯刪除規則。 在這種情況下,若指派為部門管理員的講師遭到刪除,則會同時刪除部門。 在這種情況下,限制規則會更有意義。 下列 fluent API 會設定限制規則並停用串聯刪除。

modelBuilder.Entity<Department>()
   .HasOne(d => d.Administrator)
   .WithMany()
   .OnDelete(DeleteBehavior.Restrict)

Enrollment 實體

註冊記錄是某位學生參加的一門課程。

Enrollment entity

以下列程式碼來更新 Models/Enrollment.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}

外部索引鍵及導覽屬性

FK 屬性及導覽屬性反映了下列關聯性:

註冊記錄乃針對一個課程,因此當中包含了一個 CourseID FK 屬性及一個 Course 導覽屬性:

public int CourseID { get; set; }
public Course Course { get; set; }

註冊記錄乃針對一位學生,因此當中包含了一個 StudentID FK 屬性及一個 Student 導覽屬性:

public int StudentID { get; set; }
public Student Student { get; set; }

多對多關聯性

StudentCourse 實體之間存在一個多對多關聯性。 Enrollment 實體的功能為資料庫中一個「具有承載」的多對多聯結資料表。 「具有承載」表示 Enrollment 資料表除了聯結資料表 (在此案例中為 PK 和 Grade) 的 FK 之外,還包含了額外的資料。

下列圖例展示了在實體圖表中這些關聯性的樣子。 (此圖表是使用 EF 6.x 的 EF Power Tools 產生的。建立圖表並不是此教學課程的一部分)。

Student-Course many to many relationship

每個關聯性線條都在其中一端有一個「1」,並在另外一端有一個「星號 (*)」,顯示其為一對多關聯性。

Enrollment 資料表並未包含成績資訊,則其便只需要包含兩個 FK (CourseIDStudentID)。 沒有承載的多對多聯結資料表有時候也稱為「純聯結資料表 (PJT)」。

InstructorCourse 實體具有使用了純聯結資料表的多對多關聯性。

注意:EF 6.x 支援多對多關聯性的隱含聯結資料表,但 EF Core 不支援。 如需詳細資訊,請參閱 EF Core 2.0 中的多對多關聯性

CourseAssignment 實體

CourseAssignment entity

以下列程式碼來建立 Models/CourseAssignment.cs

namespace ContosoUniversity.Models
{
    public class CourseAssignment
    {
        public int InstructorID { get; set; }
        public int CourseID { get; set; }
        public Instructor Instructor { get; set; }
        public Course Course { get; set; }
    }
}

Instructor 對 Courses 的多對多關聯性需要聯結資料表,而該聯結資料表的實體為 CourseAssignment。

Instructor-to-Courses m:M

通常會將聯結實體命名為 EntityName1EntityName2。 例如,使用此模式的 Instructor 對 Courses 聯結資料表將會是 CourseInstructor。 不過,我們建議使用可描述關聯性的名稱。

資料模型一開始都是簡單的,之後便會持續成長。 不含承載的聯結資料表 (PJT) 常常會演變成包含承載。 藉由在一開始便使用描述性的實體名稱,當聯結資料表變更時便不需要變更名稱。 理想情況下,聯結實體在公司網域中會有自己的自然 (可能為一個單字) 名稱。 例如,「書籍」和「客戶」可連結為一個名為「評分」 的聯結實體。 針對講師-課程多對多關聯性,CourseAssignment 會比 CourseInstructor 來得好。

複合索引鍵

CourseAssignment (InstructorIDCourseID) 中的兩個 FK 一起搭配使用,便可唯一的識別 CourseAssignment 資料表中的每一個資料列。 CourseAssignment 並不需要其專屬的 PK。 InstructorIDCourseID 屬性的功能便是複合 PK。 為 EF Core 指定複合 PK 的唯一方法是使用 fluent API。 下節會說明如何設定複合 PK。

複合索引鍵可確保:

  • 一個課程可以有多個資料列。
  • 一個講師可以有多個資料列。
  • 不允許相同講師和課程的多個資料列。

由於 Enrollment 聯結實體定義了其自身的 PK,因此這種種類的重複項目是可能的。 若要防止這類重複項目:

  • 在 FK 欄位中新增一個唯一的索引,或
  • Enrollment 設定一個主複合索引鍵,與 CourseAssignment 相似。 如需詳細資訊,請參閱索引

更新資料庫內容

以下列程式碼來更新 Data/SchoolContext.cs

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
        public DbSet<CourseAssignment> CourseAssignments { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
            modelBuilder.Entity<Department>().ToTable("Department");
            modelBuilder.Entity<Instructor>().ToTable("Instructor");
            modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
            modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");

            modelBuilder.Entity<CourseAssignment>()
                .HasKey(c => new { c.CourseID, c.InstructorID });
        }
    }
}

上述程式碼會新增一個新實體,並設定 CourseAssignment 實體的複合 PK。

屬性的 Fluent API 替代項目

上述程式碼中的 OnModelCreating 方法使用 fluent API 來設定 EF Core 行為。 此 API 稱為 "fluent" ,因為其常常會用於將一系列的方法呼叫串在一起,使其成為一個單一陳述式。 下列程式碼為 Fluent API 的其中一個範例:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
}

在本教學課程中,Fluent API 僅會用於無法使用屬性完成的資料庫對應。 然而,Fluent API 可指定大部分透過屬性可完成的格式、驗證及對應規則。

某些屬性 (例如 MinimumLength) 無法使用 Fluent API 來套用。 MinimumLength 不會變更結構描述。它只會套用一項最小長度驗證規則。

某些開發人員偏好單獨使用 Fluent API,使其實體類別保持「整潔」。屬性和 Fluent API 可混合使用。 有一些設定只能透過 Fluent API 完成 (指定複合 PK)。 有一些設定只能透過屬性完成 (MinimumLength)。 使用 Fluent API 或屬性的建議做法為:

  • 從這兩種方法中選擇一項。
  • 持續且盡量使用您選擇的方法。

本教學課程中使用到的某些屬性主要用於:

  • 僅驗證 (例如,MinimumLength)。
  • 僅 EF Core 組態 (例如,HasKey)。
  • 驗證及 EF Core 組態 (例如,[StringLength(50)])。

如需屬性與 Fluent API 的詳細資訊,請參閱組態方法

實體圖表

下圖顯示了 EF Power Tools 為完成的 School 模型建立的圖表。

Entity diagram

上述圖表顯示:

  • 數個一對多關聯性線條 (1 對 *)。
  • InstructorOfficeAssignment 實體之間的一對零或一關聯性線條 (1 對 0..1)。
  • InstructorDepartment 實體之間的零或一對多關聯性線條 (0..1 對 *)。

植入資料庫

更新 Data/DbInitializer.cs 中的程式碼:

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            //context.Database.EnsureCreated();

            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var students = new Student[]
            {
                new Student { FirstMidName = "Carson",   LastName = "Alexander",
                    EnrollmentDate = DateTime.Parse("2016-09-01") },
                new Student { FirstMidName = "Meredith", LastName = "Alonso",
                    EnrollmentDate = DateTime.Parse("2018-09-01") },
                new Student { FirstMidName = "Arturo",   LastName = "Anand",
                    EnrollmentDate = DateTime.Parse("2019-09-01") },
                new Student { FirstMidName = "Gytis",    LastName = "Barzdukas",
                    EnrollmentDate = DateTime.Parse("2018-09-01") },
                new Student { FirstMidName = "Yan",      LastName = "Li",
                    EnrollmentDate = DateTime.Parse("2018-09-01") },
                new Student { FirstMidName = "Peggy",    LastName = "Justice",
                    EnrollmentDate = DateTime.Parse("2017-09-01") },
                new Student { FirstMidName = "Laura",    LastName = "Norman",
                    EnrollmentDate = DateTime.Parse("2019-09-01") },
                new Student { FirstMidName = "Nino",     LastName = "Olivetto",
                    EnrollmentDate = DateTime.Parse("2011-09-01") }
            };

            context.Students.AddRange(students);
            context.SaveChanges();

            var instructors = new Instructor[]
            {
                new Instructor { FirstMidName = "Kim",     LastName = "Abercrombie",
                    HireDate = DateTime.Parse("1995-03-11") },
                new Instructor { FirstMidName = "Fadi",    LastName = "Fakhouri",
                    HireDate = DateTime.Parse("2002-07-06") },
                new Instructor { FirstMidName = "Roger",   LastName = "Harui",
                    HireDate = DateTime.Parse("1998-07-01") },
                new Instructor { FirstMidName = "Candace", LastName = "Kapoor",
                    HireDate = DateTime.Parse("2001-01-15") },
                new Instructor { FirstMidName = "Roger",   LastName = "Zheng",
                    HireDate = DateTime.Parse("2004-02-12") }
            };

            context.Instructors.AddRange(instructors);
            context.SaveChanges();

            var departments = new Department[]
            {
                new Department { Name = "English",     Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Abercrombie").ID },
                new Department { Name = "Mathematics", Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Fakhouri").ID },
                new Department { Name = "Engineering", Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Harui").ID },
                new Department { Name = "Economics",   Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Kapoor").ID }
            };

            context.Departments.AddRange(departments);
            context.SaveChanges();

            var courses = new Course[]
            {
                new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID
                },
                new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
                new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
            };

            context.Courses.AddRange(courses);
            context.SaveChanges();

            var officeAssignments = new OfficeAssignment[]
            {
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID,
                    Location = "Smith 17" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Harui").ID,
                    Location = "Gowan 27" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID,
                    Location = "Thompson 304" },
            };

            context.OfficeAssignments.AddRange(officeAssignments);
            context.SaveChanges();

            var courseInstructors = new CourseAssignment[]
            {
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Kapoor").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Fakhouri").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Literature" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
            };

            context.CourseAssignments.AddRange(courseInstructors);
            context.SaveChanges();

            var enrollments = new Enrollment[]
            {
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    Grade = Grade.A
                },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    Grade = Grade.C
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
                    Grade = Grade.B
                    },
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Li").ID,
                    CourseID = courses.Single(c => c.Title == "Composition").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Justice").ID,
                    CourseID = courses.Single(c => c.Title == "Literature").CourseID,
                    Grade = Grade.B
                    }
            };

            foreach (Enrollment e in enrollments)
            {
                var enrollmentInDataBase = context.Enrollments.Where(
                    s =>
                            s.Student.ID == e.StudentID &&
                            s.Course.CourseID == e.CourseID).SingleOrDefault();
                if (enrollmentInDataBase == null)
                {
                    context.Enrollments.Add(e);
                }
            }
            context.SaveChanges();
        }
    }
}

上述程式碼為新的實體提供了種子資料。 此程式碼中的大部分主要用於建立新的實體物件並載入範例資料。 範例資料主要用於測試。 如需如何植入多對多聯結資料表的範例,請參閱 EnrollmentsCourseAssignments

新增移轉

組建專案。

在 PMC 中,執行下列命令。

Add-Migration ComplexDataModel

上述命令會顯示關於可能發生資料遺失的警告。

An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.
To undo this action, use 'ef migrations remove'

若執行 database update 命令,便會產生下列錯誤:

The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in
database "ContosoUniversity", table "dbo.Department", column 'DepartmentID'.

在下一節中,您會看到如何針對此錯誤採取動作。

套用移轉或卸除並重新建立

現在您已經具有現有的資料庫,您需要思考如何將變更套用到該資料庫。 本教學課程示範兩種替代方法:

這兩種選擇都適用於 SQL Server。 雖然套用移轉方法更複雜且耗時,但它是現實世界生產環境的慣用方法。

卸除並重新建立資料庫

若您正在使用 SQL Server 且想要在下一節中執行套用移轉方法,請跳過此節

強制 EF Core 建立新的資料庫、卸除並更新資料庫:

  • 在 [套件管理員主控台] (PMC) 中,執行下列命令:

    Drop-Database
    
  • 刪除 Migrations 資料夾,然後執行下列命令:

    Add-Migration InitialCreate
    Update-Database
    

執行應用程式。 執行應用程式會執行 DbInitializer.Initialize 方法。 DbInitializer.Initialize 會填入新的資料庫。

在 SSOX 中開啟資料庫:

  • 若先前已開啟過 SSOX,按一下 [重新整理] 按鈕。

  • 展開 [資料表] 節點。 建立的資料表便會顯示。

    Tables in SSOX

  • 檢查 CourseAssignment 資料表:

    • 以滑鼠右鍵按一下 CourseAssignment 資料表,然後選取 [檢視資料]
    • 驗證 CourseAssignment 資料表中是否包含資料。

    CourseAssignment data in SSOX

套用移轉

本節為選擇性。 這些步驟僅適用於 SQL Server LocalDB,且只有在您跳過先前的卸除並重新建立資料庫一節時才適用。

當使用現有的資料執行移轉作業時,某些 FK 條件約束可能會無法透過現有資料滿足。 當您使用的是生產資料時,您必須進行幾個步驟才能移轉現有資料。 本節提供了修正 FK 條件約束違規的範例。 請不要在沒有備份的情況下進行這些程式碼變更。 若您已完成先前的卸除並重新建立資料庫一節,請不要變更這些程式碼。

{timestamp}_ComplexDataModel.cs 檔案包含下列程式碼:

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    type: "int",
    nullable: false,
    defaultValue: 0);

上述程式碼將一個不可為 Null 的 DepartmentID FK 新增至 Course 資料表。 先前教學課程中的資料庫在 Course 中包含了資料列,因此無法使用移轉來更新資料表。

若要讓 ComplexDataModel 與現有資料進行移轉:

  • 變更程式碼,以給予新資料行 (DepartmentID) 一個新的預設值。
  • 建立一個名為 "Temp" 的假部門以作為預設部門之用。

修正外部索引鍵條件約束

ComplexDataModel 移轉類別中,更新 Up 方法:

  • 開啟 {timestamp}_ComplexDataModel.cs 檔案。
  • 將新增 DepartmentID 資料行至 Course 資料表的程式碼全部標為註解。
migrationBuilder.AlterColumn<string>(
    name: "Title",
    table: "Course",
    maxLength: 50,
    nullable: true,
    oldClrType: typeof(string),
    oldNullable: true);
            
//migrationBuilder.AddColumn<int>(
//    name: "DepartmentID",
//    table: "Course",
//    nullable: false,
//    defaultValue: 0);

新增下列醒目提示程式碼。 新的程式碼位於 .CreateTable( name: "Department" 區塊後方:

migrationBuilder.CreateTable(
    name: "Department",
    columns: table => new
    {
        DepartmentID = table.Column<int>(type: "int", nullable: false)
            .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
        Budget = table.Column<decimal>(type: "money", nullable: false),
        InstructorID = table.Column<int>(type: "int", nullable: true),
        Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
        StartDate = table.Column<DateTime>(type: "datetime2", nullable: false)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_Department", x => x.DepartmentID);
        table.ForeignKey(
            name: "FK_Department_Instructor_InstructorID",
            column: x => x.InstructorID,
            principalTable: "Instructor",
            principalColumn: "ID",
            onDelete: ReferentialAction.Restrict);
    });

 migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
// Default value for FK points to department created above, with
// defaultValue changed to 1 in following AddColumn statement.

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    nullable: false,
    defaultValue: 1);

透過上述變更,現有的 Course 資料列將會在執行 ComplexDataModel.Up 方法後與 "Temp" 部門相關。

此處顯示處理這種情況的方式已針對本教學課程進行簡化。 生產環境的應用程式會:

  • 包含將 Department 資料列及相關 Course 資料列新增到新 Department 資料列的程式碼或指令碼。
  • 不使用 "Temp" 部門或 Course.DepartmentID 的預設值。
  • 在 [套件管理員主控台] (PMC) 中,執行下列命令:

    Update-Database
    

因為 DbInitializer.Initialize 方法的設計僅適用於空白資料庫,所以請使用 SSOX 刪除 Student 和 Course 資料表中的所有資料列。 (串聯刪除會負責處理 Enrollment 資料表。)

執行應用程式。 執行應用程式會執行 DbInitializer.Initialize 方法。 DbInitializer.Initialize 會填入新的資料庫。

下一步

接下來的兩個教學課程會示範如何讀取和更新相關資料。

先前的教學課程建立了基本的資料模型,該模型由三個實體組成。 在本教學課程中:

  • 新增更多實體和關聯性。
  • 藉由指定格式、驗證和資料庫對應規則來自訂資料模型。

下圖顯示已完成資料模型的實體類別:

Entity diagram

若您遭遇到無法解決的問題,請下載完整應用程式

使用屬性自訂資料模型

在本節中,您會使用屬性自訂資料模型。

DataType 屬性

學生頁面目前顯示了註冊日期的時間。 一般而言,日期欄位只會顯示日期,而非時間。

使用下列醒目提示的程式碼更新 Models/Student.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

DataType 屬性會指定一個比資料庫內建類型更明確的資料類型。 在此情況下,該欄位應該只顯示日期,而不會同時顯示日期和時間。 DataType 列舉提供了許多資料類型,例如 Date、Time、PhoneNumber、Currency、EmailAddress 等。DataType 屬性也可以讓應用程式自動提供限定於某些類型的功能。 例如:

  • DataType.EmailAddress 會自動建立 mailto: 連結。
  • DataType.Date 在大多數的瀏覽器中都會提供日期選取器。

DataType 屬性會發出 HTML 5 data- (發音為 data dash) 屬性,可讓 HTML 5 瀏覽器取用。 DataType 屬性不會提供驗證。

DataType.Date 未指定顯示日期的格式。 根據預設,日期欄位會依照根據伺服器的 CultureInfo 為基礎的預設格式顯示。

DisplayFormat 屬性用來明確指定日期格式:

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

ApplyFormatInEditMode 設定會指定格式也應套用在編輯 UI。 某些欄位不應該使用 ApplyFormatInEditMode。 例如,貨幣符號通常不應顯示在編輯文字方塊中。

DisplayFormat 屬性可由自身使用。 通常使用 DataType 屬性搭配 DisplayFormat 屬性是一個不錯的做法。 DataType 屬性會將資料的語意以其在螢幕上呈現方式的相反方式傳遞。 DataType 屬性提供了下列優點,並且這些優點在 DisplayFormat 中無法使用:

  • 瀏覽器可以啟用 HTML5 功能。 例如,顯示日曆控制項、適合地區設定的貨幣符號、電子郵件連結、用戶端輸入驗證等。
  • 根據預設,瀏覽器將根據地區設定,使用正確的格式呈現資料。

如需詳細資訊,請參閱 <input> 標籤協助程式文件

執行應用程式。 巡覽至 Students [索引] 頁面。 時間將不再顯示。 使用 Student 模型的每個檢視現在都只會顯示日期,而不會顯示時間。

Students index page showing dates without times

StringLength 屬性

您可使用屬性指定資料驗證規則和驗證錯誤訊息。 StringLength 屬性指定了在資料欄位中允許的最小及最大字元長度。 StringLength 屬性同時也提供了用戶端和伺服器端的驗證。 最小值對資料庫結構描述不會造成任何影響。

使用下列程式碼更新 Student 模型:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

上述的程式碼會限制名稱不得超過 50 個字元。 StringLength 屬性不會防止使用者在名稱中輸入空白字元。 RegularExpression 屬性可用於對輸入套用限制。 例如,下列程式碼會要求第一個字元必須是大寫,其餘字元則必須是英文字母:

[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]

執行應用程式:

  • 巡覽至 Students 頁面。
  • 選取 [新建],然後輸入超過 50 個字元的名稱。
  • 選取 [建立],用戶端驗證便會顯示錯誤訊息。

Students index page showing string length errors

在 [SQL Server 物件總管] (SSOX) 中,按兩下 [Student] 資料表來開啟 Student 資料表設計工具。

Students table in SSOX before migrations

上述影像顯示了 Student 資料表的結構描述。 名稱欄位的類型為 nvarchar(MAX),因為移轉尚未在資料庫中執行。 在本教學課程的稍後執行移轉後,名稱欄位便會成為 nvarchar(50)

Column 屬性

可控制類別和屬性如何對應到資料庫的屬性。 在本節中,Column 屬性會用於將 FirstMidName 屬性的名稱對應到資料庫中的 "FirstName"。

當建立資料庫時,模型上的屬性名稱會用於資料行名稱 (除了使用 Column 屬性時之外)。

Student 模型針對名字欄位使用 FirstMidName,因為欄位中可能也會包含中間名。

使用下列醒目提示的程式碼更新 Student.cs 檔案:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

經過上述變更之後,應用程式中的 Student.FirstMidName 會對應到 Student 資料表的 FirstName 資料行。

新增 Column 屬性會變更支援 SchoolContext 的模型。 支援 SchoolContext 的模型不再符合資料庫。 若應用程式在套用移轉之前執行,便會產生下列例外狀況:

SqlException: Invalid column name 'FirstName'.

若要更新資料庫:

  • 組建專案。
  • 在專案資料夾中開啟命令視窗。 輸入下列命令來建立新的移轉並更新資料庫:
Add-Migration ColumnFirstName
Update-Database

migrations add ColumnFirstName 命令會產生下列警告訊息:

An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.

產生警告的理由是名稱欄位現在限制長度為 50 個字元。 若在資料庫中有名稱超過 50 個字元,第 51 個字元到最後一個字元便會遺失。

  • 測試應用程式。

在 SSOX 中開啟 Student 資料表:

Students table in SSOX after migrations

在移轉套用之前,名稱資料行的類型為 nvarchar(MAX)。 名稱資料行現在為 nvarchar(50)。 資料行的名稱已從 FirstMidName 變更為 FirstName

注意

在下節中,在某些階段建置應用程式會產生編譯錯誤。 指令會指定何時應建置應用程式。

Student 實體更新

Student entity

以下列程式碼來更新 Models/Student.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }
        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

Required 屬性

Required 屬性會讓名稱屬性成為必要欄位。 針對不可為 Null 的類型,例如實值型別 (DateTimeintdouble 等) 等,Required 屬性是不需要的。 不可為 Null 的類型會自動視為必要欄位。

Required 屬性可由 StringLength 屬性中的最小長度參數取代:

[Display(Name = "Last Name")]
[StringLength(50, MinimumLength=1)]
public string LastName { get; set; }

Display 屬性

Display 屬性指定了文字方塊的標題應為「名字」、「姓氏」、「全名」及「註冊日期」。預設標題中沒有使用空白分隔文字,例如「姓氏」。

FullName 計算屬性

FullName 為一個計算屬性,會傳回藉由串連兩個其他屬性而建立的值。 FullName 無法進行設定,其只有 get 存取子。 資料庫中不會建立 FullName 資料行。

建立 Instructor 實體

Instructor entity

以下列程式碼來建立 Models/Instructor.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor
    {
        public int ID { get; set; }

        [Required]
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

        public ICollection<CourseAssignment> CourseAssignments { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}

在同一行上可以有多個屬性。 HireDate 屬性可以下列方式撰寫:

[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

CourseAssignments 和 OfficeAssignment 導覽屬性

CourseAssignmentsOfficeAssignment 屬性為導覽屬性。

由於講師可以教授任何數量的課程,因此 CourseAssignments 已定義為一個集合。

public ICollection<CourseAssignment> CourseAssignments { get; set; }

若導覽屬性中保留了多個實體:

  • 它必須是一種清單類型,可對其中的實體進行新增、刪除和更新。

導覽屬性類型包括:

  • ICollection<T>
  • List<T>
  • HashSet<T>

如果指定了 ICollection<T>,則 EF Core 預設會建立 HashSet<T> 集合。

CourseAssignment 實體會在本節的多對多關聯性中解釋。

Contoso 大學商務規則訂定了講師最多只能有一間辦公室。 OfficeAssignment 屬性保留了一個單一 OfficeAssignment 實體。 若沒有指派任何辦公室,則 OfficeAssignment 為 Null。

public OfficeAssignment OfficeAssignment { get; set; }

建立 OfficeAssignment 實體

OfficeAssignment entity

以下列程式碼來建立 Models/OfficeAssignment.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public Instructor Instructor { get; set; }
    }
}

Key 屬性

[Key] 屬性用於將某個屬性名稱不是 classnameID 或 ID 的屬性識別為主索引鍵 (PK)。

InstructorOfficeAssignment 實體之間有一對零或一關聯性。 辦公室指派只存在於與其受指派的講師關聯中。 OfficeAssignment PK 同時也是其連結到 Instructor 實體的外部索引鍵 (FK)。 EF Core 無法自動將 InstructorID 辨識為 OfficeAssignment 的 PK,因為:

  • InstructorID 並未遵循 ID 或 classnameID 的命名慣例。

因此,必須使用 Key 屬性將 InstructorID 識別為 PK:

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

根據預設,EF Core 會將索引鍵作為非資料庫產生的屬性處置,因為該資料行主要用於識別關聯性。

Instructor 導覽屬性

Instructor 實體的 OfficeAssignment 導覽屬性可為 Null,因為:

  • 參考型別 (例如類別可為 Null)。
  • 講師可能沒有辦公室指派。

OfficeAssignment 實體有不可為 Null 的Instructor 導覽屬性,因為:

  • InstructorID 不可為 Null。
  • 辦公室指派無法獨立於講師之外存在。

Instructor 實體有相關的 OfficeAssignment 實體時,每一個實體都會在其導覽屬性中包含一個其他實體的參考。

[Required] 屬性可套用到 Instructor 導覽屬性:

[Required]
public Instructor Instructor { get; set; }

上述程式碼指定了必須要有相關的講師。 上述程式碼是不必要的,因為 InstructorID 外部索引鍵 (同時也是 PK) 不可為 Null。

修改 Course 實體

Course entity

以下列程式碼來更新 Models/Course.cs

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        [Display(Name = "Number")]
        public int CourseID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Title { get; set; }

        [Range(0, 5)]
        public int Credits { get; set; }

        public int DepartmentID { get; set; }

        public Department Department { get; set; }
        public ICollection<Enrollment> Enrollments { get; set; }
        public ICollection<CourseAssignment> CourseAssignments { get; set; }
    }
}

Course 實體具有外部索引鍵 (FK) 屬性DepartmentIDDepartmentID 會指向相關的 Department 實體。 Course 實體具有一個 Department 導覽屬性。

當資料模型針對相關實體有一個導覽屬性時,EF Core 不需要針對該模型具備 FK 屬性。

EF Core 會自動在資料庫中需要的任何地方建立 FK。 EF Core 會為自動建立的 FK 建立陰影屬性。 在資料模型中具備 FK 可讓更新變得更為簡單和有效率。 例如,假設有一個模型,當中「不」包含 DepartmentID FK 屬性。 當擷取課程實體以進行編輯時:

  • 若沒有明確載入,Department 實體將為 Null。
  • 若要更新課程實體,必須先擷取 Department 實體。

當 FK 屬性 DepartmentID 包含在資料模型中時,便不需要在更新前擷取 Department 實體。

DatabaseGenerated 屬性

[DatabaseGenerated(DatabaseGeneratedOption.None)] 屬性會指定 PK 是由應用程式提供的,而非資料庫產生的。

[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

預設情況下,EF Core 會假定 PK 值是由 DB 產生的。 由資料庫產生 PK 值通常都是最佳做法。 針對 Course 實體,使用者指定了 PK。 例如,課程號碼 1000 系列表示數學部門的課程,2000 系列則為英文部門的課程。

DatabaseGenerated 屬性也可用於產生預設值。 例如,資料庫會自動產生日期欄位來記錄資料列建立或更新的日期。 如需詳細資訊,請參閱產生的屬性

外部索引鍵及導覽屬性

Course 實體中的外部索引鍵 (FK) 屬性和導覽屬性反映了下列關聯性:

由於一個課程已指派給了一個部門,因此當中具有 DepartmentID FK 和 Department 導覽屬性。

public int DepartmentID { get; set; }
public Department Department { get; set; }

由於課程可由任何數量的學生進行註冊,因此 Enrollments 導覽屬性為一個集合:

public ICollection<Enrollment> Enrollments { get; set; }

課程可由多個講師進行教授,因此 CourseAssignments 導覽屬性為一個集合:

public ICollection<CourseAssignment> CourseAssignments { get; set; }

CourseAssignment 會在稍後進行解釋。

建立 Department 實體

Department entity

以下列程式碼來建立 Models/Department.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}

Column 屬性

先前,Column 屬性主要用於變更資料行的名稱對應。 在 Department 實體的程式碼中,Column 屬性則用於變更 SQL 資料類型對應。 Budget 資料行則是使用資料庫中的 SQL Server 金額類型定義:

[Column(TypeName="money")]
public decimal Budget { get; set; }

通常您不需要資料行對應。 EF Core 通常會根據屬性的 CLR 類型選擇適當的 SQL Server 資料類型。 CLR decimal 類型會對應到 SQL Server 的 decimal 類型。 由於 Budget 是貨幣,因此金額資料類型會比較適合貨幣。

外部索引鍵及導覽屬性

FK 及導覽屬性反映了下列關聯性:

  • 部門可能有或可能沒有系統管理員。
  • 系統管理員一律為講師。 因此,InstructorID 已作為 FK 包含在 Instructor 實體中。

導覽屬性已命名為 Administrator,但其中保留了一個 Instructor 實體:

public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }

上述程式碼中的問號 (?) 表示屬性可為 Null。

部門中可能包含許多課程,因此當中包含了一個 Course 導覽屬性:

public ICollection<Course> Courses { get; set; }

注意:根據慣例,EF Core 會為不可為 Null 的 FK 和多對多關聯性啟用串聯刪除。 串聯刪除可能會導致循環的串聯刪除規則。 循環串聯刪除規則會在新增移轉時造成例外狀況。

例如,若已將 Department.InstructorID 屬性定義為不可為 Null:

  • EF Core 會設定串聯刪除規則,以便在刪除講師時刪除部門。

  • 在刪除講師時刪除部門並非預期的行為。

  • 下列 fluent API 會設定限制規則而非串聯。

    modelBuilder.Entity<Department>()
        .HasOne(d => d.Administrator)
        .WithMany()
        .OnDelete(DeleteBehavior.Restrict)
    

上述的程式碼會在部門-講師關聯性上停用串聯刪除。

更新 Enrollment 實體

註冊記錄是某位學生參加的一門課程。

Enrollment entity

以下列程式碼來更新 Models/Enrollment.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}

外部索引鍵及導覽屬性

FK 屬性及導覽屬性反映了下列關聯性:

註冊記錄乃針對一個課程,因此當中包含了一個 CourseID FK 屬性及一個 Course 導覽屬性:

public int CourseID { get; set; }
public Course Course { get; set; }

註冊記錄乃針對一位學生,因此當中包含了一個 StudentID FK 屬性及一個 Student 導覽屬性:

public int StudentID { get; set; }
public Student Student { get; set; }

多對多關聯性

StudentCourse 實體之間存在一個多對多關聯性。 Enrollment 實體的功能為資料庫中一個「具有承載」的多對多聯結資料表。 「具有承載」表示 Enrollment 資料表除了聯結資料表 (在此案例中為 PK 和 Grade) 的 FK 之外,還包含了額外的資料。

下列圖例展示了在實體圖表中這些關聯性的樣子。 (此圖表是使用 EF 6.x 的 EF Power Tools 產生的。建立圖表並不是此教學課程的一部分)。

Student-Course many to many relationship

每個關聯性線條都在其中一端有一個「1」,並在另外一端有一個「星號 (*)」,顯示其為一對多關聯性。

Enrollment 資料表並未包含成績資訊,則其便只需要包含兩個 FK (CourseIDStudentID)。 沒有承載的多對多聯結資料表有時候也稱為「純聯結資料表 (PJT)」。

InstructorCourse 實體具有使用了純聯結資料表的多對多關聯性。

注意:EF 6.x 支援多對多關聯性的隱含聯結資料表,但 EF Core 不支援。 如需詳細資訊,請參閱 EF Core 2.0 中的多對多關聯性

CourseAssignment 實體

CourseAssignment entity

以下列程式碼來建立 Models/CourseAssignment.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class CourseAssignment
    {
        public int InstructorID { get; set; }
        public int CourseID { get; set; }
        public Instructor Instructor { get; set; }
        public Course Course { get; set; }
    }
}

講師-課程

Instructor-to-Courses m:M

講師-課程多對多關聯性:

  • 需要一個由實體集代表的聯結資料表。
  • 為一個純聯結資料表 (沒有承載的資料表)。

通常會將聯結實體命名為 EntityName1EntityName2。 例如,使用此模式的講師-課程聯結資料表為 CourseInstructor。 不過,我們建議使用可描述關聯性的名稱。

資料模型一開始都是簡單的,之後便會持續成長。 無承載聯結 (PJT) 常常會演變為包含承載。 藉由在一開始便使用描述性的實體名稱,當聯結資料表變更時便不需要變更名稱。 理想情況下,聯結實體在公司網域中會有自己的自然 (可能為一個單字) 名稱。 例如,「書籍」和「客戶」可連結為一個名為「評分」 的聯結實體。 針對講師-課程多對多關聯性,CourseAssignment 會比 CourseInstructor 來得好。

複合索引鍵

FK 不可為 Null。 CourseAssignment (InstructorIDCourseID) 中的兩個 FK 一起搭配使用,便可唯一的識別 CourseAssignment 資料表中的每一個資料列。 CourseAssignment 並不需要其專屬的 PK。 InstructorIDCourseID 屬性的功能便是複合 PK。 為 EF Core 指定複合 PK 的唯一方法是使用 fluent API。 下節會說明如何設定複合 PK。

複合 PK 可確保:

  • 一個課程可以有多個資料列。
  • 一個講師可以有多個資料列。
  • 相同講師和課程不可有多個資料列。

由於 Enrollment 聯結實體定義了其自身的 PK,因此這種種類的重複項目是可能的。 若要防止這類重複項目:

  • 在 FK 欄位中新增一個唯一的索引,或
  • Enrollment 設定一個主複合索引鍵,與 CourseAssignment 相似。 如需詳細資訊,請參閱索引

更新資料庫內容

將下列醒目提示的程式碼新增至 Data/SchoolContext.cs

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Models
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollment { get; set; }
        public DbSet<Student> Student { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
        public DbSet<CourseAssignment> CourseAssignments { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
            modelBuilder.Entity<Department>().ToTable("Department");
            modelBuilder.Entity<Instructor>().ToTable("Instructor");
            modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
            modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");

            modelBuilder.Entity<CourseAssignment>()
                .HasKey(c => new { c.CourseID, c.InstructorID });
        }
    }
}

上述程式碼會新增一個新實體,並設定 CourseAssignment 實體的複合 PK。

屬性的 Fluent API 替代項目

上述程式碼中的 OnModelCreating 方法使用 fluent API 來設定 EF Core 行為。 此 API 稱為 "fluent" ,因為其常常會用於將一系列的方法呼叫串在一起,使其成為一個單一陳述式。 下列程式碼為 Fluent API 的其中一個範例:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
}

在此教學課程中,Fluent API 僅會用於無法使用屬性完成的資料庫對應。 然而,Fluent API 可指定大部分透過屬性可完成的格式、驗證及對應規則。

某些屬性 (例如 MinimumLength) 無法使用 Fluent API 來套用。 MinimumLength 不會變更結構描述。它只會套用一項最小長度驗證規則。

某些開發人員偏好單獨使用 Fluent API,使其實體類別保持「整潔」。屬性和 Fluent API 可混合使用。 有一些設定只能透過 Fluent API 完成 (指定複合 PK)。 有一些設定只能透過屬性完成 (MinimumLength)。 使用 Fluent API 或屬性的建議做法為:

  • 從這兩種方法中選擇一項。
  • 持續且盡量使用您選擇的方法。

此教學課程中使用到的某些屬性主要用於:

  • 僅驗證 (例如,MinimumLength)。
  • 僅 EF Core 組態 (例如,HasKey)。
  • 驗證及 EF Core 組態 (例如,[StringLength(50)])。

如需屬性與 Fluent API 的詳細資訊,請參閱組態方法

顯示關聯性的實體圖表

下圖顯示了 EF Power Tools 為完成的 School 模型建立的圖表。

Entity diagram

上述圖表顯示:

  • 數個一對多關聯性線條 (1 對 *)。
  • InstructorOfficeAssignment 實體之間的一對零或一關聯性線條 (1 對 0..1)。
  • InstructorDepartment 實體之間的零或一對多關聯性線條 (0..1 對 *)。

使用測試資料植入資料庫

更新 Data/DbInitializer.cs 中的程式碼:

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            //context.Database.EnsureCreated();

            // Look for any students.
            if (context.Student.Any())
            {
                return;   // DB has been seeded
            }

            var students = new Student[]
            {
                new Student { FirstMidName = "Carson",   LastName = "Alexander",
                    EnrollmentDate = DateTime.Parse("2010-09-01") },
                new Student { FirstMidName = "Meredith", LastName = "Alonso",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Arturo",   LastName = "Anand",
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Gytis",    LastName = "Barzdukas",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Yan",      LastName = "Li",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Peggy",    LastName = "Justice",
                    EnrollmentDate = DateTime.Parse("2011-09-01") },
                new Student { FirstMidName = "Laura",    LastName = "Norman",
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Nino",     LastName = "Olivetto",
                    EnrollmentDate = DateTime.Parse("2005-09-01") }
            };

            foreach (Student s in students)
            {
                context.Student.Add(s);
            }
            context.SaveChanges();

            var instructors = new Instructor[]
            {
                new Instructor { FirstMidName = "Kim",     LastName = "Abercrombie",
                    HireDate = DateTime.Parse("1995-03-11") },
                new Instructor { FirstMidName = "Fadi",    LastName = "Fakhouri",
                    HireDate = DateTime.Parse("2002-07-06") },
                new Instructor { FirstMidName = "Roger",   LastName = "Harui",
                    HireDate = DateTime.Parse("1998-07-01") },
                new Instructor { FirstMidName = "Candace", LastName = "Kapoor",
                    HireDate = DateTime.Parse("2001-01-15") },
                new Instructor { FirstMidName = "Roger",   LastName = "Zheng",
                    HireDate = DateTime.Parse("2004-02-12") }
            };

            foreach (Instructor i in instructors)
            {
                context.Instructors.Add(i);
            }
            context.SaveChanges();

            var departments = new Department[]
            {
                new Department { Name = "English",     Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Abercrombie").ID },
                new Department { Name = "Mathematics", Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Fakhouri").ID },
                new Department { Name = "Engineering", Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Harui").ID },
                new Department { Name = "Economics",   Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Kapoor").ID }
            };

            foreach (Department d in departments)
            {
                context.Departments.Add(d);
            }
            context.SaveChanges();

            var courses = new Course[]
            {
                new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID
                },
                new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
                new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
            };

            foreach (Course c in courses)
            {
                context.Courses.Add(c);
            }
            context.SaveChanges();

            var officeAssignments = new OfficeAssignment[]
            {
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID,
                    Location = "Smith 17" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Harui").ID,
                    Location = "Gowan 27" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID,
                    Location = "Thompson 304" },
            };

            foreach (OfficeAssignment o in officeAssignments)
            {
                context.OfficeAssignments.Add(o);
            }
            context.SaveChanges();

            var courseInstructors = new CourseAssignment[]
            {
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Kapoor").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Fakhouri").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Literature" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
            };

            foreach (CourseAssignment ci in courseInstructors)
            {
                context.CourseAssignments.Add(ci);
            }
            context.SaveChanges();

            var enrollments = new Enrollment[]
            {
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    Grade = Grade.A
                },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    Grade = Grade.C
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
                    Grade = Grade.B
                    },
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Li").ID,
                    CourseID = courses.Single(c => c.Title == "Composition").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Justice").ID,
                    CourseID = courses.Single(c => c.Title == "Literature").CourseID,
                    Grade = Grade.B
                    }
            };

            foreach (Enrollment e in enrollments)
            {
                var enrollmentInDataBase = context.Enrollment.Where(
                    s =>
                            s.Student.ID == e.StudentID &&
                            s.Course.CourseID == e.CourseID).SingleOrDefault();
                if (enrollmentInDataBase == null)
                {
                    context.Enrollment.Add(e);
                }
            }
            context.SaveChanges();
        }
    }
}

上述程式碼為新的實體提供了種子資料。 此程式碼中的大部分主要用於建立新的實體物件並載入範例資料。 範例資料主要用於測試。 如需如何植入多對多聯結資料表的範例,請參閱 EnrollmentsCourseAssignments

新增移轉

組建專案。

Add-Migration ComplexDataModel

上述命令會顯示關於可能發生資料遺失的警告。

An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.
Done. To undo this action, use 'ef migrations remove'

若執行 database update 命令,便會產生下列錯誤:

The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in
database "ContosoUniversity", table "dbo.Department", column 'DepartmentID'.

套用移轉

現在您有了現有的資料庫,您需要思考如何對其套用未來變更。 本教學課程示範兩種方法:

  • 卸除並重新建立資料庫
  • 將移轉套用至現有資料庫。 雖然這個方法更複雜且耗時,卻是實際生產環境的慣用方法。 請注意:這是本教學課程的選擇性章節。 您可以執行卸除並重新建立步驟,然後略過本節。 如果您希望遵循本章節中的步驟,請不要執行卸除並重新建立的步驟。

卸除並重新建立資料庫

更新的 DbInitializer 中的程式碼會為新的實體新增種子資料。 若要強制 EF Core 建立新的 DB,請卸除並更新資料庫:

在 [套件管理員主控台] (PMC) 中,執行下列命令:

Drop-Database
Update-Database

從 PMC 執行 Get-Help about_EntityFrameworkCore 以取得說明資訊。

執行應用程式。 執行應用程式會執行 DbInitializer.Initialize 方法。 DbInitializer.Initialize 會填入新的資料庫。

在 SSOX 中開啟資料庫:

  • 若先前已開啟過 SSOX,按一下 [重新整理] 按鈕。
  • 展開 [資料表] 節點。 建立的資料表便會顯示。

Tables in SSOX

檢查 CourseAssignment 資料表:

  • 以滑鼠右鍵按一下 CourseAssignment 資料表,然後選取 [檢視資料]
  • 驗證 CourseAssignment 資料表中是否包含資料。

CourseAssignment data in SSOX

將移轉套用至現有資料庫

本節為選擇性。 只有當您略過先前卸除並重新建立資料庫一節,這些步驟才有效。

當使用現有的資料執行移轉作業時,某些 FK 條件約束可能會無法透過現有資料滿足。 當您使用的是生產資料時,您必須進行幾個步驟才能移轉現有資料。 本節提供了修正 FK 條件約束違規的範例。 請不要在沒有備份的情況下進行這些程式碼變更。 若您已完成了先前的章節並已更新資料庫,請不要進行這些程式碼變更。

{timestamp}_ComplexDataModel.cs 檔案包含下列程式碼:

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    type: "int",
    nullable: false,
    defaultValue: 0);

上述程式碼將一個不可為 Null 的 DepartmentID FK 新增至 Course 資料表。 先前教學課程中的資料庫在 Course 中包含了資料列,導致資料表無法藉由移轉進行更新。

若要讓 ComplexDataModel 與現有資料進行移轉:

  • 變更程式碼,以給予新資料行 (DepartmentID) 一個新的預設值。
  • 建立一個名為 "Temp" 的假部門以作為預設部門之用。

修正外部索引鍵條件約束

更新 ComplexDataModel 類別 Up 方法:

  • 開啟 {timestamp}_ComplexDataModel.cs 檔案。
  • 將新增 DepartmentID 資料行至 Course 資料表的程式碼全部標為註解。
migrationBuilder.AlterColumn<string>(
    name: "Title",
    table: "Course",
    maxLength: 50,
    nullable: true,
    oldClrType: typeof(string),
    oldNullable: true);
            
//migrationBuilder.AddColumn<int>(
//    name: "DepartmentID",
//    table: "Course",
//    nullable: false,
//    defaultValue: 0);

新增下列醒目提示程式碼。 新的程式碼位於 .CreateTable( name: "Department" 區塊後方:

migrationBuilder.CreateTable(
    name: "Department",
    columns: table => new
    {
        DepartmentID = table.Column<int>(type: "int", nullable: false)
            .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
        Budget = table.Column<decimal>(type: "money", nullable: false),
        InstructorID = table.Column<int>(type: "int", nullable: true),
        Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
        StartDate = table.Column<DateTime>(type: "datetime2", nullable: false)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_Department", x => x.DepartmentID);
        table.ForeignKey(
            name: "FK_Department_Instructor_InstructorID",
            column: x => x.InstructorID,
            principalTable: "Instructor",
            principalColumn: "ID",
            onDelete: ReferentialAction.Restrict);
    });

 migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
// Default value for FK points to department created above, with
// defaultValue changed to 1 in following AddColumn statement.

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    nullable: false,
    defaultValue: 1);

透過上述變更,現有的 Course 資料列將會在執行 ComplexDataModelUp 方法後與 "Temp" 部門相關。

生產環境的應用程式會:

  • 包含將 Department 資料列及相關 Course 資料列新增到新 Department 資料列的程式碼或指令碼。
  • 不使用 "Temp" 部門或 Course.DepartmentID 的預設值。

下一個教學課程會涵蓋相關資料。

其他資源