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

推荐订阅源

大猫的无限游戏
大猫的无限游戏
H
Hacker News: Front Page
T
The Blog of Author Tim Ferriss
WordPress大学
WordPress大学
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Blog — PlanetScale
Blog — PlanetScale
Stack Overflow Blog
Stack Overflow Blog
F
Fortinet All Blogs
H
Help Net Security
罗磊的独立博客
D
DataBreaches.Net
MyScale Blog
MyScale Blog
美团技术团队
人人都是产品经理
人人都是产品经理
L
LangChain Blog
M
MIT News - Artificial intelligence
C
Check Point Blog
GbyAI
GbyAI
B
Blog RSS Feed
Microsoft Azure Blog
Microsoft Azure Blog
Y
Y Combinator Blog
雷峰网
雷峰网
Last Week in AI
Last Week in AI
F
Full Disclosure
量子位
V
Visual Studio Blog
Google DeepMind News
Google DeepMind News
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
SegmentFault 最新的问题
云风的 BLOG
云风的 BLOG
H
Hackread – Cybersecurity News, Data Breaches, AI and More
P
Proofpoint News Feed
爱范儿
爱范儿
A
About on SuperTechFans
MongoDB | Blog
MongoDB | Blog
腾讯CDC
博客园 - 【当耐特】
U
Unit 42
Martin Fowler
Martin Fowler
NISL@THU
NISL@THU
B
Blog
T
The Exploit Database - CXSecurity.com
Apple Machine Learning Research
Apple Machine Learning Research
L
Lohrmann on Cybersecurity
P
Proofpoint News Feed
有赞技术团队
有赞技术团队
C
CERT Recently Published Vulnerability Notes
The GitHub Blog
The GitHub Blog
T
Threatpost

博客园_首页

