實體框架 - 快速指南



實體框架 - 概述

什麼是實體框架?

實體框架於 2008 年首次釋出,是 Microsoft 用於 .NET 應用程式與關係資料庫之間互動的主要方法。實體框架是一個物件關係對映器 (ORM),它是一種簡化軟體中物件與關係資料庫的表和列之間對映的工具。

  • 實體框架 (EF) 是 ADO.NET 的一個開源 ORM 框架,它是 .NET Framework 的一部分。

  • ORM 負責建立資料庫連線和執行命令,以及獲取查詢結果並自動將這些結果具體化為應用程式物件。

  • ORM 還幫助跟蹤對這些物件的更改,並在收到指示時,它還會將這些更改持久儲存回資料庫。

為什麼使用實體框架?

實體框架是一個 ORM,而 ORM 的目標是透過減少應用程式中使用的資料的持久化冗餘任務來提高開發人員的生產力。

  • 實體框架可以生成讀取或寫入資料庫中資料的必要資料庫命令,併為您執行這些命令。

  • 如果您正在查詢,您可以使用 LINQ to Entities 對您的域物件表達您的查詢。

  • 實體框架將在資料庫中執行相關的查詢,然後將結果具體化為您的域物件的例項,以便您在應用程式中使用。

市場上還有其他 ORM,例如 NHibernate 和 LLBLGen Pro。大多數 ORM 通常將域型別直接對映到資料庫模式。

Typical ORM

實體框架具有更細粒度的對映層,因此您可以自定義對映,例如,透過將單個實體對映到多個數據庫表,甚至將多個實體對映到單個表。

EF Runtime Metadata
  • 實體框架是 Microsoft 建議用於新應用程式的資料訪問技術。

  • ADO.NET 似乎直接引用用於資料集和資料表的技術。

  • 實體框架是所有前瞻性投資正在進行的地方,這種情況已經持續數年了。

  • Microsoft 建議您在所有新開發中使用實體框架而不是 ADO.NET 或 LINQ to SQL。

概念模型

對於習慣於以資料庫為中心的開發的開發人員來說,使用實體框架最大的轉變在於它允許您專注於您的業務領域。您希望您的應用程式執行什麼,而不會受到資料庫功能的限制?

  • 在實體框架中,焦點被稱為概念模型。它是應用程式中物件的模型,而不是您用於持久儲存應用程式資料的資料庫的模型。

  • 您的概念模型可能恰好與您的資料庫模式一致,也可能完全不同。

  • 您可以使用 Visual Designer 定義您的概念模型,然後它可以生成您最終將在應用程式中使用的類。

  • 您只需定義您的類並使用實體框架的一個名為 Code First 的功能。然後實體框架將理解概念模型。

Conceptual Model

無論哪種方式,實體框架都會找出如何從您的概念模型移動到您的資料庫。因此,您可以針對您的概念模型物件進行查詢並直接使用它們。

功能

以下是實體框架的基本功能。此列表是根據最顯著的功能以及關於實體框架的常見問題建立的。

  • 實體框架是 Microsoft 的工具。
  • 實體框架正在作為開源產品開發。
  • 實體框架不再與 .NET 釋出週期繫結或依賴。
  • 適用於任何具有有效實體框架提供程式的關係資料庫。
  • 從 LINQ to Entities 生成 SQL 命令。
  • 實體框架將建立引數化查詢。
  • 跟蹤對記憶體中物件的更改。
  • 允許插入、更新和刪除命令生成。
  • 適用於視覺化模型或您自己的類。
  • 實體框架支援儲存過程。

實體框架 - 架構

實體框架的體系結構,從下到上,包括以下內容:

資料提供程式

這些是特定於源的提供程式,它們抽象 ADO.NET 介面以在針對概念模式進行程式設計時連線到資料庫。

它將 LINQ 等通用 SQL 語言透過命令樹轉換為本地 SQL 表示式,並在特定的 DBMS 系統上執行。

實體客戶端

此層將實體層公開給上層。實體客戶端使開發人員能夠以行和列的形式針對實體工作,使用實體 SQL 查詢,而無需生成類來表示概念模式。實體客戶端顯示實體框架層,它們是核心功能。這些層稱為實體資料模型。

Entity Data Model
  • 儲存層包含以 XML 格式儲存的整個資料庫模式。

  • 實體層也是一個 XML 檔案,它定義實體和關係。

  • 對映層是一個 XML 檔案,它將概念層中定義的實體和關係與邏輯層中定義的實際關係和表對映。

  • 元資料服務也在實體客戶端中表示,它提供集中式 API 來訪問儲存在實體、對映和儲存層中的元資料。

物件服務

物件服務層是物件上下文,它表示應用程式與資料來源之間互動的會話。

  • 物件上下文的主要用途是執行新增、刪除實體例項以及在查詢的幫助下將更改的狀態儲存回資料庫等不同操作。

  • 它是實體框架的 ORM 層,它將資料結果表示為實體的物件例項。

  • 此服務允許開發人員透過使用 LINQ 和實體 SQL 編寫查詢來使用一些豐富的 ORM 功能,例如主鍵對映、更改跟蹤等。

實體框架 - 環境設定

實體框架 6 中的新增功能?

框架具有複雜的 API,允許您對從建模到執行時行為的所有內容進行細粒度控制。實體框架 5 的一部分位於 .NET 內部。而它的另一部分位於使用 NuGet 分發的附加程式集中。

  • 實體框架的核心功能內置於 .NET Framework 中。

  • 程式碼優先支援,它允許實體框架使用類代替視覺化模型,以及與 EF 互動的更輕量級 API 位於 NuGet 包中。

  • 核心是提供查詢、更改跟蹤以及將您的查詢轉換為 SQL 查詢以及將資料返回轉換為物件的所有轉換。

  • 您可以將 EF 5 NuGet 包與 .NET 4 和 .NET 4.5 一起使用。

  • 一個主要的混淆點 - .NET 4.5 為核心實體框架 API 添加了對列舉和空間資料的支援,這意味著如果您將 EF 5 與 .NET 4 一起使用,則無法獲得這些新功能。只有將 EF5 與 .NET 4.5 結合使用時,才能獲得它們。

Framework 6

現在讓我們看一下實體框架 6。在實體框架 6 中位於 .NET 內部的核心 API 現在是 NuGet 包的一部分。

Entity Framework 6

這意味著:

  • 所有實體框架都位於由 NuGet 分發的此程式集內部

  • 您將不會依賴 .NET 來提供特定功能,例如實體框架列舉支援和特殊資料支援。

  • 您會看到 EF6 的一項功能是它支援 .NET 4 的列舉和空間資料

要開始使用實體框架,您需要安裝以下開發工具:

  • Visual Studio 2013 或更高版本
  • SQL Server 2012 或更高版本
  • 來自 NuGet 包的實體框架更新

Microsoft 提供了一個免費版本的 Visual Studio,其中還包含 SQL Server,您可以從 www.visualstudio.com 下載。

安裝

步驟 1 - 下載完成後,執行安裝程式。將顯示以下對話方塊。

Visual Studio Installer

步驟 2 − 點選“安裝”按鈕,將開始安裝過程。

Installation Process

步驟 3 − 安裝過程完成後,您將看到以下對話方塊。關閉此對話方塊,如果需要,請重新啟動計算機。

Setup Completed

步驟 4 − 從開始選單開啟 Visual Studio,將開啟以下對話方塊。第一次準備可能需要一段時間。

Visual Studio

步驟 5 − 完成後,您將看到 Visual Studio 的主視窗。

Main Window

讓我們從“檔案”→“新建”→“專案”建立一個新專案

New Project

步驟 1 − 選擇“控制檯應用程式”並點選“確定”按鈕。

步驟 2 − 在解決方案資源管理器中,右鍵點選您的專案。

Console Application

步驟 3 − 選擇“管理 NuGet 程式包”,如上圖所示,這將在 Visual Studio 中開啟以下視窗。

Visual Studio 1

步驟 4 − 搜尋“Entity Framework”並透過按下“安裝”按鈕安裝最新版本。

Preview

步驟 5 − 點選“確定”。安裝完成後,您將在輸出視窗中看到以下訊息。

Output Window

您現在可以開始您的應用程式了。

實體框架 - 資料庫設定

在本教程中,我們將使用一個簡單的大學資料庫。整個大學資料庫可能會更加複雜,但出於演示和學習目的,我們使用的是該資料庫的最簡單形式。下圖包含三個表。

  • 學生
  • 課程
  • 選課
Database

每當使用術語資料庫時,我們腦海中就會直接想到不同型別的表,這些表之間存在某種關係。表之間存在三種關係型別,不同表之間的關係取決於相關列的定義方式。

  • 一對多關係
  • 多對多關係
  • 一對一關係

一對多關係

一對多關係是最常見的關係型別。在這種關係型別中,表 A 中的一行可以在表 B 中有多行匹配行,但表 B 中的一行只能在表 A 中有一行匹配行。例如,在上圖中,學生和選課表之間存在一對多關係,每個學生可能有多個選課記錄,但每個選課記錄只屬於一個學生。

多對多關係

在多對多關係中,表 A 中的一行可以在表 B 中有多行匹配行,反之亦然。您可以透過定義一個第三個表(稱為連線表)來建立這種關係,該表的 primary key 由表 A 和表 B 的 foreign keys 組成。例如,學生和課程表之間存在多對多關係,該關係由這兩個表到選課表的每一個一對多關係定義。

一對一關係

在一對一關係中,表 A 中的一行最多隻能在表 B 中有一行匹配行,反之亦然。如果兩個相關列都是 primary key 或具有唯一約束,則會建立一對一關係。

這種關係型別並不常見,因為大多數以這種方式相關的資訊都將包含在一個表中。您可以使用一對一關係來 -

  • 將一個包含許多列的表進行分割。
  • 出於安全原因隔離表的一部分。
  • 儲存短暫的資料,可以透過簡單地刪除表來輕鬆刪除這些資料。
  • 儲存僅適用於主表子集的資訊。

實體框架 - 資料模型

實體資料模型 (EDM) 是實體關係模型的擴充套件版本,它使用各種建模技術指定資料的概念模型。它還指的是描述資料結構的一組概念,而不管其儲存形式如何。

EDM 支援一組定義概念模型中屬性的原始資料型別。我們需要考慮構成 Entity Framework 基礎的 3 個核心部分,它們統稱為實體資料模型。以下是 EDM 的三個核心部分。

  • 儲存模式模型
  • 概念模型
  • 對映模型

儲存模式模型

儲存模型也稱為儲存模式定義層 (SSDL),它表示後端資料儲存的模式表示。

EDM

概念模型

概念模型也稱為概念模式定義層 (CSDL),是真正的實體模型,我們針對它編寫查詢。

對映模型

對映層只是概念模型和儲存模型之間的對映。

邏輯模式及其與物理模式的對映表示為 EDM。

  • Visual Studio 還提供實體設計器,用於視覺化建立 EDM 和對映規範。

  • 該工具的輸出是指定模式和對映的 XML 檔案(*.edmx)。

  • Edmx 檔案包含 Entity Framework 元資料工件。

模式定義語言

ADO.NET Entity Framework 使用基於 XML 的資料定義語言,稱為模式定義語言 (SDL),來定義 EDM 模式。

  • SDL 定義與其他原始型別類似的簡單型別,包括 String、Int32、Double、Decimal 和 DateTime 等。

  • 列舉,它定義了原始值和名稱的對映,也被認為是一種簡單型別。

  • 列舉僅從框架版本 5.0 開始受支援。

  • 複雜型別是由其他型別的聚合建立的。這些型別的屬性集合定義實體型別。

資料模型主要包含三個關鍵概念來描述資料結構 -

  • 實體型別
  • 關聯型別
  • 屬性

實體型別

實體型別是描述 EDM 中資料結構的基本構建塊。

  • 在概念模型中,實體型別由屬性構成,並描述頂級概念的結構,例如業務應用程式中的學生和選課。

  • 實體表示特定物件,例如特定學生或選課記錄。

  • 每個實體都必須在實體集中具有唯一的實體鍵。實體集是特定實體型別例項的集合。實體集(和關聯集)在邏輯上分組在實體容器中。

  • 實體型別支援繼承,即一個實體型別可以從另一個實體型別派生。

Entity Type

關聯型別

它是描述 EDM 中關係的另一個基本構建塊。在概念模型中,關聯表示兩個實體型別之間的關係,例如學生和選課。

  • 每個關聯都有兩個關聯端,它們指定關聯中涉及的實體型別。

  • 每個關聯端還指定關聯端多重性,該多重性指示關聯該端的實體數量。

  • 關聯端多重性可以具有值 1、0..1 或 *。

  • 關聯一端的實體可以透過導航屬性或透過 foreign keys(如果它們在實體型別上公開)進行訪問。

屬性

實體型別包含定義其結構和特徵的屬性。例如,學生實體型別可能具有屬性,如學生 ID、姓名等。

屬性可以包含原始資料(例如字串、整數或布林值)或結構化資料(例如複雜型別)。

實體框架 - DbContext

Entity Framework 使您能夠使用公共語言執行時 (CLR) 物件(稱為實體)查詢、插入、更新和刪除資料。Entity Framework 將模型中定義的實體和關係對映到資料庫。它還提供以下功能 -

  • 將從資料庫返回的資料具體化為實體物件
  • 跟蹤對物件所做的更改
  • 處理併發
  • 將物件更改傳播回資料庫
  • 將物件繫結到控制元件

負責以物件形式與資料互動的主要類是 System.Data.Entity.DbContext。DbContext API 不是作為 .NET Framework 的一部分發布的。為了能夠更靈活、更頻繁地向 Code First 和 DbContext API 釋出新功能,Entity Framework 團隊透過 Microsoft 的 NuGet 分發功能分發 EntityFramework.dll。

  • NuGet 允許您透過從 Web 直接將相關的 DLL 拉入您的專案來向您的 .NET 專案新增引用。

  • 一個名為庫包管理器的 Visual Studio 擴充套件提供了一種簡單的方法,可以將相應的程式集從 Web 拉入您的專案。

DbContext
  • DbContext API 主要針對簡化您與 Entity Framework 的互動。

  • 它還減少了您需要訪問的常用任務的方法和屬性的數量。

  • 在早期版本的 Entity Framework 中,這些任務通常難以發現和編寫程式碼。

  • 上下文類在執行時管理實體物件,包括使用資料庫中的資料填充物件、更改跟蹤以及將資料持久化到資料庫。

定義 DbContext 派生類

使用上下文的工作推薦方法是定義一個從 DbContext 派生的類,並公開 DbSet 屬性,這些屬性表示上下文中指定實體的集合。如果您使用的是 EF Designer,則將為您生成上下文。如果您使用的是 Code First,則通常會自己編寫上下文。

以下程式碼是一個簡單的示例,它顯示 UniContext 是從 DbContext 派生的。

  • 您可以將自動屬性與 DbSet 一起使用,例如 getter 和 setter。

  • 它還使程式碼更加簡潔,但當您沒有其他邏輯要應用時,您不需要使用它來建立 DbSet。

public class UniContext : DbContext {
   public UniContext() : base("UniContext") { }
   public DbSet<Student> Students { get; set; }
   public DbSet<Enrollment> Enrollments { get; set; }
   public DbSet<Course> Courses { get; set; }
}
  • 以前,EDM 用於生成從 ObjectContext 類派生的上下文類。

  • 使用 ObjectContext 有點複雜。

  • DbContext 是 ObjectContext 的包裝器,實際上類似於 ObjectContext,在所有開發模型(如 Code First、Model First 和 Database First)中都很有用且易於使用。

查詢

您可以使用三種類型的查詢,例如 -

  • 新增新實體。
  • 更改或更新現有實體的屬性值。
  • 刪除現有實體。

新增新實體

使用 Entity Framework 新增新物件就像構造物件的新的例項並在 DbSet 上使用 Add 方法註冊它一樣簡單。以下程式碼適用於您想要將新學生新增到資料庫時。

private static void AddStudent() {

   using (var context = new UniContext()) {

      var student = new Student {
         LastName = "Khan", 
         FirstMidName = "Ali", 
         EnrollmentDate = DateTime.Parse("2005-09-01") 
      };

      context.Students.Add(student); 
      context.SaveChanges();

   }
}

更改現有實體

更改現有物件就像更新分配給要更改的屬性的值並呼叫 SaveChanges 一樣簡單。在以下程式碼中,Ali 的姓氏已從 Khan 更改為 Aslam。

private static void AddStudent() {

   private static void ChangeStudent() {

      using (var context = new UniContext()) {

         var student = (from d in context.Students
            where d.FirstMidName == "Ali" select d).Single();
         student.LastName = "Aslam";
         context.SaveChanges();

      }
   }
}

刪除現有實體

要使用 Entity Framework 刪除實體,請在 DbSet 上使用 Remove 方法。Remove 對現有實體和新新增的實體都有效。對已新增但尚未儲存到資料庫的實體呼叫 Remove 將取消新增該實體。該實體將從更改跟蹤器中刪除,並且 DbContext 也不再跟蹤它。對正在進行更改跟蹤的現有實體呼叫 Remove 將在下次呼叫 SaveChanges 時註冊要刪除該實體。以下示例顯示了一個例項,其中學生從資料庫中刪除,其名字是 Ali。

private static void DeleteStudent() {

   using (var context = new UniContext()) {
      var bay = (from d in context.Students where d.FirstMidName == "Ali" select d).Single();
      context.Students.Remove(bay);
      context.SaveChanges();
   }
}

實體框架 - 型別

在 Entity Framework 中,有兩種型別的實體允許開發人員將他們自己的自定義資料類與資料模型一起使用,而無需對資料類本身進行任何修改。

  • POCO 實體
  • 動態代理

POCO 實體

  • POCO 代表“普通舊”CLR 物件,可以用作您資料模型中現有的域物件。

  • 對映到實體的 POCO 資料類在資料模型中定義。

  • 它還支援與實體資料模型工具生成的實體型別大部分相同的查詢、插入、更新和刪除行為。

  • 您可以使用 POCO 模板從概念模型生成不依賴於持久化的實體型別。

讓我們看一下以下概念實體資料模型的示例。

Conceptual Entity Model

為上述實體模型生成 POCO 實體 -

步驟 1 − 右鍵單擊設計器視窗。將顯示以下對話方塊。

Designer Window

步驟 2 − 選擇“新增程式碼生成項…”

Code Generation

步驟 3 − 選擇 EF 6.x DbContext 生成器,輸入名稱,然後單擊“新增”按鈕。

您將在解決方案資源管理器中看到已生成 POCODemo.Context.tt 和 POCODemo.tt 模板。

Solution Explorer

POCODemo.Context 生成 DbContext 和您可以返回並用於查詢的物件集,例如上下文、學生和課程等。

Generate

另一個模板處理所有型別 Student、Courses 等。以下是自動從實體模型生成的 Student 類的程式碼。

namespace ConsoleApplication1 {

   using System;
   using System.Collections.Generic;

   public partial class Student {

      [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", 
         "CA2214:DoNotCallOverridableMethodsInConstructors")]

      public Student() {
         this.Enrollments = new HashSet<Enrollment>();
      }

      public int ID { get; set; }
      public string LastName { get; set; }
      public string FirstMidName { get; set; }
      public System.DateTime EnrollmentDate { get; set; }

      [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", 
         CA2227:CollectionPropertiesShouldBeReadOnly")]

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

   }
}

從實體模型為 Course 和 Enrollment 表生成類似的類。

動態代理

建立 POCO 實體型別的例項時,Entity Framework 通常會建立動態生成的派生型別的例項,該例項充當實體的代理。也可以說它是在執行時的代理類,就像 POCO 實體的包裝類。

  • 您可以覆蓋實體的某些屬性,以便在訪問屬性時自動執行操作。

  • 此機制用於支援關係的延遲載入和自動更改跟蹤。

  • 此技術也適用於使用 Code First 和 EF Designer 建立的模型。

如果希望 Entity Framework 支援相關物件的延遲載入並跟蹤 POCO 類中的更改,則 POCO 類必須滿足以下要求:

  • 自定義資料類必須宣告為公共訪問。

  • 自定義資料類不能是密封的。

  • 自定義資料類不能是抽象的。

  • 自定義資料類必須具有一個沒有引數的公共或受保護的建構函式。

  • 如果希望使用 CreateObject 方法建立 POCO 實體的代理,請使用沒有引數的受保護建構函式。

  • 呼叫 CreateObject 方法並不能保證建立代理:POCO 類必須遵循本主題中描述的其他要求。

  • 該類不能實現 IEntityWithChangeTracker 或 IEntityWithRelationships 介面,因為代理類實現了這些介面。

  • ProxyCreationEnabled 選項必須設定為 true。

以下示例是動態代理實體類。

public partial class Course {

   public Course() {
      this.Enrollments = new HashSet<Enrollment>();
   }

   public int CourseID { get; set; }
   public string Title { get; set; }
   public int Credits { get; set; }
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

要停用建立代理物件,請將 ProxyCreationEnabled 屬性的值設定為 false。

實體框架 - 關係

在關係資料庫中,關係是透過外部索引鍵存在於關係資料庫表之間的關係。外部索引鍵 (FK) 是一個列或列的組合,用於建立和強制兩個表之間的資料鏈接。下圖包含三個表。

  • 學生
  • 課程
  • 選課
Relational Database

在上圖中,您可以看到表之間存在某種關聯/關係。表之間存在三種類型的關係,不同表之間的關係取決於相關列的定義方式。

  • 一對多關係
  • 多對多關係
  • 一對一關係

一對多關係

  • 一對多關係是最常見的關係型別。

  • 在這種型別的關係中,表 A 中的一行可以在表 B 中有多個匹配行,但表 B 中的一行只能在表 A 中有一個匹配行。

  • 外部索引鍵在表示關係多端的表中定義。

  • 例如,在上圖中,Student 和 Enrollment 表具有一對多關係,每個學生可能有多個註冊,但每個註冊只屬於一個學生。

在實體框架中,這些關係也可以用程式碼建立。以下是與一對多關係關聯的 Student 和 Enrollment 類的示例。

public class Student {
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

public class Enrollment {

   public int EnrollmentID { get; set; }
   public int CourseID { get; set; }
   public int StudentID { get; set; }
	
   public Grade? Grade { get; set; }
   public virtual Course Course { get; set; }
   public virtual Student Student { get; set; }
}

在上面的程式碼中,您可以看到 Student 類包含 Enrollment 的集合,但 Enrollment 類有一個單獨的 Student 物件。

多對多關係

在多對多關係中,表 A 中的一行可以在表 B 中有多個匹配行,反之亦然。

  • 您可以透過定義一個第三個表(稱為連線表)來建立這種關係,該表的 primary key 由表 A 和表 B 的外部索引鍵組成。

  • 例如,Student 和 Course 表具有多對多關係,該關係由這兩個表到 Enrollment 表的一對多關係定義。

以下程式碼包含 Course 類和上述兩個類,即StudentEnrollment

public class Course {
   [DatabaseGenerated(DatabaseGeneratedOption.None)]
	
   public int CourseID { get; set; }
   public string Title { get; set; }
	
   public int Credits { get; set; } 
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

您可以看到 Course 類和 Student 類都具有 Enrollment 物件的集合,這透過連線類 Enrollment 實現了多對多關係。

一對一關係

  • 在一對一關係中,表 A 中的一行在表 B 中最多隻能有一個匹配行,反之亦然。

  • 如果兩個相關列都是 primary key 或具有唯一約束,則會建立一對一關係。

  • 在一對一關係中,primary key 還會充當外部索引鍵,並且兩個表都沒有單獨的外部索引鍵列。

這種型別的關係並不常見,因為大多數以這種方式相關的資訊都將位於一個表中。您可以使用一對一關係來:

  • 將一個包含許多列的表進行分割。
  • 出於安全原因隔離表的一部分。
  • 儲存短暫的資料,可以透過簡單地刪除表來輕鬆刪除這些資料。
  • 儲存僅適用於主表子集的資訊。

以下程式碼是新增另一個名為 StudentProfile 的類,該類包含學生的電子郵件 ID 和密碼。

public class Student {
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
   public virtual StudentProfile StudentProfile { get; set; }
}

public class StudentProfile {

   public StudentProfile() {}
   public int ID { get; set; }
   public string Email { get; set; }
   public string Password { get; set; }
	
   public virtual Student Student { get; set; }
}

您可以看到 Student 實體類包含 StudentProfile 導航屬性,而 StudentProfile 包含 Student 導航屬性。

每個學生只有一個電子郵件和密碼登入大學域。這些資訊可以新增到 Student 表中,但出於安全原因,它被分離到另一個表中。

實體框架 - 生命週期

生命週期

上下文的生命週期從例項建立時開始,到例項被處置或垃圾回收時結束。

  • 上下文生命週期是在我們使用 ORM 時需要做出的一個非常重要的決定。

  • 上下文執行類似於實體快取的操作,這意味著它儲存對所有已載入實體的引用,這些引用可能會很快導致記憶體消耗增加,並且還可能導致記憶體洩漏。

  • 在下圖中,您可以看到從應用程式到資料庫透過上下文的資料工作流的上層,反之亦然。

Data Workflow

實體生命週期

實體生命週期描述了建立、新增、修改、刪除等實體的過程。實體在其生命週期中具有許多狀態。在檢視如何檢索實體狀態之前,讓我們先了解什麼是實體狀態。狀態是型別System.Data.EntityState的列舉,它宣告以下值:

  • Added:實體被標記為已新增。

  • Deleted:實體被標記為已刪除。

  • Modified:實體已被修改。

  • Unchanged:實體尚未修改。

  • Detached:實體未被跟蹤。

實體生命週期中的狀態更改

有時實體的狀態由上下文自動設定,但也可以由開發人員手動修改。即使所有從一種狀態切換到另一種狀態的組合都是可能的,但其中一些組合毫無意義。例如,將Added實體切換到Deleted狀態,反之亦然。

讓我們討論一下不同的狀態。

Unchanged 狀態

  • 當實體處於 Unchanged 狀態時,它繫結到上下文,但尚未修改。

  • 預設情況下,從資料庫檢索到的實體處於此狀態。

  • 當實體附加到上下文(使用 Attach 方法)時,它也處於 Unchanged 狀態。

  • 上下文無法跟蹤其未引用的物件的更改,因此當它們附加時,它假設它們處於 Unchanged 狀態。

Detached 狀態

  • Detached 是新建立實體的預設狀態,因為上下文無法跟蹤程式碼中任何物件的建立。

  • 即使在上下文的 using 塊內例項化實體,也適用此規則。

  • 即使在停用跟蹤時從資料庫檢索到的實體也處於 Detached 狀態。

  • 當實體處於 Detached 狀態時,它不會繫結到上下文,因此其狀態不會被跟蹤。

  • 它可以被處置、修改、與其他類組合使用,或者以您可能需要的任何其他方式使用。

  • 由於沒有上下文跟蹤它,因此它對 Entity Framework 沒有任何意義。

Added 狀態

  • 當實體處於 Added 狀態時,您有幾個選項。實際上,您只能將其從上下文中分離。

  • 當然,即使您修改了一些屬性,狀態仍然是 Added,因為將其移動到 Modified、Unchanged 或 Deleted 沒有任何意義。

  • 它是一個新實體,並且與資料庫中的行沒有對應關係。

  • 這是處於這些狀態之一的基本先決條件(但此規則不受上下文強制執行)。

Added State

Modified 狀態

  • 當實體被修改時,這意味著它處於 Unchanged 狀態,然後某些屬性發生了更改。

  • 實體進入 Modified 狀態後,可以移動到 Detached 或 Deleted 狀態,但即使手動還原原始值,它也不能回滾到 Unchanged 狀態。

  • 它甚至不能更改為 Added,除非您分離實體並將其新增到上下文,因為資料庫中已存在具有此 ID 的行,並且在持久化它時會收到執行時異常。

Deleted 狀態

  • 實體進入 Deleted 狀態是因為它處於 Unchanged 或 Modified 狀態,然後使用了 DeleteObject 方法。

  • 這是最嚴格的狀態,因為將此狀態更改為除 Detached 之外的任何其他值都沒有意義。

using 語句,如果您希望上下文控制的所有資源在塊結束時被處置。當您使用using語句時,編譯器會自動建立一個 try/finally 塊並在 finally 塊中呼叫 dispose。

using (var context = new UniContext()) {

   var student = new Student {
      LastName = "Khan", 
      FirstMidName = "Ali", 
      EnrollmentDate = DateTime.Parse("2005-09-01")
   };

   context.Students.Add(student);
   context.SaveChanges();
}

在處理長期執行的上下文時,請考慮以下事項:

  • 隨著您將更多物件及其引用載入到記憶體中,上下文的記憶體消耗可能會迅速增加。這可能會導致效能問題。

  • 在不再需要上下文時,請務必將其處置。

  • 如果異常導致上下文處於不可恢復的狀態,則整個應用程式可能會終止。

  • 當資料查詢和更新之間的時間間隔增加時,遇到與併發相關的問題的可能性也會增加。

  • 在處理 Web 應用程式時,請每個請求使用一個上下文例項。

  • 在處理 Windows Presentation Foundation (WPF) 或 Windows 窗體時,請每個窗體使用一個上下文例項。這使您可以使用上下文提供的更改跟蹤功能。

經驗法則

Web 應用程式

  • 現在,對於 Web 應用程式,每個請求使用一個上下文已成為一種常見且最佳的做法。

  • 在 Web 應用程式中,我們處理的是非常短的請求,但包含所有伺服器事務,因此它是上下文生存的適當持續時間。

桌面應用程式

  • 對於桌面應用程式(如 Win Forms/WPF 等),每個窗體/對話方塊/頁面使用一個上下文。

  • 由於我們不希望將上下文作為應用程式的單例,因此在從一個窗體移動到另一個窗體時,我們將對其進行處置。

  • 透過這種方式,我們將獲得上下文的許多功能,並且不會受到長期執行上下文的負面影響。

Entity Framework - Code First 方法

實體框架提供三種建立實體模型的方法,每種方法都有其自身的優缺點。

  • 程式碼優先 (Code First)
  • 資料庫優先 (Database First)
  • 模型優先 (Model First)

在本章中,我們將簡要介紹程式碼優先方法。一些開發人員喜歡使用程式碼中的設計器,而另一些開發人員則寧願只使用他們的程式碼。對於這些開發人員,實體框架提供了一種稱為程式碼優先的建模工作流。

  • 程式碼優先建模工作流的目標資料庫是不存在的,程式碼優先將建立它。

  • 如果資料庫為空,它也可以使用,然後程式碼優先也將新增新表。

  • 程式碼優先允許您使用 C# 或 VB.Net 類定義模型。

  • 可以使用類和屬性上的特性或使用流暢的 API 選擇性地執行其他配置。

Code First Approach

為什麼選擇程式碼優先?

  • 程式碼優先實際上是由一組拼圖塊組成的。首先是您的領域類。

  • 領域類與實體框架無關。它們只是您的業務領域的專案。

  • 然後,實體框架有一個上下文來管理這些類與資料庫之間的互動。

  • 上下文不特定於程式碼優先。它是實體框架的一個特性。

  • 程式碼優先添加了一個模型構建器,它檢查上下文正在管理的類,然後使用一組規則或約定來確定這些類和關係如何描述模型,以及該模型應如何對映到您的資料庫。

  • 所有這些都在執行時發生。您將永遠看不到此模型,它僅存在於記憶體中。

  • 程式碼優先能夠在需要時使用該模型建立資料庫。

  • 如果模型發生更改,它還可以更新資料庫,使用稱為程式碼優先遷移的功能。

實體框架 - 模型優先方法

在本章中,讓我們學習如何在設計器中使用稱為模型優先的工作流建立實體資料模型。

  • 當您開始一個新專案並且資料庫甚至不存在時,模型優先非常有用。

  • 模型儲存在 EDMX 檔案中,可以在實體框架設計器中檢視和編輯。

  • 在模型優先中,您在實體框架設計器中定義模型,然後生成 SQL,它將建立與您的模型匹配的資料庫模式,然後您執行 SQL 以在您的資料庫中建立模式。

  • 您在應用程式中互動的類是從 EDMX 檔案自動生成的。

以下是使用模型優先方法建立新控制檯專案的簡單示例。

步驟 1 - 開啟 Visual Studio 並選擇檔案 → 新建 → 專案

Console Project

步驟 2 - 從左側窗格中選擇已安裝 → 模板 → Visual C# → Windows,然後在中間窗格中選擇控制檯應用程式。

步驟 3 - 在名稱欄位中輸入 EFModelFirstDemo。

步驟 4 - 要建立模型,首先右鍵單擊解決方案資源管理器中的控制檯專案,然後選擇新增 → 新建項…

Model

將開啟以下對話方塊。

Add New

步驟 5 - 從中間窗格中選擇 ADO.NET 實體資料模型,並在名稱欄位中輸入 ModelFirstDemoDB。

步驟 6 - 單擊新增按鈕,將啟動實體資料模型嚮導對話方塊。

Model Wizard Dialog

步驟 7 - 選擇空 EF 設計器模型,然後單擊下一步按鈕。實體框架設計器將開啟一個空白模型。現在我們可以開始向模型新增實體、屬性和關聯。

步驟 8 - 右鍵單擊設計圖面並選擇屬性。在屬性視窗中,將實體容器名稱更改為 ModelFirstDemoDBContext。

Design Surface

步驟 9 - 右鍵單擊設計圖面並選擇新增新建 → 實體…

Add New Entity

新增實體對話方塊將開啟,如下面的影像所示。

Entity Dialog

步驟 10 - 輸入 Student 作為實體名稱,輸入 Student Id 作為屬性名稱,然後單擊確定。

Student

步驟 11 - 右鍵單擊設計圖面上的新實體,然後選擇新增新建 → 標量屬性,輸入 Name 作為屬性的名稱。

New Entity

步驟 12 - 輸入 FirstName,然後新增另外兩個標量屬性,例如 LastName 和 EnrollmentDate。

Scalar Properties

步驟 13 - 透過遵循上述所有步驟,新增另外兩個實體 Course 和 Enrollment,並新增一些標量屬性,如下面的步驟所示。

Visual Designer

步驟 14 - 我們在 Visual Designer 中有三個實體,讓我們在它們之間新增一些關聯或關係。

步驟 15 - 右鍵單擊設計圖面並選擇新增新建 → 關聯…

Add Association

步驟 16 - 使關係的一端指向 Student,其多重性為 1,另一端指向 Enrollment,其多重性為多。

Multiplicity Of One

步驟 17 - 這意味著一個 Student 有多個 Enrollments,而 Enrollment 屬於一個 Student。

步驟 18 - 確保選中“向‘Post’實體新增外部索引鍵屬性”複選框,然後單擊確定。

步驟 19 - 同樣,在 Course 和 Enrollment 之間新增另一個關聯。

Course And Enrollment

步驟 20 - 在實體之間新增關聯後,您的資料模型將如下面的螢幕所示。

Data Model

現在我們有一個簡單的模型,可以從中生成資料庫並用於讀取和寫入資料。讓我們繼續生成資料庫。

步驟 1 - 右鍵單擊設計圖面並選擇從模型生成資料庫…

Database From Model

步驟 2 - 您可以選擇現有資料庫或透過單擊新建連線…來建立新連線。

Generate Database

步驟 3 - 要建立新資料庫,請單擊新建連線…

Connection Properties

步驟 4 - 輸入伺服器名稱和資料庫名稱。

Server And Database

步驟 5 - 單擊下一步。

Server And Database 1

步驟 6 - 單擊完成。這將在專案中新增 *.edmx.sql 檔案。您可以透過開啟 .sql 檔案,然後右鍵單擊並選擇執行來在 Visual Studio 中執行 DDL 指令碼。

DDL Scripts

步驟 7 - 將顯示以下對話方塊以連線到資料庫。

Connect To Database

步驟 8 - 成功執行後,您將看到以下訊息。

Successful Execution

步驟 9 - 轉到伺服器資源管理器,您將看到資料庫已建立,其中包含三個指定的表。

Database Created

接下來,我們需要交換我們的模型以生成使用 DbContext API 的程式碼。

步驟 1 - 右鍵單擊 EF 設計器中模型的空白處,然後選擇新增程式碼生成項…

EF Designer

您將看到以下新增新項對話方塊開啟。

Dialog Open

步驟 2 - 在中間窗格中選擇 EF 6.x DbContext 生成器,並在名稱欄位中輸入 ModelFirstDemoModel。

步驟 3 - 您將在解決方案資源管理器中看到生成了 ModelFirstDemoModel.Context.tt 和 ModelFirstDemoModel.tt 模板。

Template Generated

ModelFirstDemoModel.Context 生成 DbCcontext 和您可以返回並用於查詢的物件集,例如上下文、Students 和 Courses 等。

另一個模板處理所有型別 Student、Courses 等。以下是自動從實體模型生成的 Student 類。

CSharp Code

以下是 C# 程式碼,其中一些資料已輸入並從資料庫中檢索。

using System;
using System.Linq;

namespace EFModelFirstDemo {

   class Program {

      static void Main(string[] args) {

         using (var db = new ModelFirstDemoDBContext()) {

            // Create and save a new Student

            Console.Write("Enter a name for a new Student: ");
            var firstName = Console.ReadLine();

            var student = new Student {
               StudentID = 1,
               FirstName = firstName
            };
				
            db.Students.Add(student);
            db.SaveChanges();
				
            var query = from b in db.Students
               orderby b.FirstName select b;

            Console.WriteLine("All student in the database:");

            foreach (var item in query) {
               Console.WriteLine(item.FirstName);
            }

            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
         }
      }
   }
}

執行上述程式碼時,您將收到以下輸出 -

Enter a name for a new Student:
Ali Khan
All student in the database:
Ali Khan
Press any key to exit...

我們建議您逐步執行上述示例,以便更好地理解。

實體框架 - 資料庫優先方法

在本章中,讓我們學習如何使用資料庫優先方法建立實體資料模型。

  • 資料庫優先方法為實體資料模型提供了程式碼優先和模型優先方法的替代方案。它從專案中的資料庫建立模型程式碼(類、屬性、DbContext 等),這些類成為資料庫和控制器之間的連結。

  • 資料庫優先方法從現有資料庫建立實體框架。我們以與在模型優先方法中相同的方式使用所有其他功能,例如模型/資料庫同步和程式碼生成。

讓我們舉一個簡單的例子。我們已經有一個數據庫,其中包含 3 個表,如下面的影像所示。

New Console Project

步驟 1 - 讓我們建立一個名為 DatabaseFirstDemo 的新控制檯專案。

步驟 2 - 要建立模型,首先右鍵單擊解決方案資源管理器中的控制檯專案,然後選擇新增 → 新建項…

Create Model

步驟 3 - 從中間窗格中選擇 ADO.NET 實體資料模型,並在名稱欄位中輸入 DatabaseFirstModel。

步驟 4 - 單擊新增按鈕,將啟動實體資料模型嚮導對話方塊。

Model Contents

步驟 5 - 選擇來自資料庫的 EF 設計器,然後單擊下一步按鈕。

Entity Model Wizard

步驟 6 - 選擇現有資料庫,然後單擊下一步。

Existing Database

步驟 7 - 選擇 Entity Framework 6.x,然後單擊下一步。

Entity Framework

步驟 8 - 選擇要包含的所有表、檢視和儲存過程,然後單擊完成。

您將看到從資料庫生成的實體模型和 POCO 類。

POCO Classes

現在讓我們透過在 program.cs 檔案中編寫以下程式碼來檢索資料庫中的所有學生。

using System;
using System.Linq;

namespace DatabaseFirstDemo {

   class Program {

      static void Main(string[] args) {

         using (var db = new UniContextEntities()) {

            var query = from b in db.Students
               orderby b.FirstMidName select b;

            Console.WriteLine("All All student in the database:");

            foreach (var item in query) {
               Console.WriteLine(item.FirstMidName +" "+ item.LastName);
            }

            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
         }
      }
   }
}

執行上述程式時,您將收到以下輸出 -

All student in the database:
Ali Khan
Arturo   finand
Bill Gates
Carson Alexander
Gytis Barzdukas
Laura Norman
Meredith Alonso
Nino Olivetto
Peggy Justice
Yan Li
Press any key to exit...

執行上述程式時,您將看到以前在資料庫中輸入的所有學生的姓名。

我們建議您逐步執行上述示例,以便更好地理解。

實體框架 - 開發方法

在本章中,讓我們重點關注使用設計器或資料庫優先或僅使用程式碼優先構建模型。以下是一些準則,將幫助您決定選擇哪種建模工作流。

  • 我們已經看到了程式碼優先建模、資料庫優先建模和模型優先建模工作流的示例。

  • 資料庫優先和模型優先工作流使用設計器,但一個從資料庫開始建立模型,另一個從模型開始建立資料庫。

Designer Model
  • 對於那些不想使用 Visual Designer 加上程式碼生成的開發人員,實體框架提供了一種完全不同的工作流,稱為程式碼優先。

  • 程式碼優先的典型工作流非常適合全新的應用程式,在這種應用程式中,您甚至沒有資料庫。您定義自己的類和程式碼,然後讓程式碼優先確定資料庫的外觀。

  • 也可以從資料庫開始程式碼優先,這使得程式碼優先有點矛盾。但是,有一個工具可以幫助您將資料庫反向工程成類,這是一種開始編碼的好方法。

鑑於這些選項,讓我們看一下決策樹。

  • 如果您更喜歡在生成的程式碼中使用 Visual Designer,那麼您需要選擇其中一個涉及 EF Designer 的工作流。如果您的資料庫已存在,那麼資料庫優先是您的路徑。

  • 如果您想在一個沒有資料庫的全新專案上使用 Visual Designer,那麼您需要使用模型優先。

  • 如果您只想使用程式碼而不使用設計器,那麼程式碼優先可能適合您,以及使用將資料庫反向工程成類的工具的選項。

  • 如果您有現有的類,那麼最好的選擇是將它們與程式碼優先一起使用。

實體框架 - 資料庫操作

在前面的章節中,您學習了定義實體資料模型的三種不同方法。

  • 其中兩種,資料庫優先和模型優先,依賴於實體框架設計器以及程式碼生成。

  • 第三種,程式碼優先,允許您跳過視覺設計器,只需編寫自己的程式碼。

  • 無論您選擇哪種路徑,最終都會得到領域類和一個或多個實體框架 DbContext 類,這些類允許您檢索和持久化與這些類相關的

應用程式中的 DbContext API 用作類和資料庫之間的橋樑。DbContext 是實體框架中最重要的類之一。

  • 它能夠表達和執行查詢。

  • 它獲取資料庫中的查詢結果,並將它們轉換為模型類的例項。

  • 它可以跟蹤實體的更改,包括新增和刪除,然後觸發建立插入、更新和刪除語句,這些語句根據需要傳送到資料庫。

本章將對以下域廣告上下文類執行不同的操作。這是我們在“資料庫優先方法”一章中建立的相同示例。

上下文類實現

using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Core.Objects;
using System.Linq;

namespace DatabaseFirstDemo {

   public partial class UniContextEntities : DbContext {

      public UniContextEntities(): base("name = UniContextEntities") {}

      protected override void OnModelCreating(DbModelBuilder modelBuilder) {
         throw new UnintentionalCodeFirstException();
      }

      public virtual DbSet<Course> Courses { get; set; }
      public virtual DbSet<Enrollment> Enrollments { get; set; }
      public virtual DbSet<Student> Students { get; set; }
   }
}

域類實現

課程類

namespace DatabaseFirstDemo {

   using System;
   using System.Collections.Generic;
	
   public partial class Course {

      [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", 
         "CA2214:DoNotCallOverridableMethodsInConstructors")]

      public Course() {
         this.Enrollments = new HashSet<Enrollment>();
      }
	
      public int CourseID { get; set; }
      public string Title { get; set; }
      public int Credits { get; set; }
	
      [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", 
         "CA2227:CollectionPropertiesShouldBeReadOnly")]
			
      public virtual ICollection<Enrollment> Enrollments { get; set; }
   }
}

學生類

namespace DatabaseFirstDemo {

   using System;
   using System.Collections.Generic; 

   public partial class Student {

      [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", 
         "CA2214:DoNotCallOverridableMethodsInConstructors")]

      public Student() {
         this.Enrollments = new HashSet<Enrollment>();
      }

      public int ID { get; set; }
      public string LastName { get; set; }
      public string FirstMidName { get; set; }
      public System.DateTime EnrollmentDate { get; set; }

      [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", 
         "CA2227:CollectionPropertiesShouldBeReadOnly")]
			
      public virtual ICollection<Enrollment> Enrollments { get; set; }
   }
}

註冊類

namespace DatabaseFirstDemo {

   using System;
   using System.Collections.Generic; 

   public partial class Enrollment {

      public int EnrollmentID { get; set; }
      public int CourseID { get; set; }
      public int StudentID { get; set; }
      public Nullable<int> Grade { get; set; }
		
      public virtual Course Course { get; set; }
      public virtual Student Student { get; set; }
   }
}

建立操作

使用 Entity Framework 新增新物件就像構造物件的新例項並在 DbSet 上使用 Add 方法註冊它一樣簡單。以下程式碼允許您向資料庫新增新學生。

class Program {

   static void Main(string[] args) {

      var newStudent = new Student();

      //set student name

      newStudent.FirstMidName = "Bill";
      newStudent.LastName = "Gates";
      newStudent.EnrollmentDate = DateTime.Parse("2015-10-21");
      newStudent.ID = 100;

      //create DBContext object

      using (var dbCtx = new UniContextEntities()) {

         //Add Student object into Students DBset
         dbCtx.Students.Add(newStudent);

         // call SaveChanges method to save student into database
         dbCtx.SaveChanges();
      }
   }
}

更新操作

更改現有物件就像更新分配給要更改的屬性的值並呼叫 SaveChanges 一樣簡單。例如,以下程式碼用於將 Ali 的姓氏從 Khan 更改為 Aslam。

using (var context = new UniContextEntities()) {

   var student = (from d in context.Students where d.FirstMidName == "Ali" select d).Single();
   student.LastName = "Aslam";
   context.SaveChanges();
}

刪除操作

要使用 Entity Framework 刪除實體,請在 DbSet 上使用 Remove 方法。Remove 對現有實體和新新增的實體都有效。對已新增但尚未儲存到資料庫的實體呼叫 Remove 將取消該實體的新增。該實體將從更改跟蹤器中刪除,並且不再受 DbContext 跟蹤。對正在進行更改跟蹤的現有實體呼叫 Remove 將在下次呼叫 SaveChanges 時註冊該實體以進行刪除。以下程式碼示例演示了從資料庫中刪除名為 Ali 的學生的程式碼。

using (var context = new UniContextEntities()) {
   var bay = (from d in context.Students where d.FirstMidName == "Ali" select d).Single();
   context.Students.Remove(bay);
   context.SaveChanges();
}

讀取操作

從資料庫讀取現有資料非常簡單。以下是程式碼,其中檢索了 Student 表中的所有資料,然後將顯示一個程式,其中包含按字母順序排列的學生姓名。

using (var db = new UniContextEntities()) {

   var query = from b in db.Students orderby b.FirstMidName select b;
   Console.WriteLine("All All student in the database:");

   foreach (var item in query) {
      Console.WriteLine(item.FirstMidName +" "+ item.LastName);
   }

   Console.WriteLine("Press any key to exit...");
   Console.ReadKey();
}

實體框架 - 併發

任何資料訪問開發人員在回答有關資料併發性的問題時都會遇到困難,“如果多個人同時編輯相同的資料會發生什麼?”

  • 我們當中比較幸運的人會處理這樣的業務規則:“沒問題,最後一個人獲勝”。

  • 在這種情況下,併發不是問題。更有可能的是,情況並非如此簡單,並且沒有萬能的解決方案可以立即解決所有場景。

  • 預設情況下,Entity Framework 將採用“最後一個人獲勝”的路徑,這意味著即使其他人更新了資料檢索時間和資料儲存時間之間的資料,也會應用最新的更新。

讓我們舉個例子來更好地理解它。以下示例在 Course 表中添加了一個新的列 VersionNo。

Course Table

轉到設計器,右鍵單擊設計器視窗,然後選擇“從資料庫更新模型…”。

Designer

您將看到在 Course 實體中添加了另一列。

Course Entity

右鍵單擊新建立的列 VersionNo,然後選擇“屬性”,並將“併發模式”更改為“固定”,如下面的影像所示。

New Created Column

將 Course.VersionNo 的 ConcurrencyMode 設定為 Fixed 後,每次更新 Course 時,Update 命令都會使用其 EntityKey 和 VersionNo 屬性查詢 Course。

