應用程序框架實戰二十三:基礎查詢擴展


  上面兩篇已經作好准備,本文將進行基礎查詢擴展。當使用了Entity Framework這樣的ORM框架以后,我們查詢的核心被集中在IQueryable的Where方法上。

  如果UI需要通過姓名查詢一個客戶,會在UI上放置一個輸入框作為客戶姓名的查詢條件。服務端接收以后通過Where方法進行過濾,如下所示,entities表示DbContext的子類。

var queryable = entities.Customers.Where( t => t.Name == name );

  當然,也可以使用Linq語句來完成。

var queryable = from c in entities.Customers where c.Name == name select c;    

  這些代碼看上去很不錯,但不論是上面的擴展方法還是Linq語句,其結果都是錯的。如果操作人員正好在查詢條件的框中輸入了一個“張三”,確實會把名稱為“張三”的客戶全部找出來,但是如果操作人員什么也不輸入,直接點擊查詢按鈕,結果會怎樣?

  上面的代碼會強制引入查詢條件,哪怕輸入值是空的,這與我們的預期不符,所以大家的辦法是添加一個判斷,像下面這樣。

IQueryable<Customer> queryable = entities.Customers; if( name != "" ) queryable = queryable.Where( t => t.Name == name );

  將輸入值與""進行比較並不健壯,如果操作人員在某個查詢條件輸入框中不小心打了個空格,依然會引入錯誤查詢條件,所以你把代碼改造為下面這樣。

IQueryable<Customer> queryable = entities.Customers; if(!string.IsNullOrWhiteSpace( name ) ) queryable = queryable.Where( t => t.Name == name );

  但是string.IsNullOrWhiteSpace只能針對字符串,對於其它類型需要先調用ToString,代碼繼續修改。

IQueryable<Customer> queryable = entities.Customers; if( value != null && !string.IsNullOrWhiteSpace(value.ToString() ) ) queryable = queryable.Where( t => t.XXX == value );

  對於非字符串類型的查詢條件,為了保障ToString的安全,需要在之前判斷是否為null,否則可能拋出null異常。上面的代碼比較健壯了,但是非常丑陋,如果只有一個查詢條件,這不是大問題,但有10個條件呢?

IQueryable<Customer> queryable = entities.Customers; if( value1 != null && !string.IsNullOrWhiteSpace(value1.ToString() ) ) queryable = queryable.Where( t => t.F1 == value1 ); if( value2 != null && !string.IsNullOrWhiteSpace(value2.ToString() ) ) queryable = queryable.Where( t => t.F2 == value2 ); if( value3 != null && !string.IsNullOrWhiteSpace(value3.ToString() ) ) queryable = queryable.Where( t => t.F3 == value3 ); ......

  打開你自己的項目來檢查一下,應該和上面代碼類似,這些雜亂無章的判斷把查詢的主題沖淡了。

  我上面討論的是相等(==)運算符,對於像Contains這樣的Like查詢,它不害怕空字符串“”,但是如果字符串中帶了空格“   ”,查詢結果也是錯的。可見,Where這個核心查詢方法,並不適合直接在應用程序中使用,除非你的查詢條件是必填項。對於從界面傳過來的查詢條件基本都是可選的,所以我們有必要進行查詢擴展。

  以上介紹了擴展Where方法的動機,下面開始進行擴展。

  通過上面的示例代碼可以看出,每當需要調用where時,都需要進行一個判斷,我們的目標就是把這個判斷隱藏到框架背后。

  首先考慮過濾方法的名稱,我命名為Filter,表示這是一個過濾器方法。

  再考慮Filter的方法簽名,很顯然返回類型是泛型的IQueryable<>,那么參數呢?

  我最初的做法是提供兩個參數,第一個參數是Lambda表達式,第二個參數是查詢條件的輸入值。之所以需要第二個參數,是因為我當時不清楚怎么從Lambda表達式中把輸入值提取出來,方法如下所示。

        /// <summary>
        /// 過濾 /// </summary>
        /// <typeparam name="TEntity">實體類型</typeparam>
        /// <typeparam name="TMember">實體屬性類型</typeparam>
        /// <param name=" queryable">查詢對象</param>
        /// <param name="predicate">過濾條件</param>
        /// <param name="value">屬性值</param>
        public static IQueryable<TEntity> Filter<TEntity, TMember>( this IQueryable<TEntity> queryable, Expression<Func<TEntity, bool>> predicate, TMember value ){      if (value == null) return queryable; if (string.IsNullOrWhiteSpace(value.ToString())) return queryable; return queryable.Where( predicate ); }

  調用代碼如下。