Linux实操--组管理、权限管理和定时任务 Java + EasyExcel 实现单个接口导出多个Excel Mem0 源码解析系列(二):提示词工程的深度剖析 Openclaw TaskFlow究竟是什么?和普通Skill技能有什么区别 博文阅读密码验证 - 博客园 嘉立创开源:应该是全网MicroPython教程最多的开发板 Hermes Agent 集成实践:从协议到生产 2026年AI编程工具横评:Cursor、Codex、Claude Code、Zed、Windsurf Java程序员必看的RAG入门教程 2026 AI效率神器:Superpowers + Claude Code 保姆级教程 本地大模型部署全攻略:从 0 到 1 玩转 Ollama 【从0到1构建一个ClaudeAgent】内存管理-上下文压缩 .NET 高级开发 | 设计、实现一个事件总线框架 电子小白入门之NE555 3. WorkBuddy:隐藏玩法,一键召唤专家,让 AI 以"专家身份"给你干活 和AI一起搞事情#3:Claude Teammate 游戏开发翻车实录 【OpenClaw】通过 Nanobot 源码学习架构---(7)Memory C# .NET 周刊|2026年3月3期 我在 Debian 11 上把 K8s 单机搭起来了,过程没你想的那么顺(/opt 目录版) 深度学习进阶(七)Data-efficient Image Transformer CLI+Skill搭建浏览器AI自动化框架,告别一切重复枯燥任务 告别Token账单无底洞:OpenClaw本地部署,重塑企业数据主权的唯一解 FastAPI+Vue:文件分片上传+秒传+断点续传,这坑我帮你踩平了! SBTI 爆火后,我做了个程序员版的 CBTI。。已开源 + 附开发过程 多模态检索开始进入工程期:用 Sentence Transformers 搭建可落地的 Multimodal RAG 100多行代码实现一个最简单的Agent(用ReAct) Claude Code 通关手册(八):推荐 5 个 Hooks,代码质量提升 3 倍 老板:“有人截图了!”。安全部门:“收到,马上查暗水印!” - why技术 技术之外,皆是人间 C#/.NET/.NET Core技术前沿周刊 | 第 69 期(2026年4.01-4.12) Snack JSONPath 项目架构分析 Claude Code Buddy 小析:一个非核心功能,如何体现产品的细节完成度 AI新时代下的图床管理方案-Cloudflare图床+MCP+Skills方案指南 化繁为简:顺丰速运App如何通过 HarmonyOS SDK实现专业级空间测量 从零实现富文本编辑器#13-React非编辑节点的内容渲染 AI开发-python-langchain框架(3-23-OpenAI Functions风格Tool Calling智能助手) .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut PbootCMS 网站内容数量多导致访问慢?这些实用优化方案帮你提速! - 家兴网络技术工作室 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 网站漏洞怎么发现并修复?一篇实用指南(附完整流程) - 家兴网络技术工作室 开了 TUN 模式还是直连?90% 的人都踩过这个坑 Github日报|2026年04月12日 - AI一族 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术 Etsy 把 1000 个 MySQL 分片迁进 Vitess:425TB 数据背后的真正问题不是性能,而是运维规模 MicroPython LVGL基础知识和概念:底层渲染与性能优化 - FreakStudio 数据库草图算法 Python 潮流周刊#146:CPython 引入 Rust 的进展 - 豌豆花下猫 最小生成树 - mofei1116 红日靶场七:从外网入口、容器逃逸到 AD 接管的完整利用链复盘 - YouDiscovered1t 分享四款开源且实用的 Kafka 管理工具 - 追逐时光者 vLLM 权重加载机制全解析:从挑战到理想架构 LCT 学习笔记 - ACehomoxue Avalonia UI 12.0.0 正式发布:架构演进和性能飞跃 - 张善友 当 AI Agent 把调用链拉长,延迟开始成为一门生意 conhost.exe 无法显示 U+2717 - 145a 太秀了,我把自己蒸馏成了 Skill!已开源 - 程序员鱼皮 ASP.NET Core 内存缓存实战:一篇搞懂该怎么配、怎么避坑 基于 Ghostty 带有分割标签页和为 Claude 编程设计的通知终端 - BugShare AI 焊死入口:教育的“操作系统级”重塑 - 郝hai 初级Java开发工程师使用sql脚本编写代码的过程是简单而且不糊涂 - CoderOilStation Claude Code通关手册(六):MCP协议完全指南 - 暮色之狐 边框灯光环绕动画特效实现指南 - Newbe36524 开源:子木蒸馏版的 SEO 审计工具 seo-audit-skill v1.0 我所理解的Python元模型 【从0到1构建一个ClaudeAgent】规划与协调-TodoWrite - 程序员Seven Claude 和 Codex 在审计 Skill 上性能差异探究 - ACai_sec AScript如何实现中文脚本引擎 - rockey627 【渗透测试】HTB Season10 Garfield 全过程wp - dynasty_chenzi Android 开发者为什么必须掌握 AI 能力?端侧视角下的技术变革 树状数组正确性证明 - AC-wyr 你的 AI 焦虑,可能比 AI 本身更危险——ATM 机没有消灭银行柜员,但恐慌消灭了你的判断力 - 我没有三颗心脏 一个拉胯的分库分表方案有多绝望?整个部门都在救火! - 冰河团队 动态规划入门必学之走方格问题 - Ofnoname PostgREST 与 PostgreSQL 角色权限配置全解析(生产级实践) - SheepDog1998 使用 UEFI 图形输出协议 GOP 在屏幕上显示图像的方法 - 阿源- Claude Code通关手册(五):组建你的AI专家团队,子代理系统 - 暮色之狐 一个程序员到架构师的催婚路之感悟(整整10年后的催婚相亲感悟) - MisterLip 用 Agent Skill 自动生成工作周报 - 赵康
【EF Core】级联删除行为
东邪独孤 · 2026-06-28 · via 博客园_首页

DeleteBehavior枚举(位于 Microsoft.EntityFrameworkCore 命名空间)所定义的数据删除行为是争对存在相对关系的实体来说的。这个和数据库中表与表之间的关系一致。数据表之间是通过列引用实现的。假设 A 依赖 B,那么 A 中会存在一列或多列去引用 B 中的一列或多列(通常是主键)。在 A 中引用外部记录的列就成了外键。而删除行为描述的就是:当 B 被删除后,A 中的外键如何处理。

上面只是简单提一下数据库方面的知识,后面咱们会有例子。现在,咱们先看看 DeleteBehavior 枚举定义了啥。

public enum DeleteBehavior
{
    ClientNoAction,
    ClientSetNull,
    ClientCascade,
/*----------- 强力分割线 --------*/    
    Restrict,
    SetNull,
    Cascade,
    NoAction
}

