使用 EF Core Attach() 與 Entry().State 進行更新
1 | 4,291 |
昨天提到 EF Core 新增或更新資料 (UPSERT) 的簡便寫法,有讀者朋友提到是否能用 Attach() 跟 Entry().State = EntityState.Modified; 取代 Entry().CurrentValues.SetValues()?
感覺這得配合範例比較好解釋,所以關於這個議題,就專門寫一篇文章跟大家講解好了。(老高上身)
先說 Entry(),Entry(資料庫Entity物件) 會傳回該 Entity 的 EntityEntry 物件,方便我們存取它的變更追蹤資訊(Change Tracking Information),EntityEntry 最常用的屬性是 State,共有 Added、Deleted、Detached、Modified、Unchanged 等幾種狀態,我們可以透過設定 State 控制 EF Core 在 SaveChanges() 時要執行 INSERT、DELETE 還是 UPDATE 動作。
因此,Entry(record).State = EntityState.Modified 等同將 record 物件列為追蹤對象,並標註為物件已被修改,之後 SaveChanges() 時便會用它對資料執行 UPDATE。由於整顆物件被標為己修改,故 UPDATE 時每個屬性都要 SET 覆寫。(前篇文章提到的 CurrentValues.SetValues() 則會追蹤哪些欄位有改,哪些沒變,UPDATE 時只更新有異動的欄位)
至於 Attach(record),等同 Entry(record).State = EntityState.Unchanged,會將 record 列入追蹤對象。若 Primary Key 採自動跳號,Sate 可能是 Added 或 Unchanged,未指定 Primary Key 時 State 為 Added,否則為 Unchanged。當 State 為 Unchanged 時,EF Core 會假裝 Entity 所有屬性跟資料庫端一致,未被修改過,故呼叫 SaveChanges() 將不會觸發資料庫動作,因為從 EF Core 角度,Entity 沒被改何需更新?Attach(record) 後若修改 record.SomeProp,則 EF Core 會追蹤到 SomeProp 屬性改變,下次 SaveChanges() 會 UPDATE SET 更新 SomeProp。
以上就是使用 Entry(record).State = EntityState.Modified、Attach(record) 預期會發生的事。讓我們做幾個實驗來驗證一下。
先準備一個公用函式類別,提供測試所需的功能,包含建立使用 SQLite 記憶體資料庫作為資料來源的 JournalDbContext、加掛可動態開啟關閉的 Log 輸出以觀察 SQL 語法、可指定顏色 Console.WriteLine 的顯示函式:
using CRUDExample.Models;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
namespace efcore_attach
{
public static class DbCtxHelper
{
// 使用記憶體中的 SQLite 資料庫,來去不留痕跡
// https://blog.darkthread.net/blog/ef-core-test-with-in-memory-db/
const string cnStr = "Data Source=InMemoryDbName;Mode=Memory;Cache=Shared";
static SqliteConnection _persistConn;
static DbCtxHelper() {
_persistConn = new SqliteConnection(cnStr);
_persistConn.Open();
new JournalDbContext(new DbContextOptionsBuilder<JournalDbContext>()
.UseSqlite(_persistConn)
.Options)
.Database.EnsureCreated();
}
static bool enableLog = false;
public static JournalDbContext CreateDbContext()
{
var dbOpt = new DbContextOptionsBuilder<JournalDbContext>()
.UseSqlite(cnStr)
// 設定可動態開關的 Log 輸出,並限定觀察 SQL 語法
.LogTo(s =>
{
if (enableLog && s.Contains("Microsoft.EntityFrameworkCore.Database.Command"))
Console.WriteLine(s);
}, Microsoft.Extensions.Logging.LogLevel.Information)
// 連同寫入資料庫的參數一起顯示,正式環境需留意個資或敏感資料寫入Log
.EnableSensitiveDataLogging()
.Options;
return new JournalDbContext(dbOpt);
}
public static void WriteLog(string msg, ConsoleColor color) {
Console.ForegroundColor = color;
Console.WriteLine(msg);
Console.ResetColor();
}
public static void WriteRemark(string msg)
=> WriteLog(msg, ConsoleColor.Yellow);
public static void WriteError(string msg)
=> WriteLog(msg, ConsoleColor.Red);
public static void WriteError(Exception ex)
=> WriteError(ex.InnerException?.Message ?? ex.Message);
// 啟用 Log 輸出並執行 SaveChanges()
public static void SaveChangesWithLogging(this JournalDbContext dbCtx)
{
try
{
enableLog = true;
dbCtx.SaveChanges();
enableLog = false;
}
catch (Exception ex)
{
WriteError(ex.InnerException?.Message ?? ex.Message);
}
}
}
}
先看測試結果,我安排了八個實驗:
- 單純 Attach() 未指定 Id 值的 Entity,State 為 Added,SaveChanges() 時將觸發 INSERT,但因日期相同違反 UNIQUE 限制無法寫入
- 先用 dbCtx.Records.First(r => r.Date == recDate) 查詢,再 Attach() 會相同 Id 的資料會出錯
- Attach() 指定 Id 值的 Entity,State 為 Unchanged,SaveChanges() 什麼都沒發生
- Attach() 後修改 EventSummary,SaveChanges() 時跑 UPDATE SET EventSummary
- Entry().State = EntityState.Modified,會 UPDATE SET 所有欄位 (其實只有 EventySummary 需要更新)
- SetValues(),UPDATE 只更新有修改的欄位 EventSummary
- SetValues() 傳入對象可以是 ViewModel 或匿名類別,EF Core 會更新有對映且異動的欄位
- SetValues() 傳入對象也可以是 Dictionary<string, object>,EF Core 會更新有對映且異動的項目
測試程式在這邊:
using CRUDExample.Models;
using efcore_attach;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
Action<string> print = (s) =>
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine(s);
Console.ResetColor();
};
var recDate = new DateTime(2022, 1, 1);
// 產生待更新資料 / EventSummary 含隨機內容
Func<DailyRecord> GenDailyRecord = () => new DailyRecord
{
// Id 為 Primary Key,採自動編號,不需指定
Date = recDate,
EventSummary = $"Update - {new Random().NextDouble():n6}",
Remark = "",
User = "darkthread"
};
// 先新增一筆資料
using (var dbCtx = DbCtxHelper.CreateDbContext())
{
dbCtx.Records.Add(GenDailyRecord());
dbCtx.SaveChanges();
}
using (var dbCtx = DbCtxHelper.CreateDbContext())
{
// 使用 Attach 相當於 Entry(...).State = EntityState.Unchanged
print("實驗一 Attach / 不指定 Id");
var record = GenDailyRecord();
var entEntry = dbCtx.Attach(record);
DbCtxHelper.WriteRemark("未指定自動跳號 Primary Key 時,State = " + entEntry.State);
// 但日期相同,違反 Unique Index
dbCtx.SaveChangesWithLogging();
}
int recId;
using (var dbCtx = DbCtxHelper.CreateDbContext())
{
print("實驗二 Attach / 查詢過同一筆無法 Attach");
var record = GenDailyRecord();
recId = dbCtx.Records.First(r => r.Date == recDate).Id;
record.Id = recId;
try
{
dbCtx.Attach(record);
}
catch (Exception ex)
{
DbCtxHelper.WriteError(ex);
}
}
using (var dbCtx = DbCtxHelper.CreateDbContext())
{
print("實驗三 Attach / 指定 Id");
var record = GenDailyRecord();
record.Id = recId;
var entEntry = dbCtx.Attach(record);
DbCtxHelper.WriteRemark("指定自動跳號 Primary Key 時,State = " + entEntry.State);
dbCtx.SaveChangesWithLogging();
DbCtxHelper.WriteRemark("純 Attach 未改屬性,無動作");
print("實驗四 Attach 後修改 EventSummary");
record.EventSummary = "Attach 後修改";
dbCtx.SaveChangesWithLogging();
DbCtxHelper.WriteRemark("只更新異動欄位");
}
using (var dbCtx = DbCtxHelper.CreateDbContext())
{
print("實驗五 Entry().State = EntityState.Modified");
var record = GenDailyRecord();
record.Id = recId;
dbCtx.Entry(record).State = EntityState.Modified;
dbCtx.SaveChangesWithLogging();
DbCtxHelper.WriteRemark("更新所有欄位,不管是否與原來相同");
}
using (var dbCtx = DbCtxHelper.CreateDbContext())
{
print("實驗六 SetValues()");
var record = GenDailyRecord();
var exist = dbCtx.Records.Find(recId);
record.Id = recId;
dbCtx.Entry(exist).CurrentValues.SetValues(record);
dbCtx.SaveChangesWithLogging();
DbCtxHelper.WriteRemark("只會更新異動欄位 EventSummary");
}
using (var dbCtx = DbCtxHelper.CreateDbContext())
{
print("實驗七 SetValues(任意物件)");
var exist = dbCtx.Records.Find(recId);
dbCtx.Entry(exist).CurrentValues.SetValues(new {
EventSummary = "ViewModel 或匿名物件",
Remark = "",
NoMappingProp = 1234
});
dbCtx.SaveChangesWithLogging();
DbCtxHelper.WriteRemark("只更新有對映且異動的欄位");
}
using (var dbCtx = DbCtxHelper.CreateDbContext())
{
print("實驗八 SetValues(Dictionary<string, object>)");
var exist = dbCtx.Records.Find(recId);
dbCtx.Entry(exist).CurrentValues.SetValues(new Dictionary<string, object>() {
["EventSummary"] = "來自 Dictionary",
["Remark"] = "",
["NoMappingProp"] = 1234
});
dbCtx.SaveChangesWithLogging();
DbCtxHelper.WriteRemark("只更新有對映且異動的欄位");
}
希望以上測試能讓大家對 CurrentValues.SetValues()、Attach() 與 Entry().State = EntityState.Modified 更新行為及應用有更多認識。老樣子,專案我放上 Github 了,需要的同學請自取。
Tutorial of how to use Attach()/Entry().State for entity data updating.
Comments
# by Ted
謝謝黑大,太詳盡且易懂,