【EF Core】帶主鍵實體與無主鍵實體
上一次老周已介紹了 EF Core 框架自動發現實體和實體成員的原理。涉及到對源碼的分析,可能大伙伴們都看得氣壓升高了。故這一次老周不帶各位去分析源碼了,咱們聊一聊熟悉又陌生的關鍵詞——主鍵。說它熟悉,是因為只要咱們創建數據表,99%會用到;說它陌生,是指在 EF Core 中與主鍵相關的細節。
Primary Key,翻譯為“主鍵”(這個翻譯老周沒意見,但 Thread 翻譯成“線程”感覺莫名其妙)。按其命名,即是一張表中主要的鍵,用于表明某行記錄在表中是唯一的。有大伙伴會說,那 Unique 約束也可以啊。是的,但還要有一個條件,就是不能為空值,所以,可以說主鍵是 UNIQUE 和 NOT NULL 的結合。
數據表的主鍵可以是一列,也可以是多列。
好了,概念說完了,咱們說回 EF。按照預置的約定(老周上一文中介紹),將屬性發現為主鍵的原則有:
1、屬性名為 Id;
2、屬性名為實體類名 + Id,如 ProductId、OrderId 等。
最常用的類型是 int,自動增長。也可以用 GUID,GUID 屬性的類型可以定義為 Guid,也可以是 string。老周,有例子嗎?有,咱們玩幾個,咱們使用 Sqlite 數據庫來演示。
1、創建一個控制臺應用。
dotnet new console -n Demo -o .
有伙伴會問:這個用 Copilot 能不能執行?可以,比如這樣:

它生成的命令少了 -o . ,你可以手動補上。

如果你不想它自動執行命令,那不要點“繼續”,復制命令文本后,點“取消”就好。若繼續,它會直接執行命令。
盡管可以這樣用,但這樣做特愚蠢!你直接打個命令都比這個快了。寫實體類的時候,如果你不想重復敲 get 和 set,倒可以用它輔助。當然,VS其實也會提示的,你按個 Tab 就會生成了。這個東西雖然好用,但有時候也挺煩的,按個 Tab 就出一堆東西(如果不想禁掉它,可以按 Esc 鍵取消提示)。如果你真不想用它,可以到設置里面找到【文本編輯器】-【建議】,去掉 Inline Suggest: Enabled 的選項即可。