如你所见,DeleteBehavior 枚举的成员可以分为两组。第一组是带 Client 开头的,就是客户端的意思。那客户端是谁?谁连接并操作数据库?是 EF Core,所以,它是客户端。因此,这一组的意思就是由 EF Core 来处理关系实体的级联删除行为。什么情况下会这样呢?先有数据库,然后根据数据库生成(编写)实体类,但数据库并未定义任何关系,由 EF Core 来管理关系。说大白话就是,数据库只建表和存储数据,并不定义其他对象。相信很多伙伴比较喜欢这种方案,老周可喜欢这样做了,数据库的作用只存数据,老周连存储过程都不写的。哪怕 SQL Server 有很多强大功能,老周只拿来建表,连视图都很少用。这样搞的好处是好维护,除非你那里有专人维护数据库,不然像老周这样,多数项目都是自己一手包办的,把很多数据库的处理逻辑搬到程序代码中,省了很多事。正因为经常这样,导致老周在写 SQL 方面比较菜。

现在有些 SQL 查询可以用 AI 生成,但你也别指望它是万能的,只是在赶时间的时候能省些功夫。一般查询的话 AI 是没啥问题,生成的语句也基本对的。除了这个,目前 AI 比较好用的就是在 CSS 上,其他(如 C++ 之属)除非是一些很“流行”或标准算法的代码,不然错误很多。有些代码甚至是全错的(说明没有训练过),毕竟模型再大也不可能应对实际应用。所以这玩意儿用来省省时间是可以的,全信它还不如信我是汉武帝。

曾有某新同事,就是懒得写一些初始化代码,就用 AI 帮他完成的初始化(C语言)。然后他很苦恼,因为他检查来检查去都没发现哪里错了,他经过多次生成代码问题依旧。烧录进单片机后无限 Reset。害得他装逼失败,被同事笑了两个星期(因为同事是女的,所以他要在她面前装逼,也是自作多情的小伙子)。最后他厚着脸皮来找老周。老周就帮他看了下代码,错误就在 memcpy 函数拷贝错了内存。估计这代码是从网上的多个源混合训练模型的,说不定是从谁的博文中偷的代码。不仅拷错了内存,还把别的内存破坏了。你会问:编译不报错?那肯定不报错了,语法上又没错。当然,这个你不能说是C语言的问题,C语言的强项就在这里,内存管理是直接的,而且权限都给了开发者的。好处是在性能的时候,你能做到极致。你有一把很锋利的宝刀,在高手看来那可是宝物,但是你练武的时候把自己的头砍了,那你不能怪这刀太锋利。

第二组就是不带 Client 开头的,自然是在数据库上定义外键约束。这个一般用在 Code First 方案,由 EF Core 生成 SQL 语句并创建数据库。当然,DB First 也可能的,比如你的表关系是数据库上定义的,而且也定义了外键约束。

老周写这一篇水文的目的就跟大伙伴说这个事,如何选择 DeleteBehavior 的值。

好,上面的内容比较无聊,相信大伙都看得天灵盖冒烟了。下面咱们搞些有趣的。这里以 SQL Server 数据库为例。为什么用它?因为方便,其他数据库没那么方便。所以一般老周写测试代码都会用 SQL Server。

建好数据库后,创建两个表。

CREATE TABLE [dbo].[tb_address] (
    [Id]       INT           IDENTITY (1, 1) NOT NULL,
    [city]     NVARCHAR (10) NOT NULL,
    [town]     NVARCHAR (20) NOT NULL,
    [zip_code] VARCHAR (8)   NULL
);

CREATE TABLE [dbo].[tb_stu] (
    [Id]      INT           IDENTITY (1, 1) NOT NULL,
    [name]    NVARCHAR (12) NOT NULL,
    [age]     INT           NOT NULL,
    [addr_id] INT           NULL
);

很简单的两个表,一个表示地址,一个表示学生。字段也是随便写的,你不用在意是啥含义。不过,你要注意 tb_stu 表的 addr_id 字段。你能如老周一样聪明,肯定猜到了,它就是外键。是的,该字段就是引用 tb_address 表的主键的。但在数据库上没有配置外键和其他约束,

