程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 4.6 asp.net core 自動生成組合查詢,core組合查詢

4.6 asp.net core 自動生成組合查詢,core組合查詢

編輯:關於.NET

4.6 asp.net core 自動生成組合查詢,core組合查詢


在做系統的時候,經常遇到前台錄入一大堆的查詢條件,然後點擊查詢提交後台,在Controller裡面生成對應的查詢SQL或者表達式,數據庫執行再將結果返回客戶端。

例如如下頁面,輸入三個條件,日志類型、開始和結束日期,查詢後台系統操作日志,並顯示。

這種類似頁面在系統中還是比較多的,通常情況下,我們會在cshtml中放上日志類型、開始、結束日期這三個控件,controller的action有對應的三個參數,然後在action、邏輯層或者倉儲層實現將這三個參數轉換為linq,例如轉成c=>c.BeginDate>=beginDate && c.EndDate < endDate.AddDay(1) && c.OperType == operType。

這裡有個小技巧,就是結束日期小於錄入的結束日期+1天。一般大家頁面中錄入結束日期的時候都是只到日期,不帶時分秒,例如結束日期為2016年1月31日,endDate 就是2016-01-31。其實這時候,大家的想法是到2016年1月31日23:59:59秒止。如果數據庫中存儲的是帶時分秒的時間,例如2016-01-31 10:00:00.000,而程序中寫的是c.EndDate < endDate的話,那麼這個2016年1月31日零點之後的全不滿足條件。所以,這裡應該是小於錄入的結束日期+1。

如果我們有更多的條件怎麼辦?如果有的條件允許為空怎麼辦?如果有幾十個這樣的頁面的?難道要一個個的去寫麼?

基於以上的考慮,我們為了簡化操作,編寫了自動生成組合查詢條件的通用框架。做法主要有如下幾步:

  • 前端頁面采用一定的格式設置html控件的Id和Name。
  • 編寫ModelBinder,接收前台傳來的參數,生成查詢條件類
  • 將查詢條件類轉換為SQL語句(petapoco等框架)或者表達式(EF框架),我們.netcore下用的是ef,因此只說明表達式的做法。peta的我們在.net framework下也實現了,在此不做敘述。
  • 提交數據庫執行

 

下面詳細介紹下具體的過程。

1、前端頁面采用一定的格式設置Html控件的Id和Name,這裡我們約定的寫法是{Op}__{PropertyName},就是操作符、兩個下劃線、屬性名。

 1 <form asp-action="List" method="post" class="form-inline">
 2   <div class="form-group">
 3     <label class="col-md-4 col-xs-4 col-sm-4 control-label">日志類型:</label>
 4     <div class="col-md-8 col-xs-8 col-sm-8">
 5       <select id="Eq__LogOperType" name="Eq__LogOperType" class="form-control" asp-items="@operateTypes"></select>
 6     </div>
 7   </div>
 8   <div class="form-group">
 9     <label class="col-md-4 col-xs-4 col-sm-4 control-label">日期:</label>
10     <div class="col-md-8 col-xs-8 col-sm-8">
11       <input type="date" id="Gte__CreateDate" name="Gte__CreateDate" class="form-control" value="@queryCreateDateStart.ToDateString()" />
12     </div>
13   </div>
14   <div class="form-group">
15     <label class="col-md-4 col-xs-4 col-sm-4 control-label"> - </label>
16     <div class="col-md-8 col-xs-8 col-sm-8">
17       <input type="date" id="Lt__CreateDate" name="Lt__CreateDate" class="form-control" value="@queryCreateDateEnd.ToDateString()" />
18     </div>
19   </div>
20   <button class="btn btn-primary" type="submit">查詢</button>
21 </form>

例如,日志類型查詢條件要求日志類型等於所選擇的類型。日志類的日志類型屬性是LogOperType,等於的操作符是Eq,這樣Id就是Eq__LogOperType。同樣的操作日期在開始和結束日期范圍內,開始和結束日期的Id分別為Gte__CreateDate和Lt__CreateDate。

2、編寫ModelBinder,接收前端傳來的參數,生成查詢條件類。

