因 DateTime.Now 而當機:EF Core 寫入 timestamptz 的致命陷阱
Copyright Notice: This article is an original work licensed under the CC 4.0 BY-NC-ND license.
If you wish to repost this article, please include the original source link and this copyright notice.
Source link: https://v2know.com/article/1150
以下是一篇以部落格形式撰寫的技術分享,用來說明「為什麼用原生 SQL 傳入 DateTime.Now
不會出問題,但透過 Entity Framework Core(EF Core)卻會拋出 Kind=Local
到 timestamptz
的異常」,並給出三種解決方案。
EF Core 與原生 Npgsql 在 DateTime 映射上的差異
前言
在實作資料庫操作時,我們常用兩種方式:
原生 Npgsql +
NpgsqlCommand
EF Core(LINQ to Entities)
最近在開發
KobetuRirekiHelper
時,發現「原生 SQL」直接傳DateTime.Now
沒問題,卻在 EF Core 下用同樣的DateTime
居然拋出了:Cannot write DateTime with Kind=Local to PostgreSQL type 'timestamp with time zone'
原來兩者預設映射行為並不一樣——這對於多數開發者來說可能是第一次聽到。下面詳細拆解原因,並提出三種解決方案。
1. 為什麼原生 SQL 可以寫入 Local 的 DateTime?
-
表欄位類型
通常我們在建表時,若寫成:CREATE TABLE admin.kobetu_rireki_tbl ( ins_ymd timestamp without time zone NOT NULL DEFAULT statement_timestamp() -- ↑ without time zone );
這時候 Npgsql 在
NpgsqlCommand
+AddWithValue("@ins_ymd", DateTime.Now)
時,會把它當做「純粹的 timestamp」送進去,不管Kind
是Local
、Utc
或Unspecified
,都不會檢查而直接寫入。
2. EF Core 預設卻映射到 timestamptz
從 Npgsql.EntityFrameworkCore.PostgreSQL 6.x 開始,EF Core 預設把 CLR 的
public DateTime InsYmd { get; set; }
對應成 PostgreSQL 的
timestamp with time zone
(簡稱 timestamptz
)。而 timestamptz
這個類型要求:
-
只能寫入
Kind=Utc
的DateTime
-
若傳入
Kind=Local
或Unspecified
,驅動就會拋出:Cannot write DateTime with Kind=Local to PostgreSQL type 'timestamp with time zone'
3. 解決方案
解法一:指定欄位為「without time zone」
如果你業務上不需要時區資訊,最簡單的就是在 OnModelCreating
明確告訴 EF Core:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<KobetuRireki>()
.Property(x => x.InsYmd)
.HasColumnType("timestamp without time zone");
modelBuilder.Entity<KobetuRireki>()
.Property(x => x.UpdYmd)
.HasColumnType("timestamp without time zone");
// … 其他設定 …
}
如此一來,EF Core 就像原生 SQL 一樣,把 DateTime.Now
(Local)直接當「純 timestamp」送入,不會再強制檢查 Kind
。
解法二:啟用舊版行為(全局切換)
如果你不想挨個欄位去改型別,也可以在程式啟動初期(建議最早)加入:
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
這段設定會讓 Npgsql 恢復到 5.x 之前的行為:
所有 CLR
DateTime
→ 對應到 PostgreSQL 的timestamp without time zone
,完全不檢查Kind
。
解法三:統一使用 UTC
若你需要使用 timestamptz
(保留時區),那就必須保證「寫入前的 DateTime 一律是 UTC」:
// 在 Helper 裡、或在 DbContext.SaveChanges() 前集中轉一次:
var utc = inputDate.Kind == DateTimeKind.Utc
? inputDate
: inputDate.ToUniversalTime();
// 再把 utc 指派給實體屬性
entity.InsYmd = utc;
或者在呼叫 Insert/Update 方法時,就直接傳 DateTime.UtcNow
。
4. 小結
方式 | 優點 | 缺點 |
---|---|---|
without time zone | 不用理會 Kind、最接近原生 SQL 行為 | 失去時區資訊 |
legacy switch | 一次設定、全表全欄有效 | 影響其他所有 DateTime → timestamp without time zone |
統一 UTC | 保留時區意義,與 timestamptz 完美對應 |
需統一改程式邏輯,較易出漏 |
選擇最符合你專案需求的方案,就能避免 EF Core 與原生 SQL 在 DateTime
映射上的落差,確保資料正確寫入而不再遇到 Kind=Local
的惱人錯誤。
This article was last edited at