EntityFramework Core 遷移忽略主外來鍵關係

Jeffcky 發表於 2020-06-30

前言

本文來源於一位公眾號童鞋私信我的問題,在我若加思索後給出了其中一種方案,在此之前我也思考過這個問題,藉此機會我稍微看了下,目前能夠想到的也只是本文所述方案。

為何要忽略主外來鍵關係

我們不僅疑惑為何要忽略主外來鍵關係呢?不難想到,相對於大型企業而言大部分都會採用不建立主外來鍵關係(簡稱,外來鍵約束),外來鍵約束毫無疑問維護了資料一致性,但對其進行操作時很容易造成問題,級聯刪除只是其一。如果對於經常需要操作的表建立了外來鍵約束,那麼會嚴重影響插入、刪除和更新的效能,因為在執行這些操作之前,資料庫需要檢查其是否違反資料完整性,這也就是為何大多數不管是DBA或者架構師完全放棄使用外來鍵約束的原因,在分析資料庫,它們並不能以事務方式(一次一行)來處理資料,而是批量處理,效能是一切,這是其二。隨著業務需求變化在設計資料庫時,可能需要儲存歷史資料庫中的舊資料,而這些舊資料可能對資料質量和完整性沒有嚴格要求。為了能夠容納舊的髒資料,可直接清理和轉換舊資料,而放棄在資料庫級別上強制執行參照完整性,這是其三。所以基於以上幾點理由,忽略外來鍵約束是有其原因所在,當然,是否放棄外來鍵約束,可能取決於架構師或者DBA,反正決策權不在於搬磚的我們,我們知道其原因就好。

示例程式

以下示例皆在控制檯中進行,老規矩,我們先給出示例模型,依然是Blog和Post兩個實體,如下:

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<Post> Posts { get; set; }
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }

    public int BlogId { get; set; }
    public Blog Blog { get; set; }
}

接下來則是定義上下文,如下:

public class EFCoreDbContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("Server=.;Database=EFCore;Trusted_Connection=True;");
    }
}

 EntityFramework Core 遷移忽略主外來鍵關係

 EntityFramework Core 遷移忽略主外來鍵關係

忽略外來鍵約束

上述即使我們沒有顯式通過註解或Fluent APi配置關係,但是會根據約定而發現其關係,所以最終會建立外來鍵約束,那麼我們怎麼才能在遷移時不建立外來鍵約束呢?對依賴實體通過註解顯式配置不對映,如下:

[NotMapped]
public List<Post> Posts { get; set; }

請注意,這裡必須是對依賴實體進行顯式註解不對映,若是對依賴實體上的主體導航屬性配置依然會生成外來鍵約束,若是對外來鍵進行註解不對映也是同理,只不過生成的外來鍵名稱和預設的外來鍵名稱不一樣而已。很顯然,進行如上不建立外來鍵約束後,當我們通過主體新增依賴體資料時將不會持久化到表中,比如如下通過Blog新增Posts

var context = new EFCoreDbContext();

context.Add(new Blog()
{
    Name = "Jeffcky",
    Posts = new List<Post>()
    {
        new Post()
        {
            Title = "EntityFramework Core"
        }
    }
});

var effectedRows = context.SaveChanges();

同理,當通過主體進行飢餓載入時將會丟擲異常(無論是lambda表示式或字串),比如如下,因為二者已經沒有任何關聯關係

var blog = context.Blogs.Include(d => d.Posts).FirstOrDefault(d => d.Id == 1);

EntityFramework Core 遷移忽略主外來鍵關係

基於上述,似乎沒有什麼很好的方式,只能採用最原始方式生成外來鍵約束後,在遷移類中刪除外來鍵約束或資料庫表手動刪除外來鍵約束,這樣仍然可以很好的使用飢餓載入導航屬性。一旦實體比較多,手動刪除又顯得比較麻煩,我們可以寫個程式,當遷移完畢後刪除資料庫表所有外來鍵,如下擷取刪除外來鍵的程式碼片段,不知是否行得通,理論上應該是可以實現的。

public static class RemoveForeignKeyExetension
    {
        public static ModelBuilder RemoveForeignKeys(this ModelBuilder modelBuilder)
        {
            var entityTypes = modelBuilder.Model.GetEntityTypes().ToList();

            for (int i = 0; i < entityTypes.Count(); i++)
            {
                var entityType = entityTypes[i];

                var references = entityType.GetDeclaredReferencingForeignKeys().ToList();

                using (((Model)entityType.Model).Builder.Metadata.ConventionDispatcher.DelayConventions())
                {
                    foreach (var reference in references)
                    {
                        reference.DeclaringEntityType.RemoveForeignKey(reference);
                    }
                }
            }
            return modelBuilder;
        }
    }

忽略外來鍵約束(SQLite)

上述是針對SQL Server所做的測試,理論上MySQL同理,但對於SQLite資料庫,EF Core 3.x提供了全域性方案:通過資料連線字串配置【Foreign Keys = False】全域性抑制建立外來鍵約束。

optionsBuilder.UseSqlite("Database=sqlite.db;Foreign Keys=False");

總結

官方團隊好像並未提供針對SQL Server或MySQL忽略而不建立外來鍵約束而可以載入導航屬性的辦法,只能採取笨拙或者如上所述寫個程式去刪除外來鍵約束或者通過註解方式實現,但是一旦使用註解將無法載入導航屬性,那麼用EF Core就失去了很大的意義,我認為