IQueryable<Customer> queryable = entities.Customers; queryable = queryable.Filter( t => t.F1 == value1, value1 ).Filter( t => t.F2 == value2, value2 ).Filter( t => t.F3 == value3, value3 );

  可以看到,調用代碼比直接使用Where已經清爽多了,不過這個Filter不是完美的,對於值類型的輸入條件,結果是錯的。比如value1是一個int類型,它的默認值為0,它將逃過string.IsNullOrWhiteSpace的檢測。那么我們添加一個條件來檢測默認值好不好呢,比如if(value == default(TMember)) return; 。這是不行的,如果你要搜索某字段為0的記錄就會失效。

  導致這個問題的原因是值類型無法為空,對引用類型沒有影響,我的解決方案是強制使用可空值類型。對於查詢來講,一般不會直接傳遞一個條件參數,因為大部分UI都要求分頁,傳遞多個參數是不方便的。我通過創建一個查詢實體來強制實施上面的原則,查詢實體擁有一些查詢屬性,且每個屬性都是可空的,並且會幫我過濾掉字符串參數中的空格,待我介紹到應用層的時候再詳細說明。

  無獨有偶,我在園子里看到一篇文章和我上面的查詢擴展非常類似,只是他的第二個參數用了bool類型。使用bool類型的好處是更加靈活,當然代價是需要寫更多代碼。調用代碼如下所示。

IQueryable<Customer> queryable = entities.Customers; queryable = queryable.Filter( t => t.F1 == value1, !string.IsNullOrWhiteSpace(value1)).Filter( t => t.F2 == value2, value2 != 0 );

  在長時間使用了兩個參數的方案后,我感覺非常別扭,我為什么要傳入第二個值?直接從Lambda參數中提取出輸入值不是更好?下面我們說干就干。

        public static IQueryable<T> Filter<T>( this IQueryable<T> queryable, Expression<Func<T, bool>> predicate ) { if ( predicate.Value() == null ) return queryable; if ( string.IsNullOrWhiteSpace( predicate.Value().ToString() ) ) return queryable; return queryable.Where( predicate );     }

  這里的關鍵方法是Value,這個自定義方法是上一篇擴展的,它能夠從Lambda謂詞表達式中把輸入值提取出來。

  這個方案與我之前使用的方案類似,只是省下一個參數,它同樣需要使用可空值類型。

  目前的代碼還有一個問題,如果程序員一次傳入多個條件,會導致什么結果?

IQueryable<Customer> queryable = entities.Customers; queryable = queryable.Filter( t => t.F1 == value1 && t.F2 == value2 && t.F3 == value3 )

  如果value1=”a”,value2和value3是空值,我得把t.F1 == value1拆出來,再傳到where中去。當然是可以做到,但太費力,所以我想了個偷懶的方法,一次只允許傳遞一個條件,一次傳入多個條件將拋出異常。

public static IQueryable<T> Filter<T>( this IQueryable<T> queryable, Expression<Func<T, bool>> predicate ) { if ( Lambda.GetCriteriaCount( predicate ) > 1 ) throw new InvalidOperationException( String.Format( "僅允許添加一個條件,條件:{0}", predicate ) ); if ( predicate.Value() == null ) return queryable; if ( string.IsNullOrWhiteSpace( predicate.Value().ToString() ) ) return queryable; return queryable.Where( predicate ); }

  GetCriteriaCount是我在上一篇創建的第二個方法,用來獲取Lambda謂詞表達式中的條件個數,只要大於1個,就會拋出InvalidOperationException異常。

  為了保證程序員不會把null傳進來,添加一個null檢測。

public static IQueryable<T> Filter<T>( this IQueryable<T> queryable, Expression<Func<T, bool>> predicate ) { predicate.CheckNull( "predicate" ); if ( Lambda.GetCriteriaCount( predicate ) > 1 ) throw new InvalidOperationException( String.Format( "僅允許添加一個條件,條件:{0}", predicate ) ); if ( predicate.Value() == null ) return queryable; if ( string.IsNullOrWhiteSpace( predicate.Value().ToString() ) ) return queryable; return queryable.Where( predicate ); }

  CheckNull用於檢測對象是否空值,如果為null將拋出異常。

  上面介紹了Filter方法的封裝過程,現在開始擴展Util應用程序框架。

  創建一個名為Util.Datas的類庫,並添加相關依賴,這個項目用於放置數據相關公共操作。創建Extensions.Query.cs文件,它用來對查詢進行擴展,代碼如下。