咱们在数据表中放一些数据,以供测试。

image

image

可以看到,这两个表是一对一的关系,即一名学生只对应一个地址。比如,范统同学的 addr_id 为 3,对应 Id=3 的地址,即他来自乌龟市王八镇。

咱们可以用联表查询来一览两个表的关系。

select s.[name], a.city, a.town from tb_stu s
left join tb_address a
on s.addr_id = a.Id;

结果如下

image

好了,数据准备好了,下面完成客户端代码。关于实体映射,你嫌麻烦可以用 EF 工具来生成。因为代码不多,老周直接手写了。

先定义实体类。

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; } = default!;
    public int Age { get; set; }
    public Address? Addr { get; set; }
}

public class Address
{
    public int Id { get; set; }
    public string City { get; set; } = default!;
    public string Town { get; set; } = default!;
    public string? ZipCode { get; set; }
}

很 Easy,不用解释了啊。然后是从 DbContext 类派生出咱们自己的上下文(不派生也行,先初始化 Options 然后 new DbContext)。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Address>(ent =>
    {
        ent.ToTable("tb_address");
        ent.Property(x => x.Id).HasColumnName("Id");
        ent.Property(y => y.City).HasColumnName("city").HasMaxLength(10).IsUnicode();
        ent.Property(j => j.ZipCode).HasColumnName("zip_code").HasMaxLength(8).IsUnicode(false);
        ent.Property(x => x.Town).HasColumnName("town").HasMaxLength(20);
        ent.HasKey(x => x.Id);
    });
    modelBuilder.Entity<Student>(ent =>
    {
        ent.ToTable("tb_stu");
        ent.Property(x => x.Id).HasColumnName("Id");
        ent.Property(a => a.Name).HasColumnName("name").HasMaxLength(12);
        ent.Property(s => s.Age).HasColumnName("age");
        // 影子属性
        ent.Property<int?>("AddrId").HasColumnName("addr_id");

        ent.HasOne(s => s.Addr)
            .WithOne()
            .HasForeignKey<Student>("AddrId")
            .HasPrincipalKey<Address>(a => a.Id)
            .OnDelete(DeleteBehavior.ClientSetNull)
            .IsRequired(false);
        ent.Navigation(s => s.Addr).AutoInclude();
    });
}

Student 实体的 Addr 属性是一个导航属性,引用 Address 实体实例。导航的含议就是从 Student 实体就能访问关系实体 Address。毕竟 EF Core 的核心思想是要面向对象的,肯定不能搞个外键来访问关系实体的。

配置的代码很简单,你可以不看,重点关注 HasOne 一段。

HasOne 表示从当前(Student)实体起,到 s.Addr 所导航的另一个实体(Address)的关系。啥关系?一个 Student 对一个 Address。

同理,随后 WithOne 的意思是从反向找关系,从 Address 到 Student 也是一个 Address 对着一个 Student 实体。由于 Address 实体中没有指向 Student 的导航属性,故 WithOne 方法不用传参。

因此,总结起来,Student -> Address 是一对一关系。

HasForeignKey 方法配置外键,一对一的外键方法特殊,它可以指定一个类型参数,代表外键所在的实体。因为一对一比较特别,它既可以A引用B,也可以B引用A。所以,一对一关系必须交代明白谁引用谁,EF Core 无法自动识别。在本例中,是 Student 引用 Address,因此外键应当在 Student 实体上。但我们代码中基本不会去访问外键,所以老周把外键 AddrId 属性定义为影子属性——类中不定义的属性,只在 EF Core 的更改跟踪中维护。

调用 AutoInclude 方法的用途是让 EF Core 在查询 Student 实体时自动加载 Addr 导航属性,即引用相关的 Address 对象。这个会生成联表查询,就不用在LINQ语句中调用 Include 方法了。

现在,咱们做一件事: 把 Id 为 2 的 Address 记录删除。

Address? theaddr = c.Set<Address>().FirstOrDefault(a => a.Id == 2);
if(theaddr != null)
{
    // 删除
    c.Remove(theaddr);
}
// 检测更改
c.ChangeTracker.DetectChanges();
// 打印跟踪信息
Console.WriteLine(c.ChangeTracker.ToDebugString());
// 提交更新
c.SaveChanges();

