惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The Last Watchdog
The Last Watchdog
博客园_首页
Martin Fowler
Martin Fowler
S
SegmentFault 最新的问题
美团技术团队
小众软件
小众软件
V
V2EX
博客园 - Franky
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
The GitHub Blog
The GitHub Blog
Microsoft Security Blog
Microsoft Security Blog
Attack and Defense Labs
Attack and Defense Labs
S
Security Affairs
Simon Willison's Weblog
Simon Willison's Weblog
I
Intezer
T
The Exploit Database - CXSecurity.com
有赞技术团队
有赞技术团队
S
Schneier on Security
人人都是产品经理
人人都是产品经理
Security Archives - TechRepublic
Security Archives - TechRepublic
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
K
Kaspersky official blog
PCI Perspectives
PCI Perspectives
AI
AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
罗磊的独立博客
O
OpenAI News
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
The Register - Security
The Register - Security
V
Vulnerabilities – Threatpost
GbyAI
GbyAI
博客园 - 【当耐特】
C
Cisco Blogs
大猫的无限游戏
大猫的无限游戏
Help Net Security
Help Net Security
Google DeepMind News
Google DeepMind News
S
Securelist
Application and Cybersecurity Blog
Application and Cybersecurity Blog
P
Proofpoint News Feed
博客园 - 三生石上(FineUI控件)
雷峰网
雷峰网
L
LangChain Blog
SecWiki News
SecWiki News
博客园 - 叶小钗
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V2EX - 技术
V2EX - 技术
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
J
Java Code Geeks
L
LINUX DO - 热门话题
Cisco Talos Blog
Cisco Talos Blog

博客园 - 东邪独孤

【ASP.NET Core】在 Windows Forms 中运行 Web 服务器 【EF Core】级联删除行为 【EF Core】继承策略——TPC 【EF Core】继承策略——TPT 【EF Core】继承策略——TPH 【EF Core】使用自定义的值比较器 【EF Core】值转换器 【EF Core】直接更新数据 【EF Core】实体追踪——Entry中记录的数据 【EF Core】实体状态与变更追踪 【EF Core】将一个实体映射到多个表的正确方法 【EF Core】“Code First”方案下以编程方式生成迁移 【EF Core】“DB First”方案下用编程方式生成数据库模型代码 【EF Core】三种方法记录生成的 SQL 语句 【EF Core】未定义实体类的数据库模型 【EF Core】“多对多”关系与跳跃导航 【EF Core】FromExpression 方法有什么用? 【EF Core】通过 DbContext 选项扩展框架 【EF Core】框架底层的数据库连接管理 【EF Core】再谈普通实体关系与 Owned 关系的区别 【EF Core】实体类的依赖注入 【EF Core】优化后的模型 【EF Core】使用外部 Model 【EF Core】聊聊“复合”属性 【EF Core】为 DatabaseFacade 扩展“创建”与“删除”数据表功能 【EF Core】框架是如何识别实体类的属性和主键的
【EF Core】带主键实体与无主键实体
东邪独孤 · 2025-07-20 · via 博客园 - 东邪独孤

上一次老周已介绍了 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
{
    ……
}

两种方法,二选一。

好了,今天就聊到这儿了。