讓我們來看一個簡單的場景。兩個使用者同時檢索相同的課程,使用者 1 將該課程的標題更改為數學並在使用者 2 之前儲存更改。稍後,當用戶 2 更改該課程的標題(該標題是在使用者 1 儲存其更改之前檢索到的)時,在這種情況下,使用者 2 將收到併發異常“User2:發生了樂觀併發異常”

using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;

namespace DatabaseFirstDemo {

   class Program {

      static void Main(string[] args) {

         Course c1 = null;
         Course c2 = null;

         //User 1 gets Course

         using (var context = new UniContextEntities()) {
            context.Configuration.ProxyCreationEnabled = false;
            c1 = context.Courses.Where(s ⇒ s.CourseID == 1).Single();
         }

         //User 2 also get the same Course

         using (var context = new UniContextEntities()) {
            context.Configuration.ProxyCreationEnabled = false;
            c2 = context.Courses.Where(s ⇒ s.CourseID == 1).Single();
         }

         //User 1 updates Course Title
         c1.Title = "Edited from user1";

         //User 2 updates Course Title
         c2.Title = "Edited from user2";

         //User 1 saves changes first

         using (var context = new UniContextEntities()) {

            try {
               context.Entry(c1).State = EntityState.Modified;
               context.SaveChanges();
            } catch (DbUpdateConcurrencyException ex) {
               Console.WriteLine("User1: Optimistic Concurrency exception occurred");
            }
         }

         //User 2 saves changes after User 1.
         //User 2 will get concurrency exection
         //because CreateOrModifiedDate is different in the database

         using (var context = new UniContextEntities()) {

            try {
               context.Entry(c2).State = EntityState.Modified;
               context.SaveChanges();
            } catch (DbUpdateConcurrencyException ex) {
               Console.WriteLine("User2: Optimistic Concurrency exception occurred");
            }
         }
      }
   }
}

實體框架 - 事務

在所有版本的 Entity Framework 中,無論何時執行SaveChanges()來插入、更新或刪除資料庫,框架都會將該操作包裝在事務中。當您呼叫 SaveChanges 時,上下文會自動啟動事務,並根據永續性是否成功提交或回滾事務。

  • 所有這些對您來說都是透明的,您無需處理它。

  • 此事務僅持續足夠長的時間來執行操作,然後完成。

  • 當您執行另一個此類操作時,將啟動一個新事務。

Entity Framework 6 提供以下功能:

Database.BeginTransaction()

  • 這是一種在現有 DbContext 中啟動和完成使用者事務的簡單易用的方法。

  • 它允許將多個操作組合在同一事務中,因此要麼全部提交,要麼全部作為單個操作回滾。

  • 它還允許使用者更輕鬆地指定事務的隔離級別。

Database.UseTransaction()

  • 它允許 DbContext 使用在 Entity Framework 外部啟動的事務。

讓我們來看以下示例,其中在單個事務中執行多個操作。程式碼如下:

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         using (var dbContextTransaction = context.Database.BeginTransaction()) {

            try {

               Student student = new Student() {
                  ID = 200, 
                  FirstMidName = "Ali", 
                  LastName = "Khan", 
                  EnrollmentDate = DateTime.Parse("2015-12-1")
               };

               context.Students.Add(student);

               context.Database.ExecuteSqlCommand(@"UPDATE Course SET Title = 
                  'Calculus'" + "WHERE CourseID = 1045");

               var query = context.Courses.Where(c ⇒ c.CourseID == 1045);

               foreach (var item in query) {
                  Console.WriteLine(item.CourseID.ToString()
                     + " " + item.Title + " " + item.Credits);
               }

               context.SaveChanges();
               var query1 = context.Students.Where(s ⇒ s.ID == 200);

               foreach (var item in query1) {
                  Console.WriteLine(item.ID.ToString() 
                     + " " + item.FirstMidName + " " + item.LastName);
               }

               dbContextTransaction.Commit();
            } catch (Exception) {
               dbContextTransaction.Rollback();
            }

         }
      }
   }
}
  • 啟動事務需要底層儲存連線處於開啟狀態。

  • 因此,呼叫 Database.BeginTransaction() 將開啟連線(如果尚未開啟)。

  • 如果 DbContextTransaction 打開了連線,則在呼叫 Dispose() 時將關閉它。

實體框架 - 檢視

檢視是一個包含透過預定義查詢獲得的資料的物件。檢視是一個虛擬物件或表,其結果集派生自查詢。它與真實表非常相似,因為它包含資料列和行。以下是檢視的一些典型用途:

  • 篩選底層表的資料
  • 出於安全目的篩選資料
  • 集中分佈在多個伺服器上的資料
  • 建立可重用的資料集

檢視的使用方式與表類似。要將檢視用作實體,首先需要將資料庫檢視新增到 EDM。將檢視新增到模型後,您可以像處理普通實體一樣處理它,除了建立、更新和刪除操作。

讓我們看看如何從資料庫中將檢視新增到模型中。

步驟 1 - 建立一個新的控制檯應用程式專案。

Application Project

步驟 2 - 在解決方案資源管理器中右鍵單擊專案,然後選擇“新增”→“新建項”。

Project Solution Explorer

步驟 3 - 從中間窗格中選擇 ADO.NET 實體資料模型,並在“名稱”欄位中輸入 ViewModel。

步驟 4 - 單擊新增按鈕,將啟動實體資料模型嚮導對話方塊。

Add Button

步驟 5 - 選擇來自資料庫的 EF 設計器,然後單擊下一步按鈕。

Entity Model Wizard

步驟 6 - 選擇現有資料庫,然後單擊下一步。

Existing Database

步驟 7 - 選擇 Entity Framework 6.x,然後單擊下一步。

Entity Framework Next

步驟 8 - 從資料庫中選擇表和檢視,然後單擊“完成”。

Table View

您可以在設計器視窗中看到已建立檢視,並且可以在程式中將其用作實體。

在解決方案資源管理器中,您可以看到 MyView 類也是從資料庫生成的。

讓我們來看一個從檢視中檢索所有資料的示例。以下是程式碼:

class Program {

   static void Main(string[] args) {

      using (var db = new UniContextEntities()) {

         var query = from b in db.MyViews
            orderby b.FirstMidName select b;

         Console.WriteLine("All student in the database:");

         foreach (var item in query) {
            Console.WriteLine(item.FirstMidName + " " + item.LastName);
         }

         Console.WriteLine("Press any key to exit...");
         Console.ReadKey();
      }
   }
}

執行上述程式碼時,您將收到以下輸出 -

All student in the database:
Ali Khan
Arturo   finand
Bill Gates
Carson Alexander
Gytis Barzdukas
Laura Norman
Meredith Alonso
Nino Olivetto
Peggy Justice
Yan Li
Press any key to exit...

我們建議您逐步執行上述示例,以便更好地理解。

實體框架 - 索引

索引是基於表和檢視的磁碟上資料結構。在大多數情況下,索引使資料檢索更快、更高效。但是,如果表或檢視上的索引過多,可能會對其他操作(如插入或更新)的效能產生不利影響。

  • 索引是實體框架的新功能,您可以透過減少從資料庫查詢資料所需的時間來提高 Code First 應用程式的效能。

  • 您可以使用Index屬性向資料庫新增索引,並覆蓋預設的UniqueClustered設定以獲得最適合您方案的索引。

讓我們來看以下程式碼,其中在 Course 類中為 CourseID 添加了 Index 屬性。

public partial class Course {

   public Course() {
      this.Enrollments = new HashSet<Enrollment>();
   }

   [Index]
   public int CourseID { get; set; }
   public string Title { get; set; }
	
   public int Credits { get; set; }
   public byte[] VersionNo { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }

}

上面建立的鍵是非唯一的、非聚集的。有可用的過載來覆蓋這些預設值:

  • 要將索引設為聚集索引,需要指定 IsClustered = true

  • 類似地,您也可以透過指定 IsUnique = true 將索引設為唯一索引

讓我們來看以下 C# 程式碼,其中索引是聚集的且唯一的。

public partial class Course {

   public Course() {
      this.Enrollments = new HashSet<Enrollment>();
   }

   [Index(IsClustered = true, IsUnique = true)]
   public int CourseID { get; set; }
   public string Title { get; set; }
	
   public int Credits { get; set; }
   public byte[] VersionNo { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

Index 屬性可用於在資料庫中建立唯一索引。但是,這並不意味著 EF 在處理關係等時能夠推斷出該列的唯一性。此功能通常稱為對“唯一約束”的支援。

Entity Framework - 儲存過程

Entity Framework 允許您在實體資料模型中使用儲存過程,而不是或結合其自動命令生成。

  • 您可以使用儲存過程對資料庫表執行預定義的邏輯,並且許多組織都制定了要求使用這些儲存過程的策略。

  • 它還可以指定 EF 應該使用您的儲存過程來插入、更新或刪除實體。

  • 儘管動態生成的命令安全、高效,並且通常與您自己編寫的命令一樣好或更好,但在許多情況下,儲存過程已經存在,並且您的公司慣例可能會限制對錶的直接使用。

  • 或者,您可能只想對儲存上執行的操作進行顯式控制,並更願意建立儲存過程。

以下示例從“檔案”→“新建”→“專案”建立新專案。

Procedure New Project

步驟 1 - 從中間窗格中選擇“控制檯應用程式”,並在“名稱”欄位中輸入 StoredProceduresDemo。

步驟 2 - 在伺服器資源管理器中右鍵單擊您的資料庫。

步驟 3 - 選擇“新建查詢”,並在 T-SQL 編輯器中輸入以下程式碼以在資料庫中新增新表。

IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = 
   OBJECT_ID(N'[dbo].[StudentGrade]') AND type in (N'U'))

BEGIN

   CREATE TABLE [dbo].[StudentGrade](

      [EnrollmentID] [int] IDENTITY(1,1) NOT NULL,
      [CourseID] [int] NOT NULL,
      [StudentID] [int] NOT NULL,
      [Grade] [decimal](3, 2) NULL,

      CONSTRAINT [PK_StudentGrade] PRIMARY KEY CLUSTERED (
         [EnrollmentID] ASC
      )

      WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY]

   ) ON [PRIMARY]

END
GO

步驟 4 - 右鍵單擊編輯器,然後選擇“執行”。

Editor

步驟 5 - 右鍵單擊您的資料庫,然後單擊“重新整理”。您將在資料庫中看到新新增的表。

步驟 6 - 在伺服器資源管理器中,再次右鍵單擊您的資料庫。

Server Database

步驟 7 - 選擇“新建查詢”,並在 T-SQL 編輯器中輸入以下程式碼以在資料庫中新增儲存過程,該過程將返回學生成績。

IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = 
   OBJECT_ID(N'[dbo].[GetStudentGrades]') AND type in (N'P', N'PC'))

BEGIN

   EXEC dbo.sp_executesql @statement = N'
   CREATE PROCEDURE [dbo].[GetStudentGrades]
   @StudentID int
   AS
   SELECT EnrollmentID, Grade, CourseID, StudentID FROM dbo.StudentGrade 
   WHERE StudentID = @StudentID
   '
END
GO

步驟 8 - 右鍵單擊編輯器,然後選擇“執行”。

Execute

步驟 9 - 右鍵單擊您的資料庫,然後單擊“重新整理”。您將看到資料庫中建立了一個儲存過程。

Store Procedure Created

步驟 10 - 在解決方案資源管理器中右鍵單擊專案名稱,然後選擇“新增”→“新建項”。

步驟 11 - 然後在“模板”窗格中選擇 ADO.NET 實體資料模型。

Template Pane

步驟 12 - 輸入 SPModel 作為名稱,然後單擊“新增”。

步驟 13 - 在“選擇模型內容”對話方塊中,選擇“從資料庫中的 EF 設計器”,然後單擊“下一步”。

Model Contents

步驟 14 - 選擇您的資料庫,然後單擊“下一步”。

Database 1

步驟 15 - 在“選擇資料庫物件”對話方塊中,單擊“表”、“檢視”。

Database Objects

步驟 16 - 選擇“儲存過程和函式”節點下的 GetStudentGradesForCourse 函式,然後單擊“完成”。

步驟 17 - 選擇“檢視”→“其他視窗”→“實體資料模型瀏覽器”,右鍵單擊“函式匯入”下的 GetStudentGrades,然後選擇“編輯”。

Entity Browser

它將生成以下對話方塊。

Entity Browser Dialog

步驟 18 - 單擊“實體”單選按鈕,然後從組合框中選擇 StudentGrade 作為此儲存過程的返回型別,然後單擊“確定”。

讓我們來看以下 C# 程式碼,其中將透過將學生 ID 作為引數傳遞給 GetStudentGrades 儲存過程來檢索所有成績。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         int studentID = 22;
         var studentGrades = context.GetStudentGrades(studentID);

         foreach (var student in studentGrades) {
            Console.WriteLine("Course ID: {0}, Title: {1}, Grade: {2} ", 
               student.CourseID, student.Course.Title, student.Grade);
         }

         Console.ReadKey();

      }
   }
}

編譯並執行上述程式碼後,您將收到以下輸出:

Course ID: 4022, Title: Microeconomics, Grade: 3.00
Course ID: 4041, Title: Macroeconomics, Grade: 3.50

我們建議您逐步執行以上示例以更好地理解。

Entity Framework - 斷開連線的實體

在本節中,讓我們看看如何更改不受上下文跟蹤的實體。不受上下文跟蹤的實體被稱為“斷開連線”實體。

  • 對於大多數單層應用程式,其中使用者介面和資料庫訪問層在同一個應用程式程序中執行,您可能只會對受上下文跟蹤的實體執行操作。

  • 對斷開連線實體的操作在 N 層應用程式中更為常見。

  • N 層應用程式涉及在伺服器上獲取一些資料並將其透過網路返回到客戶端計算機。

  • 然後,客戶端應用程式操作這些資料,然後再將其返回到伺服器以進行持久化。

以下是需要對斷開連線的實體圖甚至單個斷開連線的實體執行的兩個步驟。

  • 將實體附加到新的上下文例項並使上下文了解這些實體。

  • 手動為這些實體設定適當的 EntityStates。

Change To Entity

讓我們看一下以下程式碼,其中 Student 實體添加了兩個 Enrollment 實體。

class Program {

   static void Main(string[] args) {

      var student = new Student {

         ID = 1001,
         FirstMidName = "Wasim",
         LastName = "Akram", 

         EnrollmentDate = DateTime.Parse("2015-10-10"), 
            Enrollments = new List<Enrollment> {

               new Enrollment{EnrollmentID = 2001,CourseID = 4022, StudentID = 1001 },
               new Enrollment{EnrollmentID = 2002,CourseID = 4025, StudentID = 1001 },
         }
      };

      using (var context = new UniContextEntities()) {

         context.Students.Add(student);
         Console.WriteLine("New Student ({0} {1}): {2}", 
            student.FirstMidName, student.LastName, context.Entry(student).State);

         foreach (var enrollment in student.Enrollments) {
            Console.WriteLine("Enrollment ID: {0} State: {1}", 
               enrollment.EnrollmentID, context.Entry(enrollment).State);
         }

         Console.WriteLine("Press any key to exit...");
         Console.ReadKey();
      }
   } 
}
  • 該程式碼構造一個新的 Student 例項,該例項還在其 Enrollments 屬性中引用了兩個新的 Enrollment 例項。

  • 然後,使用 Add 方法將新的 Student 新增到上下文中。

  • 新增 Student 後,程式碼使用 DbContext.Entry 方法訪問 Entity Framework 關於新 Student 的更改跟蹤資訊。

  • 從這些更改跟蹤資訊中,State 屬性用於輸出實體的當前狀態。

  • 然後對從新 Student 引用的每個新建立的 Enrollment 重複此過程。如果執行應用程式,您將收到以下輸出 -

New Student   (Wasim  Akram): Added
Enrollment ID: 2001 State: Added
Enrollment ID: 2002 State: Added
Press any key to exit...

雖然 DbSet.Add 用於告訴 Entity Framework 關於新實體,但 DbSet.Attach 用於告訴 Entity Framework 關於現有實體。Attach 方法將實體標記為 Unchanged 狀態。

讓我們看一下以下 C# 程式碼,其中一個斷開連線的實體附加到 DbContext。

class Program {

   static void Main(string[] args) {

      var student = new Student {

         ID = 1001,
         FirstMidName = "Wasim",
         LastName = "Akram",
         EnrollmentDate = DateTime.Parse("2015-10-10"), 

         Enrollments = new List<Enrollment> {
            new Enrollment { EnrollmentID = 2001, CourseID = 4022, StudentID = 1001 },
            new Enrollment { EnrollmentID = 2002, CourseID = 4025, StudentID = 1001 },
         }
			
      };

      using (var context = new UniContextEntities()) {

         context.Students.Attach(student);
         Console.WriteLine("New Student ({0} {1}): {2}", 
            student.FirstMidName, student.LastName, context.Entry(student).State);

         foreach (var enrollment in student.Enrollments) {
            Console.WriteLine("Enrollment ID: {0} State: {1}", enrollment.EnrollmentID, 
               context.Entry(enrollment).State);
         }

         Console.WriteLine("Press any key to exit...");
         Console.ReadKey();
      }
   }
}

當使用 Attach() 方法執行上述程式碼時,您將收到以下輸出。

New Student   (Wasim  Akram): Unchanged
Enrollment ID: 2001 State: Unchanged
Enrollment ID: 2002 State: Unchanged
Press any key to exit...

Entity Framework - 表值函式

在本節中,讓我們學習如何使用 Entity Framework 設計器對映表值函式 (TVF) 以及如何從 LINQ 查詢中呼叫 TVF。

  • TVF 目前僅在 Database First 工作流中受支援。

  • 它最早在 Entity Framework 版本 5 中引入。

  • 要使用 TVF,您必須面向 .NET Framework 4.5 或更高版本。

  • 它與儲存過程非常相似,但有一個關鍵區別,即 TVF 的結果是可組合的。這意味著 TVF 的結果可以在 LINQ 查詢中使用,而儲存過程的結果則不能。

讓我們看一下以下建立新專案的示例,方法是選擇“檔案”→“新建”→“專案”。

Create Project

步驟 1 - 從中間窗格中選擇控制檯應用程式,並在名稱欄位中輸入 TableValuedFunctionDemo。

步驟 2 - 在伺服器資源管理器中右鍵單擊您的資料庫。

Explorer Database

步驟 3 - 選擇“新建查詢”,並在 T-SQL 編輯器中輸入以下程式碼以在資料庫中新增新表。

IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id =
   OBJECT_ID(N'[dbo].[StudentGrade]') AND type in (N'U'))

BEGIN

   CREATE TABLE [dbo].[StudentGrade](

      [EnrollmentID] [int] IDENTITY(1,1) NOT NULL,
      [CourseID] [int] NOT NULL,
      [StudentID] [int] NOT NULL,
      [Grade] [decimal](3, 2) NULL,

      CONSTRAINT [PK_StudentGrade] PRIMARY KEY CLUSTERED ([EnrollmentID] ASC)

      WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY]

   ) ON [PRIMARY]

END
GO

步驟 4 - 右鍵單擊編輯器,然後選擇“執行”。

Select Execute

步驟 5 - 右鍵單擊您的資料庫,然後單擊“重新整理”。您將在資料庫中看到新新增的表。

Added Table

步驟 6 - 現在建立一個函式,該函式將返回課程的學生成績。在 T-SQL 編輯器中輸入以下程式碼。

CREATE FUNCTION [dbo].[GetStudentGradesForCourse]

(@CourseID INT)

RETURNS TABLE

RETURN
   SELECT [EnrollmentID],
      [CourseID],
      [StudentID],
      [Grade]
   FROM   [dbo].[StudentGrade]
   WHERE  CourseID = @CourseID 

步驟 7 - 右鍵單擊編輯器並選擇“執行”。

Editor Select

現在您可以看到函式已建立。

Function Created

步驟 8 - 右鍵單擊解決方案資源管理器中的專案名稱,然後選擇“新增”→“新建項”。

步驟 9 - 然後在“模板”窗格中選擇 ADO.NET 實體資料模型。

Entity Template Pane

步驟 10 - 輸入 TVFModel 作為名稱,然後單擊“新增”。

步驟 11 - 在“選擇模型內容”對話方塊中,選擇“從資料庫中選擇 EF 設計器”,然後單擊“下一步”。

Content Dialog Box

步驟 12 - 選擇您的資料庫並單擊“下一步”。

Select Database

步驟 13 - 在“選擇資料庫物件”對話方塊中,選擇表、檢視。

Object Dialog Box

步驟 14 - 選擇位於“儲存過程和函式”節點下的 GetStudentGradesForCourse 函式,然後單擊“完成”。

步驟 15 - 選擇“檢視”→“其他視窗”→“實體資料模型瀏覽器”,右鍵單擊“函式匯入”下的 GetStudentGradesForCourse,然後選擇“編輯”。

Select View

您將看到以下對話方塊。

Dialog

步驟 16 - 單擊“實體”單選按鈕,從組合框中選擇 Enrollment 作為此函式的返回型別,然後單擊“確定”。

讓我們看一下以下 C# 程式碼,其中將檢索資料庫中註冊課程 ID 為 4022 的所有學生的成績。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         var CourseID = 4022;

         // Return all the best students in the Microeconomics class.
         var students = context.GetStudentGradesForCourse(CourseID);

         foreach (var result in students) {
            Console.WriteLine("Student ID:  {0}, Grade: {1}",
               result.StudentID, result.Grade);
         }

         Console.ReadKey();
      }
   }
}

編譯並執行上述程式碼後,您將收到以下輸出:

Student ID: 1, Grade: 2
Student ID: 4, Grade: 4
Student ID: 9, Grade: 3.5

我們建議您逐步執行以上示例以更好地理解。

實體框架 - 本機SQL

在 Entity Framework 中,您可以使用 LINQ 使用實體類進行查詢。您還可以使用 DbCOntext 直接對資料庫執行原始 SQL 查詢。這些技術可以同樣應用於使用 Code First 和 EF 設計器建立的模型。

現有實體上的 SQL 查詢

DbSet 上的 SqlQuery 方法允許編寫一個原始 SQL 查詢,該查詢將返回實體例項。返回的物件將由上下文跟蹤,就像它們是由 LINQ 查詢返回的一樣。例如 -

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         var students = context.Students.SqlQuery("SELECT * FROM dbo.Student").ToList();

         foreach (var student in students) {
            string name = student.FirstMidName + " " + student.LastName;
            Console.WriteLine("ID: {0}, Name: {1}, \tEnrollment Date {2} ",
               student.ID, name, student.EnrollmentDate.ToString());
         }

         Console.ReadKey();
      }
   }
}

以上程式碼將檢索資料庫中的所有學生。

非實體型別的 SQL 查詢

可以使用 Database 類上的 SqlQuery 方法建立返回任何型別例項(包括原始型別)的 SQL 查詢。例如 -

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         var studentNames = context.Database.SqlQuery
            <string>("SELECT FirstMidName FROM dbo.Student").ToList();

         foreach (var student in studentNames) {
            Console.WriteLine("Name: {0}", student);
         }

         Console.ReadKey();
      }
   }
}

資料庫的 SQL 命令

ExecuteSqlCommnad 方法用於向資料庫傳送非查詢命令,例如 Insert、Update 或 Delete 命令。讓我們看一下以下程式碼,其中學生的姓名欄位在 ID=1 時被更新。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         //Update command

         int noOfRowUpdated = context.Database.ExecuteSqlCommand("Update 
            student set FirstMidName = 'Ali' where ID = 1");

         context.SaveChanges();

         var student = context.Students.SqlQuery("SELECT * FROM
            dbo.Student where ID = 1").Single();

         string name = student.FirstMidName + " " + student.LastName;

         Console.WriteLine("ID: {0}, Name: {1}, \tEnrollment Date {2} ", 
            student.ID, name, student.EnrollmentDate.ToString());

         Console.ReadKey();
      }
   }
}

以上程式碼將檢索資料庫中所有學生的姓名欄位。

實體框架 - 列舉支援