DetectChanges 是扫描实体并检测状态,这个在 SaveChanges 方法时也会调用。此处老周是为了打印实体状更新才这样做,实际使用时,不要调用 DetectChanges 方法。

运行一下,看看发生什么。

Address 是删除了,但 Student 中对应的外键没有设置为 NULL。这表明没达到咱们预期。

这是因为咱们只查询了 tb_address 表,EF Core 根本不知道 tb_stu 表中哪些行引用了 id=2 的地址信息,所以,EF Core 就无法进行级联处理。

修改为ID 为3的地址信息,它由 id=1 的学生记录引用。咱们要先查出 id=1 的 Student 记录,再通过 Addr 导航属性来获取对应的地址信息,然后删除。

Student? student = c.Set<Student>().FirstOrDefault(s => s.Id == 1);
if(student != null && student.Addr != null)
{
    // 删除
    c.Remove(student.Addr);
}
// 检测更改
c.ChangeTracker.DetectChanges();
// 打印跟踪信息
Console.WriteLine(c.ChangeTracker.ToDebugString());
// 提交更新
c.SaveChanges();

这一次结果正确。看看打印的实体状态变更。

Address {Id: 3} Deleted
    Id: 3 PK
    City: '乌龟市'
    Town: '王八镇'
    ZipCode: '912033'
Student {Id: 1} Modified
    Id: 1 PK
    AddrId: <null> FK Modified Originally 3
    Age: 23
    Name: '范统'
  Addr: <null>

id=3 的地址信息被删除,而引用了此记录的 Student 对象,AddrId 也从原来的 3 变为 null。生成的 SQL 语句既有 DELETE 语句也有 UPDATE 语句。

UPDATE [tb_stu] SET [addr_id] = @p0
      OUTPUT 1
      WHERE [Id] = @p1;

DELETE FROM [tb_address]
      OUTPUT 1
      WHERE [Id] = @p2;

image

如果把删除行为改为 ClientCascade,那么,Address 记录删除后,引用该记录的 Student 记录也会被删除。

ent.HasOne(s => s.Addr)
    .WithOne()
    .HasForeignKey<Student>("AddrId")
    .HasPrincipalKey<Address>(a => a.Id)
    .OnDelete(DeleteBehavior.ClientCascade)
    .IsRequired(false);

运行代码会看到,除了被删的 Address 实体,引用它的 Student 实体也标记了删除。

Address {Id: 3} Deleted
    Id: 3 PK
    City: '鸡腿市'
    Town: '牛奶镇'
    ZipCode: '256213'
Student {Id: 1} Deleted
    Id: 1 PK
    AddrId: 3 FK
    Age: 23
    Name: '范统'
  Addr: {Id: 3}

生成的SQL语句自然也会包含两条 DELETE FROM 语句。

DELETE FROM [tb_stu]
      OUTPUT 1
      WHERE [Id] = @p0;

DELETE FROM [tb_address]
      OUTPUT 1
      WHERE [Id] = @p1;

剩下的 ClientNoAction 行为就是当被引用的记录删除后,引用它的外键不会被删除,也不会设置为 NULL。就是啥也不做。这个就不演示了。

DeleteBehavior 枚举的不带 Client 开头的成员表示级联删除行为会在数据库中定义。

1、Code First 方案下,创建数据库时,EF Core 会自动生成包含外键约束的 SQL 语句;

2 、DB First 方案下,数据库已定义了外键约束。删除时如果违反规范会报错。

总的来说就是数据库一定要定义外键约束且设置了级联删除行为(或保留默认值)。

下面的示例咱们采用 Code First 方案,由数据库执行级联删除。示例用的是 SQLite 数据库,要添加以下 nuget 库。

Microsoft.EntityFrameworkCore.Sqlite

定义两个实体类。

// 暂时关闭 Nullable
#nullable disable

public class User
{
    public int Uid { get; set; }
    public string Name { get; set; }
    /// <summary>
    /// 导航属性
    /// </summary>
    public IEnumerable<ShareItem> Shares { get; set; }
}

public class ShareItem
{
    public int ShareID { get; set; }
    public string Title { get; set; }
    public string FileName { get; set; }
    public string? Comment { get; set; }
}

// 恢复 Nullable
#nullable enable

