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

推荐订阅源

T
Threat Research - Cisco Blogs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
V
Vulnerabilities – Threatpost
GbyAI
GbyAI
P
Proofpoint News Feed
L
LINUX DO - 热门话题
P
Palo Alto Networks Blog
A
About on SuperTechFans
T
Tenable Blog
M
MIT News - Artificial intelligence
IT之家
IT之家
I
Intezer
D
DataBreaches.Net
爱范儿
爱范儿
T
Threatpost
C
CERT Recently Published Vulnerability Notes
云风的 BLOG
云风的 BLOG
博客园 - 三生石上(FineUI控件)
WordPress大学
WordPress大学
K
Kaspersky official blog
大猫的无限游戏
大猫的无限游戏
A
Arctic Wolf
Y
Y Combinator Blog
Cyberwarzone
Cyberwarzone
酷 壳 – CoolShell
酷 壳 – CoolShell
D
Darknet – Hacking Tools, Hacker News & Cyber Security
H
Help Net Security
Microsoft Security Blog
Microsoft Security Blog
Spread Privacy
Spread Privacy
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
AWS News Blog
AWS News Blog
博客园 - 聂微东
C
Check Point Blog
S
Securelist
有赞技术团队
有赞技术团队
雷峰网
雷峰网
aimingoo的专栏
aimingoo的专栏
Last Week in AI
Last Week in AI
Stack Overflow Blog
Stack Overflow Blog
MongoDB | Blog
MongoDB | Blog
D
Docker
G
GRAHAM CLULEY
T
The Exploit Database - CXSecurity.com
C
Cybersecurity and Infrastructure Security Agency CISA
T
Tailwind CSS Blog
L
Lohrmann on Cybersecurity
G
Google Developers Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
L
LangChain Blog

博客园_首页

Plist 二进制格式 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent # linux红帽教程-手把手教学 UIImageView 设置图片不生效的原因排查 .NET生态下Native AOT兼容的Cron任务调度框架 Python 潮流周刊#147:Python 和 Ruby 的 JIT 故事 - 豌豆花下猫 可持久化线段树/主席树 学习笔记 如何实现 Claude Code 和 Codex 等 Agent CLI 的自动重试 - Newbe36524 WebSocket 连接池生产级实现:实时行情高可用与负载均衡 - Walter先生 关于代码注释的思考 MicroPython对接大模型:uopenai + 火山方舟实现文字聊天和图片理解 从词向量到大模型:NLP 技术演进浅记 LangChain使用deep agent并且加载SKILL 零成本打造专业域名邮箱:Cloudflare + Gmail 终极配置保姆级全攻略 【从0到1构建一个ClaudeAgent】协作-团队协议 - 程序员Seven 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里实现可靠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写了一个颜值拉满的桌面媒体播放器,全程没动一行代码,这就是AI编程新范式 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 一个面向产品化的 Electron + Vue 3 桌面应用脚手架 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 - InCerry 一文学习入门 ThingsBoard 开源物联网平台 - daidaidaiyu 如何为GIT设置全局勾子,为每次提交追加信息 - SKILL·NULL Number.isFinite和isFinite与isNaN()和isNaN的区别 - 南风晚来晚相识 PortSwigger SQL注入LAB2 - C2H5OH 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) - 狂师 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) - okkk!!! GLM模型这么火,咱们用vllm也咧一个呗! - 码甲哥不卷 深入理解 AbortController:从底层原理到跨语言设计哲学 - 革新 字符串学习笔记 - liduoduo2021 多租户系统框架的基础模块设计和分析设计 - 伍华聪 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? - ApacheSeaTunnel AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) - 万笑佛 Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 - 一名程序媛呀 ElasticSearch7.X版本配置密码 - huangSir-devops 用Manim实现动态交点计算--从一个动点问题说起 - wang_yb 团结引擎+Addressable+Instant Game打包抖音小游戏 - 威少小二orz function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 - it排球君 4.15 bubseek —— 让 Agent 的足迹,变成团队的洞察 - 老纪的技术唠嗑局 通过 C# 读取并导出 PDF 书签 - LAYONTHEGROUND 如何用 GitHub Actions 实现 Steam 自动化发布 - Newbe36524 【从0到1构建一个ClaudeAgent】并发-后台任务 - 程序员Seven .NET 高级开发 | 定制 ASP.NET Core 框架 - 痴者工良 电子小白:什么是运算放大器(运放) - Tlink zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 - 孤飞 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 - ACai_sec 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer - 哥布林学者 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 - charlee44 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 - TTGF .NET 官方团队发布的 .NET Agent Skills,告别 AI 编程幻觉! - 追逐时光者 AI工程范式的又一次演进:Harness Engineering - DeepSky丶 本地系统对接大模型智能体的若干尝试 第二本书出版了:《Transformer技术纵深:架构解析与前沿突破》 - 罗西的思考 【Azure Developer】IIS w3wp.exe 的 -m 参数:一个未被记录的管道模式标识 英雄帖招募 - 虾饺爱下棋 AI开发-python-langchain框架(3-24-Plan-and-Execute Agent) - 万笑佛 避免这些编程陷阱:七种让你代码失控的开发风格 - 暮色之狐 从写代码到问问题:2026年,AI如何重构数据科学工作流 C#从零开始: LumNote-重新定义单机Markdown编辑器 - LdotJdot 【FAQ】HarmonyOS SDK 闭源开放能力 —Media Library Kit - HarmonyOS_SDK 我用fastapi-scaff搭了个项目,两天工期缩到两小时,老板以为我开挂了 - 一名程序媛呀 Rudist v0.5.1 发布:AI 驱动的 Redis 客户端,更快、更直观 SqlSugar 接入 PostgreSQL pgvector 完整方案(增删改查 + 强类型相似度查询) - HarryPei 今天不想硬撑?来领一张《摆烂许可证》 - 汀、人工智能 一次 Android 抓包引出的疑问:Tor Browser 桌面模式下为何出现直连请求 做 AI 应用必懂:Function Call 和 Skills,到底差在哪? - it排球君 Linux实操--组管理、权限管理和定时任务 - NE_STOP
【EF Core】继承策略——TPC
东邪独孤 · 2026-06-13 · via 博客园_首页

