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

推荐订阅源

T
Threatpost
aimingoo的专栏
aimingoo的专栏
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
Tailwind CSS Blog
J
Java Code Geeks
博客园_首页
Google Online Security Blog
Google Online Security Blog
Hugging Face - Blog
Hugging Face - Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
I
Intezer
P
Palo Alto Networks Blog
V
Vulnerabilities – Threatpost
雷峰网
雷峰网
O
OpenAI News
SecWiki News
SecWiki News
小众软件
小众软件
酷 壳 – CoolShell
酷 壳 – CoolShell
美团技术团队
N
News | PayPal Newsroom
Project Zero
Project Zero
Forbes - Security
Forbes - Security
IT之家
IT之家
A
Arctic Wolf
WordPress大学
WordPress大学
Jina AI
Jina AI
T
Tor Project blog
博客园 - 三生石上(FineUI控件)
S
Secure Thoughts
Google DeepMind News
Google DeepMind News
Attack and Defense Labs
Attack and Defense Labs
博客园 - 聂微东
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
P
Privacy International News Feed
Cloudbric
Cloudbric
G
GRAHAM CLULEY
博客园 - 叶小钗
H
Hacker News: Front Page
腾讯CDC
量子位
Help Net Security
Help Net Security
人人都是产品经理
人人都是产品经理
C
Cyber Attacks, Cyber Crime and Cyber Security
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
宝玉的分享
宝玉的分享
爱范儿
爱范儿
L
Lohrmann on Cybersecurity
Hacker News - Newest:
Hacker News - Newest: "LLM"
Recorded Future
Recorded Future
C
CERT Recently Published Vulnerability Notes

博客园 - 东邪独孤

【ASP.NET Core】在 Windows Forms 中运行 Web 服务器 【EF Core】级联删除行为 【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】框架是如何识别实体类的属性和主键的
【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

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