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开发框架

  • 相关阅读:
    软件测试人员的年终绩效考核怎么应对
    收藏
    顶踩组件 前后两版
    订阅组件
    hdu 1963 Investment 完全背包
    hdu 4939 Stupid Tower Defense 动态规划
    hdu 4405 Aeroplane chess 动态规划
    cf 414B Mashmokh and ACM 动态规划
    BUPT 202 Chocolate Machine 动态规划
    hdu 3853 LOOPS 动态规划
  • 原文地址:https://www.cnblogs.com/BenDan2002/p/6019139.html
Copyright © 2011-2022 走看看