實體框架核心#
1. 導航屬性#
// A |->* B
public class A{
//...
public List<B> Bs{get;set;}
}
public class B{
// ...
public B B{set;get;}
}
導航屬性可以是任何列表類型。
2. 資料庫上下文 ⚠️#
用來協調 EF 和資料庫的管理類。#
- 一個 Context 是一個繼承自 DbContext 的非抽象類。這個類的功能首先是公開所有實體的集合,操作這些集合可以在儲存中讀取或保存資料,除此之外 Context 保存資料的模型。在使用前需要正確配置各個實體的模型以及其他必要的配置。
- Context 的另一個重要的功能就是跟蹤對實體的更改,以便我們在保存更改時,它知道該如何處理。
- Context 跟蹤的每個實體所處的以下狀態之一:未更改 /added,已修改 /modified,已添加 /added,已刪除 /deleted或已分離 /detached。可以將上下文視為沙箱,我們可以在其中對實體集合進行更改,然後通過一次保存操作應用這些更改。
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
// 實體集 DbSet 對應資料庫表
// 由於Courses表包含了 下面兩個表,因此,即使不寫下方兩表,EF也會自動包含
public DbSet<Course> Courses { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
// 覆蓋默認行為,設定表名
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");
}
}
}
DbContext 鉤子方法#
- OnConfiguring 方法:當 context 需要配置它的 provider 和 連接字串的時候,該方法會被調用,這為開發者提供了介入的入口。
- OnModelCreating 方法:在 EF Core 組裝資料模型時會被自動調用,你將會在這個方法裡添加構建模型的代碼。
- SaveChanges 方法:當我們希望將修改保存到資料庫時進行調用。 返回為受保存操作影響的記錄數。
Database Provider / 資料庫提供程序#
- EF core 是資料庫無關的。因此不同的資料庫需要為其提供特定介面
- 引入相關資料庫提供程序包,並在 Context 的 OnConfiguring 中添加對應配置方法(UseSqlServer)
3. 服務註冊#
// Startup.cs
/* Using */
using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
/* Start up */
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<SchoolContext>(
options=>options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddControllersWithViews();
// 開發環境 異常篩選功能
services.AddDatabaseDeveloperPageExceptionFilter();
}
// appsetting.json
{
"ConnectionStrings": {
"DefaultConnection": "資料庫字串"
},
}
4. 創建資料模型#
-
創建資料模型需要 實體 Entity 類型。實體類型就是一系列 C# 類,通常與資料庫對應。
-
配置 實體類型 與 資料庫實體 之間的關係。有兩種方式:”fluent API“ 和 ” 資料註釋 “
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; // 配置 對應表名 和 對應資料庫架構名 [Table("blogs",Schema="dbo")] public class Blog{ [Key] // 主鍵,通常efcore會自動識別 public int BlogId{set;get;} [MaxLength(400)] // 最大長度 public string Url{set;get;} [NotMapped] // 排除映射 public DataTime LoadedFromDatabase {set;get;} [Column("rating",TypeName="decimal(5,2)")] // 映射列名和類型 public decimal Rating {set;get;} } // 使用 fluent API 配置默認架構名 protected override void OnModelCreating(ModelBuilder modelBuilder){ modelBuilder.HasDefaultSchema("dbo"); }
“實體引用 / 引用導航” 配置 一對一#
//一Blog 對應 一個 Customer
class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<BlogImage> BlogImages { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//設定 Blog與BlogImage 一對一關係
modelBuilder.Entity<Blog>()
.HasOne(b => b.BlogImage)
.WithOne(i => i.Blog)
.HasForeignKey<BlogImage>(b => b.BlogForeignKey);
}
}
public class Blog
{
public Blog()
{
Posts = new HashSet<Post>();
}
public int BlogId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public int? ImageId { get; set; }
// 引用導航 efcore 可以自動配置 寫法見官方文檔
public BlogImage BlogImage { get; set; }
public HashSet<Post> Posts { get; set; }
}
public class BlogImage
{
public int BlogImageId { get; set; }
public string ImageUrl { get; set; }
public string Caption { get; set; }
}
“實體集合 / 集合導航” 配置 :一對多#
//實體集合屬性的配置
class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//配置 Blog 的一對多關係
modelBuilder.Entity<Blog>()
.HasMany(b => b.Posts)
.WithOne(p => p.Blog)
.HasForeignKey(p => p.BlogId);
}
}
public class Blog
{
public Blog()
{
// 因為有 HashSet 導航,構造函數要添加此內容
Posts = new HashSet<Post>();
}
public int BlogId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public int? ImageId { get; set; }
public BlogImage BlogImage { get; set; }
// 集合導航,表示一對多關係,需要HashSet輔助
public HashSet<Post> Posts { get; set; }
}
public class Post
{
[Key]
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
總結#
資料模型的作用除了說明資料結構,還需要說明資料關係。關係也是資料庫的核心。
EF core 支持多種方式描述模型關係,使用註釋是最直觀簡單的方法,特殊的關係還需要使用 FluentAPI 輔助。
但兩者都是在 ModelBuilder 中配置相關行為。
5. 遷移 從模型到資料庫#
遷移是使用 EF core 遷移工具從實體模型創建和管理資料庫。
遷移過程#
-
按照上述步驟,構建實體模型和關係,會有一個 的類。通常是你打算的資料庫名稱 + Context,這個類繼承與 DbContext。EF core 正是依據這個文件中的實體模型和關係說明來生成資料庫的。
-
打開終端
-
首先確定安裝 .NET core CLI 工具
# 全局安裝 dotnet-ef 工具 dotnet tool install --global dotnet-ef # 更新 dotnet tool update --global dotnet-ef # 為 項目添加 EFCoreDesign 包 dotnet add package Microsoft.EntityFrameworkCore.Design # 也可以使用Nuget管理器 # 檢查 工具安裝情況 dotnet ef
⚠️ EF Core 工具參考 (.NET CLI) - EF Core | Microsoft Learn 工具的詳細使用方法見此處
-
在工程目錄下打開終端:
# 創建一個遷移 dotnet ef migrations add InitialCreate ## 該命令會生成一個 migration 文件夾,用來記錄遷移,也可以直接修改其內容。 # 創建資料庫架構 dotnet ef database update
⚠️ 關於驗證問題,可以在連接字串中添加
TrustServerCertificate=true;Encrypt=true
-
完成。
發展模型#
當你的實體模型發生改變,我們需要重新更新資料庫。
首先必須要明確,遷移工具生成的 migrations/ 系列文件,是遷移記錄,是資料庫結構演進的完整說明。它包含了一份資料庫結構快照,和一份遷移文件。
因此,當模型改變時,我們可以手動更改 migrations/ 文件,並再次執行dotnet ef database update
來應用遷移。
為了保留資料庫演進的記錄,我們使用以下流程:
-
創建新的遷移
# 這裡最好使用有意義的遷移命名 dotnet ef migrations add AddSomeTable
-
應用遷移
dotnet ef database update
說明#
閱讀 migration/ 文件夾,裡面有一系列帶時間戳和遷移名稱的.cs
文件,用來指示每次遷移執行的動作。可以看出,當實體模型改變時,EFcore 會自動比較並進行增量更新,而非重新生成資料庫。
另一個文件是資料庫快照的.cs
文件,可以看出裡面是真實的資料庫表實體模型。是上一次更新有的資料庫快照。
兩種文件都是 CSharp 文件,可以加入到版本管理中,從而記錄工程開發過程中的資料庫演進。
6 其他#
排除類型#
若當前 Context 引用了其他 Context 中的類型,會在遷移時出現衝突(引用的對象在當前 Context 並不存在,出現錯誤是正常的),此時要從 Context 中排除該類型。
// 這是引用Context類
// 在模型創建鉤子中,排除不存在的類型。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IdentityUser>()
.ToTable("AspNetUsers", t => t.ExcludeFromMigrations());
}
列重命名#
EFCore 無法區分列的重命名或刪除,因此,當你重命名列時,EFcore 會生成 Delete-Create 的操作。這可能會導致資料丟失。需要手動更改。
// EFcore 生成的遷移動作
migrationBuilder.DropColumn(
name: "Name",
table: "Customers");
migrationBuilder.AddColumn<string>(
name: "FullName",
table: "Customers",
nullable: true);
// 需要將其修改為
migrationBuilder.RenameColumn(
name: "Name",
table: "Customers",
newName: "FullName");
在遷移中引入 SQL 行為#
假設要將實體中原來的 Firstname 和 LastName 合併成 FullName 重新存儲,並刪除前兩列。
EFcore 不會自動識別這個行為,需要手動引入 SQL 語句用來遷移。
migrationBuilder.AddColumn<string>(
name: "FullName",
table: "Customer",
nullable: true);
migrationBuilder.Sql(
@"
UPDATE Customer
SET FullName = FirstName + ' ' + LastName;
");
migrationBuilder.DropColumn(
name: "FirstName",
table: "Customer");
migrationBuilder.DropColumn(
name: "LastName",
table: "Customer");
⚠️ 此流程是有順序的。
當要利用舊資料進行遷移時,可以使用 SQL
自定義 SQL 遷移#
可以在不改變模型的情況下,生成一個空遷移,在手動填充 SQL 語句。
SQL 可以操作模型不知道的資料庫對象。
migrationBuilder.Sql(
@"
EXEC ('CREATE PROCEDURE getFullName
@LastName nvarchar(50),
@FirstName nvarchar(50)
AS
RETURN @LastName + @FirstName;')");
⚠️ 當某個語句必須是 SQL 批處理中的第一個或唯一一個語句時,請使用 EXEC
。 它還可以用來解決幂等遷移腳本中的分析程序錯誤,當表中當前不存在引用的列時,可能會發生此類錯誤。
⚠️ 通常 EFCore 使用事務執行遷移操作。當某些資料庫不支持該操作時,可以向migrationBuilder.Sql
中傳入參數suppressTransaction: true
來選擇不使用事務。
7. 遷移管理#
刪除遷移#
使用 dotnet ef migrations remove
來移除 上一個遷移
⚠️ 請勿移除已應用的遷移!
列出遷移#
dotnet ef migrations list
列出所有遷移
重置所有#
直接刪除 Migration 文件夾即可刪除所有遷移記錄,詳情:
管理遷移 - EF Core | Microsoft Learn
應用遷移#
-
生成 SQL 腳本,從零開始生成資料庫。這是將工程模型部署到生產環境的最佳實踐。
dotnet ef migrations script [from] [to]
From to 分別是遷移版本名稱。to 默認是最新。可以使用上述代碼回退。
-
自動比較 SQL 腳本。對於將遷移應用在現有資料庫,但並不清楚其當前版本的情況下。efcore 會檢索遷移歷史記錄,自動更新。
dotnet ef migrations script --idempotent
-
使用命令行工具升級資料庫,這是開發環境測試環境的最佳實踐
dotnet ef database update [to]
-
程序運行時執行遷移,這是本地開發測試時的最佳實踐,可以省去每次啟動都手動遷移的步驟。
// 在 .net 運行主函數中添加 var host = CreateHostBuilder(args).Build(); using(var scope = host.Services.CreateScope()){ var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); younameContext.Database.Migrate(); // 這句是關鍵。 } host.Run();
-
創建捆綁包
團隊管理#
⏰