在开始主题之前,老周分享另一个知识,碰巧这知识点也是 EF Core 的,是前些天一位新手程序猿问的,他那是一个小项目,因为小,所以采用 Code First 的方案。不过程序有两个版本,一个是用 SQLite 数据库,一个用 SQL Server。然后有些实体他设定了 CHECK 约束。众所周知,配置 CHECK 约束是直接用 SQL 表达式的。这位同仁比较负责,他觉得哪怕用 EF Core 生成数据库也要规范一点,字段名也应该用边界字符,比如,在 SQLite 中,边界是双引号,表达式应写成 "age" > 15,在 SQL Server 中写成 [age] > 15。

同仁的意思是,他不想硬编码,EF Core 有没有相关的 API 可以根据不同数据库,自动产生边界字符。于是,作为“老一辈”,老周教了他两招。

1、比较笨的方法,其实也是硬编码。

/*--------------------------------- 实体类 ------------------------------*/
public class Person
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public int Age { get; set; }
}

/*-------------------------------- 数据库上下文 ----------------------------*/
public class TestContext:DbContext
{
    public TestContext(DbContextOptions<TestContext> options)
        : base(options)
    { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        var entity = modelBuilder.Entity<Person>();
        // 配置主键
        entity.HasKey(b => b.Id).HasName("PK_People");
        // 配置表映射
        entity.ToTable("tb_people", tb =>
        {
            // 列映射
            tb.Property(a => a.Id).HasColumnName("ps_id");
            tb.Property(a => a.Name).HasColumnName("ps_name");
            tb.Property(a => a.Age).HasColumnName("ps_age");
            // 配置CHECK约束
            string delimiteLeft = "", delimiteRight = "";
            if(this.Database.IsSqlite())
            {
                delimiteLeft = delimiteRight = "\"";
            }
            if(this.Database.IsSqlServer())
            {
                delimiteLeft = "[";
                delimiteRight = "]";
            }
            tb.HasCheckConstraint("CK_Age", $"{delimiteLeft}ps_age{delimiteRight} > 20");
        });
    }
}

这套方案是使用了 IsSqlServer 方法来判断当前配置的是否为 SQL Server 数据库,IsSqlite 方法判断当前配置的是否为 SQLite 数据库。

实例化上下文时,通过构造函数来传递选项,以使用不同的数据库。

