自动生成字段值,咱们首先想到的是主键列(带 IDENTITY 的主键)。EF Core 默认的主键配置也是启用 Identity 自增长的,而且可以自动标识主键。前提是代表主键的实体属性名要符合以下规则:

1、名字叫 ID、id、或 Id,就是不分大小写;

2、名字由实体类名 + Id 构成。比如,Car 实体类,包含一个属性叫 CarID 或 CarId;

3、属性类型是整数类型(int、long、ushort 等,但不是 byte)或 GUID。

这些识别主键的规则是由一种叫“约定”(Convension)的东西实现的,具体来说,是一个叫 KeyDiscoveryConvention 的类。老周放一小段源代码给各位瞧瞧。

public class KeyDiscoveryConvention :
IEntityTypeAddedConvention,
IPropertyAddedConvention,
IKeyRemovedConvention,
IEntityTypeBaseTypeChangedConvention,
IEntityTypeMemberIgnoredConvention,
IForeignKeyAddedConvention,
IForeignKeyRemovedConvention,
IForeignKeyPropertiesChangedConvention,
IForeignKeyUniquenessChangedConvention,
IForeignKeyOwnershipChangedConvention,
ISkipNavigationForeignKeyChangedConvention
{
private const string KeySuffix = "Id"; …… public static IEnumerable<IConventionProperty> DiscoverKeyProperties(
IConventionEntityType entityType,
IEnumerable<IConventionProperty> candidateProperties)
{
Check.NotNull(entityType, nameof(entityType)); // ReSharper disable PossibleMultipleEnumeration
var keyProperties = candidateProperties.Where(p => string.Equals(p.Name, KeySuffix, StringComparison.OrdinalIgnoreCase));
if (!keyProperties.Any())
{
var entityTypeName = entityType.ShortName();
keyProperties = candidateProperties.Where(
p => p.Name.Length == entityTypeName.Length + KeySuffix.Length
&& p.Name.StartsWith(entityTypeName, StringComparison.OrdinalIgnoreCase)
&& p.Name.EndsWith(KeySuffix, StringComparison.OrdinalIgnoreCase));
} return keyProperties;
// ReSharper restore PossibleMultipleEnumeration
}
……
}

这几个逻辑 And 其实就是查找 <类名>Id 格式的属性名,如 StudentID、CarId、OrderID…… 外键的发现原理也跟主键一样。

用 Sqlite 数据举一个简单的例子。下面是实体类(假设它用来表示输入法信息):

public class InputMethod
{
public ushort RecoId { get; set; }
public string? MethodDisplay { get; set; }
public string? Description { get; set; }
public string? Culture { get; set; }
}

如你所见,这个类作为主键的属性是 RecoId,但是,它的命名是无法被自动识别的,咱们必须明确地告诉 EF,它是主键。方法有二:

1、批注法。直接在属性上应用相关的特性类。如

public class InputMethod
{
[Key]
public ushort RecoId { get; set; }
……
}

2、重写 DbContext 类的 OnModelCreating 方法。如

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<InputMethod>().HasKey(e => e.RecoId);
}

如果使用了上面重写 OnModelCreating 方法,那么,你的 DbContext 派生类已经能识别 InputMethod 实体类了。但如果你用的是在属性上应用 [Key] 特性的方式,那么 DbContext 的派生类是识别不到实体类的,你需要将它的集合声明为 DbContext 的属性。

internal class TestDBContext : DbContext
{
// 构造函数
public TestDBContext(DbContextOptions<TestDBContext> opt)
: base(opt)
{ } // 将实体集合声明为属性
public DbSet<InputMethod> InputMethods { get; set; }
}

注意,数据记录的集合要用 DbSet<>,其他类型的集合是不行的哟。比如,你改成这样,就会报错。

public List<InputMethod> InputMethods { get; set; }

说明人家只认 DbSet 集合,其他集合无效。

这里老周选用服务容器来配置。

