[渣譯文] 使用 MVC 5 的 EF6 Code First 入門 系列:爲ASP.NET MVC應用程序建立更復雜的數據模型

這是微軟官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻譯,這裏是第六篇:爲ASP.NET MVC應用程序建立更復雜的數據模型程序員

原文:Creating a More Complex Data Model for an ASP.NET MVC Applicationweb

譯文版權全部,謝絕全文轉載——但您能夠在您的網站上添加到該教程的連接。正則表達式

在以前的教程中您已經建立了由三個實體組成的簡單的數據模型。在本教程中,您將會添加更多的實體和關係,您會進一步定製數據模型,包括指定格式、驗證和數據庫映射規則。您會看到兩種自定義數據模型的方法:經過將屬性添加到實體類和經過將代碼添加到數據庫上下文類。chrome

當您完成時,實體類將組成一個完整的數據模型,以下圖所示:數據庫

 

使用特性來定製數據模型

在本節中您會看到如何使用特性來定製數據模型的屬性來用於指定格式、驗證和數據庫映射規則。在餘下的章節中您將經過向已經建立的類中添加特性及建立剩餘實體的新類來完善School數據模型。api

數據類型特性

對於學生註冊日期,雖然您僅僅關心該字段中的日期,但在全部Web頁面中都顯示爲日期和時間。經過使用數據批註特性,您可使用代碼來修復在每一個視圖中該字段的顯示格式。爲了實現這一點,您須要添加一個特性到學生類的EnrollmentDate屬性。瀏覽器

在Models\Student.cs中,添加System.ComponentModel.DataAnnotations命名空間的using語句,將DateType及DisplayFormat特性添加到EnrollmentDate屬性上,以下面的代碼所示:服務器

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
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 virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

DataType特性用於執行比數據庫內部類型更加具體的數據類型。在本示例中,咱們只想保持對日期的跟蹤,而不是日期及時間。DataType枚舉提供了多種數據類型,好比日期,時間,電話號碼,電子郵件等。DataType特性一樣可讓應用程序來自動基於數據類型的特殊功能。例如DataType.EmailAddress能夠建立mailto:的超連接,DataType.Date特性能夠在支持HTML5的瀏覽器中建立一個日期選擇器。DataType特性能夠生成HTML5瀏覽器支持的HTML5 數據特性。要注意DataType特性並不提供任何驗證。架構

DataType.Date不指定日期的顯示格式。默認狀況下, 數據字段的顯示基於服務器自己的CultureInfo的默認格式。mvc

DisplayFormat特性用於顯示指定的日期格式:

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

ApplyFormatInEditMode設置指定該值在文本框中進行編輯時也一樣適用已指定的格式(某些狀況下可能並不適用,好比針對一個貨幣值,您可能不但願在文本框中顯示一個貨幣符號並對其編輯)。

您能夠單獨使用DisplayFormat特性,但一般一個好注意是同時使用DataType這二者。DataType特性所傳達的是數據自己的表述而不是如何將它呈如今屏幕上。下面列出了一些您能夠考慮不使用DisplayFormat的狀況:

  • 目標瀏覽器能夠啓用HTML5功能(好比顯示日曆控件,區域化的貨幣符號,電郵連接,一些客戶端輸入驗證等。)。
  • 默認狀況下,瀏覽器將使用基於您的區域設置的正確格式老呈現數據。
  • DataType特性可讓MVC自動選擇正確的模板來呈現數據(DisplayFormat使用字符串模板)。

若是您在日期字段上使用DataType特性,您也應當指定DisplayFormat特性以確保該字段在Chrome瀏覽器中正確呈現。詳細信息請參見StackOverflow thread

有關如何在MVC中處理其餘數據類型,請參閱中MVC 5 Introduction: Examining the Edit Methods and Edit View的國際化部分。

再次運行學生索引頁您會注意到頁面上再也不顯示時間部分,全部使用學生模型的視圖都會有相似的改變。

StringLength特性

您還可使用特新來指定數據驗證規則和驗證錯誤信息。StringLength特性設定設定數據庫的最大長度而且提供ASP.NET MVC的客戶端及服務器端驗證。您還能夠在此特性中指定字符串的最小長度,但最小值對數據庫的架構沒有任何影響。

假設您想要確保用戶不能輸入超過50個字符的名稱,若是要添加該限制,將StringLength特性添加到LastName和FirstMidName屬性,以下面的示例:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
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 = "名字不能超過50個字符")]  
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

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

StringLength特性不會阻止用戶在姓名中輸入空白字符,但您可使用正則表達式屬性來進行該限制。例以下面的代碼要求第一個字符必須是大寫,其他的字符是字母。

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

MaxLength特性提供的功能相似於StringLength,但不提供客戶端驗證。

運行程序並單擊學生選項卡,您會收到如下的異常:

「System.InvalidOperationException」類型的異常在 EntityFramework.dll 中發生,但未在用戶代碼中進行處理

其餘信息: 支持「SchoolContext」上下文的模型已在數據庫建立後發生更改。請考慮使用 Code First 遷移更新數據庫(http://go.microsoft.com/fwlink/?LinkId=238269)。

實體框架會檢測到數據模型已經進行了更改而且要求數據庫架構也做出相應的改變。您將經過使用遷移來在不丟失數據的狀況下升級架構。若是您更改了使用Seed方法建立的數據,您在Seed方法中所使用的AddOrUpdate方法會更改回其原始狀態(AddOrUpdate是一個至關於"upsert"操做的數據庫術語)。

在程序包管理器控制檯中,輸入如下命令:

add-migration MaxLengthOnNames
update-database

add-migration命令建立一個名爲<時間戳>_MaxLengthOnName.cs的文件,此文件包含用來更新數據庫的Up方法,以匹配當前數據模型中的代碼。update-database命令運行該代碼。

實體框架使用有時間戳前綴的遷移文件名來進行遷移。您能夠在運行update-database命令以前建立多個遷移,全部的遷移會按照它們建立的順序來應用。

運行程序,新建一個學生,並在姓名中輸入超過50個字符,點擊建立,以後您會看到一條錯誤信息。

列特性

您還能夠經過使用特性來控制如何將您的類和屬性映射到數據庫。假設您曾經使用名稱FirstMidName來做爲名稱字段,由於該字段中還可能包含一箇中間名。但您想讓數據庫中的列命名爲FirstName,由於使用數據庫來編寫查詢的其餘用戶都習慣於使用該列名。要作到這一點,您須要使用列特性。

Column特性指定在建立數據庫時,Student表映射的FirstMidName屬性的列將被命名爲FirstName,換句話說,當您的代碼引用Student.FirstMidName,相應的更新等改變會在數據庫的Student表中的FirstName對應。若是您不指定列的名稱,他們會使用和屬性相同的名稱。

 在Student.cs文件中,添加 System.ComponentModel.DataAnnotations.Schema的引用,並將Column特性添加到FirstMidName上,以下面的代碼所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
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 = "名字不能超過50個字符")]
        [Column("FirstName")] 
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

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

 Column特性改變了SchoolContext的模型,因此它不會匹配數據庫。在程序包管理器通知臺中建立另外一個遷移,輸入如下命令:

add-migration ColumnFirstName
update-database

在服務器資源管理器中,雙擊Student表,打開表格設計器。

您能夠看到FirstMidName已經被命名爲FirstName,而兩個列的數據最大長度都已經變動爲50個字符。

您還可使用Fluent API來更改數據庫映射,後面的教程咱們將演示該作法。

注意:若是您在所有完成如下各節中建立的全部實體類以前嘗試編譯程序,您會收到編譯器錯誤。

完成對學生實體的更改

在Model\Student.cs中,使用下面的代碼替換原來的內容:

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

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [Required]
        [Display(Name = "")]
        [StringLength(50)]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "名字不能超過50個字符")]
        [Column("FirstName")]
        [Display(Name = "")]
        public string FirstMidName { get; set; }
        [Display(Name = "註冊日期")]
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }
        [Display(Name = "全名")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

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

 

必需特性

Required特性使屬性成爲必需的字段。值類型的字段是不須要Required特性的,好比Int,double,DateTime等,因爲值類型不能分配Null值,因此它們自己就被視爲必需字段。您也能夠刪除Required特性並使用帶有最小長度的StringLength特性來替換它。

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

 

顯示特性

Display特性指定文本框中的標題應該是"姓","名","全名","註冊日期"而不是屬性自己的名字。

FullName計算屬性

FullName是一個計算的屬性,經過串聯其餘兩個屬性來返回一個值。所以它只有get訪問器,數據庫也不會生成對應的FullName列。

建立講師實體

建立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 = "")]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Column("FirstName")]
        [Display(Name = "")]
        [StringLength(50)]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",ApplyFormatInEditMode = true)]
        [Display(Name = "聘用日期")]
        public DateTime HireDate { get; set; }

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

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

 

請注意講師實體和學生實體的屬性都是相同的,在後續的教程中,您會經過執行繼承來重構此代碼以消除冗餘。

您也能夠將多個特性放在一行上,以下面所示:

public class Instructor
{
   public int ID { get; set; }

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

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

   [DataType(DataType.Date),Display(Name = "Hire Date")]
   public DateTime HireDate { get; set; }

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

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

 

Course和OfficeAssignment導航屬性

Course和OfficeAssignment是導航屬性。正如以前所解釋的,它們一般被定義爲virtual,這樣它們就能夠利用實體框架中的延遲加載。此外,若是一個導航屬性能夠容納多個實體,則它的類型必須實現ICollection<T>接口,例如List<T>,但不能是IEnumerable<T>,由於它不實現Add。

教師能夠教授任意數量的課程,因此Courses定義爲Course實體的集合。

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

 

咱們的業務邏輯定義一個講師只能有一個辦公室,所以OfficeAssignment定義爲單個OfficeAssignment的實體(若是講師沒有辦公室,則能夠分配Null)。

        public virtual OfficeAssignment OfficeAssignment { get; set; }

建立OfficeAssignment實體

建立Models\OfficeAssignment.cs並使用下面的代碼替換自動生成的:

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

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        [ForeignKey("Instructor")]
        public int InstructorID { get; set; }

        [StringLength(50)]
        [Display(Name = "辦公室地址")]
        public string Location { get; set; }

        public virtual Instructor Instructor { get; set; }
    }
}

 

所有保存後生成項目,確保沒有彈出任何編譯器或能夠捕捉的錯誤。

Key特性

Instructor和OfficeAssignment實體之間有一個對零或一對一的關係。辦公室只和講師之間存在關係,所以其主鍵也是其Instructor實體的外鍵。可是實體框架不會自動將InstructorID識別爲實體的主鍵,由於該命名不遵循實體框架約定。所以,咱們使用Key特性來標記該屬性爲實體的主鍵:

        [Key]
        [ForeignKey("Instructor")]
        public int InstructorID { get; set; }

若是實體沒有它本身的主鍵,但您想將屬性名命名爲類名-ID或ID之外的不一樣的名稱,您一樣可使用Key特性。默認狀況下實體框架將鍵視爲非數據庫生成的,由於該列用來標識關係。

ForeignKey特性

當兩個實體之間存在有一對零或一對一關係時,實體框架沒法自動辨認出那一端的關係是主體,那一端是依賴。一對一關係在每一個類中使用導航屬性來引用其餘類。ForeignKey特性能夠應用於要創建關係的依賴類。若是您省略ForeignKey特性,當您嘗試建立遷移時系統會出現一個沒法肯定實體間關係的錯誤。

Instructor導航屬性

Instructor實體有一個可爲空的OfficeAssignment導航屬性(由於可能有講師沒有分配辦公室),而且OfficeAssignment實體有一個不可爲空的Instuctor導航屬性(由於一個辦公室不可能在沒有講師的狀況下分配出去--InstructorID是不可爲空的)。當Instructor實體具備OfficeAssignment實體關聯的時候,每一個實體在導航屬性中都有另外一個的引用。

