[小菜一碟] Entity Framework Core 如何在單一查詢中跨資料庫查詢資料?

如果資料庫都隸屬於同一台伺服器,資料庫之間要 JOIN 彼此的資料表是相對容易的,在有權限的前提下,只要在資料表的前面加上「[資料庫名稱].[結構描述]」,就能存取得到其他資料庫的資料表,就像這樣:[DB].[dbo].[Table]

但是在 Entity Framework Core 中,似乎沒有那麼容易,通常一個資料庫會建成一個 DbContext,當我們直接把隸屬於不同 DbContext 的 DbSet 拿來做 Join,我們得到以下的錯誤訊息:

Cannot use multiple context instances within a single query execution. Ensure the query uses a single context instance.

實驗情境

我們建立了兩個資料表:MemberOrder,分別各自隸屬於不同的資料庫:TestTest2,利用 EF Core Power Tools 我們建立了兩個 DbContext,分別是:TestContextTest2Context

我們直接用 EF Core 做 GroupJoin,沒意外的話應該會噴例外錯誤。

var testConnStr = "...";
var test2ConnStr = "...";

var testCtxOpt = new DbContextOptionsBuilder<TestContext>().UseSqlServer(testConnStr).Options;
var test2CtxOpt = new DbContextOptionsBuilder<Test2Context>().UseSqlServer(test2ConnStr).Options;

var testCtx = new TestContext(testCtxOpt);
var test2Ctx = new Test2Context(test2CtxOpt);

// 此段程式碼會發生錯誤
var membersWithOrders = await testCtx.Members.GroupJoin(
                                test2Ctx.Orders,
                                m => m.Id,
                                o => o.MemberId,
                                (m, olist) => new { Member = m, Orders = olist })
                            .ToListAsync();

錯誤訊息說的是 Entity Framework Core 的一個硬傷,無法在單一查詢中使用多個 DbContext,所以其中一類的解法是把單一查詢拆開成多個查詢,取出資料之後,在應用程式中進行聯結運算,這類解法要考慮的是取出資料量的大小,單一查詢拆開成多個查詢,有可能會有資料冗餘,造成「弱水三千,只取一瓢飲。」的現象,這類的解法不是我要的。

另一類的解法,就讓單一查詢維持單一 DbContext,但是可以跨資料庫存取,目前研究出來一種方式,提供給各位朋友參考,如果大家有不一樣的解決方式,還請不吝與我分享。

解法:在同一個 DbContext 建立來自其他資料庫的 DbSet

幸好 EF Core Power Tools 建出來的 DbContext 類別,是 partial class,我們可以自行擴充 TestContext,新增一個 OrdersOfTest2 的 DbSet。

public partial class TestContext
{
    public DbSet<Order> OrdersOfTest2 { get; set; }
}

這樣就符合 Entity Framework Core 的規則「單一查詢、單一 DbContext。」,但是,想也知道,如果就這麼直接拿來操作,一定是不行的,直接就噴錯了。

Invalid column name 'XXX'.

不過,這就不是 Entity Framework Core 的錯誤了,這個是 SQL Server 的錯誤,因為在這個資料庫中沒有 Order 這個資料表,自然也就沒有 Order 相關的欄位,有錯誤是正常。

接著,我們要告訴 Entity Framework Core,JOIN 的 Order 應該來自哪裡?我們使用 FromSql 直接指定 Order 的查詢語句,然後原本 GroupJoin 的目標改為 ordersInTestCtx,這樣應該就可以取得到資料了。

var ordersInTestCtx = testCtx.OrdersOfTest2.FromSql($"SELECT * FROM [Test2].[dbo].[Order]");

var membersWithOrders = await testCtx.Members.GroupJoin(
                                ordersInTestCtx,
                                m => m.Id,
                                o => o.MemberId,
                                (m, olist) => new { Member = m, Orders = olist })
                            .ToListAsync();

從 Entity Framework Core 產生的查詢語句中,可以看到,我們指定的 Order 查詢語句被包成了子查詢。

到目前,我也只研究出這一種方法,曾經嘗試過用「檢視(View)」,但是 View 沒有主索引鍵,GroupJoin 會有問題,歡迎大家分享更好的解法。

相關資源

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