static void Main(string[] args)
{
IServiceCollection services = new ServiceCollection();
// 构建连接字符串
SqliteConnectionStringBuilder constrbd = new();
constrbd.DataSource = "abc.db";
// 添加 Sqlite 功能
services.AddSqlite<TestDBContext>(
connectionString: constrbd.ToString(),
optionsAction: dcopt =>
{
dcopt.LogTo(msg => Console.WriteLine(msg), LogLevel.Information);
}
);
// 生成服务列表
var svcProd = services.BuildServiceProvider();
if(svcProd == null)
{
return;
} // 访问数据上下文
using TestDBContext dbc = svcProd.GetRequiredService<TestDBContext>();
……
}

连接字符串你可以直接用字符串写,不用 ConnectionStringBuilder。默认的 SQLite 库是不支持密码的,所以老周就不设置密码了。在调用 AddSqlite 方法时,有一个名为 optionsAction 的参数,咱们可以用它配置日志输出。LogTo 方法配置简单,只要提供一个委托,它绑定的方法只要有一个 string 类型的输入参数就行,这个字符串参数就是日志文本。

配置日志功能后,运行程序时,控制台能看到执行的 SQL 语句。

下面咱们来创建数据库,然后插入两条 InputMethod 记录。

// 访问数据上下文
using TestDBContext dbc = svcProd.GetRequiredService<TestDBContext>();
// 删除数据库
dbc.Database.EnsureDeleted();
// 创建数据库
dbc.Database.EnsureCreated(); // 尝试插入两条记录
InputMethod[] ents = [
new(){MethodDisplay = "双拼输入", Description="按两个键完成一个音节",Culture="zh-CN"},
new() {MethodDisplay = "六指输入", Description="专供六个指头的人使用",Culture="zh-CN"}
];
dbc.Set<InputMethod>().AddRange(ents);
int result = dbc.SaveChanges();
Console.WriteLine($"更新记录数:{result}"); // 打印插入的记录
foreach(InputMethod im in dbc.Set<InputMethod>())
{
Console.WriteLine($"ID={im.RecoId}, Display={im.MethodDisplay}, Culture={im.Culture}");
}

这里是为了测试,调用了 EnsureDeleted 方法,实际应用时一般不要调用。因为这个方法的功能是把现存的数据库删除。如果调用了此方法,那应用程序每次启动都会删掉数据库,那用户肯定会投诉你的。EnsureCreated 方法可以使用,它的功能是如果数据库不存在,就创建新数据库;如果数据库存在,那啥也不做。所以,调用 EnsureCreated 方法不会造成数据丢失,放心用。

插入数据和调用 SaveChanges 方法保存到数据库的代码,相信大伙都很熟了,老周就不介绍了。

程序运行之后,将得到这样的日志:

info: 2024/8/4 12:48:11.517 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (10ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
PRAGMA journal_mode = 'wal';
info: 2024/8/4 12:48:11.582 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE "tb_ims" (
"RecoId" INTEGER NOT NULL CONSTRAINT "PK_tb_ims""MethodDisplay""Description""Culture" TEXT NULL
);
info: 2024/8/4 12:48:11.700 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (3ms) [Parameters=[@p0='?' (Size = 5), @p1='?' (Size = 10), @p2='?' (Size = 4)], CommandType='Text', CommandTimeout='30']
INSERT INTO "tb_ims" ("Culture", "Description", "MethodDisplay")
VALUES (@p0, @p1, @p2)
RETURNING "RecoId";
info: 2024/8/4 12:48:11.712 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (0ms) [Parameters=[@p0='?' (Size = 5), @p1='?' (Size = 10), @p2='?' (Size = 4)], CommandType='Text', CommandTimeout='30']
INSERT INTO "tb_ims" ("Culture", "Description", "MethodDisplay")
VALUES (@p0, @p1, @p2)
RETURNING "RecoId";
更新记录数:2
info: 2024/8/4 12:48:11.849 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT "t"."RecoId", "t"."Culture", "t"."Description", "t"."MethodDisplay"
FROM "tb_ims" AS "t"
ID=1, Display=双拼输入, Culture=zh-CN
ID=2, Display=六指输入, Culture=zh-CN

这样你会发现,对于整数类型的主键,默认是自动生成递增ID的。注意,这个是由数据库生成的,而不是 EF Core 的生成器。不同数据库的 SQL 语句会有差异。

为了对比,咱们不防改为 SQL Server,看看输出的日志。

// 构建连接字符串
SqlConnectionStringBuilder constrbd = new();
constrbd.DataSource = ".\\SQLTEST";
constrbd.InitialCatalog = "CrazyDB";
constrbd.IntegratedSecurity = true;
// 不信任服务器证书有时候会连不上
constrbd.TrustServerCertificate = true;
// 可读可写
constrbd.ApplicationIntent = ApplicationIntent.ReadWrite; // 添加 SQL Server 功能
services.AddSqlServer<TestDBContext>(
connectionString: constrbd.ToString(),
optionsAction: opt =>
{
opt.LogTo(logmsg => Console.WriteLine(logmsg), LogLevel.Information);
});

其他代码不变,再次运行。输出的日志如下:

info: 2024/8/4 13:01:06.087 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (115ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
CREATE DATABASE [CrazyDB];
info: 2024/8/4 13:01:06.122 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (31ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
IF SERVERPROPERTY('EngineEdition') <> 5
BEGIN
ALTER DATABASE [CrazyDB] SET READ_COMMITTED_SNAPSHOT ON;
END;
info: 2024/8/4 13:01:06.137 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (5ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT 1
info: 2024/8/4 13:01:06.181 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (10ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE [tb_ims] (
[RecoId] int NOT NULL IDENTITY,
[MethodDisplay] nvarchar(12) NOT NULL,
[Description] nvarchar(max) NULL,
[Culture] nvarchar(max) NULL,
CONSTRAINT [PK_tb_ims] PRIMARY KEY ([RecoId])
);
info: 2024/8/4 13:01:06.317 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (30ms) [Parameters=[@p0='?' (Size = 4000), @p1='?' (Size = 4000), @p2='?' (Size = 12), @p3='?' (Size = 4000), @p4='?' (Size = 4000), @p5='?' (Size = 12)], CommandType='Text', CommandTimeout='30']
SET IMPLICIT_TRANSACTIONS OFF;
SET NOCOUNT ON;
MERGE [tb_ims] USING (
VALUES (@p0, @p1, @p2, 0),
(@p3, @p4, @p5, 1)) AS i ([Culture], [Description], [MethodDisplay], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Culture], [Description], [MethodDisplay])
VALUES (i.[Culture], i.[Description], i.[MethodDisplay])
OUTPUT INSERTED.[RecoId], i._Position;
更新记录数:2
info: 2024/8/4 13:01:06.438 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [t].[RecoId], [t].[Culture], [t].[Description], [t].[MethodDisplay]
FROM [tb_ims] AS [t]
ID=1, Display=双拼输入, Culture=zh-CN
ID=2, Display=六指输入, Culture=zh-CN

A、使用 Sqlite 数据库时,生成的 CREATE TABLE 语句,主键列是 PRIMARY KEY AUTOINCREMENT;

B、使用 SQL Server 时,主键列使用的是 IDENTITY,默认以 1 为种子,增量是 1。所以插入记录的键值是1和2。

有时候我们并不希望主键列自动生成值,同样有两种配置方法:

1、通过特性类来批注。如

public class InputMethod
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public ushort RecoId { get; set; }
public string? MethodDisplay { get; set; }
public string? Description { get; set; }
public string? Culture { get; set; }
}

将 DatabaseGeneratedOption 设置为 None,就取消列的自动生成了。

2、通过模型配置,即重写 OnModelCreating 方法实现。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<InputMethod>().HasKey(e => e.RecoId);
modelBuilder.Entity<InputMethod>()
.Property(k => k.RecoId)
.ValueGeneratedNever();
}

这种情况下,插入数据时主键列就需要咱们手动赋值了。

======================================================================================

上面的是热身运动,是比较简单的应用方案。下面老周给大伙伴解决一个问题。老周看到在 GitHub 等平台上有人提问,但没有得到解决。如果你看到老周这篇水文并且你有此困惑,那你运气不错。好,F话不多说,咱们看问题。

需求:主键不变,但是我不想让它带有 IDENTITY,插入记录时用我自定义的方式生成主键的值。这个需要的本质就是:我不要数据库给我生成递增ID,我要在程序里生成。

前面老周提过,默认行为下主键列如果是整数类型或 GUID,就会产生自增长的列。所以,咱们有一个很关键的步骤——就是怎么禁止 EF 去产生 IDENTITY 列。如果你看到 EF Core SQL Server 的源代码,可能你会知道有个约定类叫 SqlServerValueGenerationStrategyConvention。这个约定类默认会设置主键列的自动生成策略为 IdentityColumn。

 public virtual void ProcessModelInitialized(
IConventionModelBuilder modelBuilder,
IConventionContext<IConventionModelBuilder> context)
=> modelBuilder.HasValueGenerationStrategy(SqlServerValueGenerationStrategy.IdentityColumn);

于是,有大伙伴可能会想到,那我从 SqlServerValueGenerationStrategyConvention 派生出一个类,重写 ProcessModelInitialized 方法,把自动生成策略改为 None,然后在约定集合中替换掉 SqlServerValueGenerationStrategyConvention。

这个思路不是不行,就是工作量大一些。你不仅要定义个新类,还要把它注册到服务容器中替换 SqlServerValueGenerationStrategyConvention 。毕竟 EF Core 框架内部也是使用了服务容器和依赖注入的方式来组织各种组件的。具体做法是在初始化 DbContext 类(包括你派生的类)时会传递一个 DbContextOptions<TContext> 对象,它有一个 ReplaceService 方法,可以替换容器中的服务。在调用 AddSqlServer 方法时就可以配置。

 public static IServiceCollection AddSqlServer<TContext>(
this IServiceCollection serviceCollection,
string? connectionString,
Action<SqlServerDbContextOptionsBuilder>? sqlServerOptionsAction = null,
Action<DbContextOptionsBuilder>? optionsAction = null)
where TContext : DbContext

上述方案太麻烦,故老周未采用。其实,就算服务初始化时设置了生成策略是 Identity,可我们可以在构建模型时修改它呀。做法就是重写 DbContext 类的 OnModelCreating 方法,然后通过 IConventionModelBuilder.HasValueGenerationStrategy 方法就能修改生成策略。当然,这里头是有点波折的,我们不能在 ModelBuilder 实例上调用,因为这货并不是直接实现 IConventionModelBuilder 接口的,它是这么搞的:

public class ModelBuilder : IInfrastructure<IConventionModelBuilder>

IInfrastructure<T> 接口的作用是把 T 隐藏,不希望程序代码访问类型T。DbContext 类也实现这个接口,但它隐藏的是 IServiceProvider 对象,不想让咱们访问里面注册的服务。也就是说,IConventionModelBuilder 的实现者被隐藏了。不过,EF Core 并没有把事情做得太绝,好歹给了一个扩展方法 GetInfrastructure。用这个扩展方法我们能得到 IConventionModelBuilder 类型的引用。

弄清楚这个原理,代码就好写了。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
IConventionModelBuilder cvbd = modelBuilder.GetInfrastructure();
if (cvbd.CanSetValueGenerationStrategy(Microsoft.EntityFrameworkCore.Metadata.SqlServerValueGenerationStrategy.None))
{
cvbd.HasValueGenerationStrategy(Microsoft.EntityFrameworkCore.Metadata.SqlServerValueGenerationStrategy.None);
} ……
}

把生成策略改为 None 后,生成主键列时就不会有 IDENTITY 了。

如果你乐意,可以在插入记录时手动给主键列赋值也行的。不过,为了能自动生成值,我们应该写一个自己的生成类。

public class MyValueGenerator : ValueGenerator<int>
{
// 返回false表示这个生成的值不是临时,它最终要存入数据库的
public override bool GeneratesTemporaryValues => false; private static readonly Random rand = new((int)DateTime.Now.Ticks); public override int Next(EntityEntry entry)
{
// 获取所有实体
DbSet<InputMethod> ents = entry.Context.Set<InputMethod>();
int newID = default;
do
{
// 生成随机ID
newID = rand.Next();
}
// 保证不重复
while (ents.Any(x => x.RecoId == newID));
// 返回新值
return newID;
}
}

我这里的逻辑是这样的,值是随机生成的,但要用一个循环去检查这个值是不是已存在数据库中,如果存在,继续生成,直到数值不重复。

实现自定义生成器,有两个抽象类可供选择:

1、如果你生成的值,类型不确定(可能是int,可能是 long,可能是……),那就实现 ValueGenerator 类;

2、如果要生成的值是明确类型的,比如这里是 int,那就实现带泛型参数的 ValueGenerator<TValue> 类。

这两个类有继承关系,ValueGenerator<TValue> 派生自 ValueGenerator 类。需要实现的抽象成员:

A、GeneratesTemporaryValues 属性:只读,返回 bool 值。如果你生成的值是临时的,返回 true,不是临时的,返回 false。啥意思呢。临时的值表示暂时赋值给属性/字段,但 INSERT、UPDATE 时,这个值不会存入数据库;如果不是临时的值,最终会存进数据库。上面例子中,老周让它返回 false,就说明生成的这个值,要写入数据库的。

B、如果继承 ValueGenerator 类,请实现 NextValue 抽象方法,返回类型是 object,就是生成的值;如果继承的是 ValueGenerator<TValue>,请实现 Next 方法,此方法返回的类型由泛型参数决定。上面例子中是 int。

写好生成类后,要把它应用到实体模型中,同样是重写 DbContext 类的 OnModelCreating 方法。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
IConventionModelBuilder cvbd = modelBuilder.GetInfrastructure();
if (cvbd.CanSetValueGenerationStrategy(Microsoft.EntityFrameworkCore.Metadata.SqlServerValueGenerationStrategy.None))
{
cvbd.HasValueGenerationStrategy(Microsoft.EntityFrameworkCore.Metadata.SqlServerValueGenerationStrategy.None);
} modelBuilder.Entity<InputMethod>().HasKey(e => e.RecoId);
modelBuilder.Entity<InputMethod>()
.Property(k => k.RecoId)
.HasValueGenerator<MyValueGenerator>()
.ValueGeneratedOnAdd();
modelBuilder.Entity<InputMethod>().ToTable("tb_ims")
.Property(x => x.MethodDisplay)
.IsRequired()
.HasMaxLength(12);
}