// 用 SQL Server
DbContextOptionsBuilder<TestContext> opbuilder1 = new();
opbuilder1.UseSqlServer("Server=...");
using(var ctx = new TestContext(opbuilder1.Options))
{
    // 这里咱们不是真的建库,仅获取生成的 SQL
    Console.WriteLine("使用 SQL Server 数据:");
    Console.WriteLine(ctx.Database.GenerateCreateScript());
    Console.Write("\n");
}

// 使用 SQLite 数据库
DbContextOptionsBuilder<TestContext> opbuilder2 = new();
opbuilder2.UseSqlite("data source=...");
using (var ctx = new TestContext(opbuilder2.Options))
{
    Console.WriteLine("使用 SQLite 数据:");
    Console.WriteLine(ctx.Database.GenerateCreateScript());
    Console.Write("\n");
}

结果如下:

使用 SQL Server 数据:
CREATE TABLE [tb_people] (
    [ps_id] int NOT NULL IDENTITY,
    [ps_name] nvarchar(max) NOT NULL,
    [ps_age] int NOT NULL,
    CONSTRAINT [PK_People] PRIMARY KEY ([ps_id]),
    CONSTRAINT [CK_Age] CHECK ([ps_age] > 20)
);
GO

使用 SQLite 数据:
CREATE TABLE "tb_people" (
    "ps_id" INTEGER NOT NULL CONSTRAINT "PK_People" PRIMARY KEY AUTOINCREMENT,
    "ps_name" TEXT NOT NULL,
    "ps_age" INTEGER NOT NULL,
    CONSTRAINT "CK_Age" CHECK ("ps_age" > 20)
);

但这种做法还是不够“老辣”,咱们看下一个方案。

2、巧用 ISqlGenerationHelper 服务。

这个最好用,不用去判断数据库是什么,能够自动生成带边界字符的名称。

entity.ToTable("tb_people", tb =>
{
    // 列映射
    ……

    // 获取服务
    ISqlGenerationHelper sqlHelper = this.GetService<ISqlGenerationHelper>();
    // 生成带边界字符的列名
    string ageColName = sqlHelper.DelimitIdentifier("ps_age");
    // 配置CHECK约束
    tb.HasCheckConstraint("CK_Age", $"{ageColName} > 20");
});

咱们增加一个 PostgreSQL 的 provider 来测试一下。

 // 用 SQL Server
 DbContextOptionsBuilder<TestContext> opbuilder1 = new();
 opbuilder1.UseSqlServer("Server=...");
 using(var ctx = new TestContext(opbuilder1.Options))
 {
     // 这里咱们不是真的建库,仅获取生成的 SQL
     Console.WriteLine("使用 SQL Server 数据:");
     Console.WriteLine(ctx.Database.GenerateCreateScript());
     Console.Write("\n");
 }

 // 使用 PostgreSQL 数据库
 DbContextOptionsBuilder<TestContext> opbuilder2 = new();
 opbuilder2.UseNpgsql("Host=...");
 using (var ctx = new TestContext(opbuilder2.Options))
 {
     Console.WriteLine("使用 PostgreSQL 数据:");
     Console.WriteLine(ctx.Database.GenerateCreateScript());
     Console.Write("\n");
 }

 // 使用 SQLite 数据库
 DbContextOptionsBuilder<TestContext> opbuilder3 = new();
 opbuilder3.UseSqlite("data source=...");
 using (var ctx = new TestContext(opbuilder3.Options))
 {
     Console.WriteLine("使用 SQLite 数据:");
     Console.WriteLine(ctx.Database.GenerateCreateScript());
     Console.Write("\n");
 }

得到结果如下:

使用 SQL Server 数据:
CREATE TABLE [tb_people] (
    [ps_id] int NOT NULL IDENTITY,
    [ps_name] nvarchar(max) NOT NULL,
    [ps_age] int NOT NULL,
    CONSTRAINT [PK_People] PRIMARY KEY ([ps_id]),
    CONSTRAINT [CK_Age] CHECK ([ps_age] > 20)
);
GO

使用 PostgreSQL 数据:
CREATE TABLE tb_people (
    ps_id integer GENERATED BY DEFAULT AS IDENTITY,
    ps_name text NOT NULL,
    ps_age integer NOT NULL,
    CONSTRAINT "PK_People" PRIMARY KEY (ps_id),
    CONSTRAINT "CK_Age" CHECK (ps_age > 20)
);

