在項目常常要和數據表格打交道。 現在BS的通常做法都是前端用一個js的Grid控件, 然後通過ajax的方式從後台加載數據, 然後將數據和Grid綁定。 數據往往不是一頁可以顯示完的, 所以要加分頁;然後就是根據關鍵字段做排序, 做篩選過濾。 作為後端人員, 要考慮的是如何優雅的實現分頁、排序、篩選的功能。
本文先談談篩選。 因為分頁、排序、篩選這3個動作, 一定是先處理篩選的——篩選後的結果再去排序, 然後再做分頁 , 才有意義。
篩選首先要考慮如下兩個問題:
1) 字段類型
2) 比較方式
以下面的模擬數據為例( 該數據為服務器的性能監控, 包括處理器、內存的監控結果和時間)。
ServerName
ProcessorMaxValue
ProcessorMinValue
ProcessorAvgValue
MemoryMaxValue
MemoryMinValue
MemoryAvgValue
DateTime
Server1
8
3
3.29
82.18
82.11
82.14
2016/10/1
Server1
10
3
3.29
82.23
82.12
82.17
2016/10/2
Server1
11
3
3.32
82.21
82.15
82.18
2016/10/3
Server1
10
3
3.29
82.21
82.10
82.16
2016/10/4
Server1
10
3
3.42
82.20
82.12
82.15
2016/10/5
Server2
10
3
3.40
82.20
82.12
82.16
2016/10/6
Server2
9
3
4.08
82.22
82.11
82.15
2016/10/7
Server2
10
3
3.69
82.20
82.12
82.16
2016/10/8
Server3
11
3
4.13
82.21
82.14
82.16
2016/10/9
Server3
11
3
4.03
82.20
82.15
82.17
2016/10/10
對於用戶來講, 可能會使用所有的字段來做過濾。比如 "ServerName like 'Server'", "ProcessorMaxValue>10 ", "DateTime < '2016/10/9'"。
小結下, 比較常見的字段類型有字符串、數值、日期,以及boolean值。為什麼要強調字段類型, 因為一樣的值在不同的字段類型要求下, 比較結果是不同的, 比如說數字11>2 , 但字符串”11”<”2”。
其次考慮比較方式, 比較常見的有“大於、大於等於、等於、小於等於、小於、不等於”, 其次還有 “in (…set)”; 字符串類型可能有”包含”, “開頭匹配”, “結尾匹配”等。
如果需求比較固定,直接在代碼中依次處理有限的若干字段的篩選完全不是事。可是實際的項目中,這種情況很少。 更多的是, 客戶一會要加這條件, 一會要加那條件。 如果都老老實實的一個一個加, 代碼就很容易臃腫,甚至失控了。
本文中推薦的是使用Expression方案。 由於Expression是 對集合進行操作, 所以不使用於自己拼SQL然後使用SQLCommand的場景。比較適用於:
1) 使用EntityFramework作為ORM框架的
2) 直接對全集合處理的
先感受下代碼
1 public class CriteriaCollectionHandler : ICollectionHandler 2 { 3 /* By Harvey Hu. @2016 */ 4 5 protected string PropertyName { get; set; } 6 7 protected ComparerEnum Comparer { get; set; } 8 9 protected object Target { get; set; } // 10 11 public CriteriaCollectionHandler(string propertyName, object target, ComparerEnum comparer) 12 { 13 this.PropertyName = propertyName; 14 this.Comparer = comparer; 15 this.Target = target; 16 } 17 18 private IQueryable<T> Filter<T>(IQueryable<T> source, string propertyName, ComparerEnum comparer, object target) 19 { 20 var type = typeof(T); 21 var property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); 22 23 24 25 var parameter = Expression.Parameter(type, "p"); 26 Expression propertyAccess = Expression.MakeMemberAccess(parameter, property); 27 if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) 28 { 29 var getValueOrDefault = property.PropertyType.GetMethods().First(p => p.Name == "GetValueOrDefault"); 30 propertyAccess = Expression.Call(propertyAccess, getValueOrDefault); 31 } 32 var constExpression = Expression.Constant(ConvertTo(target, property.PropertyType)); // 轉換為target的類型,以作比較 33 Expression comparisionExpression; 34 switch (comparer) 35 { 36 case ComparerEnum.Eq: 37 comparisionExpression = Expression.Equal(propertyAccess, constExpression); 38 break; 39 case ComparerEnum.Ne: 40 comparisionExpression = Expression.NotEqual(propertyAccess, constExpression); 41 break; 42 case ComparerEnum.Lt: 43 comparisionExpression = Expression.LessThan(propertyAccess, constExpression); 44 break; 45 case ComparerEnum.Gt: 46 comparisionExpression = Expression.GreaterThan(propertyAccess, constExpression); 47 break; 48 case ComparerEnum.Le: 49 comparisionExpression = Expression.LessThanOrEqual(propertyAccess, constExpression); 50 break; 51 case ComparerEnum.Ge: 52 comparisionExpression = Expression.GreaterThanOrEqual(propertyAccess, constExpression); 53 break; 54 case ComparerEnum.StringLike: 55 if (property.PropertyType != typeof(string)) 56 { 57 throw new NotSupportedException("StringLike is only suitable for string type property!"); 58 } 59 60 61 var stringContainsMethod = typeof(CriteriaCollectionHandler).GetMethod("StringContains"); 62 63 comparisionExpression = Expression.Call(stringContainsMethod, propertyAccess, constExpression); 64 65 break; 66 default: 67 comparisionExpression = Expression.Equal(propertyAccess, constExpression); 68 break; 69 } 70 71 72 var compareExp = Expression.Lambda(comparisionExpression, parameter); 73 var typeArguments = new Type[] { type }; 74 var methodName = "Where"; //sortOrder == SortDirection.Ascending ? "OrderBy" : "OrderByDescending"; 75 var resultExp = Expression.Call(typeof(Queryable), methodName, typeArguments, source.Expression, Expression.Quote(compareExp)); 76 77 return source.Provider.CreateQuery<T>(resultExp); 78 } 79 80 public static bool StringContains(string value, string subValue) 81 { 82 if (value == null) 83 { 84 return false; 85 } 86 87 return value.Contains(subValue); 88 } 89 90 91 protected object ConvertTo(object convertibleValue, Type targetType) 92 { 93 if (null == convertibleValue) 94 { 95 return null; 96 } 97 98 if (!targetType.IsGenericType) 99 { 100 return Convert.ChangeType(convertibleValue, targetType); 101 } 102 else 103 { 104 Type genericTypeDefinition = targetType.GetGenericTypeDefinition(); 105 if (genericTypeDefinition == typeof(Nullable<>)) 106 { 107 var temp = Convert.ChangeType(convertibleValue, Nullable.GetUnderlyingType(targetType)); 108 var result = Activator.CreateInstance(targetType, temp); 109 return result; 110 } 111 } 112 throw new InvalidCastException(string.Format("Invalid cast from type \"{0}\" to type \"{1}\".", convertibleValue.GetType().FullName, targetType.FullName)); 113 } 114 115 116 public virtual ICollection<T> Execute<T>(ICollection<T> values) 117 { 118 var result = Filter(values.AsQueryable(), this.PropertyName, this.Comparer, this.Target).ToList(); 119 return result; 120 } 121 122 }
使用示例(偽碼):
1 var criteria1 = New CriteriaCollectionHandler(“ServerName”, “server”, ComparerEnum.StringLike); // serverName like 'server'” 2 var criteria2 = New CriteriaCollectionHandler(“ProcessorMaxValue”, 10, ComparerEnum.Gt); 3 var criteria3 = New CriteriaCollectionHandler(“Datetime”, Datetime.Parse("2016/12/9"), ComparerEnum.lt); 4 ICollection<T> result = criteria3.Execute( 5 criteria2.Execute( 6 criteria1.Execute(YourDataCollection)));
核心是Filter()方法 ——IQueryable<T> Filter<T>(IQueryable<T> source, string propertyName, ComparerEnum comparer, object target)。
ICollectionHandler是用來處理集合Collection的對象接口,前面提到的分頁、排序和篩選處理, 都可以適用於這個接口。這個接口的Execute方法處理一個集合,並返回一個集合。篩選也是這個邏輯,所以適用這個接口。
在Filter()方法中, 通過Expression構建了一個Lamda表達式, 如p=>p.Property == target。這個表達式有幾個問題需要注意下:
1) 如何取到p.Property? 通過類型反射獲取。
2) 如何取到判斷操作? 根據比較符comparer枚舉。如果是常規比較, 則直接調用Expression的相關方法生成, 比如Expression.Equal(); 如果是特殊, 則通過Expression.Call調用自定義的方法生成, 比如StringLike
3) 比較值的類型用什麼?獲取p.Property類型,並將target強制轉換為該類型;參考ConvertTo()方法。
4) 是否支持Nullable類型?支持。但這個是個比較坑的事情。因為Nullable<T>實際上不支持和T的直接比較,所以不能將target轉換為Nullable<T>類型,只能是T類型,因此lamda表達式只能用p=>p.Property.GetValueOrDefault() == target 規格來處理 。所以在ConvertTo()方法中, 對nullable<T>類型也做了判斷處理。
Lamda表達式構造好了, 就可以通過Linq的Where方法來實現篩選了。這同樣適用Expression的Call方法構造出來。最後通過IQuaryable的IQueryProvider
的CreateQuery()方法完成調用。
5) 是否支持其他比較操作? 通過適當的擴展,我想應該可以實現的。 比如StringLike就是我們自己擴展的比較方法, 當然這個不是EntityFramework提供的,所以不支持EF的Queryable。
代碼實現分析到此暫告段落。 在實際使用中, 將每個條件都分別封裝成CriteriaCollectionHandler對象, 然後依次調用即可完成”邏輯與”的操作。參考上面的實現示例。
如果要實現”邏輯或”怎麼辦?目前的考慮結果是將兩個集合intersect()處理。如果各位有什麼更好的辦法, 歡迎回復討論。
下一篇, 我將討論下一些特殊字段的情況, 比如非Public的Property過濾。
注: 使用Expression過程也參考了博客園的其他朋友的文章。在此貢獻出來, 也希望能幫助一些朋友。