User 实体代表用户信息,ShareItem 代表用户分享的文件信息。User.Shares 是导航属性,即一个用户可以分享多个文件,所以,用户与分享文件信息之间是一对多的关系。

下面是配置模型,这里老周演示一下如何不继承 DbContext 来配置。

1、先创建 ModelBuilder 实例。这里注意,咱们不用直接调用构造函数,那样约定集合是空的,就无法使用 EF Core 内置的约定功能了,很多东西就要手动配置了。所以要用 SqliteConventionSetBuilder.CreateModelBuilder 静态方法。每个数据库都会提供这个方法的,比如 SqlServerConventionSetBuilder。这些类能够创建包含内置约定的 ModelBuilder。

ModelBuilder modelBuilder = SqliteConventionSetBuilder.CreateModelBuilder();

有了 ModelBuilder 实例,剩下的事情你应该全懂了,一样的配方,熟悉的家乡味。

2、配置数据库模型——添加实体。

// 添加实体
var entUser = modelBuilder.Entity<User>();
var entShareItem = modelBuilder.Entity<ShareItem>();
// 配置主键
entUser.HasKey(u => u.Uid).HasName("PK_UserID");
entShareItem.HasKey(s => s.ShareID).HasName("PK_ShareID");
// 外键存在于 ShareItem 中,用影子属性来存储
entShareItem.Property<int?>("userId");
// 表映射
entUser.ToTable("tb_users", tb =>
{
    tb.Property(u => u.Uid).HasColumnName("u_id");
    tb.Property(u => u.Name).HasColumnName("u_name");
});
entShareItem.ToTable("tb_shares", tb =>
{
    tb.Property(s => s.ShareID).HasColumnName("sh_id");
    tb.Property(s => s.Title).HasColumnName("sh_title");
    tb.Property(s => s.FileName).HasColumnName("sh_file");
    tb.Property(s => s.Comment).HasColumnName("sh_cmt");
    // 这个是影子属性映射的
    tb.Property("userId").HasColumnName("usr_id");
});
// 一对多关系
entUser.HasMany(u => u.Shares)
        .WithOne()
        .HasForeignKey("userId")
        .HasPrincipalKey(u => u.Uid)
        .OnDelete(DeleteBehavior.Cascade)
        .HasConstraintName("FK_ShareToUser");

由于是一对多关系,所以外键只能放在 ShareItem 实体上,调用 HasForeignKey 方法不必考虑类型参数了(人家也没声明类型参数)。OnDelete设置为级联删除。也就是:如果某个 User 对象被删了,那么,与它有关的 ShareItem 对象也要一起删除(株连三族)。

3、创建 DbContextOptionsBuilder 实例,用来配置 DbContext,如数据库连接字符串等。

// 准备调料(选项)
DbContextOptionsBuilder opbuilder = new DbContextOptionsBuilder();
// 使用啥数据库
opbuilder.UseSqlite("data source=shareMan.db");
// 需要日志
opbuilder.LogTo(msg =>
{
    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine(msg);
    Console.ResetColor();
},
(eventid, _) => eventid == RelationalEventId.CommandExecuted);

LogTo 调用的重载有两个委托。第一个是输入一个 string 参数,这个参数是日志内容,可以自定义记录方式。比如写到文件中,这里老周只写到控制台。第二个委托有两个输入参数:参数A是日志关联的 EventId,参数B是日志等级(调试、严重、警告、信息),返回值是 bool 类型,这个委托是个筛选器,返回 true 表示记录它;返回 false 表示不记录该条日志。这里只判断事件 RelationalEventId.CommandExecuted,如果是它就记录,其他事件拜拜。CommandExecuted 指的是执行 SQL 之后发生。这个配置的目的就是记录 SQL 语句。

4、很重要一步,新手最容易忘记。因为咱们现在不继承 DbContext 类,就不会重写 OnModelCreating 方法了,所以,你得让 DbContext 知道咱们用的数据库模型。所以要调用 UseModel 方法关联我们前面配置好的模型。

opbuilder.UseModel(modelBuilder.Model.FinalizeModel());

调用 FinalizeModel 方法是让模型固定(只读,不能改了)。

5、实例化DbContext对象,把选项传进构造函数。其他操作不变。