ValueGeneratedOnAdd 方法表示在记录插入数据库时自动生成值,HasValueGenerator 方法设置你自定义的生成器。

现在,有了自定义生成规则,在插入数据时,主键不能赋值。一旦赋值,生成器就无效了。

// 尝试插入两条记录
InputMethod[] ents = [
new(){ MethodDisplay = "双拼输入", Description="按两个键完成一个音节",Culture="zh-CN"},
new() { MethodDisplay = "六指输入", Description="专供六个指头的人使用",Culture="zh-CN"}
];
dbc.Set<InputMethod>().AddRange(ents);
int result = dbc.SaveChanges();

运行应用程序,你会发现,这次生成的 CREATE TABLE 语句中,RecoId 列已经没有 IDENTITY 关键字了。

info: 2024/8/4 18:41:24.956 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (12ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT 1
info: 2024/8/4 18:41:24.982 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (4ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
IF SERVERPROPERTY('EngineEdition') <> 5
BEGIN
ALTER DATABASE [CrazyDB] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
END;
info: 2024/8/4 18:41:25.003 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (21ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
DROP DATABASE [CrazyDB];
info: 2024/8/4 18:41:25.104 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (82ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
CREATE DATABASE [CrazyDB];
info: 2024/8/4 18:41:25.137 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (32ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
IF SERVERPROPERTY('EngineEdition') <> 5
BEGIN
ALTER DATABASE [CrazyDB] SET READ_COMMITTED_SNAPSHOT ON;
END;
info: 2024/8/4 18:41:25.142 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT 1
info: 2024/8/4 18:41:25.194 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (6ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE [tb_ims] (
[RecoId]
int NOT NULL,
[MethodDisplay] nvarchar(12
) NOT NULL,
[Description] nvarchar(max) NULL,
[Culture] nvarchar(max) NULL,
CONSTRAINT [PK_tb_ims] PRIMARY KEY ([RecoId])
);

info: 2024/8/4 18:41:25.408 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (24ms) [Parameters=[@__newID_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT CASE
WHEN EXISTS (
SELECT 1
FROM [tb_ims] AS [t]
WHERE [t].[RecoId] = @__newID_0) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
info: 2024/8/4 18:41:25.448 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (1ms) [Parameters=[@__newID_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT CASE
WHEN EXISTS (
SELECT 1
FROM [tb_ims] AS [t]
WHERE [t].[RecoId] = @__newID_0) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
info: 2024/8/4 18:41:25.488 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (2ms) [Parameters=[@p0='?' (DbType = Int32), @p1='?' (Size = 4000), @p2='?' (Size = 4000), @p3='?' (Size = 12), @p4='?' (DbType = Int32), @p5='?' (Size = 4000), @p6='?' (Size = 4000), @p7='?' (Size = 12)], CommandType='Text', CommandTimeout='30']
SET IMPLICIT_TRANSACTIONS OFF;
SET NOCOUNT ON;
INSERT INTO [tb_ims] ([RecoId], [Culture], [Description], [MethodDisplay])
VALUES (@p0, @p1, @p2, @p3),
(@p4, @p5, @p6, @p7);
更新记录数:2
info: 2024/8/4 18:41:25.524 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [t].[RecoId], [t].[Culture], [t].[Description], [t].[MethodDisplay]
FROM [tb_ims] AS [t]
ID=427211935, Display=六指输入, Culture=zh-CN
ID=1993200136, Display=双拼输入, Culture=zh-CN

怎么样,这玩法是不是很高端?当然,如果主键是字符串类型,你也可以生成字符串的值,一切看你需求,反正原理是相同的。

最后,咱们顺便聊聊如何自动更改日期时间的问题。这个在实际开发中也很常用,比如一个计划表,其实体如下:

public class Plan
{
/// <summary>
/// 计划ID
/// </summary>
public int ID { get; set; }
/// <summary>
/// 计划简述
/// </summary>
public string? PlanDesc { get; set; }
/// <summary>
/// 计划级别
/// </summary>
public int Level { get; set; }
/// <summary>
/// 计划创建时间
/// </summary>
public DateTime? CreateTime { get; set; }
/// <summary>
/// 总计划量
/// </summary>
public float TotalTask { get; set; }
/// <summary>
/// 完成量
/// </summary>
public float Completed { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime? UpdateTime { get; set; }
}

最后一个字段 UpdateTime 表示在插入后更新的时间,所以在插入时这个字段可以留 NULL。比如我修改计划完成数 Completed,在写入数据库时自动给 UpdateTime 字段赋当前时间。这个不能用值生成器来做,因为生成器只能在数据插入前或插入后产生一次值,后面更新数据时不会再生成新值,就做不到自动设置更新时间了。所以,这里咱们可以换个思路:重写 DbContext 类的 SaveChanges 方法,在命令发送到数据库之前找出哪些记录被修改过,然后设置 UpdateTime 属性,最后才发送 SQL 语句。这样也能达到自动记录更新时间的功能。

public class MyDBContext : DbContext
{
…… public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
var modifieds = from c in ChangeTracker.Entries()
where c.State == EntityState.Modified
&& c.Entity is Plan
select c;
foreach(var obj in modifieds)
{
obj.Property(nameof(Plan.UpdateTime)).CurrentValue = DateTime.Now;
}
return base.SaveChanges(acceptAllChangesOnSuccess);
}
}

Modified 表示实体被更改过的状态。修改属性值时,应赋值给 CurrentValue,它代表的是实体当前的值,不要改 OriginalValue 的值,它指的是从数据库中读到的值,多数情况下不用去改,除非你要把当前 DbContext 实例的数据复制到另一个 DbContext 实例。

这样当 Plan 对象被修改后,在提交前会自动设置更新时间。下面是测试代码:

 // 创建上下文
using var ctx = new MyDBContext();
// 测试用,确定删除数据库
ctx.Database.EnsureDeleted();
// 确定创建数据库
ctx.Database.EnsureCreated(); // 创建三条记录
Plan p01 = new()
{
PlanDesc = "装配电池",
CreateTime = DateTime.Now,
TotalTask = 100f,
Completed = 0f,
};
Plan p02 = new Plan()
{
PlanDesc = "更换底板",
CreateTime = DateTime.Now,
Level = 4,
TotalTask = 12.0f,
Completed = 0f
};
Plan p03 = new()
{
PlanDesc = "清洗盖板",
TotalTask = 20.5f,
Completed = 0f,
CreateTime = DateTime.Now
};
ctx.Plans.Add(p01);
ctx.Plans.Add(p02);
ctx.Plans.Add(p03);
// 更新到数据库
int n = ctx.SaveChanges();
Console.WriteLine($"已插入{n}条记录"); // 打印数据
Print(ctx.Plans);
MODIFY: // 这是个标签
Console.Write("请输入要更新的记录ID:");
string? line = Console.ReadLine();
if(line == null)
{
Console.WriteLine("你输入了吗?");
goto MODIFY; // 回到标签处
}
if(!int.TryParse(line, out int id))
{
Console.WriteLine("你丫的输入的是整数吗?");
goto MODIFY; // 回到标签处
}
UPDATE: // 标签
Console.Write("请输入计划完成数:");
line = Console.ReadLine();
if (line == null)
{
Console.WriteLine("你确定你没敲错键盘?");
goto UPDATE;
}
if(!float.TryParse(line, out float comp))
{
Console.WriteLine("浮点数,浮点数,浮点数");
goto UPDATE;
}
// 查找
Plan? curPlan = ctx.Plans.FirstOrDefault(x => x.ID == id);
if (curPlan == null)
{
Console.WriteLine("找不到记录");
goto MODIFY;
}
if(comp > curPlan.TotalTask)
{
Console.WriteLine("你是在异空间工作吗?");
goto UPDATE;
}
// 更新
curPlan.Completed = comp;
ctx.SaveChanges(); // 再次打印
Print(ctx.Plans);

先插入三条数据,然后输入记录ID来修改 Completed 的值。更改后会看到更新时间。

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

【EF Core】自动生成的字段值的更多相关文章

  1. Vertica系列: 自动生成Identity 字段值的方法

    参考 https://thisdataguy.com/2015/01/05/vertica-some-uses-of-sequences/ 在 vertica 中有三种定义 identity 字段的方 ...

  2. 使用ef core自动生成mysql表和数据编码的问题

    mysql默认的编码是不支持中文的,需要改成utf8编码格式. 而我使用的Pomelo.EntityFrameworkCore.MySql组件生成mysql库和表,他是使用默认编码的. 网上大多说修改 ...

  3. 在PowerDesigner中自动生成sqlserver字段备注

    在PowerDesigner中自动生成sqlserver字段备注 PowerDesigner是数据库设计人员常用的设计工具,但其自生默认生成的代码并不会生成sqlserver数据库的字段备注说明.在生 ...

  4. 9.1.2 asp.net core 自动生成组合查询

    在做系统的时候,经常遇到前台录入一大堆的查询条件,然后点击查询提交后台,在Controller里面生成对应的查询SQL或者表达式,数据库执行再将结果返回客户端. 例如如下页面,输入三个条件,日志类型. ...

  5. WithOne 实体关系引起 EF Core 自动删除数据

    最近遇到了一个 EF Core 的恐怖问题,在添加数据时竟然会自动删除数据库中已存在的数据,经过追查发现是一个多余的实体关系配置引起的. modelBuilder.Entity<Question ...

  6. DDD Code First 迁移数据实现EF CORE的软删除,值对象迁移配置

    感谢Jeffcky大佬的博客: EntityFramework Core 2.0全局过滤 (HasQueryFilter) https://www.cnblogs.com/CreateMyself/p ...

  7. JAVA中自定义扩展Swagger的能力,自动生成参数取值含义说明,提升开发效率

    大家好,又见面了. 在JAVA做前后端分离的项目开发的时候,服务端需要提供接口文档供周边人员做接口的对接指导.越来越多的项目都在尝试使用一些基于代码自动生成接口文档的工具来替代由开发人员手动编写接口文 ...

  8. DAO以及获取自动生成主键值

    package com.alibaba.sql; import java.lang.reflect.InvocationTargetException; import java.sql.Connect ...

  9. ef core自动映射

    原回答:https://stackoverflow.com/questions/26957519/ef-core-mapping-entitytypeconfiguration 一.反射 protec ...

  10. JavaWeb学习记录(二十四)——获取插入数据后,自动生成的id值

    public Integer insertObjects(final Goods entity) { // 定义sql语句        final String sql1 = "inser ...

随机推荐

  1. java.sql.SQLException: Connection is read-only. Queries leading to data modification are not

    java.sql.SQLException: Connection is read-only. Queries leading to data modification are not 产生的原因:事 ...

  2. idea设置jdk和设置文件编码格式utf-8

    1.idea设置jdk 2.idea设置文件编码格式utf-8 create utf-8 files with NO BOM 不要更改,否则编译会出错误.

  3. redis zset 延迟合并任务处理

    redis zset 延迟合并任务处理 @Autowired public RedisTemplate redisTemplate; ##1.发送端:在接口中收集任务ID,累计时间段之后,合并处理. ...

  4. 1024程序员节,写最棒的coding,做最靓的仔

    Tips:当你看到这个提示的时候,说明当前的文章是由原emlog博客系统搬迁至此的,文章发布时间已过于久远,编排和内容不一定完整,还请谅解` 1024程序员节,写最棒的coding,做最靓的仔 日期: ...

  5. HTTP长连接、短连接、Linux网络优化

    无连接 含义:每次传输完数据后就断开连接. 因为早期互联网规模小,并且http具有瞬时性,突发性,服务器同时处理着多个请求.所以采用无连接的方式.以便于腾出资源处理其他请求. 无状态 顺便说一说无状态 ...

  6. 在Linux驱动中使用notifier通知链

    在Linux驱动中使用notifier通知链 背景 在驱动分析中经常看到fb_notifier_callback,现在趁有空学习一下. 介绍 linux中的观察者模式是最显然的就是"通知链& ...

  7. vba--数组

    Sub shishi() Range("e2") = Split(Range("e1"), "-")(0) '用短横线分隔后取第1个值 En ...

  8. (转载)linux命令英文缩写的含义(方便记忆)

    linux常用命令的英文单词缩写 命令缩写: ls:list(列出目录内容) cd:Change Directory(改变目录) su:switch user 切换用户rpm:redhat packa ...

  9. 韦东山freeRTOS系列教程之【第十章】软件定时器(software timer)

    目录 系列教程总目录 概述 10.1 软件定时器的特性 10.2 软件定时器的上下文 10.2.1 守护任务 10.2.2 守护任务的调度 10.2.3 回调函数 10.3 软件定时器的函数 10.3 ...

  10. LVS介绍与配置

    目录 LVS(Linux Virtual Server) 1. 概述 1.1 LVS简介 1.2 LVS架构 2. LVS工作模式 2.1 NAT模式(Network Address Translat ...