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

推荐订阅源

Y
Y Combinator Blog
GbyAI
GbyAI
IT之家
IT之家
V
Visual Studio Blog
J
Java Code Geeks
A
About on SuperTechFans
A
Arctic Wolf
Google Online Security Blog
Google Online Security Blog
博客园 - 【当耐特】
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
W
WeLiveSecurity
L
LINUX DO - 热门话题
Application and Cybersecurity Blog
Application and Cybersecurity Blog
G
Google Developers Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
Martin Fowler
Martin Fowler
N
News and Events Feed by Topic
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
小众软件
小众软件
Scott Helme
Scott Helme
T
Tailwind CSS Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
P
Palo Alto Networks Blog
罗磊的独立博客
V2EX - 技术
V2EX - 技术
C
Cybersecurity and Infrastructure Security Agency CISA
博客园 - 叶小钗
宝玉的分享
宝玉的分享
K
Kaspersky official blog
S
Securelist
Cyberwarzone
Cyberwarzone
Schneier on Security
Schneier on Security
PCI Perspectives
PCI Perspectives
S
Schneier on Security
Security Latest
Security Latest
腾讯CDC
SecWiki News
SecWiki News
I
InfoQ
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
T
The Blog of Author Tim Ferriss
Hacker News - Newest:
Hacker News - Newest: "LLM"
Forbes - Security
Forbes - Security
Google DeepMind News
Google DeepMind News
博客园 - 司徒正美
The Last Watchdog
The Last Watchdog
C
Check Point Blog
Hacker News: Ask HN
Hacker News: Ask HN
S
Secure Thoughts
博客园_首页
Stack Overflow Blog
Stack Overflow Blog

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 Claude code入门 - 陈彦斌 一文学习入门 ThingsBoard 开源物联网平台 GitHub 热门项目 | 2026年04月16日 如何为GIT设置全局勾子,为每次提交追加信息 Number.isFinite和isFinite与isNaN()和Number.isNaN的区别 PortSwigger SQL注入LAB2 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) GLM模型这么火,咱们用vllm也咧一个呗! 深入理解 AbortController:从底层原理到跨语言设计哲学 字符串学习笔记 多租户系统框架的基础模块设计和分析设计 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 ElasticSearch7.X版本配置密码 用Manim实现动态交点计算--从一个动点问题说起 团结引擎+Addressable+Instant Game打包抖音小游戏 function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 bubseek —— 让 Agent 的足迹,变成团队的洞察 通过 C# 读取并导出 PDF 书签 如何用 GitHub Actions 实现 Steam 自动化发布 【从0到1构建一个ClaudeAgent】并发-后台任务 .NET 高级开发 | 定制 ASP.NET Core 框架 电子小白:什么是运算放大器(运放) zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 工作的意义、工作的困难认知再思考 .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 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 开了 TUN 模式还是直连?90% 的人都踩过这个坑 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术
【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 了。

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