【EF Core】继承策略——TPC
在开始主题之前,老周分享另一个知识,碰巧这知识点也是 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 是通过主键的值来跟踪实体状态的,现在出现主键相同的实例,就不好搞了。所以,才要使用序列,保证所有派生类所在的表中,主键的值在【全局】层面不会重复。就像这样

你看,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}"); }
现在你猜猜数据集合有几个实例?答案是:
Mike 3 Peter 2
这时候,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
好了,今天咱们就水到这里了。

浙公网安备 33010602011771号