首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

【EF Core】自动生成的字段值

编程知识
2024年08月04日 22:28

自动生成字段值,咱们首先想到的是主键列(带 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 的值。更改后会看到更新时间。

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

 

From:https://www.cnblogs.com/tcjiaan/p/18340772
本文地址: http://shuzixingkong.net/article/783
0评论
提交 加载更多评论
其他文章 从零体检一个魔塔社区模型(modelscope)最简单demo
从社区拿一个模型,比如以下这个链接 https://www.modelscope.cn/models/iic/cv_mobilenet-v2_bad-image-detecting 它的代码样例如下 from modelscope.pipelines import pipeline from mod
从零体检一个魔塔社区模型(modelscope)最简单demo 从零体检一个魔塔社区模型(modelscope)最简单demo
Vue Vine:带给你全新的 Vue 书写体验!
你好,我是 Kagol,个人公众号:前端开源星球。 上个月和 TinyVue 的小伙伴们一起参加了 VueConf 24 大会,有幸认识沈青川大佬,并了解了他的 Vue Vine 项目,Vue Vine 让你可以在一个文件中通过函数方式定义多个 Vue 组件,同时可以使用所有 Vue 的模板特性。
Vue Vine:带给你全新的 Vue 书写体验! Vue Vine:带给你全新的 Vue 书写体验! Vue Vine:带给你全新的 Vue 书写体验!
英语.Net多语言开发中的问题
问题与现象 多语言开发是一件成本很高的事情。 很多公司会退而求其次选择只开发英文版本的软件分发到不同国家,但这里仍存在不同问题。 我们就遇到了这样的问题,参考下面的代码。 CultureInfo&#160;culture;double&#160;number&#160;=1.5; culture&#
程序员副业探索之电商
在腾讯广告工作期间,我主要负责小程序电商与广告业务,见证了互联网电商行业的剧变,特别是众多电商公司纷纷拥抱私域流量,直播带货成为新风尚,广告投入也在持续增加。通过这些经历,我积累了不少关于互联网电商的经验,并萌生了尝试电商副业的想法。 在小红书上,女装博主们凭借独特的穿搭分享吸引了大量粉丝,“种草”
程序员副业探索之电商 程序员副业探索之电商 程序员副业探索之电商
实现一个终端文本编辑器来学习golang语言:第三章文本查看器part1
本章我们来完成文本编辑器的文件打开和查看功能,最后成品如上图。我们将分4步,逐渐完成本章所需功能。内容比较多,会分为两个部分,第一部分主要关注于“View视图”和“buffer及文本读取”。 如上图最终效果所示,我们希望在终端的最下方增加一个状态栏,能够展示当前被打开的文件和当前的光标位置。 同时我
实现一个终端文本编辑器来学习golang语言:第三章文本查看器part1 实现一个终端文本编辑器来学习golang语言:第三章文本查看器part1
PowerBI_一分钟学会利用ALLEXCPET分组计算(以计算门店开业前3天销售金额为例)
在某些特殊场景,我们往往需要去计算一些特定的组别的聚合数据 今天,就以计算门店开业前3天的销售情况,来学习一下,利用计算列和DAX度量值,两种快捷计算此类问题的方案。 一:XMIND 二:示例数据 2.1 示例数据列说明 为了方便验证和更清晰的检查结果,数据源只用了三列,分别是3个门店,分别为A,B
PowerBI_一分钟学会利用ALLEXCPET分组计算(以计算门店开业前3天销售金额为例) PowerBI_一分钟学会利用ALLEXCPET分组计算(以计算门店开业前3天销售金额为例) PowerBI_一分钟学会利用ALLEXCPET分组计算(以计算门店开业前3天销售金额为例)
golang 指定权限是 0o755 而不是 0755
在Go语言中,当指定文件权限时,使用前缀 0o 来明确表示八进制数是一种推荐的做法。 这是因为在Go语言中,八进制字面量必须以 0o 或 0O 开头,后跟八进制数字(0-7)。 这种语法是从 Go 1.8 开始引入的,目的是为了减少由于 八进制 字面量与零开头的 十进制数 之间的混淆。 在更早的 G
推荐一款.NET开源、功能强大的二维码生成类库
前言 在日常开发需求中,生成二维码以分享文本内容或跳转至指定网站链接等场景是比较常见的。今天大姚给大家分享一款.NET开源(MIT License)、免费、简单易用、功能强大的二维码生成类库:QrCodeGenerator。 项目特点 跨平台兼容性:&#160;支持.NET Standard 2.0
推荐一款.NET开源、功能强大的二维码生成类库 推荐一款.NET开源、功能强大的二维码生成类库 推荐一款.NET开源、功能强大的二维码生成类库