這裡,我們定義一個查詢條件類,QueryConditionCollection,注釋寫的還是比較明確的:

 1     /// <summary>
 2     /// 操作條件集合
 3     /// </summary>
 4     public class QueryConditionCollection : KeyedCollection<string, QueryConditionItem>
 5     {
 6         /// <summary>
 7         /// 初始化
 8         /// </summary>
 9         public QueryConditionCollection()
10             : base()
11         {
12         }
13 
14         /// <summary>
15         /// 從指定元素提取鍵
16         /// </summary>
17         /// <param name="item">從中提取鍵的元素</param>
18         /// <returns>指定元素的鍵</returns>
19         protected override string GetKeyForItem(QueryConditionItem item)
20         {
21             return item.Key;
22         }
23     }
24 
25     /// <summary>
26     /// 操作條件
27     /// </summary>
28     public class QueryConditionItem
29     {
30         /// <summary>
31         /// 主鍵
32         /// </summary>
33         public string Key { get; set; }
34         /// <summary>
35         /// 名稱
36         /// </summary>
37         public string Name { get; set; }
38 
39         /// <summary>
40         /// 條件操作類型
41         /// </summary>
42         public QueryConditionType Op { get; set; }
43 
44         ///// <summary>
45         ///// DataValue是否包含單引號,如'DataValue'
46         ///// </summary>
47         //public bool IsIncludeQuot { get; set; }
48 
49         /// <summary>
50         /// 數據的值
51         /// </summary>
52         public object DataValue { get; set; }
53     }

按照我們的設計,上面日志查詢例子應該產生一個QueryConditionCollection,包含三個QueryConditionItem,分別是日志類型、開始和結束日期條件項。可是,如何通過前端頁面傳來的請求數據生成QueryConditionCollection呢?這裡就用到了ModelBinder。ModelBinder是MVC的數據綁定的核心,主要作用就是從當前請求提取相應的數據綁定到目標Action方法的參數上。

 1     public class QueryConditionModelBinder : IModelBinder
 2     {
 3         private readonly IModelMetadataProvider _metadataProvider;
 4         private const string SplitString = "__";
 5 
 6         public QueryConditionModelBinder(IModelMetadataProvider metadataProvider)
 7         {
 8             _metadataProvider = metadataProvider;
 9         }
10 
11         public async Task BindModelAsync(ModelBindingContext bindingContext)
12         {
13             QueryConditionCollection model = (QueryConditionCollection)(bindingContext.Model ?? new QueryConditionCollection());
14 
15             IEnumerable<KeyValuePair<string, StringValues>> collection = GetRequestParameter(bindingContext);
16 
17             List<string> prefixList = Enum.GetNames(typeof(QueryConditionType)).Select(s => s + SplitString).ToList();
18 
19             foreach (KeyValuePair<string, StringValues> kvp in collection)
20             {
21                 string key = kvp.Key;
22                 if (key != null && key.Contains(SplitString) && prefixList.Any(s => key.StartsWith(s, StringComparison.CurrentCultureIgnoreCase)))
23                 {
24                     string value = kvp.Value.ToString();
25                     if (!string.IsNullOrWhiteSpace(value))
26                     {
27                         AddQueryItem(model, key, value);
28                     }
29                 }
30             }
31 
32             bindingContext.Result = ModelBindingResult.Success(model);
33 
34             //todo: 是否需要加上這一句?
35             await Task.FromResult(0);
36         }
37 
38         private void AddQueryItem(QueryConditionCollection model, string key, string value)
39         {
40             int pos = key.IndexOf(SplitString);
41             string opStr = key.Substring(0, pos);
42             string dataField = key.Substring(pos + 2);
43 
44             QueryConditionType operatorEnum = QueryConditionType.Eq;
45             if (Enum.TryParse<QueryConditionType>(opStr, true, out operatorEnum))
46                 model.Add(new QueryConditionItem
47                 {
48                     Key = key,
49                     Name = dataField,
50                     Op = operatorEnum,
51                     DataValue = value
52                 });
53         }
54   }

主要流程是,從當前上下文中獲取請求參數(Querystring、Form等),對於每個符合格式要求的請求參數生成QueryConditionItem並加入到QueryConditionCollection中。

 

