Intro EF Core支持多種方式處理具有繼承關係的表,現在支持TPH、TPC(EF Core 7)、TPT,具體的實現方式可以參考官方文檔和這篇文章。 大致總結一下不同的方式的區別: TPH:所有的類型都放在一張表中,使用discriminator欄位用以區別不同的類型 TPT:不同的子類型有 ...
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)提供了便利,同時將數據完整性檢查後置到了資料庫中;但是進行架構調整是一件比較麻煩的工作,對分散式應用也非常不友好。
除非特殊說明,本作品由podolski創作,採用知識共用署名 4.0 國際許可協議進行許可。歡迎轉載,轉載請保留原文鏈接~喜歡的觀眾老爺們可以點下關註或者推薦~P.S. TPT的查詢性能很差,因此絕大多數場景都不推薦,僅在自己完全清楚並權衡了利弊的情況下再使用TPT。