使用 SQLite 数据:
CREATE TABLE "tb_people" (
    "ps_id" INTEGER NOT NULL CONSTRAINT "PK_People" PRIMARY KEY AUTOINCREMENT,
    "ps_name" TEXT NOT NULL,
    "ps_age" INTEGER NOT NULL,
    CONSTRAINT "CK_Age" CHECK ("ps_age" > 20)
);

-----------------------------------------------------------------------------------------------------------------------------------------------

好了,正片开始。今天咱们聊实体继承中的第三种映射策略——TPC。TPC 是地球和平联合组织……我呸,是 Table per Concrete Class 的缩写。它与 TPT 挺像,共同点是“每个类都有对应的表”,但不同点在于“具体类型”,啥意思呢?至少包含两个意思:

1、可实例化的类,抽象类就不映射了哟;

2、类中的属性(字段)成员,不管是本类中定义的还是从基类继承过来的,都会做列映射。

这么一说,TPC 的独立性更强。咱们上一次所聊的 TPT 策略,由于不映射从基类继承的成员,所以需要通过外键与基类所映射的表建立一对一关系,查询时需要表联合,带来了亿些性能上的问题。而 TPC 是包含了基类成员的,它不需要与基类的表建立相对关系,不设立外键,使用时直接单表查询即可。使查询过程变简单了。

TPC 策略很适合那种“开枝散叶”式继承的实体。典型场景是某个抽象作为公共基类,然后派生出同级别的 N 多个实现类。

比如,下面这个继承关系很是经典,高考每年必考。

/// <summary>
/// 公共基类,很抽象的
/// </summary>
public abstract class Animal
{
    /// <summary>
    /// 只是主键,无其他含义
    /// </summary>
    public int Id { get; set; }
    /// <summary>
    /// 这头野兽叫什么
    /// </summary>
    public abstract string Name { get; set; }
    /// <summary>
    /// 这头野兽多大了
    /// </summary>
    public abstract int Age { get; set; }
}

public class Cat : Animal
{
    public override string Name { get; set; } = null!;
    public override int Age { get; set; }

    /// <summary>
    /// 新增成员,毛发纹理
    /// </summary>
    public string? Texture { get; set; }
}

public class Dog : Animal
{
    public override string Name { get; set; } = "John";
    public override int Age { get; set; } = 1;

    /// <summary>
    /// 新增成员,喜欢的食物
    /// </summary>
    public string? FavFood { get; set; }
}

按照上述代码,基类是 Animal,其他两个是它的子类。且按照咱们对前两种映射策略的说明,映射策略、主键是必须在基类上配置的。正是如此,Id 属性只能定义在基类。也就是说,在模型配置时,Animal 类是要添加到实体模型中的,映射不映射由 EF Core 自己处理。

以 SQL Server 数据库为例,实现数据库上下文。

public class TestDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("Server=(localdb)\\MY;Database=畜生档案馆;MultipleActiveResultSets=True");
        // 配置一下日志,好查看SQL
        optionsBuilder.LogTo((evtid, lv) => evtid == RelationalEventId.CommandExecuted, evtdata =>
        {
            if(evtdata is CommandEventData cmddata)
            {
                // 改变文本颜色
                Console.ForegroundColor = ConsoleColor.Blue;
                // 记录SQL
                Console.WriteLine($"""
                    [SQL]
                    {cmddata.Command.CommandText}
                    """);
                // 记录完日志后,恢复颜色为默认
                Console.ResetColor();
            }
        });
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        var entAnim = modelBuilder.Entity<Animal>();
        // 映射策略
        entAnim.UseTpcMappingStrategy();
        // 主键
        entAnim.HasKey(x => x.Id);
        // 名称的最大字符数
        entAnim.Property(x => x.Name).HasMaxLength(15);

        var entDog = modelBuilder.Entity<Dog>();
        // 表映射
        entDog.ToTable("tb_dogs", tb =>
        {
            tb.Property(g => g.Id).HasColumnName("dog_id");
            tb.Property(g => g.Name).HasColumnName("dog_name");
            tb.Property(g => g.Age).HasColumnName("dog_age");
            tb.Property(g => g.FavFood).HasColumnName("fav_food");
        });

        var entCat = modelBuilder.Entity<Cat>();
        // 表映射
        entCat.ToTable("tb_cats", tb =>
        {
            tb.Property(y => y.Id).HasColumnName("cat_id");
            tb.Property(y => y.Name).HasColumnName("cat_name");
            tb.Property(y => y.Age).HasColumnName("cat_age");
            tb.Property(y => y.Texture).HasColumnName("cat_texture");
        });
    }
}