為了將ModelBinder應用到系統中,我們還得增加相關的IModelBinderProvider。這個接口的主要作用是提供相應的ModelBinder對象。為了能夠應用QueryConditionModelBinder,我們必須還要再寫一個QueryConditionModelBinderProvider,繼承IModelBinderProvider接口。

 1     public class QueryConditionModelBinderPrivdier : IModelBinderProvider
 2     {
 3         public IModelBinder GetBinder(ModelBinderProviderContext context)
 4         {
 5             if (context == null)
 6             {
 7                 throw new ArgumentNullException(nameof(context));
 8             }
 9 
10             if (context.Metadata.ModelType != typeof(QueryConditionCollection))
11             {
12                 return null;
13             }
14 
15             return new QueryConditionModelBinder(context.MetadataProvider);
16         }
17     }

下面就是是在Startup中注冊ModelBinder。

services.AddMvc(options =>

{

     options.ModelBinderProviders.Insert(0, new QueryConditionModelBinderPrivdier());

});

 

3、將查詢類轉換為EF的查詢Linq表達式。

我們的做法是在QueryConditionCollection類中編寫方法GetExpression。這個只能貼代碼了,裡面有相關的注釋,大家可以仔細分析下程序。

 

  1         public Expression<Func<T, bool>> GetExpression<T>()
  2         {
  3             if (this.Count() == 0)
  4             {
  5                 return c => true;
  6             }
  7 
  8             //構建 c=>Body中的c
  9             ParameterExpression param = Expression.Parameter(typeof(T), "c");
 10 
 11             //獲取最小的判斷表達式
 12             var list = Items.Select(item => GetExpression<T>(param, item));
 13             //再以邏輯運算符相連
 14             var body = list.Aggregate(Expression.AndAlso);
 15 
 16             //將二者拼為c=>Body
 17             return Expression.Lambda<Func<T, bool>>(body, param);
 18         }
 19 
 20         private Expression GetExpression<T>(ParameterExpression param, QueryConditionItem item)
 21         {
 22             //屬性表達式
 23             LambdaExpression exp = GetPropertyLambdaExpression<T>(item, param);
 24 
 25             //常量表達式
 26             var constant = ChangeTypeToExpression(item, exp.Body.Type);
 27 
 28             //以判斷符或方法連接
 29             return ExpressionDict[item.Op](exp.Body, constant);
 30         }
 31 
 32         private LambdaExpression GetPropertyLambdaExpression<T>(QueryConditionItem item, ParameterExpression param)
 33         {
 34             //獲取每級屬性如c.Users.Proiles.UserId
 35             var props = item.Name.Split('.');
 36 
 37             Expression propertyAccess = param;
 38 
 39             Type typeOfProp = typeof(T);
 40 
 41             int i = 0;
 42             do
 43             {
 44                 PropertyInfo property = typeOfProp.GetProperty(props[i]);
 45                 if (property == null) return null;
 46                 typeOfProp = property.PropertyType;
 47                 propertyAccess = Expression.MakeMemberAccess(propertyAccess, property);
 48                 i++;
 49             } while (i < props.Length);
 50 
 51             return Expression.Lambda(propertyAccess, param);
 52         }
 53 
 54         #region ChangeType
 55         /// <summary>
 56         /// 轉換SearchItem中的Value的類型,為表達式樹
 57         /// </summary>
 58         /// <param name="item"></param>
 59         /// <param name="conversionType">目標類型</param>
 60         private Expression ChangeTypeToExpression(QueryConditionItem item, Type conversionType)
 61         {
 62             if (item.DataValue == null)
 63                 return Expression.Constant(item.DataValue, conversionType);
 64 
 65             #region 數組
 66             if (item.Op == QueryConditionType.In)
 67             {
 68                 var arr = (item.DataValue as Array);
 69                 var expList = new List<Expression>();
 70                 //確保可用
 71                 if (arr != null)
 72                     for (var i = 0; i < arr.Length; i++)
 73                     {
 74                         //構造數組的單元Constant
 75                         var newValue = arr.GetValue(i);
 76                         expList.Add(Expression.Constant(newValue, conversionType));
 77                     }
 78 
 79                 //構造inType類型的數組表達式樹,並為數組賦初值
 80                 return Expression.NewArrayInit(conversionType, expList);
 81             }
 82             #endregion
 83 
 84             var value = conversionType.GetTypeInfo().IsEnum ? Enum.Parse(conversionType, (string)item.DataValue)
 85                 : Convert.ChangeType(item.DataValue, conversionType);
 86 
 87             return Expression.Constant(value, conversionType);
 88         }
 89         #endregion
 90 
 91         #region SearchMethod 操作方法
 92         private readonly Dictionary<QueryConditionType, Func<Expression, Expression, Expression>> ExpressionDict =
 93             new Dictionary<QueryConditionType, Func<Expression, Expression, Expression>>
 94                 {
 95                     {
 96                         QueryConditionType.Eq,
 97                         (left, right) => { return Expression.Equal(left, right); }
 98                         },
 99                     {
100                         QueryConditionType.Gt,
101                         (left, right) => { return Expression.GreaterThan(left, right); }
102                         },
103                     {
104                         QueryConditionType.Gte,
105                         (left, right) => { return Expression.GreaterThanOrEqual(left, right); }
106                         },
107                     {
108                         QueryConditionType.Lt,
109                         (left, right) => { return Expression.LessThan(left, right); }
110                         },
111                     {
112                         QueryConditionType.Lte,
113                         (left, right) => { return Expression.LessThanOrEqual(left, right); }
114                         },
115                     {
116                         QueryConditionType.Contains,
117                         (left, right) =>
118                             {
119                                 if (left.Type != typeof (string)) return null;
120                                 return Expression.Call(left, typeof (string).GetMethod("Contains"), right);
121                             }
122                         },
123                     {
124                         QueryConditionType.In,
125                         (left, right) =>
126                             {
127                                 if (!right.Type.IsArray) return null;
128                                 //調用Enumerable.Contains擴展方法
129                                 MethodCallExpression resultExp =
130                                     Expression.Call(
131                                         typeof (Enumerable),
132                                         "Contains",
133                                         new[] {left.Type},
134                                         right,
135                                         left);
136 
137                                 return resultExp;
138                             }
139                         },
140                     {
141                         QueryConditionType.Neq,
142                         (left, right) => { return Expression.NotEqual(left, right); }
143                         },
144                     {
145                         QueryConditionType.StartWith,
146                         (left, right) =>
147                             {
148                                 if (left.Type != typeof (string)) return null;
149                                 return Expression.Call(left, typeof (string).GetMethod("StartsWith", new[] {typeof (string)}), right);
150 
151                             }
152                         },
153                     {
154                         QueryConditionType.EndWith,
155                         (left, right) =>
156                             {
157                                 if (left.Type != typeof (string)) return null;
158                                 return Expression.Call(left, typeof (string).GetMethod("EndsWith", new[] {typeof (string)}), right);
159                             }
160                      }
161                 };
162         #endregion

 

4、提交數據庫執行並反饋結果

在生成了表達式後,剩下的就比較簡單了。倉儲層直接寫如下的語句即可:

var query = this.dbContext.OperLogs.AsNoTracking().Where(predicate).OrderByDescending(o => o.CreateDate).ThenBy(o => o.OperLogId);

predicate就是從QueryConditionCollection.GetExpression方法中生成的,類似

Expression<Func<OperLogInfo, bool>> predicate = conditionCollection.GetExpression<OperLogInfo>();

QueryConditionCollection從哪裡來呢?因為有了ModelBinder,Controller的Action上直接加上參數,類似

public async Task<IActionResult> List(QueryConditionCollection queryCondition) { ... }

 

至此,自動生成的組合查詢就基本完成了。之後我們程序的寫法,只需要在前端頁面定義查詢條件的控件,Controller的Action中加上QueryConditionCollection參數,然後調用數據庫前將QueryConditionCollection轉換為表達式就OK了。不再像以往一樣在cshtml、Controller中寫一大堆的程序代碼了,在條件多、甚至有可選條件時,優勢更為明顯。

 

面向雲的.net core開發框架

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved