讓 EF Core 依據 Entity 類別定義自動在資料庫新增資料表,主要有兩種做法:使用 Migration 或呼叫 DbContext.Database.EnsureCreated()

Migration 的好處是可以在 Model 修改時,自動產生 Schema 異動指令(例如:新增、修改欄位),靠dotnet ef database update一行指令就地升級資料庫結構,好不帥氣。但如之前淺談正式環境資料庫建立與換版所提,正式營運環境想這樣搞無疑痴人說夢。正式台資料庫管制嚴格,豈是想連就連,更甭提上線時所有更動細節需經層層審核,不太可能連要跑什麼指令都不清楚,放任程式自由發揮。

因此,EF Core 的自動建資料表我只用在新系統創建資料庫時,後續 Schema 如需更新,乖乖人工整理指令送審上線。採用這種運作模式,用 EnsureCreated() 手續相對簡單許多。

但 EnsureCreated() 有個問題,它的執行原則是:

  • 如果資料庫存在且具有任何資料表,則不會採取任何動作,以確保資料庫架構與 Entity Framework 模型相容。
  • 如果資料庫存在但沒有任何資料表,則會使用 Entity Framework 模型來建立資料庫架構。
  • 如果資料庫不存在,則會建立資料庫,並使用 Entity Framework 模型來建立資料庫架構。

實務上我很常運到多個 DbContext 共用一個資料庫的狀況,例如:專案有自己的 MyDbContext,將 js、css、jpg 等靜態檔移入 DB 會用到 StaticFileDbContext

使用相同連線字串指向全新資料庫,建立兩個 DbContext,分別呼叫 Database.EnsureCreated(),只有第一次會建立資料表,第二次因符合「如果資料庫存在且具有任何資料表」不會建立任何資料表:

using Drk.AspNetCore.FileProviders;
using ensurecreated_fail;
using Microsoft.EntityFrameworkCore;

var dbFileName = "shared.sqlite";
var cs = $"data source={dbFileName}";
if (File.Exists(dbFileName)) File.Delete(dbFileName);
using (var statFileDbCtx = new StaticFileDbContext(
    new DbContextOptionsBuilder<StaticFileDbContext>()
    .UseSqlite(cs).Options))
{
    statFileDbCtx.Database.EnsureCreated();
    try
    {
        Console.WriteLine($"Check StaticFileIndices Count = {statFileDbCtx.StaticFileIndices.Count()}");
    }
    catch (Exception ex)
    {
        Console.WriteLine("ERROR: " + ex.Message);
    }
}
using (var myDbCtx = new MyDbContext(
    new DbContextOptionsBuilder<MyDbContext>()
    .UseSqlite(cs).Options))
{
    myDbCtx.Database.EnsureCreated();
    try
    {
        Console.WriteLine($"Check MyEntities Counte = {myDbCtx.MyEntities.Count()}");
    }
    catch (Exception ex)
    {
        Console.WriteLine("ERROR: " + ex.Message);
    }
}

試了兩次,第一次先跑 StaticFileDbContext、再跑 MyDbContext,第二次順序交換,都是後執行的因資料表沒建立出錯:

面對這個問題,我採用的簡單解法是第二次開始改用statFileDbCtx.Database.ExecuteSqlRaw(statFileDbCtx.Database.GenerateCreateScript());取代 EnsureCreated(),如此,兩個 DbContext 的資料表都順利建立:

不過,當資料庫為 SQL,GenerateCreateScript() 輸出結果會包含 GO 指令 (請參考先前文章的擷圖),直接用 ExecuteSqlRaw() 執行會出錯。沒關係,這在使用 C#/PowerShell 執行 SSMS 所產生包含 GO 的 SQL Script 遇過了,嚇不倒我的。

最後修改版本如下:

using (var statFileDbCtx = new StaticFileDbContext(
    new DbContextOptionsBuilder<StaticFileDbContext>()
    .UseSqlite(cs).Options))
{
    var sb = new StringBuilder();
    foreach (var line in statFileDbCtx.Database.GenerateCreateScript()
        .Split(new[] { Environment.NewLine }, StringSplitOptions.None))
    {
        if (line == "GO") {
            statFileDbCtx.Database.ExecuteSqlRaw(sb.ToString());
            sb.Clear();
        }
        else sb.AppendLine(line);
    }
    statFileDbCtx.Database.ExecuteSqlRaw(sb.ToString());
}

When two DbContexts share a database, the second EnsureCreated will do nothing. This article shows how to use GenerateCreateScript() to resolve the issue.


Comments

Be the first to post a comment

Post a comment