[ASP.NET MVC][Entity Framework] Entity Framework 的 TPH 會額外產生表格?

這是一個很有趣的問題,當初我在編寫 ASP.NET MVC 5 網站開發美學 CH4 (4.6.5) 的實作繼承時,使用的是已有 Database 和 Table Schema 的狀況,沒有注意到 Database Creation 時的狀況,這個問題是由讀者反應,我做了個實驗才發現這個問題,但解決方法也很簡單,在此留下記錄以供其他讀者查閱。

Entity Framework 的繼承機制分為 TPH (Table Per Hierarchy)、TPC (Table Per Concrete-type) 與 TPT (Table Per Type) 三種,其中 TPC 和 TPT 都是以獨立表格來儲存資料,唯有 TPH 是只用一個表格來儲存每個繼承的 Model 資料,只使用 Discriminiator 欄位來區分,因此在 Database Creation 時,TPC / TPT 都會獨立產生 Table Schema,TPH 只會有一份 Table Schema,內含 Map() 所定義的欄位。

這個問題就發生在 TPH 與 Database Creation 的機制,依照 EF 的定義,當 Database 不存在時,EF 會啟動建立資料庫的程序,並且掃瞄 DbContext 中的 Entity 的定義 (使用 Model 類別上各個屬性的 Data Annotation、使用 OnModelCreating() 內的 Model Builder 的定義,或是使用預設的機制) 來建立表格,這個程序對 TPC / TPT 來說沒有問題,但到了 TPH 就有問題了,因為多個 Model 共用一個表格,按照程式的流程來說,應該是先收集完 Entity 的註冊資訊後才會產生 CREATE TABLE 指令去建立表格,但 EF 似乎不是這麼做,依照我實驗時錄下的 SQL 指令來看,它發出了兩次 CREATE TABLE 指令,我猜想可能是在 Entity() 註冊時就會有一次,Map() 又會再一次 (有興趣的讀者可以去 trace EF 的 source code 看看),同時它會偵測同名的表格是否存在 (這個部份應該是 EF 內部做掉,因為沒看到有 SQL 查詢),如果已存在的話就會加上流水號重新命名表格,因此才會出現兩個表格的問題。

這個額外的表格其實程式根本不會使用到,因為程式的對應並不會指向該表格,所以刪除它是可以的,但是對一些開發者或是資料庫管理員來說,那個多的表格看起來真的很礙眼,所以若要阻止 EF 額外建立那個表格,只要在 Entity() 中額外呼叫一個 ToTable() 方法,傳入表格名稱即可,但請注意,Map() 裡面的 ToTable() 要和 Entity().ToTable() 的表格名稱要一致,否則將又會出現兩個表格的問題。

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>().ToTable("Products")
        .Map<Camera>(m =>
            m.MapInheritedProperties()
            .ToTable("Products")
            .Requires("Discriminator")
            .HasValue("Camera")
        )
        .Map<SingleReflexCamera>(m =>
            m.MapInheritedProperties()
            .ToTable("Products")
            .Requires("Discriminator")
            .HasValue("SingleReflexCamera")
            )
        .Map<Lens>(m => 
            m.MapInheritedProperties()
            .ToTable("Products")
            .Requires("Discriminator")
            .HasValue("Lens")
            );
            
    base.OnModelCreating(modelBuilder);
}