在 Entity Framework 中,此功能允許您在域類上定義一個屬性,該屬性是列舉型別,並將其對映到整數型別的資料庫列。然後,Entity Framework 會在查詢和儲存資料時將資料庫值轉換為相關的列舉並從相關的列舉轉換回來。

  • 當使用具有固定數量響應的屬性時,列舉型別具有各種好處。

  • 當您使用列舉時,應用程式的安全性和可靠性都會提高。

  • 列舉使使用者犯錯的可能性大大降低,並且諸如注入攻擊之類的問題根本不存在。

  • 在 Entity Framework 中,列舉可以具有以下基礎型別 -

    • 位元組
    • Int16
    • Int32
    • Int64
    • SByte
  • 列舉元素的預設基礎型別為 int。

  • 預設情況下,第一個列舉器的值為 0,每個後續列舉器的值增加 1。

讓我們看一下以下示例,其中我們將建立設計器中的實體,然後新增一些屬性。

步驟 1 - 從“檔案”→“新建”→“專案”選單選項建立新專案。

步驟 2 - 在左側窗格中,選擇控制檯應用程式。

Creating Entity

步驟 3 - 輸入 EFEnumDemo 作為專案名稱,然後單擊“確定”。

步驟 4 - 右鍵單擊解決方案資源管理器中的專案名稱,然後選擇“新增”→“新建項”選單選項。

步驟 5 - 在“模板”窗格中選擇 ADO.NET 實體資料模型。

Data Model Template

步驟 6 - 輸入 EFEnumModel.edmx 作為檔名,然後單擊“新增”。

步驟 7 - 在“實體資料模型嚮導”頁面上,選擇“空 EF 設計器模型”。

Model Wizard Page

步驟 8 - 單擊“完成”。

步驟 9 - 然後右鍵單擊設計器視窗並選擇“新增”→“實體”。

Designer Window Entity

將顯示“新建實體”對話方塊,如下面的影像所示。

New Entity Dialog

步驟 10 - 輸入 Department 作為實體名稱,輸入 DeptID 作為屬性名稱,將屬性型別保留為 Int32,然後單擊“確定”。

步驟 11 - 右鍵單擊實體,然後選擇“新增新”→“標量屬性”。

Scalar Property

步驟 12 - 將新屬性重新命名為 DeptName。

步驟 13 - 將新屬性的型別更改為 Int32(預設情況下,新屬性的型別為 String)。

步驟 14 - 要更改型別,請開啟“屬性”視窗並將“型別”屬性更改為 Int32。

Type

步驟 15 - 在 Entity Framework 設計器中,右鍵單擊 Name 屬性,選擇“轉換為列舉”。

Entity Framework Designer

步驟 16 - 在“新增列舉型別”對話方塊中,輸入 DepartmentNames 作為列舉型別名稱,將“基礎型別”更改為 Int32,然後向型別新增以下成員:物理、化學、計算機和經濟學。

Add Enum

步驟 17 - 單擊“確定”。

如果切換到“模型瀏覽器”視窗,您會看到該型別也已新增到“列舉型別”節點中。

Model Browser Window

讓我們按照“模型優先”方法章節中提到的所有步驟從模型生成資料庫。

步驟 1 - 右鍵單擊實體設計器表面並選擇“從模型生成資料庫”。

將顯示“生成資料庫嚮導”的“選擇資料連線”對話方塊。

步驟 2 - 單擊“新建連線”按鈕。

Connection Button

步驟 3 - 輸入伺服器名稱和 EnumDemo 作為資料庫名稱,然後單擊“確定”。

步驟 4 - 將彈出一個對話方塊,詢問您是否要建立新資料庫,單擊“是”。

步驟 5 - 單擊“下一步”,建立資料庫嚮導將生成用於建立資料庫的資料定義語言 (DDL)。現在單擊“完成”。

步驟 6 - 右鍵單擊 T-SQL 編輯器並選擇“執行”。

TSql Editor

步驟 7 - 要檢視生成的架構,請右鍵單擊 SQL Server 物件資源管理器中的資料庫名稱,然後選擇“重新整理”。

您將在資料庫中看到 Departments 表。

Departments Table

讓我們看一下以下示例,其中一些新的 Department 物件被新增到上下文中並儲存。然後檢索計算機系。

class Program {

   static void Main(string[] args) {

      using (var context = new EFEnumModelContainer()) {

         context.Departments.Add(new Department { DeptName = DepartmentNames.Physics});
         context.Departments.Add(new Department { DeptName = DepartmentNames.Computer});
         context.Departments.Add(new Department { DeptName = DepartmentNames.Chemistry});
         context.Departments.Add(new Department { DeptName = DepartmentNames.Economics});

         context.SaveChanges();

         var department = (
            from d in context.Departments
            where d.DeptName == DepartmentNames.Computer
            select d
         ).FirstOrDefault();

         Console.WriteLine(
            "Department ID: {0}, Department Name: {1}", 
               department.DeptID, department.DeptName
         );

         Console.ReadKey();
      }
   }
}

執行上述程式碼時,您將收到以下輸出 -

Department ID: 2, Department Name: Computer

我們建議您逐步執行以上示例以更好地理解。

Entity Framework - 非同步查詢

非同步程式設計涉及在後臺執行操作,以便主執行緒可以繼續執行其自身的操作。這樣,主執行緒可以在後臺執行緒處理手頭的任務時保持使用者介面響應。

  • Entity Framework 6.0 支援用於查詢和儲存資料的非同步操作。

  • 非同步操作可以幫助您的應用程式實現以下目標 -

    • 使您的應用程式對使用者互動更具響應性
    • 提高應用程式的整體效能
  • 您可以透過多種方式執行非同步操作。但是,async/await 關鍵字是在 .NET Framework 4.5 中引入的,這使得您的工作變得簡單。

  • 您只需遵循以下程式碼片段所示的 async/await 模式即可。

讓我們看一下以下示例(不使用 async/await),其中 DatabaseOperations 方法將新學生儲存到資料庫,然後從資料庫檢索所有學生,最後在控制檯上列印一些其他訊息。

class Program {

   static void Main(string[] args) {
      Console.WriteLine("Database Operations Started");
      DatabaseOperations();
		
      Console.WriteLine();
      Console.WriteLine("Database Operations Completed");
      Console.WriteLine();
      Console.WriteLine("Entity Framework Tutorials");
      Console.ReadKey();
   }

   public static void DatabaseOperations() {

      using (var context = new UniContextEntities()) {

         // Create a new student and save it

         context.Students.Add(new Student {
            FirstMidName = "Akram", 
            LastName = "Khan", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())});

         Console.WriteLine("Calling SaveChanges.");
         context.SaveChanges();
         Console.WriteLine("SaveChanges completed.");

         // Query for all Students ordered by first name

         var students = (from s in context.Students
            orderby s.FirstMidName select s).ToList();

         // Write all students out to Console

         Console.WriteLine();
         Console.WriteLine("All Student:");

         foreach (var student in students) {
            string name = student.FirstMidName + " " + student.LastName;
            Console.WriteLine(" " + name);
         }
      }
   }
}

執行上述程式碼時,您將收到以下輸出 -

Calling SaveChanges.
SaveChanges completed.
All Student:
Akram Khan
Ali Khan
Ali Alexander
Arturo Anand
Bill Gates
Gytis Barzdukas
Laura  Nornan
Meredith fllonso
Nino Olioetto
Peggy Justice
Yan Li

Entity Framework Tutorials

讓我們使用新的 async 和 await 關鍵字,並對 Program.cs 進行以下更改。

  • 新增 System.Data.Entity 名稱空間,它將提供 EF 非同步擴充套件方法。

  • 新增 System.Threading.Tasks 名稱空間,它將允許我們使用 Task 型別。

  • DatabaseOperations 更新為標記為 async 並返回 Task

  • 呼叫 SaveChanges 的 Async 版本並等待其完成。

  • 呼叫 ToList 的非同步版本並等待結果。

class Program {

   static void Main(string[] args) {
      var task = DatabaseOperations();
      Console.WriteLine();
      Console.WriteLine("Entity Framework Tutorials");
      task.Wait();
      Console.ReadKey();
   }

   public static async Task DatabaseOperations() {

      using (var context = new UniContextEntities()) {

         // Create a new blog and save it

         context.Students.Add(new Student {
            FirstMidName = "Salman", 
            LastName = "Khan", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())});

         Console.WriteLine("Calling SaveChanges.");
         await context.SaveChangesAsync();
         Console.WriteLine("SaveChanges completed.");

         // Query for all Students ordered by first name

         var students = await (from s in context.Students 
            orderby s.FirstMidName select s).ToListAsync();

         // Write all students out to Console

         Console.WriteLine();
         Console.WriteLine("All Student:");

         foreach (var student in students) {
            string name = student.FirstMidName + " " + student.LastName; 
            Console.WriteLine(" " + name);
         }
      }
   }
}

執行時,它將產生以下輸出。

Calling SaveChanges.
Entity Framework Tutorials
SaveChanges completed.
All Student:
Akram Khan
Ali Khan
Ali Alexander
Arturo Anand
Bill Gates
Gytis Barzdukas
Laura  Nornan
Meredith fllonso
Nino Olioetto
Peggy Justice
Salman Khan
Yan Li

現在程式碼是非同步的,您可以觀察到程式的不同執行流程。

  • SaveChanges 開始將新的 Student 推送到資料庫,然後 DatabaseOperations 方法返回(即使它尚未執行完成),並且 Main 方法中的程式流程繼續。

  • 然後將訊息寫入控制檯。

  • 託管執行緒在 Wait 呼叫上阻塞,直到資料庫操作完成。完成後,將執行 DatabaseOperations 的其餘部分。

  • SaveChanges 完成。

  • 從資料庫中檢索所有學生並寫入控制檯。

我們建議您逐步執行以上示例以更好地理解。

實體框架 - 永續性

現在,Entity Framework 允許您從 Entity Framework 中獲益,而無需強制應用程式的每個部分都瞭解 Entity Framework,從而將實體與基礎結構分離。您可以建立專注於其業務規則的類,而無需考慮如何持久化它們(資料儲存在哪裡以及資料如何在物件之間來回傳遞)。

建立持久化無感知實體

上一段描述了一種對所使用資料來源一無所知的方法。這突出了持久化無感知的本質,即您的類和圍繞它們的許多應用程式層不關心資料如何儲存。

  • 在 Entity Framework 的 .NET 3.5 版本中,如果您想使用預先存在的類,則需要透過強制它們派生自 EntityObject 來修改它們。

  • 在 .NET 4 中,這不再必要。您無需修改實體即可讓它們參與 Entity Framework 操作。

  • 這使我們能夠構建採用松耦合和關注點分離的應用程式。

  • 使用這些編碼模式,您的類只關心它們自己的工作,並且您的應用程式的許多層(包括 UI)都不依賴於外部邏輯(例如 Entity Framework API),但這些外部 API 能夠與我們的實體互動。

使用 Entity Framework 持久化實體時,有兩種方式(連線和斷開連線)。這兩種方式都有其自身的意義。在連線場景中,更改由上下文跟蹤,但在斷開連線的場景中,我們需要通知上下文實體的狀態。

連線場景

連線場景是指從資料庫中檢索實體並在同一上下文中修改實體。對於連線場景,假設我們有一個 Windows 服務,並且我們正在使用該實體執行一些業務操作,因此我們將開啟上下文,迴圈遍歷所有實體,執行我們的業務操作,然後使用最初開啟的相同上下文儲存更改。

讓我們看一下以下示例,其中從資料庫中檢索學生並更新學生的姓,然後將更改儲存到資料庫。

class Program {

   static void Main(string[] args) {

      using (var context = new MyContext()) {

         var studentList = context.Students.ToList();

         foreach (var stdnt in studentList) {
            stdnt.FirstMidName = "Edited " + stdnt.FirstMidName;
         }

         context.SaveChanges();

         //// Display all Students from the database

         var students = (from s in context.Students
            orderby s.FirstMidName select s).ToList<Student>();

         Console.WriteLine("Retrieve all Students from the database:");

         foreach (var stdnt in students) {
            string name = stdnt.FirstMidName + " " + stdnt.LastName;
            Console.WriteLine("ID: {0}, Name: {1}", stdnt.ID, name);
         }

         Console.ReadKey();
      }
   }
}

當編譯並執行上述程式碼時,您將收到以下輸出,並且您會看到“Edited”一詞附加在姓之前,如下面的輸出所示。

Retrieve all Students from the database: 
ID: 1, Name: Edited Edited Alain Bomer 
ID: 2, Name: Edited Edited Mark Upston 

斷開連線場景

斷開連線場景是指從資料庫中檢索實體並在不同的上下文中修改實體。假設我們想要在表示層中顯示一些資料,並且我們正在使用某個 N 層應用程式,因此最好開啟上下文,獲取資料,最後關閉上下文。由於我們在此處獲取了資料並關閉了上下文,因此我們獲取的實體不再被跟蹤,這就是斷開連線的場景。

讓我們看一下以下程式碼,其中使用 Add 方法將新的斷開連線的 Student 實體新增到上下文中。

class Program {

   static void Main(string[] args) {

      var student = new Student {
         ID = 1001, 
         FirstMidName = "Wasim", 
         LastName = "Akram", 
         EnrollmentDate = DateTime.Parse( DateTime.Today.ToString())
      };

      using (var context = new MyContext()) {

         context.Students.Add(student);
         context.SaveChanges();

         //// Display all Students from the database

         var students = (from s in context.Students 
            orderby s.FirstMidName select s).ToList<Student>();

         Console.WriteLine("Retrieve all Students from the database:");

         foreach (var stdnt in students) {
            string name = stdnt.FirstMidName + " " + stdnt.LastName;
            Console.WriteLine("ID: {0}, Name: {1}", stdnt.ID, name);
         }

         Console.ReadKey();
      }
   }
}

當編譯並執行上述程式碼時,您將收到以下輸出。

Retrieve all Students from the database:
ID: 1, Name: Edited Edited Edited Alain Bomer
ID: 2, Name: Edited Edited Edited Mark Upston
ID: 3, Name: Wasim Akram

Entity Framework - 投影查詢

LINQ to Entities

理解 LINQ to Entities 最重要的概念之一是它是一種宣告式語言。重點在於定義您需要什麼資訊,而不是如何獲取資訊。

  • 這意味著您可以將更多時間用於處理資料,而減少用於弄清楚執行諸如訪問資料庫等任務所需的底層程式碼的時間。

  • 重要的是要理解,宣告式語言實際上並沒有消除開發人員的任何控制權,但它有助於開發人員將注意力集中在重要的事情上。

LINQ to Entities 核心關鍵字

瞭解用於建立 LINQ 查詢的基本關鍵字非常重要。只需要記住幾個關鍵字,但您可以以各種方式組合它們以獲得特定的結果。以下列表包含這些基本關鍵字,並提供每個關鍵字的簡單描述。

序號 關鍵字及描述
1

Ascending

指定排序操作從範圍的最小(或最低)元素到範圍的最高元素進行。這通常是預設設定。例如,在執行字母排序時,排序範圍將是從 A 到 Z。

2

By

指定用於實現分組的欄位或表示式。欄位或表示式定義用於執行分組任務的鍵。

3

Descending

指定排序操作從範圍的最大(或最高)元素到範圍的最低元素進行。例如,在執行字母排序時,排序範圍將是從 Z 到 A。

4

Equals

用於連線語句的左右子句之間,將主要上下文資料來源連線到輔助上下文資料來源。equals 關鍵字左側的欄位或表示式指定主要資料來源,而 equals 關鍵字右側的欄位或表示式指定輔助資料來源。

5

From

指定用於獲取所需資訊的資料來源並定義範圍變數。此變數與迴圈中用於迭代的變數具有相同的用途。

6

Group

使用您指定的鍵值將輸出組織成組。使用多個 group 子句建立多級輸出組織。group 子句的順序決定了特定鍵值在分組順序中出現的深度。您可以將此關鍵字與 by 結合使用以建立特定的上下文。

7

In

以多種方式使用。在這種情況下,關鍵字確定查詢使用的上下文資料庫源。在使用連線時,in 關鍵字用於連線使用的每個上下文資料庫源。

8

Into

指定一個識別符號,您可以將其用作 LINQ 查詢子句(如 join、group 和 select)的引用。

9

Join

從兩個相關資料來源(例如在主/從設定中)建立一個單一資料來源。連線可以指定內部連線、組連線或左外部連線,其中內部連線為預設值。您可以在 msdn.microsoft.com 上閱讀有關連線的更多資訊。

10

Let

定義一個範圍變數,您可以使用它在查詢表示式中儲存子表示式結果。通常,範圍變數用於提供額外的列舉輸出或提高查詢效率(以便無需多次執行特定任務,例如查詢字串的小寫值)。

11

On

指定用於實現連線的欄位或表示式。欄位或表示式定義一個對兩個上下文資料來源都通用的元素。

12

Orderby

為查詢建立排序順序。您可以新增 ascending 或 descending 關鍵字來控制排序順序。使用多個 orderby 子句建立多級排序。orderby 子句的順序決定了排序表示式處理的順序,因此使用不同的順序將導致不同的輸出。

13

Where

定義 LINQ 應從資料來源中檢索什麼。您可以使用一個或多個布林表示式來定義要檢索內容的細節。布林表示式使用 &&(AND)和 ||(OR)運算子彼此分隔。

14

Select

透過指定要返回的資訊來確定 LINQ 查詢的輸出。此語句定義 LINQ 在迭代過程中返回的元素的資料型別。

Projection

投影查詢透過僅從資料庫中檢索特定欄位來提高應用程式的效率。

  • 獲取資料後,您可能需要根據需要對其進行投影或篩選,以在輸出之前塑造資料。

  • 任何 LINQ to Entities 表示式的主要任務是獲取資料並將其作為輸出提供。

本章的“開發 LINQ to Entities 查詢”部分演示了執行此基本任務的技術。

讓我們看一下以下程式碼,其中將檢索學生列表。

using (var context = new UniContextEntities()) {

   var studentList = from s in context.Students select s;

   foreach (var student in studentList) {
      string name = student.FirstMidName + " " + student.LastName;
      Console.WriteLine("ID : {0}, Name: {1}", student.ID, name);
   }
}

單個物件

要檢索單個學生物件,可以使用 First() 或 FirstOrDefault 可列舉方法,它們返回序列的第一個元素。First 和 FirstOrDefault 之間的區別在於,如果為提供的條件沒有結果資料,則 First() 將引發異常,而 FirstOrDefault() 將返回預設值 null,如果為提供的條件沒有結果資料。

using (var context = new UniContextEntities()) {

   var student = (from s in context.Students where s.FirstMidName 
      == "Ali" select s).FirstOrDefault<Student>();

   string name = student.FirstMidName + " " + student.LastName;
   Console.WriteLine("ID : {0}, Name: {1}", student.ID, name);
}

您還可以使用 Single() 或 SingleOrDefault 獲取單個學生物件,它返回序列的單個特定元素。在以下示例中,檢索了 ID 為 2 的單個學生。

using (var context = new UniContextEntities()) {

   var student = (from s in context.Students where s.ID 
      == 2 select s).SingleOrDefault<Student>();
   string name = student.FirstMidName + " " + student.LastName;
	
   Console.WriteLine("ID : {0}, Name: {1}", student.ID, name);
   Console.ReadKey();
}

物件列表

如果要檢索姓為 Ali 的學生列表,則可以使用 ToList() 可列舉方法。

using (var context = new UniContextEntities()) {

   var studentList = (from s in context.Students where s.FirstMidName 
      == "Ali" select s).ToList();

   foreach (var student in studentList) {
      string name = student.FirstMidName + " " + student.LastName;
      Console.WriteLine("ID : {0}, Name: {1}", student.ID, name);
   }

   Console.ReadKey();
}

排序

要以任何特定順序檢索資料/列表,可以使用 orderby 關鍵字。在以下程式碼片段中,將按升序檢索學生列表。

using (var context = new UniContextEntities()) {

   var studentList = (from s in context.Students orderby
      s.FirstMidName ascending select s).ToList();

   foreach (var student in studentList) {
      string name = student.FirstMidName + " " + student.LastName;
      Console.WriteLine("ID : {0}, Name: {1}", student.ID, name);
   }

   Console.ReadKey();
}

標準與投影 Entity Framework 查詢

假設您有一個 Student 模型,其中包含 ID、FirstMidName、LastName 和 EnrollmentDate。如果要返回學生列表,則標準查詢將返回所有欄位。但如果您只想獲取包含 ID、FirstMidName 和 LastName 欄位的學生列表。此時,您應該使用投影查詢。以下是投影查詢的簡單示例。

using (var context = new UniContextEntities()) {

   var studentList = from s in context.Students
      orderby s.FirstMidName ascending
      where s.FirstMidName == "Ali"

   select new {s.ID, s.FirstMidName, s.LastName};

   foreach (var student in studentList) {
      string name = student.FirstMidName + " " + student.LastName;
      Console.WriteLine("ID : {0}, Name: {1}", student.ID, name);
   }

   Console.ReadKey();
}

上面的投影查詢排除了 EnrollmentDate 欄位。這將使您的應用程式更快。

Entity Framework - 命令日誌記錄

在 Entity Framework 6.0 中,引入了一項新功能,稱為**日誌記錄 SQL**。在使用 Entity Framework 時,它會向資料庫傳送命令或等效的 SQL 查詢以執行 CRUD(建立、讀取、更新和刪除)操作。

  • Entity Framework 的此功能旨在捕獲 Entity Framework 內部生成的等效 SQL 查詢並將其作為輸出提供。

  • 在 Entity Framework 6 之前,每當需要跟蹤資料庫查詢和命令時,開發人員別無選擇,只能使用某些第三方跟蹤實用程式或資料庫跟蹤工具。

  • 在 Entity Framework 6 中,此新功能提供了一種簡單的方法來記錄 Entity Framework 執行的所有操作。

  • Entity Framework 執行的所有活動都使用 DbContext.Database.Log 進行記錄。

讓我們看一下以下程式碼,其中將新學生新增到資料庫中。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         context.Database.Log = Console.Write;

         // Create a new student and save it

         context.Students.Add(new Student {
            FirstMidName = "Salman", 
            LastName = "Khan", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
         });

         context.SaveChanges();
         Console.ReadKey();
      }
   }
}

當執行上述程式碼時,您將收到以下輸出,這實際上是 EF 在上述程式碼中執行的所有活動的日誌。

Opened connection at 10/28/2015 6:27:35 PM +05:00
Started transaction at 10/28/2015 6:27:35 PM +05:00
INSERT [dbo].[Student]([LastName], [FirstMidName], [EnrollmentDate])
VALUES (@0, @1, @2)
SELECT [ID]
FROM [dbo].[Student]
WHERE @@ROWCOUNT > 0 AND [ID] = scope_identity()
-- @0: 'Khan' (Type = String, Size = -1)
-- @1: 'Salman' (Type = String, Size = -1)
-- @2: '10/28/2015 12:00:00 AM' (Type = DateTime)
-- Executing at 10/28/2015 6:27:35 PM +05:00
-- Completed in 5 ms with result: SqlDataReader
Committed transaction at 10/28/2015 6:27:35 PM +05:00
Closed connection at 10/28/2015 6:27:35 PM +05:00

當設定 Log 屬性時,將記錄以下活動:

  • 所有不同型別命令的 SQL,例如查詢,包括作為 SaveChanges 部分生成插入、更新和刪除操作的查詢

  • 引數

  • 命令是否正在非同步執行

  • 指示命令何時開始執行的時間戳

  • 命令成功完成還是失敗

  • 結果值的一些指示

  • 執行命令大約花費的時間

日誌記錄到其他位置

如果您已經有某個日誌記錄框架並且它定義了日誌記錄方法,那麼您也可以將其記錄到其他位置。

讓我們看一下以下示例,其中我們有另一個類 MyLogger。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         context.Database.Log = s ⇒ MyLogger.Log("EFLoggingDemo", s);

         // Create a new student and save it

         context.Students.Add(new Student {
            FirstMidName = "Salman", 
            LastName = "Khan", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
         });

         context.SaveChanges();
         Console.ReadKey();
      }
   }
}

public class MyLogger {

   public static void Log(string application, string message) {
      Console.WriteLine("Application: {0}, EF Message: {1} ",application, message);
   }
}

我們建議您逐步執行以上示例以更好地理解。

實體框架 - 命令攔截