6、先动态创建一下数据库,存入些测试数据。

 using (DbContext ctx = new(opbuilder.Options))
 {
     bool done = ctx.Database.EnsureCreated();
     if(done)
     {
         User u1 = new()
         {
             Name = "吴淼水",
             Shares = [
                 new ShareItem{
                     Title = "大宋神探",
                     FileName = "神碳吴四水.docx",
                     Comment = "时代楷模"
                 }
             ]
         };

         User u2 = new()
         {
             Name = "高大上",
             Shares = new List<ShareItem>{
                     new ShareItem
                     {
                         Title = "想要飞得更高",
                         FileName = "励志王.pdf",
                         Comment = "笑死人"
                     },
                     new ShareItem
                     {
                         Title = "一氧化二氢居然有毒",
                         FileName = "2026智慧档案.doc",
                         Comment = "民用化学开山之祖"
                     }
             }
         };

         // 将两实体标记为 Added 状态
         ctx.AddRange(u1, u2);
         // 保存数据
         ctx.SaveChanges();
         Console.WriteLine("\n---------- 初始化后 ----------");
         PrintUsers(ctx.Set<User>().Include(u => u.Shares).ToArray());
     }
 }

访问 DbContextOptionsBuilder 对象的 Options 属性,你就能拿到已配置的选项了。

PrintUsers 是个自定义方法,用来打印 User 与跟它有关的 ShareItem 信息。

static void PrintUsers(IEnumerable<User> usrs)
{
    foreach(User u in usrs)
    {
        Console.WriteLine($"用户:{u.Name} ({u.Uid})");
        // 遍历导航属性
        foreach(ShareItem share in u.Shares)
        {
            Console.WriteLine($"\t{share.Title} | {share.FileName}");
        }
    }
    Console.WriteLine();
}

创建数据表的 SQL 如下:

CREATE TABLE "tb_users" (
          "u_id" INTEGER NOT NULL CONSTRAINT "PK_UserID" PRIMARY KEY AUTOINCREMENT,
          "u_name" TEXT NULL
      );

CREATE TABLE "tb_shares" (
          "sh_id" INTEGER NOT NULL CONSTRAINT "PK_ShareID" PRIMARY KEY AUTOINCREMENT,
          "sh_title" TEXT NULL,
          "sh_file" TEXT NULL,
          "sh_cmt" TEXT NULL,
          "usr_id" INTEGER NULL,
          CONSTRAINT "FK_ShareToUser" FOREIGN KEY ("usr_id") REFERENCES "tb_users" ("u_id") ON DELETE CASCADE
      );

数据库中在 tb_shares 表中已定义了外键约束,且级联删除。

8、我们现在删除一个用户。

using (DbContext ctx = new(opbuilder.Options))
{
    // 找出吴四水
    User? theuser = ctx.Set<User>()
        .Include(u => u.Shares)
        .FirstOrDefault(u => u.Name.Equals("吴淼水"));
    if(theuser != null)
    {
        // 把它删除
        ctx.Remove(theuser);
        // 保存
        ctx.SaveChanges();

        // 删除后
        Console.WriteLine("\n---------- 删除用户后 ----------");
        PrintUsers(ctx.Set<User>().Include(u => u.Shares).ToArray());
    }
}

Include 方法就是连同关系表一起查询。由于数据量小,咱们不用考虑笛卡尔乘积问题,所以就不必分开查询了。

生成的SQL如下:

DELETE FROM "tb_shares"
      WHERE "sh_id" = @p0
      RETURNING 1;

DELETE FROM "tb_users"
      WHERE "u_id" = @p0
      RETURNING 1;

对比一下,删除前后打印的数据。

---------- 初始化后 ----------
用户:吴淼水 (1)
        大宋神探 | 神碳吴四水.docx
用户:高大上 (2)
        想要飞得更高 | 励志王.pdf
        一氧化二氢居然有毒 | 2026智慧档案.doc

---------- 删除用户后 ----------
用户:高大上 (2)
        想要飞得更高 | 励志王.pdf
        一氧化二氢居然有毒 | 2026智慧档案.doc

哈哈,这个对比其实没B用,反正 User 已经没了一个,自然查不到 ShareItem 了。

好了,今天咱们就水到这里了。