您能夠把一個Required特性添加給Instructor導航屬性來指明必須有相關的講師,但您不須要這樣作。由於InstructorId外鍵(一樣也是表的主鍵)是不可爲null的。

修改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 = "編號")]
        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 virtual Department Department { get; set; }
        public virtual ICollection<Enrollment> Enrollments { get; set; }
        public virtual ICollection<Instructor> Instructors { get; set; }
    }
}

課程實體有一個DepartmentID外鍵屬性,用來指向相關的Department實體,它有一個Department導航屬性。當一個關聯實體有一個導航屬性時,實體框架不須要您添加外鍵屬性到您的實體模型,實體框架在須要時會在數據庫中自動建立外鍵。但在實體模型中擁有一個外鍵會讓更新更簡單、高效。例如,當您讀取一個Course實體進行編輯,若是您選擇不加載Department實體,那Department實體是空的。因此當您更新Course實體時,您必須先取得該實體關聯的Department實體。若是在數據模型中包含了外鍵DepartmentID,您就不須要在更新前先取得Department實體。

DatabaseGenerated特性

CourseID屬性有一個提供了None參數的DatabaseGenerated特性,該特性指明主鍵值將由用戶提供,而不是由數據庫自動生成。

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

默認狀況下,實體框架會假定主鍵應當由數據庫生成。這在大多數狀況下都是必要的。然而對於Course實體,您會使用一個用戶指定的課程編號,好比1000系列表示一類課程,2000系列是另外一類等等。

外鍵和導航屬性

Course實體中的外鍵和導航屬性反映瞭如下關係:

  • 一門課程被分配到一個系,因此如以前您看到的,有一個DepartmentID外鍵和Department導航屬性。
            public int DepartmentID { get; set; }
            public virtual Department Department { get; set; } 
  • 一門課程能夠有任意數量的學生選修,因此Enrollments導航屬性是一個集合:
            public virtual ICollection<Enrollment> Enrollments { get; set; }
  • 一門課程可能由多個講師來教授,因此Instructors導航屬性是一個集合
            public virtual ICollection<Instructor> Instructors { get; set; }

建立系實體

使用下面的代碼建立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 = "起始日期")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

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

Column特性

以前您已經使用過Column特性來更改列名稱映射。在系實體代碼中,Column特性被用於更改SQL數據類型的映射,以指明列定義將在數據庫中使用money類型。

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

列映射一般是沒必要要的,由於實體框架一般會基於您定義屬性的CLR類型來自動選擇適當的SQL SERVER數據類型做爲數據列的類型。CLR的decimal類型是映射到SQL Server decimal類型的,但在這種狀況下,您知道該屬性將保存貨幣金額,因此指明瞭比decimal更適合的money數據類型來做爲列的數據類型。有關CLR數據類型及它們如何匹配到SQL Server數據類型的詳細信息,請參見SqlClient for Entity FrameworkTypes

外鍵和導航屬性

外鍵和導航屬性反映瞭如下關係:

  • 一個系可能沒有管理員,但管理員始終是一名講師。所以,InstructorID屬性是Instructor實體的外鍵,而且在屬性類型後添加了一個問號,將其標記爲可空的。導航屬性被命名爲Administrator,但持有Instructor實體。
            public int? InstructorID { get; set; }
            public virtual Instructor Administrator { get; set; }  
  • 一個系可能有不少課程,因此有一個Courses導航屬性
            public virtual ICollection<Course> Courses { get; set; }

注意:基於約定,實體框架針對非空外鍵和多對多關係會啓用級聯刪除。這可能會致使循環的級聯刪除規則,使得在您嘗試添加一個遷移時致使一場。例如,若是您不將Department.InstructorID屬性定義爲可爲空的,您會獲得一個引用關係錯誤的異常。若是您的業務規則須要InstructorID屬性設置爲不可爲空,則必須使用如下fluent API來聲明在關係上禁用級聯刪除。

