zoukankan      html  css  js  c++  java
  • 9.1.2 asp.net 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.Convert(((Expression<Func<object>>)(() => value)).Body, 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开发框架

  • 相关阅读:
    Spring Boot 常用注解
    python类的理解
    深入理解JavaScript的执行机制(同步和异步)
    HBuilderX scss/sass 使用教程
    uniapp引入微信小程序直播组件
    常见正则表达式例子
    远程桌面提示:身份验证错误 要求的函数不受支持
    ORCAL使用中存在的问题记录
    SQLSERVER常用函数
    vue-router.esm.js: Error: "Loading chunk 0 failed"
  • 原文地址:https://www.cnblogs.com/BenDan2002/p/6019139.html
Copyright © 2011-2022 走看看