2、定義實體類。這次咱們用一個 Pet 類,表示你家的寵物。
public class Pet { public int id { get; set; } public string Name { get; set; } public int Age { get; set; } public string Cate { get; set; } }
這個你倒可以用輔助工具寫。注意這里老周故意把標識屬性改為小寫,即 id,而不是 Id。待會咱們看看 EF 能不能識別。
3、為項目添加包。
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
4、寫數據上下文類。
public class MyDbContext : DbContext { public MyDbContext(DbContextOptions<MyDbContext> options) : base(options) { } public DbSet<Pet> Pets { get; set; } }
5、這一次咱們的連接字符串不在 MyDbContext 內部配置,而是外部構建 Options 來配置。
// 創建選項類實例 DbContextOptions<MyDbContext> options = new DbContextOptionsBuilder<MyDbContext>() .UseSqlite("Data Source=mydb.db") .Options; // 實例化 DbContext using var dc = new MyDbContext(options);
6、這個例子中,咱們不創建數據庫,只是驗證一下,全小寫的 id 屬性是否能被識別為主鍵。
/* 此處我們不創建數據庫,只是看看它能不能識別出主鍵 */ // 獲取實體列表 foreach (var ent in dc.Model.GetEntityTypes()) { Console.Write($"表名: {ent.GetTableName()}"); // 查找主鍵 var rmykey = ent.FindPrimaryKey(); if (rmykey != null) { Console.WriteLine($",主鍵: {string.Join(", ", rmykey.Properties.Select(p => p.Name))}"); } // 實體中的列(屬性) foreach (var property in ent.GetProperties()) { Console.WriteLine($"\t列名: {property.Name} 類型: {property.ClrType.Name}"); } }
dc.Model.GetEntityTypes 方法能夠返回模型中所有實體的信息。GetTableName 返回實體對應的數據表名,FindPrimaryKey 方法找出此實體類的主鍵。最后,GetProperties 方法獲取實體類屬性對應的列。
上述代碼運行后得到的結果如下:
表名: Pets,主鍵: id
列名: id 類型: Int32
列名: Age 類型: Int32
列名: Cate 類型: String
列名: Name 類型: String
好,看來,小寫的 id 屬性是可以被識別為主鍵的(老周不再分析 EF Core 源代碼了,不然這博文就沒人看了,其實是通過約定實現的)。同理,我們還可以驗證一下,全小寫的 petid 能不能識別。把 Pet 類改為:
public class Pet { public int petid { get; set; } …… }
至少咱們知道,PetId 是肯定能被識別為主鍵的,現在驗證一下全小寫的<類名>id的屬性。再次運行程序,得到:
表名: Pets,主鍵: petid
列名: petid 類型: Int32
列名: Age 類型: Int32
列名: Cate 類型: String
列名: Name 類型: String
這個示例證明:Id 和 <類名>Id 都能被約定識別為主鍵,并且不區分大小寫。
那么,如果屬性的名稱不是 <類名>Id 呢,比如這樣改:
public class Pet { public int BugId { get; set; } public string Name { get; set; } = string.Empty; public int Age { get; set; } public string? Cate { get; set; } }
再次運行一下,結果不出所料。

這段鳥語說了啥?它說 Pet 這廝必須定義主鍵,如果你不想要主鍵,那得明確地把實體配置為無主鍵。怎么配置為無主鍵咱們后文再說,現在先說說“預制菜”約定無法自動識別出主鍵,咱們如何手動配置。
1、簡單做法,用特性在 BugId 屬性上批注一下。
[PrimaryKey(nameof(BugId))] public class Pet { …… }
這種方法最簡單,但老周個人不推薦,因為不集中配置,不好管理。當然,只是老周不推薦,沒說不可以用啊。
2、通過 ModelBuilder 來配置,這個在 DbContext 的派生類中重寫 OnModelCreating 方法。
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Pet>().HasKey(x => x.BugId); }
老周比較推薦這種方法,因為它把所有實體的配置全集中一處,將來有改動也好搞,也不容易忘這個忘那個的。兩種方法任選其一,不需要同時用。
再次運行程序,看到想要的結果了。
表名: Pets,主鍵: BugId
列名: BugId 類型: Int32
列名: Age 類型: Int32
列名: Cate 類型: String
列名: Name 類型: String
有時候你可能會想:我的代碼中并不需要訪問主鍵,主鍵僅留給 EF 自己用于生成 SQL 語句,那我能不能把影子屬性作為主鍵呢?答案是 Yes 的。先簡單說說影子屬性(Shadow Property)是什么,一句話斯基:你的實體類中未定義的,但模型中定義了的屬性。
同理,你的 DbContext 子類需要重寫 OnModelCreating 方法。
protected override void OnModelCreating(ModelBuilder modelBuilder) { // 這一行很重要 modelBuilder.Entity<Pet>().Property(typeof(int), "HideId"); // 設置主鍵 modelBuilder.Entity<Pet>() .HasKey("HideId"); }
Pet 類可以去掉作為主鍵的屬性。
public class Pet { public string Name { get; set; } = string.Empty; public int Age { get; set; } public string? Cate { get; set; } }
由于影子屬性在實體類未定義,EF Core 并不能確定其類型能不能成為主鍵。因此,在定義主鍵前應該讓 EF 知道作為主鍵的影子屬性是支持的類型,如 int。
modelBuilder.Entity<Pet>().Property(typeof(int), "HideId");
上面的例子就是把影子屬性 HideId 作為 Pet 實體的主鍵。運行結果如下:
表名: Pets,主鍵: HideId
列名: HideId 類型: Int32
列名: Age 類型: Int32
列名: Cate 類型: String
列名: Name 類型: String
主鍵也可以由多個屬性(列)組成。比如,咱們讓 HideId 和 Name 組成主鍵。
protected override void OnModelCreating(ModelBuilder modelBuilder) { // 這一行很重要 modelBuilder.Entity<Pet>().Property(typeof(int), "HideId"); // 設置主鍵 modelBuilder.Entity<Pet>() .HasKey("HideId", nameof(Pet.Name)); }
得到的運行結果如下:
表名: Pets,主鍵: HideId, Name
列名: HideId 類型: Int32
列名: Name 類型: String
列名: Age 類型: Int32
列名: Cate 類型: String
下面咱們演示一下把 string 類型的屬性映射到 SQL Server 數據表的 unique identifier 列。
1、用以下 SQL 腳本(使用的是 SQL Server)創建數據庫和數據表。
-- 創建數據庫 CREATE DATABASE Test; GO -- 切換到剛剛創建的數據庫 USE Test; GO -- 創建表 CREATE TABLE Productions ( -- 這個是主鍵,插入時如果未提供值,則用 NEWID() 產生的值 Pid UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(), -- 產品名稱 ProdName NVARCHAR(40) NOT NULL, -- 生產年份 Year INT, -- 產品尺寸 Size DECIMAL(6,2), -- 產品顏色 Color NVARCHAR(10), -- 備注 Remark NVARCHAR(MAX) );
2、創建控制臺 .NET 項目(此處省略250個字)。
3、定義實體類(這個可以用 dotnet ef dbcontext 命令生成,不過老周一向習慣純手寫,生成的實體類有時候要回頭修改)。
public class Production { public string Pid { get; set; } = null!; public string ProdName { get; set; } = string.Empty; public int? Year { get; set; } public decimal? Size { get; set; } public string? Color { get; set; } public string? Remark { get; set; } }
Pid 屬性要作為主鍵用的,注意這里老周故意讓其默認值為 null,這樣在 EF 上下文添加實體時使用數據庫生成的值(否則會報錯)。null 后面有個感嘆號(!)這個可以避免編譯器的 Nullable 警告,具體情況你可以找微軟官方文檔,有詳細說明。就是微軟文檔寫得太好了,導致很多基礎知識老周都不必重復介紹了。
4、派生 DbContext 的子類。
public class MyContext : DbContext { public DbSet<Production> Productions { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // 配置連接字符串 optionsBuilder.UseSqlServer(@"Server=(localdb)\MSSQLLocalDB;Database=Test;Trusted_Connection=True"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Production>(entbd => { entbd.Property(p => p.Pid) .ValueGeneratedOnAdd(); // 產品名稱為必須,且有長度限制 entbd.Property(p => p.ProdName) .IsRequired() .HasMaxLength(40); // 產品尺寸的精度要求 entbd.Property(p => p.Size) .HasPrecision(6, 2); // 產品顏色的長度限制 entbd.Property(p => p.Color) .HasMaxLength(10); // 主鍵 entbd.HasKey(p => p.Pid); }); } }
ModelBuilder 的 Entity 方法可以獲得一個 EntityTypeBuilder 對象(上面老周是調用了帶 Action 委托的重載,方便多次調用 EntityTypeBuilder 實例的成員)。EntityTypeBuilder 類內部封裝了 InternalEntityTypeBuilder 對象,各種配置方法實際調用了此 InternalEntityTypeBuilder 對象的成員。
5、在 Program.cs 文件中,寫一下測試代碼。咱們向數據庫存入兩條記錄。
// 實例化上下文 MyContext dc = new MyContext(); // 新建兩條記錄 Production p1 = new() { ProdName = "五角褲", Year = 2025, Size = 67.33m, Color = "白色", Remark = "純化學合成纖維,無自然成份" }; Production p2 = new() { ProdName = "無領襯衫", Year = 2025, Size = 47.00m, Color = "黃色", Remark = "冰絲,炎炎夏日,如同把冰塊披在身上" }; dc.Productions.AddRange(p1, p2); // 保存到數據庫 dc.SaveChanges(); dc.Dispose();
如果代碼順利運行,則數據庫中就有兩條新記錄了。
select * from dbo.Productions

當然了,對于 UNIQUE IDENTIFIER 類型的主鍵,.NET CLR 實體類的屬性除了可以用字符串類型,也可以用 Guid 類型。原理也是一樣的,這里老周就不演示了,相信大伙伴們都會的。
===========================================================================================================
接下來看看無主鍵的實體。這個其實沒什么特別的知識要掌握的,但你得記住一條:無主鍵的實體只能 SELECT,不能用于 INSERT、UPDATE、DELETE 操作。一句話斯基總結就是:只能查詢不能更新。
咱們還是整個例子吧。
1、用以下SQL腳本創建數據庫和數據表。
create database DemoSome; GO use DemoSome; GO -- 創建表 create table HandsomeBoys ( BoyID int IDENTITY, [Name] NVARCHAR(25) not null, Age int, City NVARCHAR(10), PhoneNo NVARCHAR(11), Email NVARCHAR(40), CONSTRAINT [PK_HandsomeBoys] PRIMARY KEY CLUSTERED (BoyID ASC) ); GO
2、向數據表 INSERT 幾條數據用于測試,隨便寫,略。
3、創建.NET控制臺應用程序,略。
4、定義實體類(可以用 dotnet ef 工具生成,也可以純手打)。
public class HandsomBoy { public int ID { get; set; } public string Name { get; set; } = string.Empty; public string? Email { get; set; } public string? City { get; set; } public int? Age { get; set; } public string? PhoneNo { get; set; } }
5、寫數據庫上下文類,構建模型。
public class DemoDB : DbContext { public DemoDB(DbContextOptions<DemoDB> options) :base(options) { } public DbSet<HandsomBoy> HandsomeBoys { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { // 無主鍵 modelBuilder.Entity<HandsomBoy>().HasNoKey(); // 表映射 var entbd = modelBuilder.Entity<HandsomBoy>().ToTable("HandsomeBoys"); // 屬性映射 entbd.Property(p => p.ID).HasColumnName("BoyID"); entbd.Property(p => p.Name) .IsRequired() .HasMaxLength(25); entbd.Property(p => p.City).HasMaxLength(10); entbd.Property(p => p.Email).HasMaxLength(40); entbd.Property(p => p.PhoneNo).HasMaxLength(11); } }
注意要調用 HasNoKey 方法配置實體為無主鍵,不然會報錯。
6、測試。
// 配置選項 DbContextOptions<DemoDB> options = new DbContextOptionsBuilder<DemoDB>() .UseSqlServer("Server=(localdb)\\MSSQLLocalDB;Database=DemoSome;Trusted_Connection=True") // 記錄日志 .LogTo(msg => Console.WriteLine(msg)) .EnableSensitiveDataLogging() .Options; using var dc = new DemoDB(options); // 查詢數據 var q = from b in dc.HandsomeBoys select b; foreach (var x in q) { Console.WriteLine($"ID={x.ID}, Name={x.Name}, Age={x.Age}, City={x.City}, Phone={x.PhoneNo}"); }
結果如下:
ID=1, Name=小陳, Age=35, City=珠海, Phone=15562021200 ID=2, Name=老周, Age=105, City=東莞, Phone=13888582588 ID=3, Name=老丁, Age=45, City=中山, Phone=15840991234
無主鍵實體也可以用特性批注。
[Keyless] public class HandsomBoy { …… }
兩種方法,二選一。
好了,今天就聊到這兒了。

浙公網安備 33010602011771號