modelBuilder.Entity().HasRequired(d => d.Administrator).WithMany().WillCascadeOnDelete(false);

修改Enrollment實體

在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 = "沒有成績")]
        public Grade? Grade { get; set; }

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

外鍵和導航屬性

外鍵和導航屬性反映了下列關係:

  • 一條註冊記錄對應單個課程,所以,有CourseID外鍵和Course導航屬性:
    public int CourseID { get; set; }
    public virtual Course Course { get; set; }
  • 一條註冊記錄對應單個學生,所以,有StudentID外檢屬性和Student導航屬性:
    public int StudentID { get; set; }
    public virtual Student Student { get; set; }

多對多關係

在學生和課程之間有多對多的關係,而且註冊實體做爲一個多對多的數據庫鏈接表。這意味着Enrollment數據表包含了鏈接表的外鍵除外的附加數據(在本例中,主鍵和Grade屬性)。

下圖顯示了在實體關係圖中這些關係的關聯狀況(本圖使用實體框架Power Tools生成,建立關係圖不是本教程的一部分,在此處僅僅是作示例)。

每一個關係線都有一個結束和一個型號,代表這是一個一對多的關係。

若是Enrollment數據表不包含成績信息,它只須要包含兩個外鍵CourseID和StudentID,在這種狀況下,他將會在數據庫中對應無有效載荷(或純鏈接表)的多對多鏈接表,您就無需針對它們單首創建一個模型類。Instructor和Course實體都有多對多關係,而且您能夠看到,它們之間沒有實體類:

數據庫須要一個鏈接表,以下圖的數據庫關係圖所示:

實體框架會自動建立CourseInstructor表,並經過Instructor.Course和Course.Instructor導航屬性來間接地讀取和更新它。

在實體關係圖中顯示關係

下面的插圖顯示了使用是實體框架Power Tools建立的完整的學校模型:

除了多對多關係線(*到*)和一對多關係線(1到*),您還能看到Instructor和OfficeAssignment實體之間的一到零或1關係線(1到0..1),以及Istructor和Department實體之間的零或一對多(0..1到*)關係線。

添加代碼到數據庫上下文來自定義數據模型

下一步您將添加新實體到SchoolContext類中並使用fluent API來自定義映射。該API常用一系列的方法調用來合併爲單個語句,以下面的示例:

 modelBuilder.Entity<Course>()
     .HasMany(c => c.Instructors).WithMany(i => i.Courses)
     .Map(t => t.MapLeftKey("CourseID")
         .MapRightKey("InstructorID")
         .ToTable("CourseInstructor"));

 

在本教程中,您將在不使用特性來進行的數據庫映射的部分使用fluent API,但您還能夠如同使用大多數特性那樣來使用fluent API指定格式、驗證和映射規則。某些特性不能使用fluent API,例如MinimumLength,如前文所述,MinimumLength不會更改數據庫架構,它僅適合用戶客戶端和服務器端驗證。

某些開發人員喜歡徹底使用fluent API來保持它們的實體類"乾淨"。若是您想要的話,您能夠混用特性和fluent API,要注意某些自定義功能呢只能使用fluent API來實現,但通常建議是僅選擇這二者中之一。

要添加新的實體模型數據到數據模型而且執行沒有使用特性的數據庫映射,請將DAL\SchoolContext.cs中的代碼使用下面的替換:

using ContosoUniversity.Models;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;

namespace ContosoUniversity.DAL
{
   public class SchoolContext : DbContext
   {
      public DbSet<Course> Courses { get; set; }
      public DbSet<Department> Departments { get; set; }
      public DbSet<Enrollment> Enrollments { get; set; }
      public DbSet<Instructor> Instructors { get; set; }
      public DbSet<Student> Students { get; set; }
      public DbSet<OfficeAssignment> OfficeAssignments { get; set; }

