自動生成欄位值,咱們首先想到的是主鍵列(帶 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 的值。更改後會看到更新時間。
好了,今天咱們就水到這裡了。