比较重要的几点,老周逐个说明一下。

1、数据库的连接字符串,要加上 MultipleActiveResultSets=True,批量插入数据时会返回多个结果,不加这个会报错。和 TPH、TPT 一样,使用 TPC 策略也是在配置基类实体时调用 UseTpcMappingStrategy 方法。

2、由于 TPC 策略下每个表是独立的,因此,每个表的名称,以及列的名称都可以自定义。注意要调用 ToTable 方法,再通过 TableBuilder 对象来配置列名,不要在 PropertyBuilder 上配置。在上一篇水文中,老周给大伙伴演示过,EF Core 在建立数据库 Model 的时候,若实体间存在继承关系,那么属性元数据是共享的。比如,Name 属性,从 Animal 到 Cat、Dog 实体都是共享元数据的。如果使用 PropertyBuilder.HasColumnName 来配置列名,那么,只有最后设置的名称生效,就无法做到每个派生类的列名称独立了。因此,一定要用 ToTable 方法,让表映射变成 Override 版本,EF Core 内部会自动保存每个覆盖的属性配置。

3、也正因为存在继承关系的成员是共享元数据的,所以,像 Name 属性那样要配置最大字符数为 15,也只能在 Animal 类上配置,而且所以派生类所映射的表中,各个继承的成员所对应的列,其类型和参数也必须相同的。即 cat_name 列和 dog_name 列的类型和所占空间大小是相同的,cat_age 与 dog_age 列也是如此。

下面咱们来测试一下。由 EF Core 负责创建数据库。然后向数据库插入四条记录。

static async Task Main(string[] args)
{
    // 由运行时自动创建数据库
    using(var c = new TestDbContext())
    {
        _ = await c.Database.EnsureCreatedAsync();
    }

    // 插入一些记录试试
    using(var c = new TestDbContext())
    {
        Animal[] chuShengs = [
                new Cat() { Name = "Jack", Age = 2, Texture = "虎斑" },
                new Dog() { Name = "Mike", Age = 3, FavFood = "鸡屁股" },
                new Dog() { Name = "Peter", Age = 2, FavFood = "狗粮" },
                new Cat() { Name = "Lily", Age = 2, Texture = "三花" }
            ];
        await c.AddRangeAsync(chuShengs);
        // 保存数据
        _ = await c.SaveChangesAsync();
    }
}

在上述代码中,老周用的是异步等待版本。在 ASP.NET Core 项目中推荐这样,其他项目就随意吧。

咱们看看 EF Core 在创建数据库时生成的 SQL 语句。

CREATE DATABASE [畜生档案馆];

CREATE SEQUENCE [AnimalSequence] START WITH 1 INCREMENT BY 1 NO CYCLE;