在 Entity Framework 6.0 中,還有一個名為**攔截器**或攔截的新功能。攔截程式碼圍繞**攔截介面**的概念構建。例如,IDbCommandInterceptor 介面定義了在 EF 呼叫 ExecuteNonQuery、ExecuteScalar、ExecuteReader 和相關方法之前呼叫的方法。

  • 透過使用攔截,實體框架可以真正發揮其優勢。使用這種方法,您可以捕獲更多瞬態資訊,而無需弄亂您的程式碼。

  • 要實現此目的,您需要建立自己的自定義攔截器並相應地註冊它。

  • 一旦建立了實現 IDbCommandInterceptor 介面的類,就可以使用 DbInterception 類將其註冊到實體框架。

  • IDbCommandInterceptor 介面有六個方法,您需要實現所有這些方法。以下是這些方法的基本實現。

讓我們看一下以下程式碼,其中實現了 IDbCommandInterceptor 介面。

public class MyCommandInterceptor : IDbCommandInterceptor {

   public static void Log(string comm, string message) {
      Console.WriteLine("Intercepted: {0}, Command Text: {1} ", comm, message);
   }

   public void NonQueryExecuted(DbCommand command, 
      DbCommandInterceptionContext<int> interceptionContext) {
         Log("NonQueryExecuted: ", command.CommandText);
   }

   public void NonQueryExecuting(DbCommand command, 
      DbCommandInterceptionContext<int> interceptionContext) {
         Log("NonQueryExecuting: ", command.CommandText);
   }

   public void ReaderExecuted(DbCommand command, 
      DbCommandInterceptionContext<DbDataReader> interceptionContext) {
         Log("ReaderExecuted: ", command.CommandText);
   }

   public void ReaderExecuting(DbCommand command, 
      DbCommandInterceptionContext<DbDataReader> interceptionContext) {
         Log("ReaderExecuting: ", command.CommandText);
   }

   public void ScalarExecuted(DbCommand command, 
      DbCommandInterceptionContext<object> interceptionContext) {
         Log("ScalarExecuted: ", command.CommandText);
   }

   public void ScalarExecuting(DbCommand command, 
      DbCommandInterceptionContext<object> interceptionContext) {
         Log("ScalarExecuting: ", command.CommandText);
   }

}

註冊攔截器

一旦建立了實現一個或多個攔截介面的類,就可以使用 DbInterception 類將其註冊到 EF,如下面的程式碼所示。

DbInterception.Add(new MyCommandInterceptor());

還可以使用 DbConfiguration 基於程式碼的配置在應用程式域級別註冊攔截器,如下面的程式碼所示。

public class MyDBConfiguration : DbConfiguration {

   public MyDBConfiguration() {
      DbInterception.Add(new MyCommandInterceptor());
   }
}

您還可以使用以下程式碼配置攔截器配置檔案:

<entityFramework>
   <interceptors>
      <interceptor type = "EFInterceptDemo.MyCommandInterceptor, EFInterceptDemo"/>
   </interceptors>
</entityFramework>

實體框架 - 空間資料型別

實體框架 5 中引入了空間型別支援。還包括一組運算子,允許查詢分析空間資料。例如,查詢可以根據兩個地理位置之間的距離進行篩選。

  • 實體框架將允許將新的空間資料型別作為屬性公開到您的類中,並將它們對映到資料庫中的空間列。

  • 您還可以編寫 LINQ 查詢,這些查詢利用空間運算子根據資料庫中執行的空間計算進行篩選、排序和分組。

主要有兩種空間資料型別:

  • geography 資料型別儲存橢球資料,例如 GPS 緯度和經度座標。

  • geometry 資料型別表示歐幾里得(平面)座標系。

讓我們看一下以下板球場的示例。

步驟 1 - 從“檔案”→“新建”→“專案”選單選項建立新專案。

步驟 2 - 在左側窗格中,選擇控制檯應用程式。

Cricket Project

**步驟 3** - 右鍵單擊專案名稱,然後選擇“管理 NuGet 包…”。

NuGet Project

**步驟 4** - 安裝實體框架。

**步驟 5** - 新增對 System.Data.Entity 程式集的引用,併為空間資料型別新增 System.Data.Spatial using 語句。

Add Reference

**步驟 6** - 在 Program.cs 檔案中新增以下類。

public class CricketGround {
   public int ID { get; set; }
   public string Name { get; set; }
   public DbGeography Location { get; set; }
}

**步驟 7** - 除了定義實體之外,您還需要定義一個從 DbContext 派生的類,並公開 DbSet<TEntity> 屬性。

在 Program.cs 中新增上下文定義。

public partial class CricketGroundContext : DbContext {
   public DbSet<CricketGround> CricketGrounds { get; set; }
}

**步驟 8** - 將以下程式碼新增到 Main 函式中,這將向上下文新增兩個新的 CricketGround 物件。

class Program {

   static void Main(string[] args) {

      using (var context = new CricketGroundContext()) {

         context.CricketGrounds.Add(new CricketGround() {
            Name = "Shalimar Cricket Ground", 
            Location = DbGeography.FromText("POINT(-122.336106 47.605049)"), 
         });

         context.CricketGrounds.Add(new CricketGround() {
            Name = "Marghazar Stadium", Location = DbGeography
               .FromText("POINT(-122.335197 47.646711)"), 
         });

         context.SaveChanges();

         var myLocation = DbGeography.FromText("POINT(-122.296623 47.640405)");

         var cricketGround = (from cg in context.CricketGrounds
            orderby cg.Location.Distance(myLocation) select cg).FirstOrDefault();

         Console.WriteLine("The closest Cricket Ground to you is: {0}.", cricketGround.Name);
      }
   }
}

使用 DbGeography.FromText 方法初始化空間屬性。表示為 WellKnownText 的地理點傳遞給該方法,然後儲存資料。之後將檢索 CricketGround 物件,其位置最接近指定位置。

執行上述程式碼時,您將收到以下輸出 -

The closest Cricket Ground to you is: Marghazar Stadium

我們建議您逐步執行以上示例以更好地理解。

實體框架 - 繼承

繼承使建立更能反映開發人員思維方式的複雜模型成為可能,並且減少了與這些模型互動所需的工作量。與實體一起使用的繼承與與類一起使用的繼承具有相同的目的,因此開發人員已經瞭解此功能的工作原理。

讓我們看一下以下示例,並透過建立一個新的控制檯應用程式專案。

**步驟 1** - 透過右鍵單擊專案名稱並選擇“新增”→“新建項…”來新增 ADO.NET 實體資料模型。

**步驟 2** - 透過遵循模型優先方法一章中提到的所有步驟,新增一個實體並將其命名為 Person。

**步驟 3** - 新增一些標量屬性,如下面的影像所示。

Add Scalar Properties

**步驟 4** - 我們將新增另外兩個實體**Student** 和**Teacher**,它們將繼承 Person 表的屬性。

**步驟 5** - 現在新增 Student 實體,並從“基型別”組合框中選擇 Person,如下面的影像所示。

Base Type ComboBox

**步驟 6** - 同樣新增 Teacher 實體。

**步驟 7** - 現在將 EnrollmentDate 標量屬性新增到 Student 實體,並將 HireDate 屬性新增到 Teacher 實體。

EnrollmentDate

**步驟 8** - 讓我們繼續生成資料庫。

**步驟 9** - 右鍵單擊設計圖面,然後選擇“從模型生成資料庫…”。

Generate Database

**步驟 10** - 要建立新資料庫,請單擊“新建連線…”。將開啟以下對話方塊。單擊“確定”。

New Database

**步驟 11** - 單擊“完成”。這將在專案中新增 *.edmx.sql 檔案。您可以透過開啟 .sql 檔案在 Visual Studio 中執行 DDL 指令碼。現在右鍵單擊並選擇“執行”。

**步驟 12** - 轉到伺服器資源管理器,您將看到資料庫已建立幷包含三個指定的表。

Database Created Table

**步驟 13** - 您還可以看到以下域類也自動生成了。

public partial class Person {
   public int ID { get; set; }
   public string FirstMidName { get; set; }
   public string LastName { get; set; }
}

public partial class Student : Person {
   public System.DateTime EnrollmentDate { get; set; }
}

public partial class Teacher : Person {
   public System.DateTime HireDate { get; set; }
}

以下是 Context 類。

public partial class InheritanceModelContainer : DbContext {

   public InheritanceModelContainer() : 
      base("name = InheritanceModelContainer") {}

   protected override void OnModelCreating(DbModelBuilder modelBuilder) {
      throw new UnintentionalCodeFirstException();
   }

   public virtual DbSet<Person> People { get; set; }
}

讓我們向資料庫新增一些學生和教師,然後從資料庫中檢索它們。

class Program {

   static void Main(string[] args) {

      using (var context = new InheritanceModelContainer()) {

         var student = new Student {
            FirstMidName = "Meredith", 
            LastName = "Alonso", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
         };

         context.People.Add(student);

         var student1 = new Student {
            FirstMidName = "Arturo", 
            LastName = "Anand", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
         };

         context.People.Add(student1);

         var techaer = new Teacher {
            FirstMidName = "Peggy", 
            LastName = "Justice", 
            HireDate = DateTime.Parse(DateTime.Today.ToString())
         };

         context.People.Add(techaer);

         var techaer1 = new Teacher {
            FirstMidName = "Yan", 
            LastName = "Li", 
            HireDate = DateTime.Parse(DateTime.Today.ToString())
         };

         context.People.Add(techaer1);
         context.SaveChanges();
      }
   }
}

學生和教師已新增到資料庫中。要檢索學生和教師,需要使用**OfType**方法,該方法將返回與指定部門相關的 Student 和 Teacher。

Console.WriteLine("All students in database"); 
Console.WriteLine("");

foreach (var student in context.People.OfType<Student>()) {
   string name = student.FirstMidName + " " + student.LastName;
   Console.WriteLine("ID: {0}, Name: {1}, \tEnrollment Date {2} ", 
      student.ID, name, student.EnrollmentDate.ToString());
}

Console.WriteLine("");
Console.WriteLine("************************************************************ *****");
Console.WriteLine("");
Console.WriteLine("All teachers in database");
Console.WriteLine("");

foreach (var teacher in context.People.OfType<Teacher>()) {
   string name = teacher.FirstMidName + " " + teacher.LastName;
   Console.WriteLine("ID: {0}, Name: {1}, \tHireDate {2} ", 
      teacher.ID, name, teacher.HireDate.ToString()); 
}

Console.WriteLine("");
Console.WriteLine("************************************************************ *****");
Console.ReadKey();

在第一個查詢中,當您使用 OfType<Student>() 時,您將無法訪問 HireDate,因為 HireDate 屬性是 Teacher 實體的一部分,類似地,當您使用 OfType<Teacher>() 時,EnrollmentDate 屬性將不可訪問。

執行上述程式碼時,您將收到以下輸出 -

All students in database
ID: 1, Name: Meredith Alonso,   Enrollment Date 10/30/2015 12:00:00 AM
ID: 2, Name: Arturo Anand,      Enrollment Date 10/30/2015 12:00:00 AM
*****************************************************************  
All teachers in database
ID: 3, Name: Peggy Justice,     HireDate 10/30/2015 12:00:00 AM
ID: 4, Name: Yan Li,    HireDate 10/30/2015 12:00:00 AM
*****************************************************************

我們建議您逐步執行以上示例以更好地理解。

實體框架 - 遷移

在實體框架 5 和以前的實體框架版本中,程式碼在作為 .NET Framework 部分提供的核心庫(主要是 System.Data.Entity.dll)和使用 NuGet 分發和提供的其他庫(主要是 EntityFramework.dll)之間拆分,如下面的圖表所示。

DotNet Framework

在實體框架 6 中,以前是 .NET 框架一部分的核心 API 也作為 NuGet 包的一部分分發。

Core APIs

這是為了允許實體框架開源。但是,結果,每當需要將應用程式從舊版本的實體框架遷移或升級到 EF 6 時,都需要重新構建應用程式。

如果您的應用程式使用 DbContext(在 EF 4.1 及更高版本中提供),則遷移過程很簡單。但是,如果您的應用程式是 ObjectContext,則需要稍微多做一些工作。

讓我們看一下將現有應用程序升級到 EF6 需要經歷的以下步驟。

**步驟 1** - 第一步是將目標設定為 .NET Framework 4.5.2 及更高版本,右鍵單擊您的專案並選擇“屬性”。

Upgrade EF6

**步驟 2** - 再次右鍵單擊您的專案,然後選擇“管理 NuGet 包…”。

Manage NuGet Packages

**步驟 3** - 在“聯機”選項卡下選擇 EntityFramework 並單擊“安裝”。確保已刪除對 System.Data.Entity.dll 的程式集引用。

當您安裝 EF6 NuGet 包時,它應該會自動為您刪除專案中對 System.Data.Entity 的任何引用。

**步驟 4** - 如果您有任何使用 EF 設計器建立的模型,那麼您還需要更新程式碼生成模板以生成與 EF6 相容的程式碼。

**步驟 5** - 在解決方案資源管理器中,在您的 edmx 檔案下,刪除現有的程式碼生成模板,這些模板通常將命名為 <edmx_file_name>.tt 和 <edmx_file_name>.Context.tt。

Edmx

**步驟 6** - 在 EF 設計器中開啟您的模型,右鍵單擊設計圖面,然後選擇“新增程式碼生成項…”。

**步驟 7** - 新增適當的 EF 6.x 程式碼生成模板。

Code Generation Template

它還將自動生成與 EF6 相容的程式碼。

如果您的應用程式使用 EF 4.1 或更高版本,則無需在程式碼中更改任何內容,因為 DbContext 和 Code First 型別的名稱空間沒有更改。

但是,如果您的應用程式使用的是舊版本的實體框架,則以前位於 System.Data.Entity.dll 中的 ObjectContext 等型別已移至新的名稱空間。

**步驟 8** - 您需要更新 using 或 Import 指令以針對 EF6 構建。

名稱空間更改的一般規則是,System.Data.* 中的任何型別都將移至 System.Data.Entity.Core.*。以下是一些示例:

  • System.Data.EntityException ⇒ System.Data.Entity.Core.EntityException
  • System.Data.Objects.ObjectContext ⇒ System.Data.Entity.Core.Objects.ObjectContext;
  • System.Data.Objects.DataClasses.RelationshipManager ⇒ System.Data.Entity.Core.Objects.DataClasses.RelationshipManager;

某些型別位於Core名稱空間中,因為它們不是大多數基於 DbContext 的應用程式直接使用的。

  • System.Data.EntityState ⇒ System.Data.Entity.EntityState
  • System.Data.Objects.DataClasses.EdmFunctionAttribute ⇒ System.Data.Entity.DbFunctionAttribute

您現有的實體框架專案將在實體框架 6.0 中執行,無需進行任何重大更改。

實體框架 - 渴望載入

急切載入是指查詢一種型別的實體的過程,同時還將相關實體作為查詢的一部分載入。急切載入是透過使用**Include 方法**實現的。

這意味著請求將相關資料與來自資料庫的查詢結果一起返回。只進行一次到資料來源的連線,在初始查詢中返回大量資料。

例如,在查詢學生時,急切載入他們的註冊資訊。學生及其註冊資訊將在單個查詢中檢索。

讓我們看一下以下示例,其中所有學生及其各自的註冊資訊都透過使用急切載入從資料庫中檢索。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {
         // Load all students and related enrollments
         var students = context.Students
            .Include(s ⇒ s.Enrollments).ToList();
			
         foreach (var student in students) {
            string name = student.FirstMidName + " " + student.LastName;
            Console.WriteLine("ID: {0}, Name: {1}", student.ID, name);
				
            foreach (var enrollment in student.Enrollments) {
               Console.WriteLine("Enrollment ID: {0}, Course ID: {1}", 
                  enrollment.EnrollmentID, enrollment.CourseID);
            }
         }

         Console.ReadKey();
      }
   }
}

當編譯並執行上述程式碼時,您將收到以下輸出。

ID: 1, Name: Ali Alexander
       Enrollment ID: 1, Course ID: 1050
       Enrollment ID: 2, Course ID: 4022
       Enrollment ID: 3, Course ID: 4041
ID: 2, Name: Meredith Alonso
       Enrollment ID: 4, Course ID: 1045
       Enrollment ID: 5, Course ID: 3141
       Enrollment ID: 6, Course ID: 2021
ID: 3, Name: Arturo Anand
       Enrollment ID: 7, Course ID: 1050
ID: 4, Name: Gytis Barzdukas
       Enrollment ID: 8, Course ID: 1050
       Enrollment ID: 9, Course ID: 4022

以下是可使用的其他一些急切載入查詢形式。

// Load one Student and its related enrollments

var student1 = context.Students
   .Where(s ⇒ s.FirstMidName == "Ali")
   .Include(s ⇒ s.Enrollments).FirstOrDefault();

// Load all Students and related enrollments
// using a string to specify the relationship

var studentList = context.Students
   .Include("Enrollments").ToList();

// Load one Student and its related enrollments
// using a string to specify the relationship

var student = context.Students
   .Where(s ⇒ s.FirstMidName == "Salman")
   .Include("Enrollments").FirstOrDefault();

多級

還可以急切載入多級相關實體。以下查詢顯示了 Student、Enrollments 和 Course 的示例。

// Load all Students, all related enrollments, and all related courses

var studentList = context.Students
   .Include(s ⇒ s.Enrollments.Select(c ⇒ c.Course)).ToList();

// Load all Students, all related enrollments, and all related courses
// using a string to specify the relationships

var students = context.Students
   .Include("Enrollments.Course").ToList();

我們建議您逐步執行以上示例以更好地理解。

實體框架 - 延遲載入

延遲載入是指在第一次訪問引用實體/實體的屬性時自動從資料庫載入實體或實體集合的過程。延遲載入意味著延遲載入相關資料,直到您明確請求它。

  • 當使用 POCO 實體型別時,延遲載入是透過建立派生代理型別的例項,然後覆蓋虛擬屬性以新增載入掛鉤來實現的。

  • 延遲載入幾乎是預設設定。

  • 如果使用預設配置,並且在查詢中沒有顯式地告訴 Entity Framework 你想要除了延遲載入之外的其他東西,那麼你將獲得延遲載入。

  • 例如,當使用 Student 實體類時,相關的 Enrollments 將在第一次訪問 Enrollments 導航屬性時載入。

  • 導航屬性應定義為 public,virtual。如果屬性未定義為 virtual,則 Context **不會**執行延遲載入。

下面是一個包含 Enrollments 導航屬性的 Student 類。

public partial class Student {

   public Student() {
      this.Enrollments = new HashSet<Enrollment>();
   }
	
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public System.DateTime EnrollmentDate { get; set; }
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

讓我們來看一個簡單的例子,其中首先從資料庫中載入學生列表,然後在你需要的時候載入特定學生的報名資訊。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         //Loading students only
         IList<Student> students = context.Students.ToList<Student>();

         foreach (var student in students) {

            string name = student.FirstMidName + " " + student.LastName;
            Console.WriteLine("ID: {0}, Name: {1}", student.ID, name);
	
            foreach (var enrollment in student.Enrollments) {
               Console.WriteLine("Enrollment ID: {0}, Course ID: {1}", 
                  enrollment.EnrollmentID, enrollment.CourseID);
            }
         }

         Console.ReadKey();
      }
   }
}	

當編譯並執行上述程式碼時,您將收到以下輸出。

ID: 1, Name: Ali Alexander
       Enrollment ID: 1, Course ID: 1050
       Enrollment ID: 2, Course ID: 4022
       Enrollment ID: 3, Course ID: 4041
ID: 2, Name: Meredith Alonso
       Enrollment ID: 4, Course ID: 1045
       Enrollment ID: 5, Course ID: 3141
       Enrollment ID: 6, Course ID: 2021
ID: 3, Name: Arturo Anand
       Enrollment ID: 7, Course ID: 1050
ID: 4, Name: Gytis Barzdukas
       Enrollment ID: 8, Course ID: 1050
       Enrollment ID: 9, Course ID: 4022
ID: 5, Name: Yan Li
       Enrollment ID: 10, Course ID: 4041
ID: 6, Name: Peggy Justice
       Enrollment ID: 11, Course ID: 1045
ID: 7, Name: Laura Norman
       Enrollment ID: 12, Course ID: 3141

關閉延遲載入

延遲載入和序列化並不相容,如果不注意,你可能會因為啟用了延遲載入而最終查詢整個資料庫。在序列化實體之前關閉延遲載入是一個好習慣。

關閉特定導航屬性的延遲載入

可以透過將 Enrollments 屬性設定為非 virtual 來關閉 Enrollments 集合的延遲載入,如下例所示。

public partial class Student { 

   public Student() { 
      this.Enrollments = new HashSet<Enrollment>(); 
   }
	
   public int ID { get; set; } 
   public string LastName { get; set; } 
   public string FirstMidName { get; set; } 
   public System.DateTime EnrollmentDate { get; set; }
   public ICollection<Enrollment> Enrollments { get; set; } 
}

關閉所有實體的延遲載入

可以透過將 Configuration 屬性上的標誌設定為 false 來關閉上下文中所有實體的延遲載入,如下例所示。

public partial class UniContextEntities : DbContext { 

   public UniContextEntities(): base("name=UniContextEntities") {
      this.Configuration.LazyLoadingEnabled = false;
   }
	
   protected override void OnModelCreating(DbModelBuilder modelBuilder) { 
      throw new UnintentionalCodeFirstException(); 
   } 
}

關閉延遲載入後,現在再次執行上面的示例,你將看到 Enrollments 沒有載入,並且只檢索了學生資料。

ID: 1, Name: Ali Alexander
ID: 2, Name: Meredith Alons
ID: 3, Name: Arturo Anand
ID: 4, Name: Gytis Barzduka
ID: 5, Name: Yan Li
ID: 6, Name: Peggy Justice
ID: 7, Name: Laura Norman
ID: 8, Name: Nino Olivetto

我們建議您逐步執行上述示例,以便更好地理解。

實體框架 - 顯式載入

停用延遲載入後,仍然可以延遲載入相關實體,但必須透過顯式呼叫來完成。

  • 與延遲載入不同,關於何時執行查詢沒有歧義或混淆的可能性。

  • 為此,你可以在相關實體的條目上使用 Load 方法。

  • 對於一對多關係,在集合上呼叫 Load 方法。

  • 對於一對一關係,在引用上呼叫 Load 方法。

讓我們來看下面的例子,其中延遲載入被停用,然後檢索名為 Ali 的學生。

然後將學生資訊寫入控制檯。如果檢視程式碼,報名資訊也被寫入,但 Enrollments 實體尚未載入,因此 foreach 迴圈不會執行。

之後,顯式載入 Enrollments 實體,現在學生資訊和報名資訊將寫入控制檯視窗。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         context.Configuration.LazyLoadingEnabled = false;

         var student = (from s in context.Students where s.FirstMidName == 
            "Ali" select s).FirstOrDefault<Student>();

         string name = student.FirstMidName + " " + student.LastName;
         Console.WriteLine("ID: {0}, Name: {1}", student.ID, name);

         foreach (var enrollment in student.Enrollments) {
            Console.WriteLine("Enrollment ID: {0}, Course ID: {1}", 
               enrollment.EnrollmentID, enrollment.CourseID);
         }

         Console.WriteLine();
         Console.WriteLine("Explicitly loaded Enrollments");
         Console.WriteLine();

         context.Entry(student).Collection(s ⇒ s.Enrollments).Load();
         Console.WriteLine("ID: {0}, Name: {1}", student.ID, name);

         foreach (var enrollment in student.Enrollments) {
            Console.WriteLine("Enrollment ID: {0}, Course ID: {1}", 
               enrollment.EnrollmentID, enrollment.CourseID);
         }

         Console.ReadKey();
      }
   }
}

執行上述示例時,你將收到以下輸出。首先只顯示學生資訊,然後顯式載入 Enrollments 實體後,顯示學生及其相關報名資訊。

ID: 1, Name: Ali Alexander
Explicitly loaded Enrollments
ID: 1, Name: Ali Alexander
       Enrollment ID: 1, Course ID: 1050
       Enrollment ID: 2, Course ID: 4022
       Enrollment ID: 3, Course ID: 4041

我們建議您逐步執行以上示例以更好地理解。

實體框架 - 驗證

在本章中,讓我們學習可以在 ADO.NET Entity Framework 中使用的驗證技術來驗證模型資料。Entity Framework 提供了各種各樣的驗證功能,可以實現到使用者介面以進行客戶端驗證,也可以用於伺服器端驗證。

  • 在 Entity Framework 中,資料驗證是應用程式中捕獲錯誤資料解決方案的一部分。

  • Entity Framework 在將資料寫入資料庫之前預設會驗證所有資料,並使用各種資料驗證方法。

  • 但是,Entity Framework 在使用者介面資料驗證之後出現。因此,在這種情況下,需要實體驗證來處理 EF 引發的任何異常並顯示通用訊息。

  • 有一些資料驗證技術可以改進錯誤檢查以及如何將錯誤訊息傳遞迴使用者。

