因 DateTime.Now 而當機:EF Core 寫入 timestamptz 的致命陷阱

| .NET | 7 Reads

以下是一篇以部落格形式撰寫的技術分享,用來說明「為什麼用原生 SQL 傳入 DateTime.Now 不會出問題,但透過 Entity Framework Core(EF Core)卻會拋出 Kind=Localtimestamptz 的異常」,並給出三種解決方案。


EF Core 與原生 Npgsql 在 DateTime 映射上的差異

前言
在實作資料庫操作時,我們常用兩種方式:

  1. 原生 Npgsql + NpgsqlCommand

  2. 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」送進去,不管 KindLocalUtcUnspecified,都不會檢查而直接寫入。


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=UtcDateTime

  • 若傳入 Kind=LocalUnspecified,驅動就會拋出:

    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