      protected override void OnModelCreating(DbModelBuilder modelBuilder)
      {
         modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

         modelBuilder.Entity<Course>()
             .HasMany(c => c.Instructors).WithMany(i => i.Courses)
             .Map(t => t.MapLeftKey("CourseID")
                 .MapRightKey("InstructorID")
                 .ToTable("CourseInstructor"));
      }
   }
}

 

在OnModelCreating方法中,咱們使用了新語句來配置多對多鏈接表:

  • 爲Instructor和Course實體間配置多對多關係,該代碼指定了鏈接表的名稱和列名。Code First能夠在您沒有編寫這段代碼的狀況下配置多對多關係,但若是您不聲明它,您將獲取如同InstructorID列的默認名稱,好比InstructorInstructorID。
    modelBuilder.Entity<Course>()
        .HasMany(c => c.Instructors).WithMany(i => i.Courses)
        .Map(t => t.MapLeftKey("CourseID")
            .MapRightKey("InstructorID")
            .ToTable("CourseInstructor"));

下面的代碼舉例說明若是使用fluent API而不是特性來指定Instructor和OfficeAssignment實體之間的關係:

modelBuilder.Entity<Instructor>()
    .HasOptional(p => p.OfficeAssignment).WithRequired(p => p.Instructor);

 

有關fluent API的更多信息,請參閱Fluent API

填充測試數據到數據庫中

將Migrations\Configuration.cs文件中的代碼使用下面的替換:

namespace ContosoUniversity.Migrations
{
    using ContosoUniversity.Models;
    using ContosoUniversity.DAL;
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;
    
    internal sealed class Configuration : DbMigrationsConfiguration<SchoolContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
        }

        protected override void Seed(SchoolContext context)
        {
            var students = new List<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") }
            };


            students.ForEach(s => context.Students.AddOrUpdate(p => p.LastName, s));
            context.SaveChanges();

            var instructors = new List<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") }
            };
            instructors.ForEach(s => context.Instructors.AddOrUpdate(p => p.LastName, s));
            context.SaveChanges();

            var departments = new List<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 }
            };
            departments.ForEach(s => context.Departments.AddOrUpdate(p => p.Name, s));
            context.SaveChanges();

            var courses = new List<Course>
            {
                new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
                  DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
                  DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
                  DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
            };
            courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
            context.SaveChanges();

            var officeAssignments = new List<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" },
            };
            officeAssignments.ForEach(s => context.OfficeAssignments.AddOrUpdate(p => p.InstructorID, s));
            context.SaveChanges();

            AddOrUpdateInstructor(context, "Chemistry", "Kapoor");
            AddOrUpdateInstructor(context, "Chemistry", "Harui");
            AddOrUpdateInstructor(context, "Microeconomics", "Zheng");
            AddOrUpdateInstructor(context, "Macroeconomics", "Zheng");

            AddOrUpdateInstructor(context, "Calculus", "Fakhouri");
            AddOrUpdateInstructor(context, "Trigonometry", "Harui");
            AddOrUpdateInstructor(context, "Composition", "Abercrombie");
            AddOrUpdateInstructor(context, "Literature", "Abercrombie");

            context.SaveChanges();

            var enrollments = new List<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();
        }

        void AddOrUpdateInstructor(SchoolContext context, string courseTitle, string instructorName)
        {
            var crs = context.Courses.SingleOrDefault(c => c.Title == courseTitle);
            var inst = crs.Instructors.SingleOrDefault(i => i.LastName == instructorName);
            if (inst == null)
                crs.Instructors.Add(context.Instructors.Single(i => i.LastName == instructorName));
        }
    }
}

正如您以前在第一個教程中看到的,大部分代碼只是簡單地更新或建立了新的實體對象而且讀取測試數據到屬性用於測試。可是請注意Course實體,它和Instructor實體之間存在多對多的關聯:

var courses = new List<Course>
{
    new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
      DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
      Instructors = new List<Instructor>() 
    },
    ...
};
courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
context.SaveChanges();

