在資料的海洋裡,每一筆 Entity 都像一艘小船。有的剛啟航(Added),有的正返港(Modified),有的靜靜停泊(Unchanged),而有的早已脫離航道(Detached)。
EF Core 就像港口的管理員,它用 Entry() 觀察著每艘船的狀態,用 Attach() 告訴它:「這艘船其實還在港裡,只是暫時沒動而已。」
在 EF Core 中,每個資料表對應一個「Entity 類別」,例如 Student
1 2 3 4 5 public class Student { public int Id { get ; set ; } public string Name { get ; set ; } }
有時候,我們想知道 EF Core 現在怎麼看這個物件,例如:它是不是新資料?是不是被修改過?是不是要刪掉?
這時候可以用
1 var entry = context.Entry(student);
這行會回傳一個 EntityEntry 物件。可以想成:EF Core 給你一張「狀態說明書」,上面寫著這個 student 物件目前的狀態
entry.State 就是 EF Core 對這個物件的「態度」,常見有 5 種狀態
狀態
意思
SaveChanges() 時會發生什麼事
Added
這是新資料,還沒存進資料庫
會執行 INSERT
Modified
資料有被修改過
會執行 UPDATE
Deleted
要刪除這筆資料
會執行 DELETE
Unchanged
資料跟資料庫一樣,沒變
不動作
Detached
不被追蹤(EF 不理它)
不動作
這樣設定代表:「EF Core,這個物件已經被修改了,等一下請幫我 UPDATE。」
例如
1 2 3 var student = new Student { Id = 1 , Name = "小明" };context.Entry(student).State = EntityState.Modified; context.SaveChanges();
這樣就算你沒有真的改過 Name,EF Core 還是會執行,因為你直接告訴 EF:「整個物件都改了」,EF 只好「整筆更新」
1 UPDATE Students SET Name = '小明' WHERE Id = 1 ;
1 context.Entry(student).Property(x => x.Name).IsModified = true ;
這樣 EF Core 只會更新 Name 欄位,不會動其他欄位。這就比較精確,也比較有效率
Attach() 是讓 EF Core 開始追蹤一個物件,但假設它「目前沒有被修改」
1 context.Attach(student);
等同於
1 context.Entry(student).State = EntityState.Unchanged;
EF Core 的想法是:「OK,我知道這個物件存在資料庫,而且它跟資料庫目前的資料是一樣的。」
所以當你呼叫 SaveChanges() 時,EF 不會做任何事
但如果你之後修改它
1 2 student.Name = "小華" ; context.SaveChanges();
這時 EF Core 就會知道「喔!Name 改了!」,於是執行
1 UPDATE Students SET Name = '小華' WHERE Id = 1 ;
如果你 Attach() 一個物件時,沒設定主鍵 (Primary Key),EF Core 會以為它是「新資料」,狀態就會變成 Added,因為 EF Core 不知道它要對應哪一筆資料。 反之,如果主鍵有值,它會判定為「舊資料」,狀態是 Unchanged
1 2 3 4 5 this ._printColorThing("=== Exp1 Attach / 不指定 Id===" , ConsoleColor.Magenta);var newGenrd = _generateCuteRd();_dbContext.Attach(newGenrd); this ._printColorThing("未指定自動跳號 Primary Key 時,State = \" + entEntry.State" , ConsoleColor.Green);await _dbContext.SaveChangesAsync();
EF Core 看到沒有主鍵 (Id) 值,會誤以為這是新資料,所以 record 的 State 會是 Added,SaveChanges() 時會執行 INSERT,須注意若資料表 Date 欄位有 UNIQUE 限制,而你插入的日期在資料庫裡已經存在,那就會違反 UNIQUE 限制,產生錯誤。
1 2 3 4 5 6 7 8 9 10 11 === Exp1 Attach / 不指定 Id=== warn: Microsoft.EntityFrameworkCore.Model.Validation[10400] Sensitive data logging is enabled. Log entries and exception messages may include sensitive application data; this mode should only be enabled during development. 未指定自動跳號 Primary Key 時,State = Added info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (20ms) [Parameters=[@p0='CUTE' (Size = 5), @p1='CuteRD' (Size = 100)], CommandType='Text' , CommandTimeout='30' ] SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; INSERT INTO [RD] ([DeptCode], [RD_Name]) OUTPUT INSERTED.[RD_ID] VALUES (@p0, @p1);
1 2 3 4 5 6 7 8 9 10 11 12 13 this ._printColorThing("=== 實驗二 Attach / 查詢過同一筆無法 Attach" , ConsoleColor.Magenta);var newGenrd2 = _generateCuteRd();var lookupRd = await _dbContext.Rds.FirstAsync(rd => rd.RdName == newGenrd2.RdName);newGenrd2.RdId = lookupRd.RdId; _dbContext.Attach(newGenrd); try { _dbContext.Attach(newGenrd2); } catch (Exception ex){ this ._printColorThing(ex.ToString(), ConsoleColor.Red); }
因為這筆資料已經被 dbCtx 查出來(已被追蹤),EF Core 的追蹤系統中已經有相同 Id 的實體存在,再 Attach() 一個相同 Id 的物件會出錯
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 === 實驗二 Attach / 查詢過同一筆無法 Attach info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (2ms) [Parameters=[@__newGenrd2_RdName_0='CuteRD' (Size = 100)], CommandType='Text' , CommandTimeout='30' ] SELECT TOP(1) [r].[RD_ID], [r].[DeptCode], [r].[RD_Name] FROM [RD] AS [r] WHERE [r].[RD_Name] = @__newGenrd2_RdName_0 System.InvalidOperationException: The instance of entity type 'Rd' cannot be tracked because another instance with the key value '{RdId: 1004}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.ThrowIdentityConflict(InternalEntityEntry entry) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry, Boolean updateDuplicate) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(InternalEntityEntry entry) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(InternalEntityEntry entry) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState oldState, EntityState newState, Boolean acceptChanges, Boolean modifyProperties) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState entityState, Boolean acceptChanges, Boolean modifyProperties, Nullable`1 forceStateWhenUnknownKey, Nullable`1 fallbackState) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.PaintAction(EntityEntryGraphNode`1 node) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1 node, Func`2 handleNode) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.AttachGraph(InternalEntityEntry rootEntry, EntityState targetState, EntityState storeGeneratedWithKeySetTargetState, Boolean forceStateWhenUnknownKey) at Microsoft.EntityFrameworkCore.DbContext.SetEntityState(InternalEntityEntry entry, EntityState entityState) at Microsoft.EntityFrameworkCore.DbContext.SetEntityState[TEntity](TEntity entity, EntityState entityState) at Microsoft.EntityFrameworkCore.DbContext.Attach[TEntity](TEntity entity) at WTF.Console.Repository.AdventureRepository.AttchStateExp() in C:\91APP\AI_Devs\Random\nine1.wtf.did.i.code\WTF.Console\WTF.Console\Repository\AdventureRepository.cs:line 182
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 int rdId;this ._printColorThing("=== 實驗二 Attach / 查詢過同一筆無法 Attach" , ConsoleColor.Magenta);var newGenrd2 = _generateCuteRd();var lookupRd = await _dbContext.Rds.FirstOrDefaultAsync(rd => rd.RdName == newGenrd2.RdName);rdId = lookupRd.RdId; newGenrd2.RdId = lookupRd.RdId; try { _dbContext.Attach(newGenrd2); } catch (Exception ex){ this ._printColorThing(ex.ToString(), ConsoleColor.Red); } using (var scope = _serviceProvider.CreateScope()){ var dbCtx = scope.ServiceProvider.GetRequiredService<AdventureWorks2022DbContext>(); this ._printColorThing("=== 實驗三 Attach / 指定 Id" , ConsoleColor.Magenta); var newGenrd3 = _generateCuteRd(); newGenrd3.RdId = rdId; var entry3 = dbCtx.Attach(newGenrd3); this ._printColorThing($"未指定自動跳號 Primary Key 時,State = {entry3.State} " , ConsoleColor.Green); await dbCtx.SaveChangesAsync(); }
1 2 === 實驗三 Attach / 指定 Id 未指定自動跳號 Primary Key 時,State = Unchanged
因有設定 Id,EF Core 知道這是「舊資料」,所以狀態是 Unchanged,SaveChanges() 時什麼都不會發生,因為 EF 以為「資料跟資料庫一樣」
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 using (var scope = _serviceProvider.CreateScope()){ var dbCtx = scope.ServiceProvider.GetRequiredService<AdventureWorks2022DbContext>(); this ._printColorThing("=== 實驗三 Attach / 指定 Id" , ConsoleColor.Magenta); var newGenrd3 = _generateCuteRd(); newGenrd3.RdId = rdId; var entry3 = dbCtx.Attach(newGenrd3); this ._printColorThing($"未指定自動跳號 Primary Key 時,State = {entry3.State} " , ConsoleColor.Green); await dbCtx.SaveChangesAsync(); this ._printColorThing("=== 實驗四 Attach 後修改某欄位" , ConsoleColor.Magenta); newGenrd3.RdName = "AllenIsGood" ; await dbCtx.SaveChangesAsync(); this ._printColorThing("更新異動了的欄位!" , ConsoleColor.Green); }
EF Core 的變更追蹤機制會偵測到屬性變更,自動把 State 從 Unchanged 改成 Modified,SaveChanges() 時只會執行
1 UPDATE RD SET RDName = 'AllenIsGood' WHERE Id = 1004 ;
1 2 3 4 5 6 7 8 === 實驗四 Attach 後修改某欄位 info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (10ms) [Parameters=[@p1='1004' , @p0='AllenIsGood' (Size = 100)], CommandType='Text' , CommandTimeout='30' ] SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; UPDATE [RD] SET [RD_Name] = @p0 OUTPUT 1 WHERE [RD_ID] = @p1;
這是最正常、最安全的更新方式
1 2 3 4 5 6 7 8 9 10 using (var scope = _serviceProvider.CreateScope()){ var dbCtx = scope.ServiceProvider.GetRequiredService<AdventureWorks2022DbContext>(); this ._printColorThing("=== 實驗五 Entry().State = EntityState.Modified" , ConsoleColor.Magenta); var newGenrd4 = this ._generateCuteRd(); newGenrd4.RdId = rdId; dbCtx.Entry(newGenrd4).State = EntityState.Modified; await dbCtx.SaveChangesAsync(); this ._printColorThing("更新所有欄位,不管是否與原來相同" , ConsoleColor.Green); }
1 2 3 UPDATE [RD] SET [DeptCode] = @p0 , [RD_Name] = @p1 OUTPUT 1 WHERE [RD_ID] = @p2 ;
1 2 3 4 5 6 7 8 9 === 實驗五 Entry().State = EntityState.Modified info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (7ms) [Parameters=[@p2='1005' , @p0='CUTE' (Size = 5), @p1='CuteRD' (Size = 100)], CommandType='Text' , CommandTimeout='30' ] SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; UPDATE [RD] SET [DeptCode] = @p0, [RD_Name] = @p1 OUTPUT 1 WHERE [RD_ID] = @p2; 更新所有欄位,不管是否與原來相同
這樣做會把整個物件標示為「被修改」,即使只有一個欄位真的改了,EF Core 仍會執行
1 2 3 4 5 6 7 8 9 10 11 using (var scope = _serviceProvider.CreateScope()){ var dbCtx = scope.ServiceProvider.GetRequiredService<AdventureWorks2022DbContext>(); this ._printColorThing("=== 實驗六 SetValues()" , ConsoleColor.Magenta); var newGenrd6 = this ._generateCuteRd(); var dbRd = dbCtx.Rds.Find(rdId); newGenrd6.RdId = rdId; dbCtx.Entry(dbRd).CurrentValues.SetValues(newGenrd6); await dbCtx.SaveChangesAsync(); this ._printColorThing("只會更新異動欄位" , ConsoleColor.Green); }
SetValues() 會把來源物件的屬性值貼到已追蹤的 newGenrd6,但它只會標示出真正不同的屬性為 Modified 結果就是只更新異動的欄位
1 2 3 UPDATE [RD] SET [RD_Name] = @p0 OUTPUT 1 WHERE [RD_ID] = @p1 ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 === 實驗六 SetValues() info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (3ms) [Parameters=[@__p_0='1004' ], CommandType='Text' , CommandTimeout='30' ] SELECT TOP(1) [r].[RD_ID], [r].[DeptCode], [r].[RD_Name] FROM [RD] AS [r] WHERE [r].[RD_ID] = @__p_0 info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (5ms) [Parameters=[@p1='1004' , @p0='rrD6' (Size = 100)], CommandType='Text' , CommandTimeout='30' ] SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; UPDATE [RD] SET [RD_Name] = @p0 OUTPUT 1 WHERE [RD_ID] = @p1; 只會更新異動欄位
SetValues() 可以吃不同型別的來源,只要屬性名稱對得上
✅ ViewModel (常見) 、✅ 匿名類別
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 using (var scope = _serviceProvider.CreateScope()){ var dbCtx = scope.ServiceProvider.GetRequiredService<AdventureWorks2022DbContext>(); this ._printColorThing("=== 實驗七 SetValues(任意物件)" , ConsoleColor.Magenta); this ._printColorThing($" rdId : {rdId} " , ConsoleColor.DarkGreen); var dbRd = dbCtx.Rds.Find(rdId); this ._printColorThing($" name : {dbRd.RdName} , deptCode : {dbRd.DeptCode} " , ConsoleColor.DarkGreen); dbCtx.Entry(dbRd).CurrentValues.SetValues(new { RdName = "RandomName2" , DeptCode = "RD123" }); await dbCtx.SaveChangesAsync(); this ._printColorThing("只會更新異動欄位" , ConsoleColor.Green); }
1 2 3 UPDATE [RD] SET [DeptCode] = @p0 , [RD_Name] = @p1 OUTPUT 1 WHERE [RD_ID] = @p2 ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 === 實驗七 SetValues(任意物件) rdId : 1008 info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (4ms) [Parameters=[@__p_0='1008' ], CommandType='Text' , CommandTimeout='30' ] SELECT TOP(1) [r].[RD_ID], [r].[DeptCode], [r].[RD_Name] FROM [RD] AS [r] WHERE [r].[RD_ID] = @__p_0 name : CuteRD, deptCode : CUTE info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (9ms) [Parameters=[@p2='1008' , @p0='RD123' (Size = 5), @p1='RandomName2' (Size = 100)], CommandType='Text' , CommandTimeout='30' ] SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; UPDATE [RD] SET [DeptCode] = @p0, [RD_Name] = @p1 OUTPUT 1 WHERE [RD_ID] = @p2; 只會更新異動欄位
在此更新了 RdName、DeptCode,因為都與 dbRd 值有所不同
✅ Dictionary<string, object>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 using (var scope = _serviceProvider.CreateScope()){ var dbCtx = scope.ServiceProvider.GetRequiredService<AdventureWorks2022DbContext>(); this ._printColorThing("=== 實驗八 SetValues(Dictionary<string, object>) ===" , ConsoleColor.Magenta); this ._printColorThing($" rdId : {rdId} " , ConsoleColor.DarkGreen); var dbRd = dbCtx.Rds.Find(rdId); this ._printColorThing($" name : {dbRd.RdName} , deptCode : {dbRd.DeptCode} " , ConsoleColor.DarkGreen); dbCtx.Entry(dbRd).CurrentValues.SetValues(new Dictionary<string ,object >() { {"RdName" , "RandomName3" }, {"DeptCode" , "RD456" } }); await dbCtx.SaveChangesAsync(); this ._printColorThing("只會更新異動欄位" , ConsoleColor.Green); }
1 2 3 UPDATE [RD] SET [DeptCode] = @p0 , [RD_Name] = @p1 OUTPUT 1 WHERE [RD_ID] = @p2 ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 === 實驗八 SetValues(Dictionary<string, object>) === rdId : 1009 info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (3ms) [Parameters=[@__p_0='1009' ], CommandType='Text' , CommandTimeout='30' ] SELECT TOP(1) [r].[RD_ID], [r].[DeptCode], [r].[RD_Name] FROM [RD] AS [r] WHERE [r].[RD_ID] = @__p_0 name : CuteRD, deptCode : CUTE info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (9ms) [Parameters=[@p2='1009' , @p0='RD456' (Size = 5), @p1='RandomName3' (Size = 100)], CommandType='Text' , CommandTimeout='30' ] SET IMPLICIT_TRANSACTIONS OFF; SET NOCOUNT ON; UPDATE [RD] SET [DeptCode] = @p0, [RD_Name] = @p1 OUTPUT 1 WHERE [RD_ID] = @p2; 只會更新異動欄位
EF Core 會比對屬性名稱 → 只更新對得上的欄位 → 只更新值真的不同的欄位!
每個 Entity,其實都是我們在資料海上的分身。有時我們被追蹤(Tracked),有時選擇脫離(Detached);有時整個重來(Modified),有時只是微調(Property.IsModified = true)。Attach() 教我們——不是所有變動都需要重新出發;Entry().State 告訴我們——理解狀態,比盲目更新更重要。
在日常開發中,懂得何時該「讓 EF Core 靜靜觀察」,何時該「主動標記變更」,就像在大海上航行時,懂得何時升帆、何時收錨