DbContext 有一個可重寫的方法,稱為 ValidateEntity。當你呼叫 SaveChanges 時,Entity Framework 將為其快取中狀態不是 Unchanged 的每個實體呼叫此方法。你可以將驗證邏輯直接放在這裡,如下例中針對 Student 實體所示。

public partial class UniContextEntities : DbContext {

   protected override System.Data.Entity.Validation
      .DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, 
      System.Collections.Generic.IDictionary<object, object> items) {

         if (entityEntry.Entity is Student) {

            if (entityEntry.CurrentValues.GetValue<string>("FirstMidName") == "") {

               var list = new List<System.Data.Entity
                  .Validation.DbValidationError>();

               list.Add(new System.Data.Entity.Validation
                  .DbValidationError("FirstMidName", "FirstMidName is required"));

               return new System.Data.Entity.Validation
                  .DbEntityValidationResult(entityEntry, list);
            }
         }

         if (entityEntry.CurrentValues.GetValue<string>("LastName") == "") {

            var list = new List<System.Data.Entity
               .Validation.DbValidationError>();

            list.Add(new System.Data.Entity.Validation
               .DbValidationError("LastName", "LastName is required"));

            return new System.Data.Entity.Validation
               .DbEntityValidationResult(entityEntry, list);
         }

         return base.ValidateEntity(entityEntry, items);
   }
}

在上面的 ValidateEntity 方法中,檢查 Student 實體的 FirstMidName 和 LastName 屬性,如果這些屬性中的任何一個為空字串,則將返回錯誤訊息。

讓我們來看一個簡單的例子,其中建立一個新學生,但學生的 FirstMidName 為空字串,如下面的程式碼所示。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         Console.WriteLine("Adding new Student to the database");
         Console.WriteLine();

         try {

            context.Students.Add(new Student() {
               FirstMidName = "",
               LastName = "Upston"
            });

            context.SaveChanges();
         } catch (DbEntityValidationException dbValidationEx) {

            foreach (DbEntityValidationResult entityErr in 
               dbValidationEx.EntityValidationErrors) {

               foreach (DbValidationError error in entityErr.ValidationErrors) {
                  Console.WriteLine("Error: {0}",error.ErrorMessage);
               }
            }
         }

         Console.ReadKey();
      }
   }
}

編譯並執行上述示例時,你將在控制檯視窗中收到以下錯誤訊息。

Adding new Student to the database  
Error: FirstMidName is required 

我們建議您逐步執行上述示例,以便更好地理解。

實體框架 - 跟蹤更改

Entity Framework 提供了跟蹤對實體及其關係所做的更改的功能,因此當呼叫上下文的 SaveChanges 方法時,將在資料庫上進行正確的更新。這是 Entity Framework 的一個關鍵特性。

  • 更改跟蹤在向實體集合新增新記錄、修改或刪除現有實體時跟蹤更改。

  • 然後 DbContext 級別保留所有更改。

  • 如果在銷燬 DbContext 物件之前未儲存這些跟蹤更改,則會丟失這些更改。

  • DbChangeTracker 類提供有關上下文當前跟蹤的所有實體的資訊。

  • 要讓上下文跟蹤任何實體,該實體必須具有主鍵屬性。

在 Entity Framework 中,更改跟蹤預設啟用。你還可以透過將 DbContext 的 AutoDetectChangesEnabled 屬性設定為 false 來停用更改跟蹤。如果此屬性設定為 true,則 Entity Framework 會維護實體的狀態。

using (var context = new UniContextEntities()) {
   context.Configuration.AutoDetectChangesEnabled = true;
}

讓我們來看下面的例子,其中從資料庫中檢索學生及其報名資訊。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         context.Configuration.AutoDetectChangesEnabled = true;
         Console.WriteLine("Retrieve Student");

         var student = (from s in context.Students where s.FirstMidName == 
            "Ali" select s).FirstOrDefault<Student>();

         string name = student.FirstMidName + " " + student.LastName;
         Console.WriteLine("ID: {0}, Name: {1}", student.ID, name);
         Console.WriteLine();
         Console.WriteLine("Retrieve all related enrollments");

         foreach (var enrollment in student.Enrollments) {
            Console.WriteLine("Enrollment ID: {0}, Course ID: {1}", 
               enrollment.EnrollmentID, enrollment.CourseID);
         }

         Console.WriteLine();

         Console.WriteLine("Context tracking changes of {0} entity.", 
            context.ChangeTracker.Entries().Count());

         var entries = context.ChangeTracker.Entries();

         foreach (var entry in entries) {
            Console.WriteLine("Entity Name: {0}", entry.Entity.GetType().Name);
            Console.WriteLine("Status: {0}", entry.State);
         }

         Console.ReadKey();
      }
   }
}

編譯並執行上述示例時,你將收到以下輸出。

Retrieve Student 
ID: 1, Name: Ali Alexander
Retrieve all related enrollments
       Enrollment ID: 1, Course ID: 1050
       Enrollment ID: 2, Course ID: 4022
       Enrollment ID: 3, Course ID: 4041
Context tracking changes of 4 entity.
Entity Name: Student
Status: Unchanged
Entity Name: Enrollment
Status: Unchanged
Entity Name: Enrollment
Status: Unchanged
Entity Name: Enrollment
Status: Unchanged

你可以看到,所有資料都只從資料庫中檢索,因此所有實體的狀態都是未更改的。

現在讓我們來看另一個簡單的例子,我們將向資料庫中新增一個報名資訊並刪除一個學生。以下是新增新報名資訊和刪除一個學生的程式碼。

class Program {

   static void Main(string[] args) {

      using (var context = new UniContextEntities()) {

         context.Configuration.AutoDetectChangesEnabled = true;

         Enrollment enr = new Enrollment() { 
            StudentID = 1, CourseID = 3141 
         };

         Console.WriteLine("Adding New Enrollment");
         context.Enrollments.Add(enr);
         Console.WriteLine("Delete Student");

         var student = (from s in context.Students where s.ID == 
            23 select s).SingleOrDefault<Student>();

         context.Students.Remove(student);
         Console.WriteLine("");

         Console.WriteLine("Context tracking changes of {0} entity.", 
            context.ChangeTracker.Entries().Count());
         var entries = context.ChangeTracker.Entries();

         foreach (var entry in entries) {
            Console.WriteLine("Entity Name: {0}", entry.Entity.GetType().Name);
            Console.WriteLine("Status: {0}", entry.State);
         }

         Console.ReadKey();
      }
   }
}

編譯並執行上述示例時,你將收到以下輸出。

Adding New Enrollment
Delete Student
Context tracking changes of 2 entity.
Entity Name: Enrollment
Status: Added
Entity Name: Student
Status: Deleted

你現在可以看到,報名實體的狀態設定為已新增,學生實體的狀態設定為已刪除,因為已添加了新的報名資訊,並且從資料庫中刪除了一個學生。

我們建議您逐步執行以上示例以更好地理解。

實體框架 - 彩色實體

在 Entity Framework 中,彩色實體主要與更改設計器中實體的顏色有關,以便開發人員可以輕鬆識別 Visual Studio 設計器中相關的實體組。此功能首次在 Entity Framework 5.0 中引入。

  • 此功能與效能方面無關。

  • 當你的專案規模很大並且在一個 edmx 檔案中有多個實體時,此功能非常有助於將實體分離到不同的模組中。

如果你正在使用 edmx 檔案並在設計器中開啟它,要更改顏色,請在設計視窗上選擇一個實體。然後右鍵單擊並選擇屬性。

Edmx File Designer Edmx File

在屬性視窗中,選擇填充顏色屬性。

Color Property Window

使用有效的顏色名稱(例如 Green)或有效的 RGB(255, 128, 128)指定顏色,或者也可以從顏色拾取器中選擇。

Color

要一次更改多個實體的顏色,請選擇多個實體並使用屬性視窗更改所有實體的填充顏色。

Multiple Entities

你還可以透過選擇以下任一選項來更改屬性的格式:

  • 顯示名稱
  • 顯示名稱和型別

預設情況下,選擇顯示名稱選項。要更改屬性格式,請右鍵單擊設計器視窗。

Property Format

選擇標量屬性格式→顯示名稱和型別。

Scalar Property Format

你現在可以看到型別也與名稱一起顯示。

Entity Framework - Code First 方法

實體框架提供三種建立實體模型的方法,每種方法都有其自身的優缺點。

  • 程式碼優先 (Code First)
  • 資料庫優先 (Database First)
  • 模型優先 (Model First)

在本章中,我們將簡要介紹程式碼優先方法。一些開發人員喜歡使用程式碼中的設計器,而另一些開發人員則寧願只使用他們的程式碼。對於這些開發人員,實體框架提供了一種稱為程式碼優先的建模工作流。

  • 程式碼優先建模工作流的目標資料庫是不存在的,程式碼優先將建立它。

  • 如果你的資料庫為空,然後 Code First 將向其中新增新表,它也可以使用。

  • 程式碼優先允許您使用 C# 或 VB.Net 類定義模型。

  • 可以使用類和屬性上的特性或使用流暢的 API 選擇性地執行其他配置。

Code First Approach

為什麼選擇程式碼優先?

  • 程式碼優先實際上是由一組拼圖塊組成的。首先是您的領域類。

  • 領域類與實體框架無關。它們只是您的業務領域的專案。

  • 然後,實體框架有一個上下文來管理這些類與資料庫之間的互動。

  • 上下文不特定於程式碼優先。它是實體框架的一個特性。

  • 程式碼優先添加了一個模型構建器,它檢查上下文正在管理的類,然後使用一組規則或約定來確定這些類和關係如何描述模型,以及該模型應如何對映到您的資料庫。

  • 所有這些都在執行時發生。您將永遠看不到此模型,它僅存在於記憶體中。

  • Code First 還可以使用該模型建立資料庫(如果需要)。

  • 如果模型發生更改,它還可以更新資料庫,使用稱為程式碼優先遷移的功能。

環境設定

要開始使用 EF Code First 方法,你需要在你的系統上安裝以下工具。

  • Visual Studio 2013(.net framework 4.5.2)或更高版本。
  • MS SQL Server 2012 或更高版本。
  • 透過 NuGet 包安裝 Entity Framework。

透過 NuGet 包安裝 EF

**步驟 1** - 首先,從檔案→新建→專案…建立控制檯應用程式。

**步驟 2** - 從左側窗格中選擇 Windows,從模板窗格中選擇控制檯應用程式。

Installing EF

**步驟 3** - 輸入 EFCodeFirstDemo 作為名稱,然後選擇確定。

**步驟 4** - 在解決方案資源管理器中右鍵單擊你的專案,然後選擇管理 NuGet 包…

NuGet Package Manager

這將開啟 NuGet 包管理器,並搜尋 EntityFramework。這將搜尋與 Entity Framework 相關的所有包。

**步驟 5** - 選擇 EntityFramework 並單擊安裝。或者從工具選單中單擊 NuGet 包管理器,然後單擊包管理器控制檯。在包管理器控制檯視窗中,輸入以下命令:Install-Package EntityFramework。

Installed Entity Framework6

安裝完成後,你將在輸出視窗中看到以下訊息“已成功將 'EntityFramework 6.1.2' 安裝到 EFCodeFirstDemo”。

安裝後,EntityFramework.dll 將包含在你的專案中,如下面的影像所示。

Entity Framework dll

現在你可以開始使用 Code First 方法了。

實體框架 - 第一個示例

讓我們使用類定義一個非常簡單的模型。我們只是在 Program.cs 檔案中定義它們,但在實際應用程式中,你將把你的類拆分到單獨的檔案中,並可能拆分到單獨的專案中。以下是我們將使用 Code First 方法建立的資料模型。

Model Using Classes

建立模型

使用以下程式碼為 Student 類在 Program.cs 檔案中新增以下三個類。

public class Student {
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}
  • ID 屬性將成為與該類對應的資料庫表的 PRIMARY KEY 列。

  • Enrollments 屬性是一個導航屬性。導航屬性儲存與該實體相關的其他實體。

  • 在這種情況下,Student 實體的 Enrollments 屬性將儲存與該 Student 實體相關的所有 Enrollment 實體。

  • 導航屬性通常定義為 virtual,以便它們可以利用 Entity Framework 的某些功能,例如延遲載入。

  • 如果導航屬性可以儲存多個實體(如多對多或一對多關係),則其型別必須是列表,其中可以新增、刪除和更新條目,例如 ICollection。

以下是 Course 類的實現。

public class Course {
   public int CourseID { get; set; }
   public string Title { get; set; }
   public int Credits { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

Enrollments 屬性是一個導航屬性。Course 實體可以與任意數量的 Enrollment 實體相關聯。

以下是 Enrollment 類和列舉的實現。

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; }
   public Grade? Grade { get; set; }
	
   public virtual Course Course { get; set; }
   public virtual Student Student { get; set; }
}
  • EnrollmentID 屬性將是 PRIMARY KEY。

  • Grade 屬性是一個列舉。Grade 型別聲明後面的問號表示 Grade 屬性是可為空的。

  • 空等級與零等級不同。空表示等級未知或尚未分配。

  • StudentID 和 CourseID 屬性是外部索引鍵,相應的導航屬性是 Student 和 Course。

  • Enrollment 實體與一個 Student 和一個 Course 實體相關聯,因此該屬性只能儲存單個 Student 和 Course 實體。

建立資料庫上下文

協調給定資料模型的 Entity Framework 功能的主要類是資料庫上下文類,它允許查詢和儲存資料。你可以透過從 DbContext 類派生併為模型中的每個類公開一個型別化的 DbSet 來建立此類。以下是 MyContext 類的實現,它派生自 DbContext 類。

public class MyContext : DbContext {
   public virtual DbSet<Course> Courses { get; set; }
   public virtual DbSet<Enrollment> Enrollments { get; set; }
   public virtual DbSet<Student> Students { get; set; }
}

以下是 Program.cs 檔案中的完整程式碼。

using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EFCodeFirstDemo {

   class Program {
      static void Main(string[] args) {}
   }

   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; }
      public Grade? Grade { get; set; }
		
      public virtual Course Course { get; set; }
      public virtual Student Student { get; set; }
   }

   public class Student {
      public int ID { get; set; }
      public string LastName { get; set; }
      public string FirstMidName { get; set; }
      public DateTime EnrollmentDate { get; set; }
		
      public virtual ICollection<Enrollment> Enrollments { get; set; }
   }

   public class Course {
      public int CourseID { get; set; }
      public string Title { get; set; }
      public int Credits { get; set; }
		
      public virtual ICollection<Enrollment> Enrollments { get; set; }
   }

   public class MyContext : DbContext {
      public virtual DbSet<Course> Courses { get; set; }
      public virtual DbSet<Enrollment> Enrollments { get; set; }
      public virtual DbSet<Student> Students { get; set; }
   }

}

以上程式碼是我們開始儲存和檢索資料所需的所有內容。讓我們新增一些資料,然後檢索它。以下是 main 方法中的程式碼。

static void Main(string[] args) {

   using (var context = new MyContext()) {
      // Create and save a new Students
      Console.WriteLine("Adding new students");

      var student = new Student {
         FirstMidName = "Alain", LastName = "Bomer", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
      };

      context.Students.Add(student);
		
      var student1 = new Student {
         FirstMidName = "Mark", LastName = "Upston", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
      };

      context.Students.Add(student1);
      context.SaveChanges();

      // Display all Students from the database
      var students = (from s in context.Students 
         orderby s.FirstMidName select s).ToList<Student>();

      Console.WriteLine("Retrieve all Students from the database:");

      foreach (var stdnt in students) {
         string name = stdnt.FirstMidName + " " + stdnt.LastName;
         Console.WriteLine("ID: {0}, Name: {1}", stdnt.ID, name);
      }
		
      Console.WriteLine("Press any key to exit...");
      Console.ReadKey();
   }
}

執行上述程式碼時,你將收到以下輸出。

Adding new students
Retrieve all Students from the database:
ID: 1, Name: Alain Bomer
ID: 2, Name: Mark Upston
Press any key to exit...

現在想到的問題是,資料在哪裡以及我們添加了一些資料然後從資料庫中檢索到的資料庫在哪裡。按照約定,DbContext 為你建立了一個數據庫。

  • 如果本地 SQL Express 例項可用,則 Code First 會在該例項上建立資料庫。

  • 如果 SQL Express 不可用,則 Code First 將嘗試使用 LocalDb。

  • 資料庫以派生上下文的完全限定名稱命名。

在我們的例子中,SQL Express 例項可用,資料庫名稱為 EFCodeFirstDemo.MyContext,如下面的圖片所示。

SQL Express Instance
  • 這些僅僅是預設約定,還有多種方法可以更改 Code First 使用的資料庫。

  • 如上圖所示,它建立了 Students、Courses 和 Enrollments 表,並且每個表都包含具有適當資料型別和長度的列。

  • 列名和資料型別也與相應域類的屬性匹配。

資料庫初始化

在上面的示例中,我們看到了 Code First 自動建立資料庫,但是如果您想更改資料庫和伺服器的名稱,讓我們看看 Code First 在初始化資料庫時如何決定資料庫名稱和伺服器。請檢視下圖。

Database Initialization

您可以透過以下方式定義上下文類的基本建構函式。

  • 無引數
  • 資料庫名稱
  • 連線字串名稱

無引數

如果您在上下文類的基本建構函式中不指定任何引數,如上例所示,則實體框架將在您的本地 SQLEXPRESS 伺服器上建立一個名為 {名稱空間}.{上下文類名} 的資料庫。

在上面的示例中,自動建立的資料庫名稱為 EFCodeFirstDemo.MyContext。如果您檢視名稱,您會發現 EFCodeFirstDemo 是名稱空間,MyContext 是上下文類名,如下面的程式碼所示。

public class MyContext : DbContext {
   public MyContext() : base() {}

   public virtual DbSet<Course> Courses { get; set; }
   public virtual DbSet<Enrollment> Enrollments { get; set; }
   public virtual DbSet<Student> Students { get; set; }
}

資料庫名稱

如果您在上下文類的基本建構函式中將資料庫名稱作為引數傳遞,則 Code First 將再次自動建立資料庫,但這次名稱將是在本地 SQLEXPRESS 資料庫伺服器上的基本建構函式中作為引數傳遞的名稱。

在下面的程式碼中,MyContextDB 指定為基本建構函式中的引數。如果執行您的應用程式,則名為 MyContextDB 的資料庫將在您的本地 SQL 伺服器中建立。

public class MyContext : DbContext {
   public MyContext() : base("MyContextDB") {}
   public virtual DbSet<Course> Courses { get; set; }
   public virtual DbSet<Enrollment> Enrollments { get; set; }
   public virtual DbSet<Student> Students { get; set; }
}

連線字串名稱

這是一種簡單的方法,可以告訴 DbContext 使用除 SQL Express 或 LocalDb 之外的資料庫伺服器。您可以選擇將連線字串放在 app.config 檔案中。

  • 如果連線字串的名稱與您的上下文名稱匹配(無論是否使用名稱空間限定),則 DbContext 在使用無引數建構函式時將找到它。

  • 如果連線字串名稱與您的上下文名稱不同,則可以透過將連線字串名稱傳遞給 DbContext 建構函式來告訴 DbContext 在 Code First 模式下使用此連線。

public class MyContext : DbContext {
   public MyContext() : base("name = MyContextDB") {}
   public virtual DbSet<Course> Courses { get; set; }
   public virtual DbSet<Enrollment> Enrollments { get; set; }
   public virtual DbSet<Student> Students { get; set; }
}
  • 在上面的程式碼中,上下文類連線字串的程式碼片段指定為基本建構函式中的引數。

  • 連線字串名稱必須以“name=”開頭,否則將將其視為資料庫名稱。

  • 此表單明確表明您期望在配置檔案中找到連線字串。如果找不到具有給定名稱的連線字串,則會丟擲異常。

<connectionStrings>
   <add name = "MyContextDB"
      connectionString = "Data Source =.;Initial Catalog = EFMyContextDB;Integrated Security = true"
      providerName = "System.Data.SqlClient"/>
</connectionStrings>
  • app.config 中連線字串中的資料庫名稱為 **EFMyContextDB**。CodeFirst 將建立一個新的 **EFMyContextDB** 資料庫或使用本地 SQL Server 中現有的 **EFMyContextDB** 資料庫。

域類

到目前為止,我們只是讓 EF 使用其預設約定發現模型,但有時我們的類不遵循這些約定,我們需要能夠執行進一步的配置。但是您可以透過配置域類來為 EF 提供所需的資訊來覆蓋這些約定。有兩種選項可以配置您的域類 -

  • 資料註釋
  • Fluent API

資料註釋

DataAnnotations 用於配置您的類,這將突出顯示最常用的配置。DataAnnotations 也被許多 .NET 應用程式理解,例如 ASP.NET MVC,這些應用程式允許這些應用程式利用相同的註釋進行客戶端驗證。

以下是學生類中使用的資料註釋。

public class Enrollment {

   [Key]
   public int EnrollmentID { get; set; }
   public int CourseID { get; set; }
   public int StudentID { get; set; }
   public Grade? Grade { get; set; }

   [ForeignKey("CourseID")]
   public virtual Course Course { get; set; }

   [ForeignKey("ID")]
   public virtual Student Student { get; set; }
}

Fluent API

大多數模型配置可以使用簡單的資料註釋來完成。Fluent API 是一種指定模型配置的高階方法,它涵蓋了資料註釋可以執行的所有操作,此外還有一些資料註釋無法實現的更高階的配置。資料註釋和 Fluent API 可以一起使用。

要訪問 Fluent API,您需要重寫 DbContext 中的 OnModelCreating 方法。現在讓我們將學生表中的列名從 FirstMidName 重新命名為 FirstName,如下面的程式碼所示。

public class MyContext : DbContext {

   protected override void OnModelCreating(DbModelBuilder modelBuilder) {
      modelBuilder.Entity<Student>().Property(s ⇒ s.FirstMidName)
         .HasColumnName("FirstName");
   }

   public virtual DbSet<Course> Courses { get; set; }
   public virtual DbSet<Enrollment> Enrollments { get; set; }
   public virtual DbSet<Student> Students { get; set; }
}

實體框架 - 資料註解

DataAnnotations 用於配置類,這將突出顯示最常用的配置。DataAnnotations 也被許多 .NET 應用程式理解,例如 ASP.NET MVC,這些應用程式允許這些應用程式利用相同的註釋進行客戶端驗證。DataAnnotation 屬性覆蓋預設的 CodeFirst 約定。

**System.ComponentModel.DataAnnotations** 包含以下影響列的可空性或大小的屬性。

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

**System.ComponentModel.DataAnnotations.Schema** 名稱空間包含以下影響資料庫模式的屬性。

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

Key

實體框架依賴於每個實體都具有一個鍵值,它用於跟蹤實體。Code First 依賴的約定之一是它如何暗示每個 Code First 類中的哪個屬性是鍵。

  • 約定是查詢名為“Id”的屬性或一個將類名和“Id”組合在一起的屬性,例如“StudentId”。

  • 該屬性將對映到資料庫中的主鍵列。

  • Student、Course 和 Enrollment 類遵循此約定。

現在假設 Student 類使用 StdntID 而不是 ID。當 Code First 未找到與此約定匹配的屬性時,它將丟擲異常,因為實體框架要求您必須具有鍵屬性。您可以使用鍵註釋來指定哪個屬性用作 EntityKey。

讓我們看看以下 Student 類的程式碼,其中包含 StdntID,但它不遵循預設的 Code First 約定。因此,為了處理這個問題,添加了一個 Key 屬性,這將使其成為主鍵。

public class Student {

   [Key]
   public int StdntID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

當您執行應用程式並在 SQL Server 資源管理器中檢視資料庫時,您會看到主鍵現在是 Students 表中的 StdntID。

Primary Key

實體框架也支援組合鍵。**組合鍵** 也是主鍵,由多個屬性組成。例如,您有一個 DrivingLicense 類,其主鍵是 LicenseNumber 和 IssuingCountry 的組合。

public class DrivingLicense {

