EF Core從TPH遷移到TPT

2023-04-04 12:03:35

Intro

EF Core支援多種方式處理具有繼承關係的表,現在支援TPHTPC(EF Core 7)、TPT,具體的實現方式可以參考官方檔案這篇文章

大致總結一下不同的方式的區別:
TPH:所有的型別都放在一張表中,使用discriminator欄位用以區別不同的型別
TPT:不同的子型別有單獨的表存放子類獨有的欄位,父虛型別也有一張單獨的表存放共有的欄位。
TPC:不為父虛類新建表,只有子型別有單獨的表,並且表內有父類別和子類所有的欄位。

由於TPT兩張表的外來鍵關聯設計,在進行查詢時,會自動進行的JOIN等連表查詢操作,因此極限效能不太行。需要經常用查詢父類別的情況,TPH就挺好;需要經常查詢子類的時候,TPC就非常適合。按照官方的說法,正常情況TPH就已經滿足大多數的場景(這也是EF Core的預設設定),效能也是數一數二的,如果遇到了需要經常單獨查詢子型別的問題,可以優先考慮TPC,僅在一些特殊情況下應該考慮TPT。哪些是特殊情況?

請查閱官網這篇文章的詳細討論以瞭解三種不同方式對EF Core生成SQL的影響。

可能適合的場景

我遇到的這麼一個場景,有以下特點:

  • 子類非常多,並且不同的子類欄位的區別也很大,使用TPH會使得這個表格的規格非常大,並且空欄位非常多。
  • 繼承的層級很短,只有一層繼承關係。
  • 需要經常進行基於父類別的查詢,直接在一張表執行查詢的效率要比在的TPC分佈在不同表中查詢的效率高。(注意,這裡說的父類別的查詢是指直接使用Raw SQL的查詢,使用EF Core在父類別的查詢會翻譯成非常多的LEFT JOIN,導致效能低下。)

直接使用TPH或者使用TPC都不是非常滿意,而TPT提供了一張父類別的表儲存公共的欄位的這種方法,就顯得非常適合。

注:TPC不符合資料庫正規化設計原則,TPH在空欄位非常多的情況下也非常不優雅,強迫症可以使用TPT。

遷移

如果是空表的話,直接使用EF Migration就可以了,麻煩的已經有既有資料的情況,由於資料表參照的物件從的總錶轉移到了子類表,因此直接執行的資料庫遷移會提示違反了外來鍵約束。

23503: insert or update on table "AD_AnimalCamera_Data" violates foreign key constraint "FK_AD_AnimalCamera_Data_AD_AnimalCamera_Infos_AttachDeviceId"

解決方案:

  1. 手動建立表,並將TPH表中的不同的子型別記錄轉移到不同的子類表中。
  2. 通過自程式設計序載入物件,進行持久化,然後清空所有表的資料,建立表,載入資料並通過EF Core插入。

由於資料量比較大,而且還有繼承關係,手動去操作還是麻煩了一些,可以使用SQL查詢進行簡化;而第二個方案將由EF Core幫我們將資料插入到正確的位置。

方案1

準備臨時資料庫

將原來的資料庫結構複製一份,並設定為開發環境。接下來修改資料庫結構,TPH遷移到TPT模式,只需要在每一個子類表上使用[Table("")]標記就行了(當然也可以使用FluentAPI)。標記好了之後,使用EF Migration:

add-migration migrateTPT

由於是隻有結構的空表,直接操作就可以成功了。

遷移資料到臨時資料庫

將舊有資料傳輸到新的資料表中,尤其注意TPH與TPT之間表的在處理繼承關係時的不同。

以AttachDeviceInfo為abstract類,AD_Insect_Info作為其中的一個子類

更新之後TPH表中的大量欄位轉移到了子類表中,因此可以使用資料庫同步工具進行資料同步,忽略多餘的欄位就可以了。對於的TPT生成的子類表,通過Id欄位與抽象類表進行匹配連線,因此需要手動插入對應類別的資料。

INSERT into "AD_Insect_Infos"
SELECT "Id",FALSE from "AttachDeviceInfos" WHERE "AttachDeviceTypeId" = 1

如果沒有AttachDeviceTypeId欄位,那麼需要在TPH階段先通過discriminator將不同子類區分開,這個會麻煩一點。

轉移回資料庫

清空目標資料庫(包括結構),並將臨時資料庫中的表同步到目標資料庫中,手動調整_EFMigration表格的記錄(指向最新版本),完成切換。

方案2

備份資料

在資料庫還是原來結構的情況下,我們需要將現有的資料進行序列化,之前我寫過一篇序列化文章,使用的是PROTOBUF序列化。這裡由於傳輸的資料結構比較簡單,可以使用System.Text.Json類庫Json序列化到檔案。

對於有繼承關係的表的序列化,.NET 7的System.Text.Json新增了對應的支援,可以參考檔案的相關實現。

準備臨時資料庫

將原來的資料庫結構複製一份,並設定為開發環境。接下來修改資料庫結構,TPH遷移到TPT模式,只需要在每一個子類表上使用[Table("")]標記就行了(當然也可以使用FluentAPI)。標記好了之後,使用EF Migration:

add-migration migrateTPT

由於是隻有結構的空表,直接操作就可以成功了。

遷移資料到臨時資料庫

由於臨時資料庫結構已經和既有資料庫不同,無法通過程式直接連線兩個資料庫進行資料匯入的操作,因此需要將資料反序列化到的新的資料庫。

轉移回資料庫

清空目標資料庫(包括結構),並將臨時資料庫中的表同步到目標資料庫中,手動調整_EFMigration表格的記錄(指向最新版本),完成切換。

總結

遷移到TPT時,可以使用臨時資料庫中轉,將資料庫的資料以新的結構儲存下來,然後再同步到新資料庫。當然也可以直接在正式資料庫中操作:直接持久化,清空資料,然後再還原資料。當然這麼風險更高,強調一點,在生產的資料庫中進行操作需要格外謹慎,務必做好備份。

可以發現,在資料庫中使用外來鍵約束時,雖然給基於導航屬性的應用(例如OData)提供了便利,同時將資料完整性檢查後置到了資料庫中;但是進行架構調整是一件比較麻煩的工作,對分散式應用也非常不友好。

P.S. TPT的查詢效能很差,因此絕大多數場景都不推薦,僅在自己完全清楚並權衡了利弊的情況下再使用TPT。