CREATE TABLE [tb_cats] (
    [cat_id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [cat_name] nvarchar(15) NOT NULL,
    [cat_age] int NOT NULL,
    [cat_texture] nvarchar(max) NULL,
    CONSTRAINT [PK_tb_cats] PRIMARY KEY ([cat_id])
);

CREATE TABLE [tb_dogs] (
    [dog_id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [dog_name] nvarchar(15) NOT NULL,
    [dog_age] int NOT NULL,
    [fav_food] nvarchar(max) NULL,
    CONSTRAINT [PK_tb_dogs] PRIMARY KEY ([dog_id])
);

TPC 的情况特殊,咱们看到,生成的SQL中包含在 SQL Server 中创建递增序列 AnimalSequence。数据表创建了两个:tb_cats 和 tb_dogs。而它们的主键不再是 IDENTITY,而是由序列来产生下一个值。

为什么会这样呢?这是为了 EF Core 的实体追踪(跟踪)。从面向对象的角度看,Animal 是公共基类,那么,一个 Animal 对象的集合,它既可以包含 Cat 实例,也可以包含 Dog 实例。

你猜猜下面代码中,数据集合中会有几个实例?

using(var ctx = new TestDbContext())
{
    // 获取数据集合
    var animals = ctx.Set<Animal>();
    foreach(Animal anm in animals)
    {
        Console.WriteLine($"{anm.Name}\t{anm.Age}");
    }
}

答案是:

Jack    2
Lily    2
Mike    3
Peter   2

现在咱们假设一下,如果主键由 IDENTITY 生成,而不是序列,那么,就会有一条 Cat 记录的 ID 是 1,一条 Dog 记录的 ID 也是1。结果是,Animal 类型的集合里,有两个实例的主键是 1。同理,如果继续插入数据,就会出现 ID 同时为 2 的 Cat 和 Dog 实例。EF Core 是通过主键的值来跟踪实体状态的,现在出现主键相同的实例,就不好搞了。所以,才要使用序列,保证所有派生类所在的表中,主键的值在【全局】层面不会重复。就像这样

image

你看,tb_cats 表中的主键值依次为 1、2,而 tb_dogs 表中的主键值则依次为 3、4。这样一来,在 Animal 集合中,这四条记录的 ID 值就不重复了,EF Core 就能进行跟踪了。

EF Core 在数据集合的查询中是遵守面向对象规则的。比如,咱们上面的集合—— Set<Animal>,它可以包含 Cat 和 Dog 实例,这是本着类型兼容性原则,Cat 和 Dog 都是派生类,可以赋值给声明为 Animal 的对象。如果把代码这样改呢。

// 获取数据集合
var animals = ctx.Set<Dog>();
foreach(Animal anm in animals)
{
    Console.WriteLine($"{anm.Name}\t{anm.Age}");
}

现在你猜猜数据集合有几个实例?答案是:

这时候,Dog 集合只能兼容 Dog 类,除非有 Dog 的派生类。

虽然 TPC 策略中我们不需要配置类型鉴别器,但在查询时,生成的SQL语句,EF Core 也会插入鉴别标识的。比如前面查询 Animal 集合的,生成的 SQL 如下:

SELECT [t].[cat_id], [t].[cat_age], [t].[cat_name], [t].[cat_texture], NULL AS [fav_food], N'Cat' AS [Discriminator]
FROM [tb_cats] AS [t]
UNION ALL
SELECT [t0].[dog_id] AS [cat_id], [t0].[dog_age] AS [cat_age], [t0].[dog_name] AS [cat_name], NULL AS [cat_texture], [t0].[fav_food], N'Dog' AS [Discriminator]
FROM [tb_dogs] AS [t0]

咱们看到,EF Core 加了一个名为 Discriminator 的字段,字段的值就是类名。

咱们还有一个问题没解决:像 SQLite 这样不能用序列的数据库,在 TPC 映射策略下如何处理主键呢。最简单粗暴的方法,就是插入新记录时直接给它分配一个——我们手动赋值。

当然,咱们还有简单不粗暴的方法,那就是使用客户端生成器,即由 EF Core 来生成。就是用 ValueGenerator,这货在很多场合还是很有用的。

先看本示例的主角——实体类。

/// <summary>
/// 抽象类,卡牌游戏
/// </summary>
public abstract class CardGame
{
    /// <summary>
    /// 主键
    /// </summary>
    public string CardId { get; set; } = null!;
    /// <summary>
    /// 名称
    /// </summary>
    public abstract string Name { get; set; }
    /// <summary>
    /// 是否为主牌
    /// </summary>
    public abstract bool IsMajor { get; set; }
}

/// <summary>
/// 扑克牌
/// </summary>
public class Poker : CardGame
{
    public required override string Name { get; set; }
    public override bool IsMajor { get; set; }
    /// <summary>
    /// 牌上数字,新增
    /// </summary>
    public int Number { get; set; }
}

/// <summary>
/// 库洛牌
/// </summary>
public class ClowCard : CardGame
{
    public required override string Name { get; set; }
    /// <summary>
    /// 是否为四大元素牌
    /// </summary>
    public override bool IsMajor { get; set; }
}

公共基类表示卡牌游戏的共同特征。然后就是扑克牌和库洛牌,其实二者还有些像的,扑克牌有四大主牌,库洛牌有四大元素牌。

用当天的日期 + GUID。这个我相信就算你一天要插入 10 的 99 次方条记录,应该也不会遇上有重复值的。

public class MyIDValueGenerator : ValueGenerator<string>
{
    public override string Next(EntityEntry entry)
    {
        // 当前日期
        DateTime currdt = DateTime.Now;
        string firstPart = currdt.ToString("yyMMdd");
        // GUID
        string secondPart = Guid.NewGuid().ToString("N");
        // 组成新值返回
        return firstPart + "_" + secondPart;
    }

    // 此时,生成的值可不是临时值,而是要存入数据库的,所以返回 false
    public override bool GeneratesTemporaryValues => false;
}

ValueGenerator 是派生自 ValueGenerator 的泛型抽象类。带类型参数的基类继承起来更舒服。我们要实现两个成员:

1、GeneratesTemporaryValues 属性:只读属性,表示此生成器生成的值是不是临时的。啥意思呢?就是生成的值只在 EF Core 跟踪实体过程用,不会存入数据库。比如自增长列,每次生成新值都是数据库完成的,但是,新的实体实例在保存到数据库前,是没有生成的值的,这时候,可以给它临时分配一个值。咱们这里生成的值是要存入数据库的,所以,要返回 false,表示非临时值。

2、Next 方法。返回生成的新值。本例中,老周用日期和 GUID 组成新值,用“_”字符连接。

下面,写一下 DbContext 的派生类,配置数据库模型。

public class TestContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        // 日志配置
        ILoggerFactory logfac = LoggerFactory.Create(logbuilder =>
        {
            // 添加控制台日志
            logbuilder.AddConsole();
            // 过滤
            logbuilder.AddFilter((cate, lv) =>
            {
                return cate == "Microsoft.EntityFrameworkCore.Database.Command" && lv == LogLevel.Information;
            });
        });
        // 配置数据库
        optionsBuilder.UseSqlite("data source=cards.db")
            .EnableSensitiveDataLogging(true)
            .UseLoggerFactory(logfac);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<CardGame>(cgm =>
        {
            // 主键
            cgm.HasKey(c => c.CardId);
            // 映射策略
            cgm.UseTpcMappingStrategy();
            // 配置值生成器
            cgm.Property(x => x.CardId).HasValueGenerator<MyIDValueGenerator>();
        });

        modelBuilder.Entity<Poker>(pkt =>
        {
            pkt.ToTable("tb_poker", tb =>
            {
                tb.Property(w => w.Name).HasColumnName("pk_name");
                tb.Property(w => w.CardId).HasColumnName("pk_id");
                tb.Property(w => w.IsMajor).HasColumnName("pk_major");
                tb.Property(k => k.Number).HasColumnName("pk_num");
            });
        });

        modelBuilder.Entity<ClowCard>(cwt =>
        {
            cwt.ToTable("tb_clowcard", tb =>
            {
                tb.Property(t => t.CardId).HasColumnName("cc_id");
                tb.Property(t => t.Name).HasColumnName("cc_name");
                tb.Property(g => g.IsMajor).HasColumnName("cc_major");
            });
        });
    }
}

