[料理佳餚] 使用 Entity Framework Code First 應該要知道的 TPH、TPT、TPC

[料理佳餚] Entity Framework Code First 不算太難用這篇有講到要使用 Code First 不難,難的地方是不要讓 Entity Framework 拿我們設計出來的 Model 去建出低效的資料表。

要避免 Entity Framework Code First 建出低效資料表,了解 TPH、TPT、TPC 這三種資料表被建立的方式是必須要做的功課之一。

我一樣拿用過的例子來說明 TPH、TPT、TPC 的差異。

TPH(Table Per Hierarchy)

TPH 會幫我們把有繼承關係的所有子類別建立在同一個資料表內,我們調整一下 GoodsContext 內宣告的 DbSet<>,只留 DbSet<Product>

class GoodsContext : DbContext
{
    public DbSet<Product> Products { get; set; }

    public GoodsContext()
        : base("GoodsContext") // connectionString's name
    {
    }
}

建立出來的資料表我們可以看到,有繼承 Product 的子類別中的所有 Property 都被建立在同一個資料表中。

另外資料表中會多了一個 Discriminator 欄位,這個欄位是用來識別該筆資料是屬於哪一個子類別的,我們試著填入一筆 Car 資料就可以看到 Discriminator 欄位被填入 Car。

TPT(Table Per Type)

TPT 顧名思義在建立資料表的時候為每一個類別建立一個資料表,而要建立出這種資料表結構 GoodsContext 的原始碼內容則保持跟 TPH 一樣的原始碼內容,只留 DbSet<Product>,我們要調整的是繼承 Product 的子類別,幫每個子類別加上一個 Data Annotation - [Table("子類別的資料表名稱")]

這樣子建出來的資料表,我們可以看一下它就會為每個類別,包含父類別、子類別都建一個資料表。

TPC(Table Per Concrete Class)

TPC 建立的資料表結構從字義上可以看出來,它會為每個具體類別建立一個資料表,被繼承的父類別的 Property 會重覆出現在子類別所建出來的資料表中,我們在 GoodsContext 內個別為子類別宣告一個 DbSet<> 泛型型別的 Property。

class GoodsContext : DbContext
{
    public DbSet<Car> Cars { get; set; }

    public DbSet<CellPhone> CellPhones { get; set; }

    public DbSet<Clothing> Clothing { get; set; }

    public GoodsContext()
        : base("GoodsContext") // connectionString's name
    {
    }
}

建出來的資料表就是各自獨立的資料表,其中父類別的 Property 就會出現在子類別資料表之中。

結論

TPH、TPT、TPC 如何選擇全得看需求,如果我們對效能是有要求的,可以選擇 TPH,因為 TPH 會少掉 JOIN 的 effort,但是 TPH 會多消耗一些儲存空間,因為 TPH 會多出一堆 NULL 值,所以在做類別設計時子類別的 Property 數量就不能太多。

而 TPT 是三種之中把儲存空間利用地恰到好處的一種方式,不過相對的,我們在 Query 資料的時候就要使用到多一些 JOIN 的功夫,不太適合繼承關係太多的類別結構。

最後是 TPC,無論是效能、資料表的彈性、資料庫的驗證、儲存空間的使用上都是三種裡面表現最差的,但是偏偏我們在一知半解的情況之下用 Entity Framework 預設很容易就會建出這樣的資料表,這或許也是 Entity Framework 容易得到負面評價的原因之一。

工具是無辜的,微軟的工具在很多地方對於開發者是很友善的,可是友善的背後我們必須要了解它是去權衡了多少東西換來的。

參考資料

相關資源

C# 指南
ASP.NET 教學
ASP.NET MVC 指引
Azure SQL Database 教學
SQL Server 教學
Xamarin.Forms 教學