天天看點

3種重構EF Linq查詢的方法而不扼殺性能枚舉<問題>傳回解決方案[0]但等等,還有更多或是LinqKit?總結

目錄

枚舉<問題>

傳回解決方案[0]

但等等,還有更多

或是LinqKit?

總結

從實體架構LINQ查詢中提取方法會悄然扼殺性能。這裡有三個簡單的解決方案,包括:表達式,擴充方法和LinqKit。

枚舉<問題>

上周,我驚訝地發現通過提取方法重構實體架構LINQ查詢的可讀性或可重用性會悄悄地将查詢從SQL交換到記憶體中處理并扼殺性能。

這是我的問題的簡化版本。

private async Task<List<User>> GetUsersMatching(IMainFilterDto filter, string prefix)
{
   var usersQuery = Users.Where(u =>
      (filter.StartDate == null || u.CreationTime > filter.StartDate) &&
      (filter.EndDate == null || u.CreationTime <= filter.EndDate) &&
      u.Name.StartsWith(prefix));
   return await usersQuery.ToListAsync();
}           

我有一個由前端提供的站點範圍的過濾對象,但是之後我需要做一些特定于手頭任務的其他事情,比如.StartsWith()。

然後在别處,我需要一些非常類似的東西

private async Task<List<User>> GetUsersWithoutRoles(IMainFilterDto filter)
{
       var usersQuery = Users.Include(i => i.Roles).Where(u =>
              (filter.StartDate == null || u.CreationTime > filter.StartDate) &&
              (filter.EndDate == null || u.CreationTime <= filter.EndDate) &&
              !u.Roles.Any()
              );

       return usersQuery.ToList();
}           

呃。兩者之間的共同代碼不是DRY,而且感覺很糟糕。如果我需要去改變它,也許通過用>=更換>,我要找出所有與該代碼一樣的地方。我很想提取它:

private bool ApplyMainFilter(IMainFilterDto filter, User u)
{
       return (filter.StartDate == null || u.CreationTime > filter.StartDate) &&
              (filter.EndDate == null || u.CreationTime <= filter.EndDate);
}           

并像這樣使用它:

private async Task<List<User>> GetUsersWithoutRoles(IMainFilterDto filter)
{
    var usersQuery = Users.Where(u =>
        ApplyMainFilter(filter, u) &&
        u.Name.StartsWith(prefix));           

這當然讀得更好。當我測試它時,它傳回完全相同的結果。遺憾的是,當我通過LINQPad運作它時,原始查詢(其中過濾器具有非null開始日期但是null結束日期)由以下:

SELECT [stuff]
FROM [Users] AS [u]
WHERE ([u].[CreationTime] > @__filter_StartDate_0) AND (([u].[Name] LIKE @__prefix_1 + N'%' _
AND (LEFT([u].[Name], LEN(@__prefix_1)) = @__prefix_1)) OR (@__prefix_1 = N''))           

變成:

SELECT [stuff]
FROM [Users] AS [u]WHERE ([u].[Name] LIKE @__prefix_1 + N'%' AND _
(LEFT([u].[Name], LEN(@__prefix_1)) = @__prefix_1)) OR (@__prefix_1 = N'')           

它删除了ApplyMainFilter()中的所有代碼!在這個簡單的例子中,這可能看起來并不可怕,但想象一下更複雜的場景。這可能會導緻很多更多的記錄從資料庫中傳回。它可能會造成網絡瓶頸或對中間件造成過大壓力。

最糟糕的是,它可能會阻止資料庫執行它最擅長的操作:使用索引來優化查詢執行。這可能意味着繞過現有索引,阻止使用未來索引進行查詢優化,或者通過完全隐藏資料庫中的問題來降低Azure SQL資料庫中性能建議的有效性。

順便提一下,如果你想看一個問題和解決方案的視訊,請檢視Code Hour的第22集:https://youtu.be/hYry3i5Nvzw

傳回解決方案[0]

一旦發現問題,解決方案就變得相當容易了。了解實體架構如何在内部是有幫助的。這是關于表達樹的所有内容,我之前已經寫過了(好吧,我在11年前寫過,但它所描述的基本原理仍然是可靠的)。

預測所有可能的方式,有人可能會将任意C#語言傳遞給一個where子句并将其全部轉換為SQL是一個難題。我需要提供實體架構。一種方法是傳回一個完全可解析的表達式樹,Expression<func bool=""><Func<User, bool>>而不僅僅是bool或Func<User, bool>。它看起來像這樣:

private Expression<Func<User, bool>> GetMainFilterQuery(IMainFilterDto filter)
{
    return u => (filter.StartDate == null || u.CreationTime > filter.StartDate) &&
        (filter.EndDate == null || u.CreationTime <= filter.EndDate);
}           

執行方式如下:

private async Task<List<User>> GetUsersMatching(IMainFilterDto filter, string prefix)
{
       var usersQuery = Users
              .Where(GetMainFilterQuery(filter))
              .Where(u => u.Name.StartsWith(prefix));           

這不是一個美學上令人愉悅的解決方案嗎?它可重用,讀取良好,并轉換為SQL。

但等等,還有更多

但是,如果你想進一步閱讀,我想我會提出一個更有趣的選擇。如果您使用的是流式API,那麼擴充方法方法可能是完美的:

public static class QueryUtils
{
    public static IQueryable<user> AppendMainFilterQuery(
        this IQueryable<user> existingQuery, IMainFilterDto filter)
    {
        return existingQuery.Where(u => (
            filter.StartDate == null ||  u.CreationTime > filter.StartDate) &&
            (filter.EndDate == null || u.CreationTime <= filter.EndDate));
    }
}           

這有點難以閱讀,但允許這樣:

private async Task<List<User>> GetUsersMatching(IMainFilterDto filter, string prefix)
{
    var usersQuery = Users
        .Where(u => u.Name.StartsWith(prefix))
        .AppendMainFilterQuery(filter);           

這讀得很好,是可重用的,就像第一個解決方案一樣,保持SQL的最初狀态。

或是LinqKit?

我是由一位聰明的同僚來管理這一切的,他建議我檢視LinqKit以防萬一我需要做更複雜的事情。除此之外,LinqKit允許您跨多個方法建構表達式。例如,如果我需要一個OR子句而不是一個AND子句,它可能看起來像這樣:

private ExpressionStarter<User> GetMainFilterPredicate(IMainFilterDto filter)
{
    var predicate = PredicateBuilder.New<User>().Start(u => 
        (filter.StartDate == null || u.CreationTime > filter.StartDate) &&
        (filter.EndDate == null || u.CreationTime <= filter.EndDate));
    return predicate;
}

private <list ser="">Task<List<User>> GetUsersMatching(IMainFilterDto filter, string prefix)
{
    var predicate = GetMainFilterPredicate(filter);
    predicate = predicate.Or(u => u.Name.StartsWith(prefix));
    return Users.Where(predicate).ToListAsync();
}           

很漂亮。

總結

如果我不需要任何更複雜的東西,我喜歡第一種方法,但無論如何,确定如何不重構LINQ查詢是重要的部分。如果您有任何其他創意解決方案,請分享以下評論。