Intro
EF Core支援多種方式處理具有繼承關係的表,現在支援TPH
、TPC
(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"
解決方案:
- 手動建立表,並將TPH表中的不同的子型別記錄轉移到不同的子類表中。
- 透過自程式設計序載入物件,進行持久化,然後清空所有表的資料,建立表,載入資料並透過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。