   [Key, Column(Order = 1)]
   public int LicenseNumber { get; set; }
   [Key, Column(Order = 2)]
   public string IssuingCountry { get; set; }
   public DateTime Issued { get; set; }
   public DateTime Expires { get; set; }
}

當您有組合鍵時,實體框架要求您定義鍵屬性的順序。您可以使用 Column 註釋來指定順序。

Column Annotation

Timestamp

Code First 將 Timestamp 屬性與 ConcurrencyCheck 屬性相同對待,但它還將確保 Code First 生成的資料庫欄位不可為空。

  • 更常見的是使用 rowversion 或 timestamp 欄位進行併發檢查。

  • 與其使用 ConcurrencyCheck 註釋,不如使用更具體的 TimeStamp 註釋,只要屬性的型別是位元組陣列即可。

  • 在給定的類中,您只能有一個 timestamp 屬性。

讓我們看一個簡單的例子,將 TimeStamp 屬性新增到 Course 類中 -

public class Course {

   public int CourseID { get; set; }
   public string Title { get; set; }
   public int Credits { get; set; }
   [Timestamp]
   public byte[] TStamp { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

如上例所示,Timestamp 屬性應用於 Course 類的 Byte[] 屬性。因此,Code First 將在 Courses 表中建立一個名為 TStamp 的時間戳列。

ConcurrencyCheck

ConcurrencyCheck 註釋允許您標記一個或多個屬性,以便在使用者編輯或刪除實體時在資料庫中用於併發檢查。如果您一直在使用 EF 設計器,這與將屬性的 ConcurrencyMode 設定為 Fixed 相一致。

讓我們看一個 ConcurrencyCheck 如何工作的簡單示例,將其新增到 Course 類中的 Title 屬性中。

public class Course {

   public int CourseID { get; set; }
   [ConcurrencyCheck]
   public string Title { get; set; }
   public int Credits { get; set; }
   [Timestamp, DataType("timestamp")]
   public byte[] TimeStamp { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

在上面的 Course 類中,ConcurrencyCheck 屬性應用於現有的 Title 屬性。現在,Code First 將在更新命令中包含 Title 列以檢查樂觀併發,如下面的程式碼所示。

exec sp_executesql N'UPDATE [dbo].[Courses]
   SET [Title] = @0
   WHERE (([CourseID] = @1) AND ([Title] = @2))
   ',N'@0 nvarchar(max) ,@1 int,@2 nvarchar(max) ',@0=N'Maths',@1=1,@2=N'Calculus'
go

Required 註釋

Required 註釋告訴 EF 特定的屬性是必需的。讓我們看看以下 Student 類,其中 Required id 新增到 FirstMidName 屬性中。Required 屬性將強制 EF 確保該屬性包含資料。

public class Student {

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

   [Required]
   public string LastName { get; set; }

   [Required]
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

如上例所示,Required 屬性應用於 FirstMidName 和 LastName。因此,Code First 將在 Students 表中建立 NOT NULL FirstMidName 和 LastName 列,如下面的圖片所示。

Not Null

MaxLength

MaxLength 屬性允許您指定其他屬性驗證。它可以應用於域類的字串或陣列型別屬性。EF Code First 將根據 MaxLength 屬性中指定的內容設定列的大小。

讓我們看看以下 Course 類,其中 MaxLength(24) 屬性應用於 Title 屬性。

public class Course {

   public int CourseID { get; set; }
   [ConcurrencyCheck]
   [MaxLength(24)]
   public string Title { get; set; }
   public int Credits { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

當您執行上述應用程式時,Code First 將在 CourseId 表中建立一個名為 nvarchar(24) 的列 Title,如下面的圖片所示。

nvarchar Column

當用戶設定包含超過 24 個字元的 Title 時,EF 將丟擲 EntityValidationError。

MinLength

MinLength 屬性也允許您指定其他屬性驗證,就像您使用 MaxLength 一樣。MinLength 屬性也可以與 MaxLength 屬性一起使用,如下面的程式碼所示。

public class Course {

   public int CourseID { get; set; }
   [ConcurrencyCheck]
   [MaxLength(24) , MinLength(5)]
   public string Title { get; set; }
   public int Credits { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

如果將 Title 屬性的值設定為小於 MinLength 屬性中指定的長度或大於 MaxLength 屬性中指定的長度,則 EF 將丟擲 EntityValidationError。

StringLength

StringLength 也允許您指定其他屬性驗證,如 MaxLength。唯一的區別是 StringLength 屬性只能應用於域類的字串型別屬性。

public class Course {

   public int CourseID { get; set; }
   [StringLength (24)]
   public string Title { get; set; }
   public int Credits { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

實體框架還驗證 StringLength 屬性的屬性值。如果使用者設定包含超過 24 個字元的 Title,則 EF 將丟擲 EntityValidationError。

Table

預設的 Code First 約定建立類似於類名的表名。如果您讓 Code First 建立資料庫,並且還想更改它正在建立的表的名稱。然後 -

  • 您可以將 Code First 與現有資料庫一起使用。但並非總是類名與資料庫中表名匹配的情況。

  • Table 屬性覆蓋此預設約定。

  • EF Code First 將為給定的域類建立具有 Table 屬性中指定名稱的表。

讓我們看下面的例子,其中類名為 Student,根據約定,Code First 假設這將對映到名為 Students 的表。如果不是這種情況,您可以使用 Table 屬性指定表名,如下面的程式碼所示。

[Table("StudentsInfo")]
public class Student {

   [Key]
   public int StdntID { get; set; }
   [Required]
   public string LastName { get; set; }
   [Required]
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

現在您可以看到 Table 屬性將表指定為 StudentsInfo。生成表時,您將看到表名為 StudentsInfo,如下面的圖片所示。

StudentsInfo

您不僅可以指定表名,還可以使用 Table 屬性指定表的模式,如下面的程式碼所示。

[Table("StudentsInfo", Schema = "Admin")] 
public class Student {

   [Key]
   public int StdntID { get; set; }
   [Required]
   public string LastName { get; set; }
   [Required]
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

您可以在上面的示例中看到,表使用 admin 模式指定。現在 Code First 將在 Admin 模式下建立 StudentsInfo 表,如下面的圖片所示。

Admin Schema

Column

它與 Table 屬性相同,但 Table 屬性會覆蓋表的行為,而 Column 屬性會覆蓋列的行為。預設的 Code First 約定會建立一個與屬性名稱類似的列名稱。如果您讓 Code First 建立資料庫,並且還希望更改表中列的名稱。然後 -

  • Column 屬性會覆蓋預設約定。

  • EF Code First 將為給定屬性的 Column 屬性中指定的名稱建立一個列。

讓我們看一下下面的示例,其中屬性名為 FirstMidName,按照約定,Code First 假設這將對映到名為 FirstMidName 的列。

如果不是這種情況,您可以使用 Column 屬性指定列的名稱,如下面的程式碼所示。

public class Student {

   public int ID { get; set; }
   public string LastName { get; set; }
   [Column("FirstName")]
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

您可以看到 Column 屬性將列指定為 FirstName。生成表時,您將看到列名稱 FirstName,如下面的影像所示。

FirstName

Index

Index 屬性是在 Entity Framework 6.1 中引入的。如果您使用的是早期版本,則本節中的資訊不適用。

  • 您可以使用 IndexAttribute 在一個或多個列上建立索引。

  • 將屬性新增到一個或多個屬性將導致 EF 在建立資料庫時在資料庫中建立相應的索引。

  • 在大多數情況下,索引可以使資料檢索更快更高效。但是,在表或檢視上載入過多的索引可能會對其他操作(例如插入或更新)的效能產生不利影響。

  • 索引是 Entity Framework 中的新功能,您可以透過減少從資料庫查詢資料所需的時間來提高 Code First 應用程式的效能。

  • 您可以使用 Index 屬性向資料庫新增索引,並覆蓋預設的 Unique 和 Clustered 設定以獲得最適合您方案的索引。

  • 預設情況下,索引將命名為 IX_<property name>

讓我們看一下下面的程式碼,其中在 Course 類中為 Credits 添加了 Index 屬性。

public class Course {
   public int CourseID { get; set; }
   public string Title { get; set; }
   [Index]
   public int Credits { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

您可以看到 Index 屬性已應用於 Credits 屬性。生成表時,您將在索引中看到 IX_Credits。

IX Credits

預設情況下,索引是非唯一的,但您可以使用IsUnique命名引數指定索引應該是唯一的。以下示例介紹了一個唯一的索引,如下面的程式碼所示。

public class Course {
   public int CourseID { get; set; }
   [Index(IsUnique = true)]
	
   public string Title { get; set; }
   [Index]
	
   public int Credits { get; set; }
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

ForeignKey

Code First 約定將處理模型中最常見的關聯,但有些情況下它需要幫助。例如,透過更改 Student 類中主鍵屬性的名稱,導致其與 Enrollment 類之間的關聯出現問題。

public class Enrollment {
   public int EnrollmentID { get; set; }
   public int CourseID { get; set; }
   public int StudentID { get; set; }
   public Grade? Grade { get; set; }
	
   public virtual Course Course { get; set; }
   public virtual Student Student { get; set; }
}

public class Student {
   [Key]
   public int StdntID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

在生成資料庫時,Code First 會看到 Enrollment 類中的 StudentID 屬性,並根據約定(它與類名加“ID”匹配)將其識別為對 Student 類的外部索引鍵。但是,Student 類中沒有 StudentID 屬性,但 Student 類中是 StdntID 屬性。

解決此問題的方案是在 Enrollment 中建立一個導航屬性,並使用 ForeignKey DataAnnotation 來幫助 Code First 瞭解如何在兩個類之間建立關聯,如下面的程式碼所示。

public class Enrollment {
   public int EnrollmentID { get; set; }
   public int CourseID { get; set; }
   public int StudentID { get; set; }
	
   public Grade? Grade { get; set; }
   public virtual Course Course { get; set; }
   [ForeignKey("StudentID")]
	
   public virtual Student Student { get; set; }
}

您現在可以看到 ForeignKey 屬性已應用於導航屬性。

ForeignKey Attribute

NotMapped

根據 Code First 的預設約定,每個支援的資料型別的屬性(包括 getter 和 setter)都將在資料庫中表示。但這並非始終適用於您的應用程式。NotMapped 屬性會覆蓋此預設約定。例如,您可能在 Student 類中有一個名為 FatherName 的屬性,但不需要儲存它。您可以將 NotMapped 屬性應用於您不想在資料庫中建立列的 FatherName 屬性,如下面的程式碼所示。

public class Student {
   [Key]
   public int StdntID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
	
   public DateTime EnrollmentDate { get; set; }
   [NotMapped]

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

您可以看到 NotMapped 屬性已應用於 FatherName 屬性。生成表時,您將看到資料庫中不會建立 FatherName 列,但它存在於 Student 類中。

NotMapped Attribute

Code First 不會為沒有 getter 或 setter 的屬性建立列,如下面的 Student 類 Address 和 Age 屬性示例所示。

InverseProperty

當您在類之間有多個關聯時,使用 InverseProperty。在 Enrollment 類中,您可能希望跟蹤誰註冊了 Current Course 和 Previous Course。讓我們為 Enrollment 類新增兩個導航屬性。

public class Enrollment {
   public int EnrollmentID { get; set; }
   public int CourseID { get; set; }
   public int StudentID { get; set; }
   public Grade? Grade { get; set; }
	
   public virtual Course CurrCourse { get; set; }
   public virtual Course PrevCourse { get; set; }
   public virtual Student Student { get; set; }
}

類似地,您還需要在這些屬性引用的 Course 類中新增。Course 類具有返回 Enrollment 類的導航屬性,其中包含所有當前和以前的註冊資訊。

public class Course {

   public int CourseID { get; set; }
   public string Title { get; set; }
   [Index]

   public int Credits { get; set; }
   public virtual ICollection<Enrollment> CurrEnrollments { get; set; }
   public virtual ICollection<Enrollment> PrevEnrollments { get; set; }
}

如果外部索引鍵屬性未包含在特定類中,則 Code First 會建立{Class Name}_{Primary Key}外部索引鍵列,如上面的類所示。生成資料庫時,您將看到以下外部索引鍵。

Foreign Keys

如您所見,Code First 無法自行匹配兩個類中的屬性。Enrollments 的資料庫表應該有一個 CurrCourse 的外部索引鍵和一個 PrevCourse 的外部索引鍵,但 Code First 將建立四個外部索引鍵屬性,即

  • CurrCourse _CourseID
  • PrevCourse _CourseID
  • Course_CourseID,以及
  • Course_CourseID1

要解決這些問題,您可以使用 InverseProperty 註釋來指定屬性的對齊方式。

public class Course {

   public int CourseID { get; set; }
   public string Title { get; set; }
   [Index]

   public int Credits { get; set; }
   [InverseProperty("CurrCourse")]

   public virtual ICollection<Enrollment> CurrEnrollments { get; set; }
   [InverseProperty("PrevCourse")]

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

如您所見,InverseProperty 屬性在上面的 Course 類中透過指定它屬於 Enrollment 類的哪個引用屬性來應用。現在,Code First 將生成一個數據庫,並在 Enrollments 表中僅建立兩個外部索引鍵列,如下面的影像所示。

Foreign Key Columns

我們建議您逐步執行以上示例以更好地理解。

實體框架 - Fluent API

Fluent API 是一種指定模型配置的高階方法,它涵蓋了資料註釋可以執行的所有操作,以及資料註釋無法執行的一些更高階的配置。資料註釋和 Fluent API 可以一起使用,但 Code First 優先考慮 Fluent API > 資料註釋 > 預設約定。

  • Fluent API 是配置域類的另一種方法。

  • Code First Fluent API 最常透過覆蓋派生 DbContext 上的 OnModelCreating 方法來訪問。

  • Fluent API 提供比 DataAnnotations 更多的配置功能。Fluent API 支援以下型別的對映。

在本章中,我們將繼續使用簡單的示例,其中包含 Student、Course 和 Enrollment 類以及一個名為 MyContext 的上下文類,如下面的程式碼所示。

using System.Data.Entity; 
using System.Linq; 
using System.Text;
using System.Threading.Tasks;  

namespace EFCodeFirstDemo {

   class Program {
      static void Main(string[] args) {}
   }
   
   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; }
      public Grade? Grade { get; set; }
		
      public virtual Course Course { get; set; }
      public virtual Student Student { get; set; }
   }

   public class Student {
      public int ID { get; set; }
      public string LastName { get; set; }
      public string FirstMidName { get; set; }
		
      public DateTime EnrollmentDate { get; set; }
		
      public virtual ICollection<Enrollment> Enrollments { get; set; }
   }

   public class Course {
      public int CourseID { get; set; }
      public string Title { get; set; }
      public int Credits { get; set; }
		
      public virtual ICollection<Enrollment> Enrollments { get; set; }
   }

   public class MyContext : DbContext {
      public virtual DbSet<Course> Courses { get; set; }
      public virtual DbSet<Enrollment> Enrollments { get; set; }
      public virtual DbSet<Student> Students { get; set; }
   }

}	  

要訪問 Fluent API,您需要覆蓋 DbContext 中的 OnModelCreating 方法。讓我們看一下一個簡單的示例,我們將其中學生表中的列名稱從 FirstMidName 重新命名為 FirstName,如下面的程式碼所示。

public class MyContext : DbContext {

   protected override void OnModelCreating(DbModelBuilder modelBuilder) {
      modelBuilder.Entity<Student>().Property(s ⇒ s.FirstMidName)
      .HasColumnName("FirstName");}

      public virtual DbSet<Course> Courses { get; set; }
      public virtual DbSet<Enrollment> Enrollments { get; set; }
      public virtual DbSet<Student> Students { get; set; }
}

DbModelBuilder 用於將 CLR 類對映到資料庫模式。它是主要類,您可以在其中配置所有域類。這種以程式碼為中心的構建實體資料模型 (EDM) 的方法稱為 Code First。

Fluent API 提供了許多重要的方法來配置實體及其屬性以覆蓋各種 Code First 約定。以下是一些示例。

序號 方法名稱和說明
1

ComplexType<TComplexType>

將型別註冊為模型中的複雜型別,並返回一個可用於配置複雜型別的物件。此方法可以對同一型別呼叫多次以執行多行配置。

2

Entity<TEntityType>

將實體型別註冊為模型的一部分,並返回一個可用於配置實體的物件。此方法可以對同一實體呼叫多次以執行多行配置。

3

HasKey<TKey>

配置此實體型別的 primary key 屬性。

4

HasMany<TTargetEntity>

配置從此實體型別到目標實體型別的多對多關係。

5

HasOptional<TTargetEntity>

配置從此實體型別到目標實體型別的可選關係。實體型別的例項可以在未指定此關係的情況下儲存到資料庫。資料庫中的外部索引鍵將是可為空的。

6

HasRequired<TTargetEntity>

配置從此實體型別到目標實體型別的必需關係。除非指定此關係,否則實體型別的例項將無法儲存到資料庫。資料庫中的外部索引鍵將是非可為空的。

7

Ignore<TProperty>

從模型中排除屬性,以便它不會對映到資料庫。(繼承自 StructuralTypeConfiguration<TStructuralType>)

8

Property<T>

配置在此型別上定義的結構屬性。(繼承自 StructuralTypeConfiguration<TStructuralType>)

9

ToTable(String)

配置此實體型別對映到的表名稱。

Fluent API 允許您配置實體或其屬性,無論您是想更改它們對映到資料庫的方式還是它們彼此關聯的方式。您可以使用配置影響各種對映和建模。以下是 Fluent API 支援的主要對映型別 -

  • 實體對映
  • 屬性對映

實體對映

實體對映只是一些簡單的對映,將影響 Entity Framework 對類如何對映到資料庫的理解。所有這些我們都在資料註釋中討論過,在這裡我們將看到如何使用 Fluent API 實現相同的功能。

  • 因此,與其進入域類新增這些配置,不如在上下文中執行此操作。

  • 首先要覆蓋 OnModelCreating 方法,該方法提供 modelBuilder 進行操作。

預設架構

生成資料庫時,預設架構為 dbo。您可以使用 DbModelBuilder 上的 HasDefaultSchema 方法指定要用於所有表、儲存過程等的資料庫架構。

讓我們看一下下面的示例,其中應用了 admin 架構。

public class MyContext : DbContext {
   public MyContext() : base("name = MyContextDB") {}

   protected override void OnModelCreating(DbModelBuilder modelBuilder) {
      //Configure default schema
      modelBuilder.HasDefaultSchema("Admin");
   }
	
   public virtual DbSet<Course> Courses { get; set; }
   public virtual DbSet<Enrollment> Enrollments { get; set; }
   public virtual DbSet<Student> Students { get; set; }
}

將實體對映到表

使用預設約定,Code First 將使用上下文類中 DbSet 屬性的名稱建立資料庫表,例如 Courses、Enrollments 和 Students。但如果您想要不同的表名,則可以覆蓋此約定,並提供與 DbSet 屬性不同的表名,如下面的程式碼所示。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   //Map entity to table
   modelBuilder.Entity<Student>().ToTable("StudentData");
   modelBuilder.Entity<Course>().ToTable("CourseDetail");
   modelBuilder.Entity<Enrollment>().ToTable("EnrollmentInfo");
}

生成資料庫時,您將看到 OnModelCreating 方法中指定的表名。

OnModel Method

實體拆分(將實體對映到多個表)

實體拆分允許您將來自多個表的資料組合到單個類中,並且只能與表之間存在一對一關係的表一起使用。讓我們看一下下面的示例,其中 Student 資訊對映到兩個表中。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {
   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   //Map entity to table
   modelBuilder.Entity<Student>().Map(sd ⇒ {
      sd.Properties(p ⇒ new { p.ID, p.FirstMidName, p.LastName });
      sd.ToTable("StudentData");
   })

   .Map(si ⇒ {
      si.Properties(p ⇒ new { p.ID, p.EnrollmentDate });
      si.ToTable("StudentEnrollmentInfo");
   });

   modelBuilder.Entity<Course>().ToTable("CourseDetail");
   modelBuilder.Entity<Enrollment>().ToTable("EnrollmentInfo");
}

在上面的程式碼中,您可以看到 Student 實體透過使用 Map 方法將某些屬性對映到 StudentData 表,並將某些屬性對映到 StudentEnrollmentInfo 表,從而拆分為以下兩個表。

  • StudentData - 包含 Student FirstMidName 和 Last Name。

  • StudentEnrollmentInfo - 包含 EnrollmentDate。

生成資料庫時,您將在資料庫中看到以下表,如下面的影像所示。

Entity Splitting

屬性對映

Property 方法用於配置屬於實體或複雜型別的每個屬性的屬性。Property 方法用於獲取給定屬性的配置物件。您還可以使用 Fluent API 對映和配置域類的屬性。

配置主鍵

主鍵的預設約定為 -

  • 類定義了一個名為“ID”或“Id”的屬性
  • 類名後跟“ID”或“Id”

如果您的類不遵循主鍵的預設約定,如下面的 Student 類程式碼所示 -

public class Student {
   public int StdntID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

然後要顯式設定屬性為 primary key,您可以使用 HasKey 方法,如下面的程式碼所示 -

protected override void OnModelCreating(DbModelBuilder modelBuilder) {
   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");
	
   // Configure Primary Key
   modelBuilder.Entity<Student>().HasKey<int>(s ⇒ s.StdntID); 
}

配置列

在 Entity Framework 中,預設情況下,Code First 會為屬性建立具有相同名稱、順序和資料型別的列。但是您也可以覆蓋此約定,如下面的程式碼所示。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   //Configure EnrollmentDate Column
   modelBuilder.Entity<Student>().Property(p ⇒ p.EnrollmentDate)
	
   .HasColumnName("EnDate")
   .HasColumnType("DateTime")
   .HasColumnOrder(2);
}

配置 MaxLength 屬性

在下面的示例中,課程標題屬性的長度不能超過 24 個字元。當用戶指定的字元長度超過 24 個字元時,使用者將收到 DbEntityValidationException 異常。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {
   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");
   modelBuilder.Entity<Course>().Property(p ⇒ p.Title).HasMaxLength(24);
}

配置 Null 或 NotNull 屬性

在下面的示例中,課程標題屬性是必需的,因此使用 IsRequired 方法建立 NotNull 列。類似地,學生入學日期是可選的,因此我們將使用 IsOptional 方法允許此列中出現空值,如下面的程式碼所示。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");
   modelBuilder.Entity<Course>().Property(p ⇒ p.Title).IsRequired();
   modelBuilder.Entity<Student>().Property(p ⇒ p.EnrollmentDate).IsOptional();
	
   //modelBuilder.Entity<Student>().Property(s ⇒ s.FirstMidName)
   //.HasColumnName("FirstName"); 
}

配置關係

在資料庫的上下文中,關係是指兩個關係型資料庫表之間存在的一種情況,其中一個表具有引用另一個表主鍵的外部索引鍵。使用 Code First 時,您可以透過定義域 CLR 類來定義模型。預設情況下,Entity Framework 使用 Code First 約定將您的類對映到資料庫模式。

  • 如果您使用 Code First 命名約定,在大多數情況下,您可以依靠 Code First 根據外部索引鍵和導航屬性來設定表之間的關係。

  • 如果它們不符合這些約定,您還可以使用一些配置來影響類之間的關係,以及在 Code First 中新增配置時這些關係如何在資料庫中實現。

  • 其中一些可以在資料註釋中使用,您甚至可以使用 Fluent API 應用一些更復雜的配置。

配置一對一關係

當您在模型中定義一對一關係時,您會在每個類中使用一個引用導航屬性。在資料庫中,兩個表在關係的任一側都只能有一條記錄。每個主鍵值只與相關表中的一條記錄(或沒有記錄)相關聯。

  • 如果兩個相關列都是 primary key 或具有唯一約束,則會建立一對一關係。

  • 在一對一關係中,primary key 還會充當外部索引鍵,並且兩個表都沒有單獨的外部索引鍵列。

  • 這種型別的關係並不常見,因為大多數以這種方式關聯的資訊都將位於一個表中。

讓我們看一下下面的示例,我們將在模型中新增另一個類以建立一對一關係。

public class Student {
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual StudentLogIn StudentLogIn { get; set; }
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

public class StudentLogIn {
   [Key, ForeignKey("Student")]
   public int ID { get; set; }
   public string EmailID { get; set; }
   public string Password { get; set; }
	
   public virtual Student Student { get; set; }
}

如您在上面的程式碼中看到的,Key 和 ForeignKey 屬性用於 StudentLogIn 類中的 ID 屬性,以便將其標記為主鍵以及外部索引鍵。

要使用 Fluent API 配置 Student 和 StudentLogIn 之間的一對零或一對一關係,您需要覆蓋 OnModelCreating 方法,如下面的程式碼所示。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   // Configure ID as PK for StudentLogIn
   modelBuilder.Entity<StudentLogIn>()
   .HasKey(s ⇒ s.ID);

   // Configure ID as FK for StudentLogIn
   modelBuilder.Entity<Student>()
   
   .HasOptional(s ⇒ s.StudentLogIn) //StudentLogIn is optional
   .WithRequired(t ⇒ t.Student); // Create inverse relationship
}

在大多數情況下,Entity Framework 可以推斷出關係中哪個型別是依賴型別,哪個型別是主體型別。但是,當關系的兩端都必需或兩端都可選時,Entity Framework 無法識別依賴項和主體。當關系的兩端都必需時,您可以使用 HasRequired,如下面的程式碼所示。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   // Configure ID as PK for StudentLogIn
   modelBuilder.Entity<StudentLogIn>()
   .HasKey(s ⇒ s.ID);

   // Configure ID as FK for StudentLogIn
   modelBuilder.Entity<Student>()
   .HasRequired(r ⇒ r.Student)
   .WithOptional(s ⇒ s.StudentLogIn);  
}

生成資料庫後,您將看到關係建立如下面的影像所示。

Created Relationship

配置一對多關係

主鍵表只包含一條記錄,該記錄與相關表中的零條、一條或多條記錄相關聯。這是最常用的關係型別。

  • 在這種型別的關係中,表 A 中的一行可以在表 B 中有多個匹配行,但表 B 中的一行只能在表 A 中有一個匹配行。

  • 外部索引鍵定義在表示關係多端的那張表上。

  • 例如,在上圖中,Student 和 Enrollment 表具有一對多關係,每個學生可能有多個註冊,但每個註冊只屬於一個學生。

以下是具有多對一關係的 Student 和 Enrollment,但 Enrollment 表中的外部索引鍵不遵循預設的 Code First 約定。

public class Enrollment {
   public int EnrollmentID { get; set; }
   public int CourseID { get; set; }
	
   //StdntID is not following code first conventions name
   public int StdntID { get; set; }
   public Grade? Grade { get; set; }
	
   public virtual Course Course { get; set; }
   public virtual Student Student { get; set; }
}

public class Student {
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual StudentLogIn StudentLogIn { get; set; }
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

在這種情況下,要使用 Fluent API 配置一對多關係,您需要使用 HasForeignKey 方法,如下面的程式碼所示。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   //Configure FK for one-to-many relationship
   modelBuilder.Entity<Enrollment>()

   .HasRequired<Student>(s ⇒ s.Student)
   .WithMany(t ⇒ t.Enrollments)
   .HasForeignKey(u ⇒ u.StdntID);  
}

生成資料庫後,您將看到關係建立如下面的影像所示。

HasRequired Method

在上面的示例中,HasRequired 方法指定 Student 導航屬性必須為 Null。因此,每次新增或更新 Enrollment 時,都必須將 Student 與 Enrollment 實體一起分配。為了處理這種情況,我們需要使用 HasOptional 方法而不是 HasRequired 方法。

配置多對多關係

兩個表中的每條記錄都可以與另一個表中的任意數量的記錄(或沒有記錄)相關聯。

  • 您可以透過定義一個第三個表(稱為連線表)來建立這種關係,該表的 primary key 由表 A 和表 B 的外部索引鍵組成。

  • 例如,Student 表和 Course 表具有多對多關係。

以下是 Student 和 Course 類,其中 Student 和 Course 具有多對多關係,因為這兩個類都具有 Students 和 Courses 導航屬性,它們都是集合。換句話說,一個實體具有另一個實體集合。

public class Student {
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Course> Courses { get; set; }
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

public class Course {
   public int CourseID { get; set; }
   public string Title { get; set; }
   public int Credits { get; set; }
	
   public virtual ICollection<Student> Students { get; set; }
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

要配置 Student 和 Course 之間的多對多關係,您可以使用 Fluent API,如下面的程式碼所示。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   // Configure many-to-many relationship
   modelBuilder.Entity<Student>()
   .HasMany(s ⇒ s.Courses) 
   .WithMany(s ⇒ s.Students);
}

生成資料庫時,預設的 Code First 約定用於建立聯接表。結果,建立了 StudentCourses 表,其中包含 Course_CourseID 和 Student_ID 列,如下面的影像所示。

Join Table

如果要指定聯接表名以及表中列的名稱,則需要使用 Map 方法進行其他配置。

protected override void OnModelCreating(DbModelBuilder modelBuilder) {

   //Configure default schema
   modelBuilder.HasDefaultSchema("Admin");

   // Configure many-to-many relationship 
   modelBuilder.Entity<Student>()

   .HasMany(s ⇒ s.Courses)
   .WithMany(s ⇒ s.Students)
   
   .Map(m ⇒ {
      m.ToTable("StudentCoursesTable");
      m.MapLeftKey("StudentID");
      m.MapRightKey("CourseID");
   }); 
}

您可以看到,當資料庫生成時,表和列的名稱將按照上述程式碼中指定的建立。

Join Table

我們建議您逐步執行以上示例以更好地理解。

實體框架 - 種子資料庫

在 Entity Framework 中,Seed 引入於 EF 4.1,並與資料庫初始化程式一起使用。**Seed 方法** 的基本思想是將資料初始化到由 Code First 建立或由 Migrations 演變的資料庫中。這些資料通常是測試資料,但也可能是參考資料,例如已知學生、課程等的列表。初始化資料時,它執行以下操作:

  • 檢查目標資料庫是否存在。
  • 如果存在,則將當前 Code First 模型與資料庫元資料中儲存的模型進行比較。
  • 如果當前模型與資料庫中的模型不匹配,則刪除資料庫。
  • 如果資料庫已刪除或最初不存在,則建立資料庫。
  • 如果建立了資料庫,則呼叫初始化程式 Seed 方法。

Seed 方法將資料庫上下文物件作為輸入引數,方法中的程式碼使用該物件將新實體新增到資料庫中。要將資料播種到資料庫中,您需要覆蓋 Seed 方法。讓我們看一下下面的示例,其中一些預設資料在內部類中初始化到資料庫中。

private class UniDBInitializer<T> : DropCreateDatabaseAlways<MyContext> {

   protected override void Seed(MyContext context) {

      IList<Student> students = new List<Student>();

      students.Add(new Student() {
         FirstMidName = "Andrew", 
         LastName = "Peters", 
         EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
      });

      students.Add(new Student() {
         FirstMidName = "Brice", 
         LastName = "Lambson", 
         EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
      });

      students.Add(new Student() {
         FirstMidName = "Rowan", 
         LastName = "Miller", 
         EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
      });

      foreach (Student student in students)
      context.Students.Add(student);
      base.Seed(context);
   }
}

在上面的程式碼中,初始化了 student 表。您需要在上下文類中設定此 DB 初始化程式類,如下面的程式碼所示。

public MyContext() : base("name=MyContextDB") {
   Database.SetInitializer<MyContext>(new UniDBInitializer<MyContext>());
}

以下是 MyContext 類的完整類實現,它還包含 DB 初始化程式類。

public class MyContext : DbContext {

   public MyContext() : base("name=MyContextDB") {
      Database.SetInitializer<MyContext>(new UniDBInitializer<MyContext>());
   }

   public virtual DbSet<Course> Courses { get; set; }
   public virtual DbSet<Enrollment> Enrollments { get; set; }
   public virtual DbSet<Student> Students { get; set; }
	
   private class UniDBInitializer<T> : DropCreateDatabaseAlways<MyContext> {

      protected override void Seed(MyContext context) {

         IList<Student> students = new List<Student>();
			
         students.Add(new Student() {
            FirstMidName = "Andrew", 
            LastName = "Peters", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString()) 
         });

         students.Add(new Student() {
            FirstMidName = "Brice", 
            LastName = "Lambson", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
         });

         students.Add(new Student() {
            FirstMidName = "Rowan", 
            LastName = "Miller", 
            EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
         });

         foreach (Student student in students)
         context.Students.Add(student);
         base.Seed(context);
      }
   } 
}

編譯並執行上述示例後,您可以在資料庫中看到資料,如下面的影像所示。

Data In Database

我們建議您逐步執行以上示例以更好地理解。

Entity Framework - Code First 遷移

Entity Framework 4.3 包含一個新的 Code First 遷移功能,允許您隨著時間的推移隨著模型的變化而逐步演變資料庫模式。對於大多數開發人員來說,這比 4.1 和 4.2 版本中資料庫初始化程式選項有了很大的改進,這些選項要求您在模型更改時手動更新資料庫或刪除並重新建立資料庫。

  • 在 Entity Framework 4.3 之前,如果您已經在資料庫中擁有資料(種子資料除外)或現有的儲存過程、觸發器等,這些策略將刪除整個資料庫並重新建立它,因此您將丟失資料和其他資料庫物件。

  • 使用遷移,它將在您的模型更改時自動更新資料庫模式,而不會丟失任何現有資料或其他資料庫物件。

  • 它使用一個名為 MigrateDatabaseToLatestVersion 的新資料庫初始化程式。

遷移有兩種型別:

  • 自動遷移
  • 基於程式碼的遷移

自動遷移

自動遷移首次引入於 Entity Framework 4.3。在自動遷移中,您無需在程式碼檔案中手動處理資料庫遷移。例如,對於每個更改,您還需要更改域類。但是,使用自動遷移,您只需在程式包管理器控制檯中執行一個命令即可完成此操作。

讓我們看一下自動遷移的分步過程。

當您使用 Code First 方法時,您的應用程式沒有資料庫。

在此示例中,我們將從 3 個基本類開始,例如 Student、Course 和 Enrollment,如下面的程式碼所示。

public class Enrollment {
   public int EnrollmentID { get; set; }
   public int CourseID { get; set; }
   public int StudentID { get; set; }
   public Grade? Grade { get; set; }
	
   public virtual Course Course { get; set; }
   public virtual Student Student { get; set; }

}

public class Student {
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }

}

public class Course {
   public int CourseID { get; set; }
   public string Title { get; set; }
   [Index]
   public int Credits { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }

}

以下是上下文類。

public class MyContext : DbContext {
   public MyContext() : base("MyContextDB") {}
   public virtual DbSet<Course> Courses { get; set; }
   public virtual DbSet<Enrollment> Enrollments { get; set; }
   public virtual DbSet<Student> Students { get; set; }
}

在執行應用程式之前,您需要啟用自動遷移。

**步驟 1** - 從“工具”→“NuGet 包管理器”→“程式包管理器控制檯”中開啟程式包管理器控制檯。

**步驟 2** - 要啟用自動遷移,請在程式包管理器控制檯中執行以下命令。

PM> enable-migrations -EnableAutomaticMigrations:$true
Command

**步驟 3** - 命令成功執行後,它會在專案的 Migration 資料夾中建立一個內部密封的 Configuration 類,如下面的程式碼所示。

namespace EFCodeFirstDemo.Migrations {

   using System;
   using System.Data.Entity;
   using System.Data.Entity.Migrations;
   using System.Linq;
	
   internal sealed class Configuration : DbMigrationsConfiguration<EFCodeFirstDemo.MyContext> {

      public Configuration() {
         AutomaticMigrationsEnabled = true;
         ContextKey = "EFCodeFirstDemo.MyContext";
      }

      protected override void Seed(EFCodeFirstDemo.MyContext context) {

         //  This method will be called after migrating to the latest version.
         //  You can use the DbSet<T>.AddOrUpdate() helper extension method
         //  to avoid creating duplicate seed data. E.g.

         //  context.People.AddOrUpdate(
            //  p ⇒ p.FullName, 
            //  new Person { FullName = "Andrew Peters" }, 
            //  new Person { FullName = "Brice Lambson" }, 
            //  new Person { FullName = "Rowan Miller" }
         //  );
      }
   }
}

**步驟 4** - 使用新的 DB 初始化策略 MigrateDatabaseToLatestVersion 在上下文類中設定資料庫初始化程式。

public class MyContext : DbContext {

   public MyContext() : base("MyContextDB") {
      Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, 
         EFCodeFirstDemo.Migrations.Configuration>("MyContextDB"));
   }

   public virtual DbSet<Course> Courses { get; set; }
   public virtual DbSet<Enrollment> Enrollments { get; set; }
   public virtual DbSet<Student> Students { get; set; }

}

**步驟 5** - 您已設定自動遷移。當您執行應用程式時,當您更改模型時,它將自動處理遷移。

Migration

**步驟 6** - 如您所見,一個系統表 __MigrationHistory 也與其他表一起在您的資料庫中建立。在 __MigrationHistory 中,自動遷移維護資料庫更改的歷史記錄。

**步驟 7** - 當您將另一個實體類作為域類新增並執行應用程式時,它將在您的資料庫中建立該表。讓我們新增以下 StudentLogIn 類。

public class StudentLogIn {
   [Key, ForeignKey("Student")]
   public int ID { get; set; }
   public string EmailID { get; set; }
   public string Password { get; set; }
	
   public virtual Student Student { get; set; }
}

**步驟 8** - 不要忘記在您的上下文類中為上述類新增 DBSet,如下面的程式碼所示。

public virtual DbSet<StudentLogIn> StudentsLogIn { get; set; }

**步驟 9** - 再次執行您的應用程式,您將看到 StudentsLogIn 表已新增到您的資料庫中。

StudentsLogIn

上述針對自動遷移提到的步驟僅適用於您的實體。例如,新增另一個實體類或刪除現有實體類,它將成功遷移。但是,如果您向實體類新增或刪除任何屬性,則它將引發異常。

步驟 10 − 要處理屬性遷移,您需要在配置類建構函式中設定 AutomaticMigrationDataLossAllowed = true。

public Configuration() {
   AutomaticMigrationsEnabled = true;
   AutomaticMigrationDataLossAllowed = true;
   ContextKey = "EFCodeFirstDemo.MyContext";
}

基於程式碼的遷移

當您開發一個新的應用程式時,您的資料模型會頻繁更改,並且每次模型更改時,它都會與資料庫不同步。您已將 Entity Framework 配置為每次更改資料模型時自動刪除並重新建立資料庫。基於程式碼的遷移在您希望對遷移有更多控制權時非常有用。

  • 當您新增、刪除或更改實體類或更改您的 DbContext 類時,下次執行應用程式時,它會自動刪除您現有的資料庫,建立一個與模型匹配的新資料庫,並使用測試資料填充它。

  • Code First 遷移功能透過啟用 Code First 更新資料庫模式而不是刪除和重新建立資料庫來解決此問題。要部署應用程式,您必須啟用遷移。

以下是遷移資料庫中更改的基本規則:

  • 啟用遷移
  • 新增遷移
  • 更新資料庫

讓我們看一下以下基於程式碼的遷移的分步過程。

當您使用 Code First 方法時,您的應用程式沒有資料庫。

在本例中,我們將使用我們 3 個基本類(如 Student、Course 和 Enrollment)重新開始,如下面的程式碼所示。

public class Enrollment {
   public int EnrollmentID { get; set; }
   public int CourseID { get; set; }
   public int StudentID { get; set; }
   public Grade? Grade { get; set; }
	
   public virtual Course Course { get; set; }
   public virtual Student Student { get; set; }

}

public class Student {
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }

}

public class Course {
   public int CourseID { get; set; }
   public string Title { get; set; }
   [Index]
   public int Credits { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }

}

以下是上下文類。

public class MyContext : DbContext {

   public MyContext() : base("MyContextDB") {
      Database.SetInitializer(new MigrateDatabaseToLatestVersion<
         MyContext, EFCodeFirstDemo.Migrations.Configuration>("MyContextDB"));
   }

   public virtual DbSet<Course> Courses { get; set; }
   public virtual DbSet<Enrollment> Enrollments { get; set; }
   public virtual DbSet<Student> Students { get; set; }

}

步驟 1 − 在執行應用程式之前,您需要啟用遷移。

步驟 2 − 從“工具”→“NuGet 包管理器”→“包管理器控制檯”開啟包管理器控制檯。

步驟 3 − 遷移已啟用,現在透過執行以下命令在您的應用程式中新增遷移。

PM> add-migration "UniDB Schema"

步驟 4 − 當命令成功執行後,您將看到在 Migration 資料夾中建立了一個新檔案,其名稱為傳遞給命令的引數,並在其前面添加了時間戳字首,如下面的影像所示。

TimeStamp Prefix

步驟 5 − 您可以使用“update-database”命令建立或更新資料庫。

PM> Update-Database -Verbose

“-Verbose”標誌指定在控制檯中顯示應用於目標資料庫的 SQL 語句。

步驟 6 − 讓我們在 Student 類中再新增一個屬性“Age”,然後執行更新語句。

public class Student {
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public int Age { get; set; }
   public DateTime EnrollmentDate { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }

}

當您執行 PM → Update-Database –Verbose 時,如果命令成功執行,您將看到新列 Age 已新增到您的資料庫中。

New Column Age.

我們建議您逐步執行以上示例以更好地理解。

Entity Framework - 多個 DbContext

在本章中,我們將學習如何在應用程式中存在多個 DbContext 類時將更改遷移到資料庫。

  • 多個 DbContext 最初在 Entity Framework 6.0 中引入。
  • 多個上下文類可能屬於單個數據庫或兩個不同的資料庫。

在我們的示例中,我們將為同一個資料庫定義兩個上下文類。在以下程式碼中,有兩個用於 Student 和 Teacher 的 DbContext 類。

public class Student {
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
}

public class MyStudentContext : DbContext {
   public MyStudentContext() : base("UniContextDB") {}
   public virtual DbSet<Student> Students { get; set; }
}

public class Teacher {
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime HireDate { get; set; }
}

public class MyTeacherContext : DbContext {
   public MyTeacherContext() : base("UniContextDB") {}
   public virtual DbSet<Teacher> Teachers { get; set; }
}

如您在上面的程式碼中看到的,有兩個名為“Student”和“Teacher”的模型。每個模型都與特定的對應上下文類相關聯,即 Student 與 MyStudentContext 相關聯,Teacher 與 MyTeacherContext 相關聯。

以下是在專案中存在多個上下文類時遷移資料庫更改的基本規則。

  • enable-migrations -ContextTypeName <DbContext-Name-with-Namespaces> MigrationsDirectory:<Migrations-Directory-Name>

  • Add-Migration -configuration <DbContext-Migrations-Configuration-Class-withNamespaces> <Migrations-Name>

  • Update-Database -configuration <DbContext-Migrations-Configuration-Class-withNamespaces> -Verbose

讓我們透過在包管理器控制檯中執行以下命令來為 MyStudentContext 啟用遷移。

PM→ enable-migrations -ContextTypeName:EFCodeFirstDemo.MyStudentContext
Package Manager Console

執行後,我們將模型新增到遷移歷史記錄中,為此,我們必須在同一個控制檯中觸發 add-migration 命令。

PM→ add-migration -configuration EFCodeFirstDemo.Migrations.Configuration Initial

現在讓我們在資料庫中的 Students 和 Teachers 表中新增一些資料。

static void Main(string[] args) {

   using (var context = new MyStudentContext()) {
	
      //// Create and save a new Students
      Console.WriteLine("Adding new students");

      var student = new Student {
         FirstMidName = "Alain", 
         LastName = "Bomer", 
         EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
         //Age = 24
      };

      context.Students.Add(student);

      var student1 = new Student {
         FirstMidName = "Mark",
         LastName = "Upston", 
         EnrollmentDate = DateTime.Parse(DateTime.Today.ToString())
         //Age = 30
      };

      context.Students.Add(student1);
      context.SaveChanges();
      // Display all Students from the database
      var students = (from s in context.Students orderby s.FirstMidName
         select s).ToList<Student>();
		
      Console.WriteLine("Retrieve all Students from the database:");

      foreach (var stdnt in students) {
         string name = stdnt.FirstMidName + " " + stdnt.LastName;
         Console.WriteLine("ID: {0}, Name: {1}", stdnt.ID, name);
      }

      Console.WriteLine("Press any key to exit...");
      Console.ReadKey();
   }

   using (var context = new MyTeacherContext()) {

      //// Create and save a new Teachers
      Console.WriteLine("Adding new teachers");

      var student = new Teacher {
         FirstMidName = "Alain", 
         LastName = "Bomer", 
         HireDate = DateTime.Parse(DateTime.Today.ToString())
         //Age = 24
      };

      context.Teachers.Add(student);

      var student1 = new Teacher {
         FirstMidName = "Mark", 
         LastName = "Upston", 
         HireDate = DateTime.Parse(DateTime.Today.ToString())
         //Age = 30
      };

      context.Teachers.Add(student1);
      context.SaveChanges();
  
      // Display all Teachers from the database
      var teachers = (from t in context.Teachers orderby t.FirstMidName
         select t).ToList<Teacher>();
		
      Console.WriteLine("Retrieve all teachers from the database:");

      foreach (var teacher in teachers) {
         string name = teacher.FirstMidName + " " + teacher.LastName;
         Console.WriteLine("ID: {0}, Name: {1}", teacher.ID, name);
      }

      Console.WriteLine("Press any key to exit...");
      Console.ReadKey();
   }
}

執行上述程式碼後,您將看到為兩個不同的模型建立了兩個不同的表,如下面的影像所示。

Executed Code

我們建議您逐步執行以上示例以更好地理解。

Entity Framework - 巢狀實體型別

在 Entity Framework 6 之前,Entity Framework 無法識別巢狀在其他實體或複雜型別中的實體或複雜型別。當 Entity Framework 生成模型時,巢狀型別會消失。

讓我們來看一個簡單的示例,其中我們有三個實體 Student、Course 和 Enrollment 的基本模型。

  • 讓我們新增一個屬性 Identity,它是一個 Person 型別。Person 是另一個實體,包含 BirthDate 和 FatherName 屬性。

  • 在 Entity Framework 術語中,因為它沒有標識並且是實體的一部分,所以它是 Entity Framework 複雜型別,並且我們實際上從 Entity Framework 的第一個版本開始就支援複雜型別。

  • Person 型別沒有像以下程式碼所示那樣巢狀。

public class Student {
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
   public DateTime EnrollmentDate { get; set; }
   public Person Identity { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }
}

public class Person {

   public Person(string fatherName, DateTime birthDate) {
      FatherName = fatherName;
      BirthDate = birthDate;
   }
	
   public string FatherName { get; set; }
   public DateTime BirthDate { get; set; }
}

Entity Framework 將知道如何在以前版本中使用它時持久化 Person 型別。

透過使用 Entity Framework Power Tool,我們將看到 Entity Framework 如何解釋模型。右鍵單擊 Program.cs 檔案並選擇 Entity Framework → 檢視實體資料模型(只讀)

Framework Power Tool

現在您將看到 Identity 屬性在 Student 類中定義。

Identity Property

如果此 Person 類不會被任何其他實體使用,那麼我們可以將其巢狀在 Student 類中,但是此較早版本的 Entity Framework 無法識別巢狀型別。

在舊版本中,您重新生成模型,不僅無法識別該型別,而且由於該型別不存在,該屬性也不存在,因此 Entity Framework 根本不會持久化 Person 型別。

public class Student {
   public int ID { get; set; }
   public string LastName { get; set; }
   public string FirstMidName { get; set; }
	
   public DateTime EnrollmentDate { get; set; }
   public Person Identity { get; set; }
	
   public virtual ICollection<Enrollment> Enrollments { get; set; }

   public class Person {

      public Person(string fatherName, DateTime birthDate) {
         FatherName = fatherName;
         BirthDate = birthDate;
      }

      public string FatherName { get; set; }
      public DateTime BirthDate { get; set; }
   }
}

使用 Entity Framework 6,巢狀實體和複雜型別將被識別。在上面的程式碼中,您可以看到 Person 巢狀在 Student 類中。

當您使用 Entity Framework Power Tool 顯示 Entity Framework 如何解釋模型時,這次有一個真正的 Identity 屬性和 Person 複雜型別。因此,Entity Framework 將持久化該資料。

Nested Entity Type

現在您可以看到 Identity 是一個巢狀實體型別,這在 Entity Framework 6 之前不受支援。

我們建議您逐步執行以上示例以更好地理解。

廣告