在开始主题之前,老周分享另一个知识,碰巧这知识点也是 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())
{
_ = awaitc.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 = "三花" }
];
awaitc.AddRangeAsync(chuShengs);
// 保存数据
_ = awaitc.SaveChangesAsync();
}
}
在上述代码中,老周用的是异步等待版本。在 ASP.NET Core 项目中推荐这样,其他项目就随意吧。
咱们看看 EF Core 在创建数据库时生成的 SQL 语句。
CREATE DATABASE [畜生档案馆];
CREATE SEQUENCE [AnimalSequence] START WITH 1 INCREMENT BY 1NO 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
好了,今天咱们就水到这里了。