當建立Course對象時,Instructor導航屬性被使用代碼Instructors = new List<Instructor>()來初始化爲一個空的集合。這使它可以使用Instructors.Add方法來添加Instructor實體相關的Course實體。若是您沒有建立一個空的列表,您就不可以進行添加,由於Instructors屬性爲null,因此也不會有一個Add方法來添加這些關係。您一樣能夠在構造函數中初始化該列表。

添加遷移和更新數據庫

從程序包管理器控制檯中,輸入add-migration命令(先不要運行update-database命令):

add-Migration ComplexDataModel

若是您試圖再次運行update-database命令,您會收到一個外鍵衝突錯誤。

當您在保存現有數據的狀態下執行遷移時,您須要將存根數據插入到數據庫以知足外鍵約束要求,因此咱們如今就來作這些。在ComplexDataModel中的up方法生成的代碼將爲Course數據表添加一個非空DepartmentID外鍵。由於針對Course數據表中的已有行執行代碼時AddColumn操做將失敗,由於SQL Server不知道使用何值來填充不可爲空的列。所以,必須更改代碼,提供一個默認值給新列,並建立一個"Temp"做爲默認系的存根。所以,在Up方法中使用Temp系來分配給Course的現有行。您能夠在Seed方法中從新分配給它們正確的系。

編輯<時間戳>_ComplexDataModel.cs文件,註釋掉Course數據表中添加DepartmentID行的代碼,並添加如下高亮的代碼:

   CreateTable(
        "dbo.CourseInstructor",
        c => new
            {
                CourseID = c.Int(nullable: false),
                InstructorID = c.Int(nullable: false),
            })
        .PrimaryKey(t => new { t.CourseID, t.InstructorID })
        .ForeignKey("dbo.Course", t => t.CourseID, cascadeDelete: true)
        .ForeignKey("dbo.Instructor", t => t.InstructorID, cascadeDelete: true)
        .Index(t => t.CourseID)
        .Index(t => t.InstructorID);

    // Create a department for course to point to.
    Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())"); // default value for FK points to department created above.
    AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false, defaultValue: 1)); //AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false)); 

    AlterColumn("dbo.Course", "Title", c => c.String(maxLength: 50));

當Seed方法運行時,它會在Department數據表中插入行而且會涉及現有的Course行到新的Department行。若是您尚未在UI中添加任何課程,您可能以後再也不須要Temp系或Course.DepartmentID行的缺省值。若是要考慮使用應用程序的人可能已經添加了課程的可能性,您也許會更新Seed方法代碼來確保在您從列中刪除默認值和Temp系以前全部Course行(不光是較早由Seed方法插入的)都具備有效的DepartmentID值。

編輯完文件後,在程序包資源管理器中輸入update-database命令。

updata-database

注意,在遷移數據並進行架構變動時您可能會獲得某些錯誤,若是您不能解決遷移錯誤,您能夠嘗試更改鏈接字符串的名稱或刪除數據庫。最簡單的方法是重命名web.config文件中的數據庫名稱來建立一個新的。

新的數據庫沒有數據須要遷移,而且updata-database命令更有可能在沒有錯誤的狀況下完成。若是失敗,您能夠嘗試從新初始化數據庫,經過輸入如下命令:

update-database -TargetMigration:0

在服務器資源管理器中打開數據庫,展開表節點來觀察是否全部的表都已經成功建立(若是您較早已經打開過,嘗試刷新一下)。

您並無針對CourseInstructor數據表建立數據模型,如前所述,這是Instructor和Course實體之間的多對多關係的鏈接表。

右鍵單擊CourseInstructor表,選擇顯示錶數據以驗證其中的數據。

總結

您如今擁有一個更加複雜的數據模型和顯影的數據庫。在以後的教程中您會了解更多關於使用不一樣方式來訪問數據的方法。

 

做者信息

  Tom Dykstra - Tom Dykstra是微軟Web平臺及工具團隊的高級程序員,做家。

相關文章
相關標籤/搜索