在配置模型时,调用 HasValueGenerator 方法应用我们自己写的值生成器。注意,值生成器是针对列的,所以你得在属性成员上配置。

这一次的日志记录,老周玩了点新花样,用到了 .NET 的 Logging 功能,相信大伙伴在 ASP.NET Core 上都玩得很 6 的了。如果是控制台项目,记得引用这个 Nuget 库:Microsoft.Extensions.Logging.Console。

这里咱们比较关心执行过的 SQL 语句,所以,在 Logging 的配置中,老周做了过滤。

// 添加控制台日志
logbuilder.AddConsole();
// 过滤
logbuilder.AddFilter((cate, lv) =>
{
    return cate == "Microsoft.EntityFrameworkCore.Database.Command" && lv == LogLevel.Information;
});

.NET Logging 是按日志类别(Category)来输出的,而不是 EF Core 内部使用的 Event ID,输出 SQL 语句的类别是 Microsoft.EntityFrameworkCore.Database.Command。配置之后,控制台只打印这个类别,且属于“信息”级别的日志(错误,调试等级别就不打印)。EnableSensitiveDataLogging 方法表示在打印日志显示查询参数的值,为了安全,一般我们不开启它,如果你想看到参数的具体的值,那就开启,投入生产环境后注释掉就好了。

运行程序。下面是创建表的 SQL 语句。

CREATE TABLE "tb_clowcard" (
          "cc_id" TEXT NOT NULL CONSTRAINT "PK_tb_clowcard" PRIMARY KEY,
          "cc_name" TEXT NOT NULL,
          "cc_major" INTEGER NOT NULL
);

CREATE TABLE "tb_poker" (
          "pk_id" TEXT NOT NULL CONSTRAINT "PK_tb_poker" PRIMARY KEY,
          "pk_name" TEXT NOT NULL,
          "pk_major" INTEGER NOT NULL,
          "pk_num" INTEGER NOT NULL
);

我们插入一些数据。

using(TestContext ctx =new())
{
    // 获取集合
    DbSet<CardGame> cardset = ctx.Set<CardGame>();
    cardset.AddRange([
        new ClowCard{ Name = "Watery", IsMajor = true },          // 水牌
        new ClowCard{ Name = "Move", IsMajor = false },             // 移牌
        new ClowCard{ Name = "Firey", IsMajor = true },              // 火牌
        new Poker{ Name = "Hearts", IsMajor = true, Number = 3 },   // 红桃3
        new Poker{ Name = "Clubs", IsMajor = true, Number = 1 }     // 梅花A
        ]);
    // 提交
    ctx.SaveChanges();
}

产生的 INSERT SQL 语句如下:

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (4ms) [Parameters=[@p0='260613_2bd2b99c9f604a3b850cda8fab96c2ea' (Nullable = false) (Size = 39), @p1='False', @p2='Move' (Nullable = false) (Size = 4)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "tb_clowcard" ("cc_id", "cc_major", "cc_name")
      VALUES (@p0, @p1, @p2);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[@p0='260613_c59f0714bbbc4dcfa08d8c268f4756c9' (Nullable = false) (Size = 39), @p1='True', @p2='Watery' (Nullable = false) (Size = 6)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "tb_clowcard" ("cc_id", "cc_major", "cc_name")
      VALUES (@p0, @p1, @p2);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[@p0='260613_d1ba8b41a9bb4d3a86b10ea4da8e5d05' (Nullable = false) (Size = 39), @p1='True', @p2='Firey' (Nullable = false) (Size = 5)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "tb_clowcard" ("cc_id", "cc_major", "cc_name")
      VALUES (@p0, @p1, @p2);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[@p0='260613_1d8ea807b3244147ade7e66e7c32863e' (Nullable = false) (Size = 39), @p1='True', @p2='Hearts' (Nullable = false) (Size = 6), @p3='3'], CommandType='Text', CommandTimeout='30']
      INSERT INTO "tb_poker" ("pk_id", "pk_major", "pk_name", "pk_num")
      VALUES (@p0, @p1, @p2, @p3);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[@p0='260613_5fbd80b854b14ddc9933259876d436af' (Nullable = false) (Size = 39), @p1='True', @p2='Clubs' (Nullable = false) (Size = 5), @p3='1'], CommandType='Text', CommandTimeout='30']
      INSERT INTO "tb_poker" ("pk_id", "pk_major", "pk_name", "pk_num")
      VALUES (@p0, @p1, @p2, @p3);

正是因为开启了 EnableSensitiveDataLogging,所以在日志咱们能看到 p0、p1、p2 等查询参数的值。

最后,把刚刚插入的记录全查询出来,并打印到控制台。

using (var c = new TestContext())
{
    var cards = c.Set<CardGame>();
    foreach(CardGame cg in cards)
    {
        Console.Write("{0,-13}", cg.GetType().Name);
        Console.Write(cg.CardId + "\t");
        Console.Write(cg.Name + "\n");
    }
}

结果为

ClowCard     260613_2bd2b99c9f604a3b850cda8fab96c2ea    Move
ClowCard     260613_c59f0714bbbc4dcfa08d8c268f4756c9    Watery
ClowCard     260613_d1ba8b41a9bb4d3a86b10ea4da8e5d05    Firey
Poker        260613_1d8ea807b3244147ade7e66e7c32863e    Hearts
Poker        260613_5fbd80b854b14ddc9933259876d436af    Clubs

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