using System; using System.Linq; using System.Linq.Expressions; using Util.Datas.Queries; namespace Util.Datas { /// <summary>
    /// 查詢擴展 /// </summary>
    public static class Extensions { /// <summary>
        /// 過濾 /// </summary>
        /// <typeparam name="T">實體類型</typeparam>
        /// <param name="queryable">查詢對象</param>
        /// <param name="predicate">謂詞</param>
        public static IQueryable<T> Filter<T>( this IQueryable<T> queryable, Expression<Func<T, bool>> predicate ) { predicate = QueryHelper.ValidatePredicate( predicate ); if ( predicate == null ) return queryable; return queryable.Where( predicate ); } } }

  檢測代碼移到一個名為QueryHelper的internal類中,因為我后面還需要用到這段邏輯,代碼如下。

using System; using System.Linq.Expressions; namespace Util.Datas.Queries { /// <summary>
    /// 查詢操作 /// </summary>
    internal class QueryHelper { /// <summary>
        /// 驗證謂詞,無效返回null /// </summary>
        /// <typeparam name="T">實體類型</typeparam>
        /// <param name="predicate">謂詞</param>
        public static Expression<Func<T, bool>> ValidatePredicate<T>( Expression<Func<T, bool>> predicate ) { predicate.CheckNull( "predicate" ); if ( Lambda.GetCriteriaCount( predicate ) > 1 ) throw new InvalidOperationException( String.Format( "僅允許添加一個條件,條件:{0}", predicate ) ); if ( predicate.Value() == null ) return null; if ( string.IsNullOrWhiteSpace( predicate.Value().ToString() ) ) return null; return predicate; } } }

  為了讓大家可以把Demo運行起來,我還創建了Util.Datas.Ef.Tests測試項目,SqlScripts目錄中的Test.sql用來建庫,數據庫名為UnitTest,之所以不使用Test,是害怕把你本地的Test數據庫給刪掉了,這個數據庫安裝在你的D:\Data目錄中,如果不合適請自行修改。

  Samples目錄中的Employee類是測試的實體,它非常簡單,只有一個Name屬性。

  Repositories目錄中的EmployeeRepository是測試倉儲,為了簡單,沒有創建倉儲的接口,因為這里沒什么用。

  本文的集成測試FilterTest位於QueryTests目錄,代碼如下。

using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using Util.Datas.Ef.Tests.Repositories; using Util.Datas.Ef.Tests.Samples; namespace Util.Datas.Ef.Tests.QueryTests { /// <summary>
    /// 過濾測試 /// </summary>
 [TestClass] public class FilterTest { /// <summary>
        /// 測試初始化 /// </summary>
 [TestInitialize] public void TestInit() { EmployeeRepository repository = GetEmployeeRepository(); repository.Clear(); repository.Add( Employee.GetEmployee() ); repository.Add( Employee.GetEmployee2() ); } /// <summary>
        /// 獲取員工倉儲 /// </summary>
        private EmployeeRepository GetEmployeeRepository() { return new EmployeeRepository( new TestUnitOfWork() ); } /// <summary>
        /// 測試Filter過濾 /// </summary>
 [TestMethod] public void TestFilter() { EmployeeRepository repository = GetEmployeeRepository(); //用where查詢
            var result = repository.Find().Where( t => t.Name == "" ); Assert.AreEqual( 0, result.Count() ); //用Fileter查詢
            result = repository.Find().Filter( t => t.Name == "" ); Assert.AreEqual( 2, result.Count() ); Assert.AreEqual( Employee.GetEmployee().Name, result.ToList()[0].Name ); Assert.AreEqual( Employee.GetEmployee2().Name, result.ToList()[1].Name ); } } }

  我在測試中比較了Where與Filter的不同,你可以自己運行一下,如果還不知道如何運行測試,請參考Util應用程序框架公共操作類(二):數據類型轉換公共操作類(源碼篇)

  當然使用Where查詢比較死板,你需要在編譯時期固定查詢字段和操作符,這對於某些需要更靈活的場景並不合適,不過一般的系統對查詢靈活性要求都不高。

  本文雖然是針對IQueryable進行擴展,但思路上對於更原始的Ado.Net直接操作Sql同樣適用。可以看出,.Net Framework給你提供的API比較原始,如果需要滿足自己的需求,就需要擴展你的應用程序框架。另外不要輕視這個小小的擴展和封裝,因為你的大多業務都需要查詢,如果你有100個模塊,每個模塊有5個查詢條件,能幫你省下500個判斷。判斷語句不僅枯燥而且容易喧賓奪主,擾亂你的查詢主題。

 

  .Net應用程序框架交流QQ群: 386092459,歡迎有興趣的朋友加入討論。

  謝謝大家的持續關注,我的博客地址:http://www.cnblogs.com/xiadao521/

  如果需要下載代碼,請參考Util應用程序框架公共操作類(六):驗證擴展

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM