【EFCore】利用Entityframework Core建立資料庫模型
阿新 • • 發佈:2020-08-17
# 利用Entityframework Core建立資料庫模型
> 本文中Entityframework Core版本為v3.1.6
## 簡介
Entity Framework (EF) Core 是微軟輕量化、可擴充套件、開源和跨平臺版的常用 Entity Framework 資料訪問技術。
EF Core 可用作物件關係對映程式 (O/RM),以便於.NET開發人員能夠使用 .NET 物件來處理資料庫,這樣就不必經常編寫大部分資料訪問程式碼了。
在.NET整個技術棧方向上,EFCore有著舉足輕重的地位,它是使用率第一的資料庫訪問框架,雖然在某些效能速度上不如Dapper、FreeSql等框架,但是在普適性方面是最好的。EFCore支援LINQ和拓展方法的資料庫操作,在程式碼可讀性、便捷性都有很大優勢。
## 預先準備
本文的環境是 *.NET Core3.1,Entityframwork Core 3.1.x,MS SQL SERVER2019*。我們首先建立一個控制檯應用,利用*dotnet cli、Powershell或VS中的Nuage包管理控制器*在專案目錄輸入以下指令安裝必備的包:
這裡使用的是 *dotnet cli*:
``` bash
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Relational
dotnet add package Microsoft.EntityFrameworkCore.Design
```
## 資料庫上下文
什麼是資料庫上下文?我在初學EFCore之時也有這種困惑。在EFCore中,*DbContext*的實現類、子類就是一個數據庫上下文。資料庫上下文主要是作為承前啟後的左右存在,它儲存著一切支撐資料庫與程式訪問的內容。例如以下(節選自知乎使用者[@付鵬](https://www.zhihu.com/question/26387327)的部分內容):
``` bash
....
林沖大叫一聲“啊也!”
....
問:這句話林沖的“啊也”表達了林沖怎樣的心裡?
答:啊你媽個頭啊!
看,一篇文章,給你摘錄一段,沒前沒後,你讀不懂,因為有語境,就是語言環境存在,一段話說了什麼,要通過上下文(文章的上下文)來推斷。
```
事實上EFCore也是這樣的,我們在使用EFCore的時候其實是不太關注框架在為我們做了什麼。事實上,EFCore中已經儲存了各種資料集模型、連線屬性等等。
![dbcontext](https://images.cnblogs.com/cnblogs_com/WarrenRyan/1643641/o_200723074231aspnetcore_dbcontext_1.png)
在實際運用中,我們通常這樣去實現這樣一個數據庫上下文:
``` C#
class TestContext : DbContext
{
public DbSet Tests { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}
```
一般來說寫到這個程度也就可以了,我們利用一個類去繼承*DbContext*類,這個類就是我們的資料庫上下文了。在這個類中,我們可以配置資料庫的相關內容,同時也可以重寫部分函式邏輯。
我們可以看到有一個DbSet型別的屬性,這就是我們常說的模型實體,在這裡他也是一個數據集合。
而OnConfiguring方法就是資料庫上下文的配置函數了,裡面可以配置資料庫的連線、攔截器等等各種東西。OnModelCreating主要用於描述各個實體類之間的關係和實體內部的設定。
我們操作資料庫實際上都是在通過資料庫上下文進行操作。
## 模型構建
在EFCore中,無論你採用何種方式構建資料庫,我們都應該利用類對模型進行構建,已達到O/RM的效果。這裡我們先提出一個簡單的問題:
現在有一個簡單的學生選課系統的資料庫需要構建,已知涉及到了
- 教師資料
- 學生資料
- 課程資料
- 選課資料
請利用EFCore知識進行構建。
這個問題只是在此處提一嘴,因為後面我們的開發都是基於這個假定的資料庫模型進行操作構建。
### FluentApi與Attribute
首先我們介紹FluentApi和Attribute,這裡不會涉及太多的程式碼,程式碼方面都在後面的文章中進行闡述。這兩個東西主要用於標註實體的特性,例如實體對映的表名、列名等。
FlunetApi通過配置領域類來覆蓋預設的約定。在EFCore中,我們通過DbModelBuilder類來使用FluentApi,它的功能比資料註釋屬性更強大,並且在二者衝突或相同時,EFCore會優先選擇FluentApi中所定義的內容。通常我們使用FluentApi都是在OnModelCreating使用,利用DbModelBuilder中的方法和lambda表示式,實現配置整個模型類。舉個簡單的例子:
``` C#
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// 指定模型
modelBuilder.Entity()
.ToTable("xxx");
modelBuilder.Entity()
.HasKey(p=>p.Id);
}
```
也就是我們通過modelBuilder來指定領域模型類進行設定,這種方式相對來說我比較的推薦,因為它能有效減少問題和方便我們進行維護。
Attribute是通過特性標籤的方法來進行設定相關配置,特性標籤的文章見:[.NET Core CSharp 中級篇2-8 特性標籤](https://www.cnblogs.com/WarrenRyan/p/11484963.html)。通過特性標籤去註解領域模型的好處就在於沒有額外的配置程式碼在其他的檔案中,有點類似於資料庫模型完整對應整個的領域模型類。不過使用Attribute進行操作的時候,對於外來鍵之類的東西,處理起來會非常麻煩,因此不推薦完全使用Attribute的方式對模型進行操作註解。示例如下:
``` C#
[Table("xxx")]
public class TestModel
{
[Column("TestId"),Key]
public int Id {get;set;}
}
```
至此,文章已經對資料模型的兩種配置方式進行了一個非常簡單的講解,接下來,我們將會利用這兩種配置方式構建一個簡單的資料庫。
### 實體型別
在上下文中包含一種型別的DbSet,這意味著它包含在 EF Core 的模型中;我們通常將此類型別稱為實體。EFCore 可以從/向資料庫中讀取和寫入實體例項,如果使用的是關係資料庫,EFCore可以通過遷移為實體建立表。也就是說,模型和資料庫表是可以畫等價關係的。
實體常見的操作是指定表名(或檢視)、指定鍵、指定列和不匹配。對應的FluentApi函式和Attribute如下:
```C#
// 指定表名 name為表名,Schema為架構名
[Table(name:"",Schema ="")]
modelBuilder.Entity().ToTable("",schema:"")//或ToView
// 指定主鍵、外來鍵
[Key]、[ForeignKey]
modelBuilder.Entity().HasKey()//或HasForeignKey、HasNoKey
// 指定列
[Column("")]
modelBuilder.Entity().ToProperty()
// 不匹配(不檢索)
[NotMapped]
modelBuilder.Entity().Ingore()//排除屬性
modelBuilder.Ignore();
```
對於定義實體,我們有三種方式可以進行定義:
#### 通過DbSet宣告
在資料庫上下文中宣告的非私有DbSet屬性會被EFCore識別成實體模型,DbSet就是資料庫對映到程式中的資料集合,對此集合的一切操作都會被視為對資料庫的操作。
定義DbSet如下,利用泛型指定實體模型:
``` C#
public class Context : DbContext
{
public virtual DbSet TestModels { get; set;}
}
```
當DbSet所設定的模型下沒有使用FluentApi或Attribute時,DbSet的屬性名則會對應資料庫中實體表的名稱。
#### 通過導航屬性
導航屬性就是一種類之間的關係屬性,EFCore在構建我們的資料庫模型的時候,並不是單單隻盯著被顯式宣告的內容,EFCore會像遞迴一樣一層一層的進行遍歷查詢,將有關係的內容統統認證為實體模型。
例如:
```C#
public class A
{
public virtual B B { get; set;}
}
public class B
{
public virtual A A { get; set;}
}
public class Context : DbContext
{
public virtual DbSet A { get; set;}
}
```
像以上這種情況,很明顯A,B之間有著一一對應的關係,因此EFCore會將A和B都作為模型實體進行載入,而並不會因為B未宣告而不載入。
#### 通過OnModelCreating宣告
我們也可以通過重寫OnModelCreating宣告一個模型類,例如
``` C#
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// 指定模型
modelBuilder.Entity();
}
```
綜上所述,EFCore對模型的查詢是隻要有所提及,無論是顯示的利用DbSet或是利用導航屬性,亦或是在OnModelCreating中有所提及,都會被作為模型類進行操作。不過在這裡,除非是你非常不需要顯示宣告,否則,建議使用DbSet進行顯示宣告,避免對開發帶來不必要的麻煩。
不過有時候我們不希望我們模型和資料庫對應,因為有些模型只是為了輔助存在或作為一個子屬性存在,在資料庫中並不存在這個表,那麼我們可以使用 *[NotMapped]* 標記,這樣該類就不會被EFCore所識別為模型。
#### 示例程式碼
前文中我們提到了一個學生選課系統的概念模型,這裡我們進行一個原始的建庫操作。這裡會交叉使用FluentApi和Attribute的方式進行書寫以方便讀者理解。
首先我們建立簡單的領域模型
```C#
public enum Gender
{
Male,
Female
}
[Table("Test_Student")]
public class Student
{
public int Id { get; set;}
public string Name { get; set; }
public Gender Gender { get; set; }
public string Remark { get; set; }
public string StudentId{ get; set; }
}
[Table("Test_Teacher")]
public class Teacher
{
public int Id {get;set;}
public string Name { get;set; }
public Gender Gender { get; set; }
public string Remark { get; set; }
}
public class Course
{
public int Id {get;set;}
public string Name {get;set;}
public string CourseId{ get; set; }
}
public class Context : DbContext
{
public virtual DbSet Students { get; set;}
public virtual DbSet Teachers { get; set;}
public virtual DbSet Courses { get; set;}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity().ToTable("Test_Student");
}
}
```
這樣我們就大概做好了一個簡單的資料庫框架,不過這個資料模型是不符合我們實際設計的模型的,這裡只作為講解作用,並不作為實際專案的設計。
### 屬性型別和轉換
光有表設定顯然是不夠的,我們還需要對錶中的欄位和屬性進行設定。表中的欄位,如果是非引用型別,都會被識別成資料庫中的列,引用型別在表中通常會作為導航屬性存在。按照約定,具有getter和setter的所有公共屬性都將包括在模型中對映到表中的列。接下來仍然會分為FluentApi和Attribute進行編寫。
在資料庫中,我們對一個列的常見操作通常就是操作列名、列型別、排序規則、可空等。這裡進行一個歸納和整理
#### 屬性型別示例
這裡先用資料註解Attribute的方式進行操作,限於篇幅,在此僅對一個類進行操作,讀者可根據自己需求對其他類進行編寫。
``` C#
[Table("Test_Student")]
public class Student
{
public int Id { get; set;}
//屬性不能為空
[Required]
// 列名,排序順序,列型別
[Column(name:"Name",Order =1,TypeName ="varchar(25)")]
//等效於[Column(name:"",Order =1,TypeName ="varchar(25)"),Required]
//屬性後接等號就是預設值的設定方法
public string Name { get; set; } = "abcd";
public Gender Gender { get; set; }
// 也可以通過設定maxLength屬性進行限制長度
[Column("Remark"),MaxLength(150)]
public string Remark { get; set; }
public string StudentId{ get; set; }
}
```
設定可空型別則是在型別後加?號,但是通過資料標註的方式進行設定的方法作者查閱了文件和資料並沒有發現,只有FluentApi可以設定。應該是微軟直接預設就是空,因此無需指定設定。
這裡是FluentApi的方式
``` C#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity().ToTable("Test_Student");
modelBuilder.Entity().Property(p => p.Name)
//非空
.IsRequired()
//列名
.HasColumnName("Name")
//列型別
.HasColumnType("varchar(25)")
//預設值
.HasDefaultValue("");
modelBuilder.Entity().Property(p => p.Remark)
//可空
.IsRequired()
//列名
.HasColumnName("Remark")
//可使用.IsFixedLength()設定為定長
//列最大長度
.HasMaxLength(150);
}
```
#### 值對映轉換
值轉換提供了在寫入資料庫前的一次資料處理,試想一下,假如你不希望將加密過程放在業務層,或者說你習慣用列舉去儲存一些值,但是在資料庫中實際需要使用列舉名而不是列舉值,那麼我們就需要進行值對映轉換。
值轉換隻在FluentApi中提供,我們可以在OnModelCreating中進行定義,例如:
```C#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity().Property(p => p.Gender)
.HasConversion(
p => p.ToString(),
p => (Gender)Enum.Parse(typeof(Gender), p));
}
```
在這裡,第一個lambda表示式表示你的模型出,第二個則是資料庫映射回你的模型類所需的處理。你也可以使用ValueConverter類來定義,例如:
``` C#
var converter = new ValueConverter(
p => p.ToString(),
p => (Gender)Enum.Parse(typeof(Gender), p));
```
將此物件傳入HasConversion方法亦可。
### 鍵與索引
鍵是資料庫最精華的部分了,鍵又被成為碼,是關係模型的重要概念,它是資料庫的邏輯部分,並不是資料庫的物理結構。通常情況下,鍵(非外來鍵)時不可以被重複的。
#### 主鍵和備用(候選)鍵
主鍵用於唯一標識一條資料,在我們的資料庫教程中講到,主鍵可以是一個值,也可以是組合值。
候選鍵有兩個要求:始終能夠確保在關係中能唯一標識元組;在屬性集中找不出真子集能夠滿足條件。其中第一個條件就是超鍵的標準,所以我們可以把候選鍵理解為不能再“縮小”的超鍵。
這裡我們依舊使用兩種方式進行編寫。
### 索引
索引可以理解為書籍的目錄,這將會是資料庫的查詢更加快速,預設情況之下,索引並不唯一。索引不能使用資料批註建立索引,可以使用FluentApi按如下方式為配置指定索引:
``` C#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity()
.HasIndex(p=>p.StudentId)
}
```
當然,如果有需要,你也可以對多個列進行索引定義,並且可以指定索引的名稱,同時可以將索引修改成唯一值:
``` C#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity()
.HasIndex(p=>new {p.Name,p.StudentId})
.HasName("Test")
.IsUnique();
}
```
索引也有一些較為高階的用法,例如索引過濾器以及索引包含列。
過濾器可以對索引進行篩選,只針對部分資料建立索引,從而減少對硬碟空間的需求,如下所示:
```C#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity()
.HasIndex(p=>p.StudentId)
.HasFilter("[StudentId] IS NOT NULL");
}
```
包含列是某些關係資料庫允許配置一組列,這些列包含在索引中,但不是其的一部分。 當查詢中的所有列都作為鍵列或非鍵列包含在索引中時,這可以顯著提高查詢效能,因為表本身無需訪問。例如:
```C#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity()
.HasIndex(p=>p.StudentId)
.IncludeProperties(p=>new {p.Name});
}
```
對於上面的內容,你使用StudentId的時候可以便捷的使用整個索引,但是當你僅需要訪問Name時,便不會載入索引,這樣對整體的效能有顯著的提高。
### 從屬實體(值實體)與無鍵實體
#### 從屬實體
從屬實體就是一個屬性集合,但是它並不作為一個完整的實體存在,它是所有者的一部分。例如你的家庭住址資訊,它並不適合在你的個人資訊表中存在,如果將其放置在你的個人資訊表中,會使得你的表過於冗餘。如果沒有實體,從屬實體是毫無意義的。並且通常而言從屬實體並不唯一且從屬實體的狀態並不一定實時隨實體改變,舉個例子,點外賣的時候,你的個人資訊和你的收貨地址的關係就是一個實體和從屬實體的關係,你下單時的地址和你修改了收獲地址時沒有關係的,並且對於地址而言,倘若脫離了個人資訊時毫無意義的。
使用從屬實體可以用資料標籤(EF Core>2.1)和FluentApi的方式,這裡引用官方文件的例子:
```C#
[Owned]
public class StreetAddress
{
public string Street { get; set; }
public string City { get; set; }
}
public class Order
{
public int Id { get; set; }
public StreetAddress ShippingAddress { get; set; }
}
//或者
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity().OwnsOne(p => p.ShippingAddress);
//倘若ShippingAddress是私有屬性,則可以使用以下
modelBuilder.Entity().OwnsOne(typeof(StreetAddress), "ShippingAddress");
}
```
#### 無鍵實體
無鍵實體就很簡單了,就是沒有主鍵的實體,但是和從屬實體是有區別的,假如有一個表是記錄各個班級人數的表,那麼它其實就不是那麼需要主鍵。定義如下:
```C#
[Keyless]
public class ClassCount
{
public string Name { get; set; }
public int Count { get; set; }
}
// 或
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity()
.HasNoKey();
}
```
無鍵實體也不一定對應一張實體表,也有可能是一個檢視等這種沒有主鍵的資料。它們不同於常規實體型別,因為它們:
- 不能定義鍵
- 永遠不會對DbContext中的更改進行跟蹤,因此不會在資料庫中插入、更新或刪除這些更改,你需要手動呼叫對應方法進行操作。
- 絕不會按約定發現。
- 僅支援導航對映功能的子集,具體如下:
- 它們永遠不能充當關係的主體端
- 它們可能沒有到擁有的實體的導航
- 它們只能包含指向常規實體的引用導航屬性
- 實體不能包含無鍵實體型別的導航屬性
### 隱藏(Shadow)屬性與自動生成屬性
#### 隱藏(Shadow)屬性
隱藏屬性就是在資料庫生成時,你在類中未定義卻在表中出現的一些屬性。例如你定義了外來鍵導航屬性卻並未定義外來鍵屬性,那麼EF會生成一個自動的屬性去做外來鍵關係。同時還有主鍵,EF預設一切實體都有主鍵,即使你不定義主鍵,EFCore也會生成一個自增的Id作為表的主鍵。
有時候隱藏屬性也可以是你未對映到類中的列,或者你也可以用以下方法設定隱藏屬性:
```C#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity()
.Property("LastUpdated");
}
```
訪問隱藏屬性則通過EF的拓展方法進行訪問,如下:
```C#
context.Entry(student).Property("LastUpdated").CurrentValue = DateTime.Now;
//或在Linq
var blogs = context.Student
.OrderBy(b => EF.Property(b, "LastUpdated"));
```
#### 自動生成屬性
自動生成屬性就是分為兩種,在新增資料時自動生成值或在資料是更新值。
在新增、更新時生成值可以使用資料註釋和FluentApi定義,具體如下:
```C#
public class Student
{
//自增
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
//[DatabaseGenerated(DatabaseGeneratedOption.Computed)] 在更新時生成
public int Id { get; set;}
}
// 或
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity()
.Property(b => b.Id)
.ValueGeneratedOnAdd();
}
```
我們還可以設定計算列,有些列需要在更新或新增時通過表中的資料進行計算,那麼我們可以使用fluentApi進行實現:
``` C#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity()
.Property(p => p.DisplayName)
.HasComputedColumnSql("[LastName] + ', ' + [FirstName]");
}
```
我們也可以配置一些預設值,直接對類中屬性進行操作也沒有問題,但是也可以使用FluentApi的方法:
```C#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity()
.Property(b => b.EditTime)
.HasDefaultValueSql("getdate()");
}
```
### 關係型資料
關係型資料庫是目前資料庫的主流,同時,在絕大多數資料庫的設計上,都會涉及到表之間的關係。
#### 一對一
一對一關係就是A表中的一條資料和B表中的一條資料是一一對應關係,例如一個人的身份證和他的社保就是一種一一對應給的關係。在EF中進行配置的時候,我們需要在A,B表中定義相關的導航屬性,並且設定好相關的外來鍵,在配置的時候需要設定好外來鍵所在的類,例如:
```C#
public class A
{
public int BId { get; set; }
public B B{ get; set; }
}
public class B
{
public A A{ get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity()
.HasOne(p => p.B)
.WithOne(p => p.A)
.HasForeignKey(p=>p.BId};
}
```
如果你不指定外來鍵,EFCore會預設生成一條外來鍵的隱藏屬性。
#### 一對多
一對多關係就類似於學生和班級,一個學生只有一個班級,但是一個班級有許多學生,配置方法和一對一類似,但是值得注意的是,外來鍵始終在“一”的那個表中。
```C#
public class A
{
public int BId { get; set; }
public B B{ get; set; }
}
public class B
{
//也可以使用IList,IEnumerable作為多的關係
public ICollection As{ get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity()
.HasOne(p => p.B)
.WithMany(p => p.A)
.HasForeignKey(p=>p.BId};
}
```
#### 多對多
多對多的關係就不展開贅述,簡要提一句,多對多關係是無法通過EF配置的,我們需要引入一箇中間表,將關係轉化成和中間表的一對多關係就可以曲線救國實現多對多關係。
## 資料庫遷移
資料庫的遷移就是將你的類資料遷移至資料庫轉換為表資料,這裡你需要安裝dotnet-ef或Nuget包"Microsoft.EntityframeworkCore.Tools、Microsoft.EntityframeworkCore.Design",並且配置好DbContext。使用包管理控制檯或普通控制檯。
Nuget包管理控制檯:
```pwsh
Add-Migrations [name]
Update-Database
```
普通控制檯使用dotnet-ef
``` bash
dotnet ef migration add [name]
dotnet ef database update
```
如果你的類配置完全正確,那麼你所連線的資料庫將會自動的生成你的表資料。
## 分散配置關係型資料庫
有時候我們的表的配置會非常多,並且資料庫所需要配置的表又非常多的時候,我們最好使用一個單獨的類進行配置各個表,使得程式碼可讀性、維護性更高,我們需要建立一個類去繼承EF的配置介面,例如:
```C#
public class TestMap : IEntityTypeConfiguration
{
public void Configure(EntityTypeBuilder builder)
{
}
}
```
這樣你就可以在不同的類中進行資料庫的配置了。
更多內容請關注我的BiliBili地址以及我的部落格。
> [Github](https://github.com/StevenEco/.NetCoreGuide)
>
> [BiliBili主頁](https://space.bilibili.com/33311288)
>
> [WarrenRyan's Blog](https://blog.tity.xyz)
>
> [部落格園](https://cnblogs.com/warrenryan)