在 Entity Framework Core 裡面,我們可以很方便地利用 Linq 來下達查詢的條件、而 EF Core 會自動把 Linq 轉換成 SQL 語言去跟資料庫進行查詢動作。實務上,我們很容易遇到一些查詢條件是會在我們的程式中反覆出現的,例如:
var kiosks = db.Kiosk.Where(k =>
k.MerchantKiosk.Any(mk =>
mk.Type == (int)MerchantKioskType.Admin &&
mk.MerchantId == merchantId
)
).ToList();
我們不禁會想要寫一個自訂的方法以便我們可以重複使用這個條件:
private bool HasAdmin(Kiosk kiosk, string merchantId) {
return kiosk.MerchantKiosk.Any(mk =>
mk.Type == (int)MerchantKioskType.Admin &&
mk.MerchantId == merchantId
);
}
...
var kiosks = db.Kiosk.Where(k => HasAdmin(k, merchantId)).ToList();
改寫成這樣之後,既省下了重複寫冗長條件的工、又增加了程式碼的可讀性。本來要是可以這樣那就好了,偏偏如果我們用這樣的條件去下達資料庫查詢的話,是會丟出錯誤的:
LINQ to Entities does not recognize the method ‘bool HasAdmin(Kiosk kiosk, string merchantId)’ method, and this method cannot be translated into a store expression.
意思就是說,EF Core 不知道「HasAdmin」這個自訂的方法要怎樣翻譯成 SQL 語法。
關於這個問題,如果去翻一翻 StackOverflow,基本上每一篇討論都會告訴你「總之你就是沒辦法在 Linq to entities 裡面使用自訂方法,請乖乖地把 Linq 完整寫出來」。偏偏我就是不死心,乃至我試著開發出自己的套件來讓 Linq to entities 支援自訂方法。我一路研究到知道可以藉由註冊 IQueryTranslationPreprocessorFactory
服務來達到自訂翻譯機制的目的,正想要找看看有沒有一些範例可以參考、而在 GitHub 上面搜尋這個關鍵字的時候,好笑的事情就發生了;原來一直有一個套件 Expressionify 完全就是在做我想做的事情,而且早就已經非常完備了,真的是相見恨晚。這個套件一用下去之後,專案中一堆又臭又長的查詢條件都瞬間變乾淨了。本篇中就來簡單介紹一下這個套件的使用方法。
內容目錄
安裝
這個套件一共有兩個成份:Clave.Expressionify 和 Clave.Expressionify.Generator,前者只要安裝在整個方案的某個相依底層讓其它專案相依即可,而後者則是每一個會宣告自訂方法的組件都要個別安裝。安裝好了之後,在我們註冊資料庫服務的地方加入:
services.AddDbContext<MyDbContext>(options => {
...
// 這個加在最後面,或者至少要在指定了資料庫連線之後
options.UseExpressionify();
});
然後就可以來開心定義自訂方法了!
使用
Expressionify 在使用上有幾個條件限制:
- 自訂的方法必須是擴充方法(extension method),不能是實體方法或純粹的靜態方法。
- 目前不支援泛型方法,參數均必須為固定型別。
- 自訂方法的主體必須採用表達式(expression )的寫法而不能用大括號 + return 的一般寫法,且其內容只能夠是可以被 Linq 翻譯的語法、或是其它的 Expressionify 擴充方法。
- 宣告擴充方法的靜態類別必須設定為 partial 類別。
雖然有這些限制,但實務上用起來還是非常好用的。再來就只要在擴充方法上加上 [Expressionify]
特性就行了:
static partial class KioskExtension {
[Expressionify]
public static bool HasAdmin(this Kiosk kiosk, string merchantId) =>
kiosk.MerchantKiosk.Any(mk =>
mk.Type == (int)MerchantKioskType.Admin &&
mk.MerchantId == merchantId
);
}
...
// 真的能用,不會出錯!
var kiosks = db.Kiosk.Where(k => k.HasAdmin(merchantId)).ToList();
編譯的時候 Generator 套件會自動替擴充方法產生一個對應的 lambda 表達式,並且在 Linq 準備要翻譯成 SQL 之前用這些表達式替換掉查詢語法中的擴充方法。整套機制真的還滿有趣的,對底層開發有興趣的朋友可以多觀摩裡頭的手法。
留言