ASP.NET MVC & EF 构建智能查询 二、模型的设计与ModelBinder

在第一篇中,我讲解了我们要做智能查询的原因,以及基本的解决方案设计。从这篇开始我们开始讲解它的实现过程。

其实在写这一系列文章之初,我其实是想由底至上去讲解,但是我又整理了一遍代码才发现,其实如果不了解最表面的东西,也是不太好深入的。

所以我们的第二篇文章就来讲一下我们这个智能查询框架中最浅,但也是使用最频繁的部分,也就是Model。

首先我们的Entity  或者说数据库的结构如下

image

另外如下面代码,我们有一个用于传递name=value对,及查询谓词的model

   1: public ActionResult Index(QueryModel model)
   2: {
   3:     using(var db=new DbEntities())
   4:     {
   5:         var list = db.Users.Where(model).ToList();
   6:         return View(list);
   7:     }
   8: }

我命名之为QueryModel。它由Action的参数传入,再传入EF的Where扩展方法,它是构建Lambda表达式的原型,上面是我们的一个通用的查询Action的代码。

 

而QueryModel的代码为

image

QueryModel的唯一一个属性Items,是一个ConditionItem的集合,它里面存着所有的查询条件。

而ConditionItem里面的属性

  1. Field表示要查询的目标属性的名称,我们的设定它是支持子属性查询的,例如 “Profile.MyUser.Id”
  2. Method则是当前使用的谓词,它是QueryMethod的一个枚举值
  3. OrGroup是一个字符串,是一个OrGroup的多个表达式将会以Or操作符进行关联,然后再And
  4. Prefix是一个分类的前缀,我们假定一个Action可能处理多个查询条件组的时候为了分开这些查询条件而加的属性
  5. Value则是这个表达试的值

 

而我们在页面上类似

   1: <form action="" method="post">
   2: 姓名:<input id="Name" name="[Like]Name" type="text" value="" />   
   3: Email:<input id="Email" name="[Equal]Email" type="text" value="" /><br />
   4: Id: <input id="Id" name="[Equal]Id" type="text" value="" />
   5: 生日: <input id="Birthday" name="[Equal]Birthday" type="text" value="" /><br />
   6: <input type="submit" value="查询" />
   7: </form>

这样的表单,我们提交到服务器端,我们要进行ASP.NET MVC 中IModelBinder的转换,要将querystring 或 postdata中的KeyValuePair转换为我们的ConditionItem。

当然,我们除了谓词Method之外,还有Prefix以及OrGroup要从客户端提交过来所以我们就要制定一个规则

我们约定

  1. 中括号[]中的为查询谓词
  2. 小括号()中的为前缀Prefix
  3. 大括号{}中的为OrGroup

我们可以通过以下IModelBinder的实现将Request.QueryString或Request.Form中的值转换到QueryModel中:

   1: public class SearchModelBinder : IModelBinder
   2:    {
   3:        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) 
   4:        {
   5:            var model = (QueryModel)(bindingContext.Model ?? new QueryModel());
   6:            var dict = controllerContext.HttpContext.Request.Params;
   7:            var keys = dict.AllKeys.Where(c => c.StartsWith("["));//我们认为只有[开头的为需要处理的
   8:            if (keys.Count() != 0)
   9:            {
  10:                foreach (var key in keys)
  11:                {
  12:                    if (!key.StartsWith("[")) continue;
  13:                    var val = dict[key];
  14:                    //处理无值的情况
  15:                    if (string.IsNullOrEmpty(val)) continue;
  16:                    AddSearchItem(model, key, val);
  17:                }
  18:            }
  19:            return model; 
  20:        }
  21:  
  22:        /// <summary>
  23:        /// 将一组key=value添加入QueryModel.Items
  24:        /// </summary>
  25:        /// <param name="model">QueryModel</param>
  26:        /// <param name="key">当前项的HtmlName</param>
  27:        /// <param name="val">当前项的值</param>
  28:        public static void AddSearchItem(QueryModel model, string key, string val)
  29:        {
  30:            string field = "", prefix = "", orGroup = "", method = "";
  31:            var keywords = key.Split(']', ')', '}');
  32:            //将Html中的name分割为我们想要的几个部分
  33:            foreach (var keyword in keywords)
  34:            {
  35:                if (Char.IsLetterOrDigit(keyword[0])) field = keyword;
  36:                var last = keyword.Substring(1);
  37:                if (keyword[0] == '(') prefix = last;
  38:                if (keyword[0] == '[') method = last;
  39:                if (keyword[0] == '{') orGroup = last;       
  40:            }
  41:            if (string.IsNullOrEmpty(method)) return;
  42:            if (!string.IsNullOrEmpty(field))
  43:            {
  44:                var item = new ConditionItem
  45:                               {
  46:                                   Field = field,
  47:                                   Value = val.Trim(),
  48:                                   Prefix = prefix,
  49:                                   OrGroup = orGroup,
  50:                                   Method = (QueryMethod) Enum.Parse(typeof (QueryMethod), method)
  51:                               };
  52:                model.Items.Add(item);
  53:            }
  54:        }
  55:    }

 

当然我们还要在Global.asax中添加

   1: ModelBinders.Binders.Add(typeof (QueryModel), new SearchModelBinder());

这样我们就可以在Action中获取到相应的查询条件了

image

我们可以看到,从表单提交过来的数据我们已经正确地存放在QueryModel中了。

 

ASP.NET MVC & EF 构建智能查询 一、智能查询的需求与设计

posted @ 2010-12-28 00:35 重典 阅读(4419) 评论(28) 编辑 收藏

 回复 引用 查看   
#1楼2010-12-28 01:10 | 撞破南墙      
对MVC的掌握,文章等等都挺好,向重典学习!
 回复 引用 查看   
#2楼2010-12-28 01:22 | 逸之羊      
支持典典,最近也在尝试和你这个差不多的东西,不过还没成功,呵呵
 回复 引用 查看   
#3楼2010-12-28 08:57 | 向世界出发      
其实我一直想做个这样的QueryExtender

 回复 引用 查看   
#4楼2010-12-28 09:19 | allentranks      
支持!
期待下一篇解释Where方法里如何解析QueryModel

 回复 引用 查看   
#5楼2010-12-28 19:24 | Vincent Yang      
@向世界出发
自己扩展啊,网上这个例子多得是,或者你去看看dynamicquery也行

 回复 引用 查看   
#6楼2010-12-29 08:47 | 向世界出发      
@Vincent Yang
DynamicQuery我看就像拼SQL那样,是吗?

 回复 引用 查看   
#7楼2010-12-29 11:13 | Tso      
这个不错,要顶要学习。
 回复 引用 查看   
#8楼2010-12-29 11:15 | 吴波      
很好!最近正在做类似的事情。
再请问一下,VS的Class Diagram里的类都是圆角矩形吧。
您的UML图是用啥画的?是别的工具还是您自己定制的主题?

 回复 引用 查看   
#9楼[楼主]2010-12-29 11:28 | 重典      
@吴波
VS2010 有UML的绘制功能

 回复 引用 查看   
#10楼2010-12-29 12:10 | 搏击的小船      
比较通用
 回复 引用 查看   
#11楼2010-12-29 14:25 | 吴波      
@重典
明白了,原来在ModelingProject里创建的类图,类是方形的。

 回复 引用 查看   
#12楼2010-12-29 15:24 | Silent Void      
引用向世界出发:
@Vincent Yang
DynamicQuery我看就像拼SQL那样,是吗?

表面上看是在拼SQL(L2S-SQL),但里面的实现,就是解析字符串,构造Expression,然后传给L2S,有L2S解析得到最终的SQL方言。

 回复 引用 查看   
#13楼2010-12-29 16:07 | Silent Void      
我命名之为QueryModel。它由Action的参数传入,再传入EF的Where扩展方法,它是构建Lambda表达式的原型
--------------------
借问楼主,Where方法里面的实现,是不是也是构造Expression,传入给EF,最终由EF来解析出最终的sql?还是自己实现了一个Provider,来解析Expression得到sql?

如果是前者,建议可以将QueryModel解析得到Expression的过程,放在QueryModel中,然后直接将结果Expression传给EF。这样的好处就是,碰到有些有别于普通用法的特殊情况,则可以写个QueryModel的子类,重写里面构造Expression中的那部分方法。如果是在扩展方法Where中处理,这里牵一发而动全身,如果系统中同时要保留普通用法和特殊用法,就办不到了。举个例子,Like在SQL中三种方式(这里只是一个例子,L2S已经很好地支持这些情况了),like '%Value%',like '%Value', like 'value%',诸如此类的多元化支持。
以前的公司里面搞了个山寨框架,随便表面上看功能很强大,但扩展性太差;碰到复杂的特殊情况,基本上罢工了;然后这些页面就得避开框架自己写回原生代码,非常别扭。
我觉得框架应该不是只处理局限在预先设定好的一些常用场景,碰到特殊用法就无能为力了;而应该提供良好的扩展性,允许用户去扩展,搞定一些特殊情况。

 回复 引用 查看   
#14楼[楼主]2010-12-29 16:22 | 重典      
@Silent Void
是的,是构建Expression然后传入EF,让EF去生成SQL
至于解析中的特殊情况我是使用在QueryModel to Expression的过程中添加Provider来实现的,这样的话对于添加不定的多种特殊情况比较容易处理。
至于流程我在文中说的应该比较清楚,未来的两篇将会说他们的实现

另外放到特殊场景是因为上下文配合需要,由于ASP.NET MVC的ModelBinder和Helper的Wrapper的配合,所以提供了相对稳定的流程,使得只要更改Helper的使用处就可以更改一定的功能,当然并不是说它不能用在一般查询和更新,只是那样的话UI界面其实要有另外的代码去配合

另外多谢你的意见,呵呵

 回复 引用 查看   
#15楼2010-12-31 22:07 | Y_Y      
谢谢分享,等有空好好学习一下,期待下篇,呵呵
 回复 引用 查看   
#16楼2011-01-02 20:13 | kan5kan      
正在学习MVC,谢谢重典。
 回复 引用 查看   
#17楼2011-01-04 20:52 | 秋色      
邹建 (用户名:zjcxc) ?这个是老大嘛????

 回复 引用 查看   
#18楼[楼主]2011-01-04 21:06 | 重典      
@秋色
不是 SQL的神人
在下是
邹健(chsword)

 回复 引用 查看   
#19楼2011-01-19 16:56 | kan5kan      
重典老师什么时候出下一章,等得好心焦!!!
能否把源代码也发一下。
谢谢。

 回复 引用 查看   
#20楼[楼主]2011-01-19 17:42 | 重典      
@kan5kan
不要意思,这两周又比较忙,估计要过了这周日才有时间,呵呵

 回复 引用 查看   
#21楼2011-02-20 14:12 | weiva      
请问这种方式如何用在linq里面呢?
QueryModel如何给linq用呢?

谢谢!

 回复 引用 查看   
#22楼2011-02-23 15:00 | wunaigong      
重大哥,等您好的下一篇啊。。
等的我头发都白了。。。。。
啥里出下一篇?

 回复 引用 查看   
#23楼2011-03-20 22:38 | 草莽      
找ef的动态查询找到这里来了,好像很久都没有发布后面的两篇啊?啥时候能更新啊
 回复 引用 查看   
#24楼2011-05-16 16:20 | gongzhw      
老大 发个源码吧
 回复 引用 查看   
#25楼[楼主]2011-05-17 13:02 | 重典      
 回复 引用 查看   
#26楼2011-08-31 13:43 | 新瓶老酒      
这个EfSearchModel有一个问题查询时 Like 匹配不到值

数据中有CustomerID为Jack001的人,但是以下的形式使终检索不到数据。

View代码如下:
客户编号:@Html.TextBox("CustomerID").ForSearch(QueryMethod.Like)

 回复 引用 查看   
#27楼[楼主]2011-08-31 14:18 | 重典      
@新瓶老酒
LikeTransformProvider 这个文件中有一个默认逻辑,如果只是输入文字 Like按Equals来计算 如果是有通配符* 则按模糊查询算

如果不需要可以去掉

 回复 引用 查看   
#28楼2011-10-07 02:28 | 沙漠孤鹰      
你好,我直接在controller 中使用,请问orGroup如何使用!
if (!string.IsNullOrEmpty(query) && !query.Equals("*"))
            {
                ConditionItem item2 = new ConditionItem();
                item2.Field = "Name";
                item2.Method = QueryMethod.Contains;
                item2.OrGroup = "1";
                item2.Value = query;

                queryList.Add(item2);

                ConditionItem item3 = new ConditionItem();
                item3.Field = "Pinyin";
                item3.Method = QueryMethod.Contains;
                item3.OrGroup = "1";
                item3.Value = query;


                queryList.Add(item3);
            }            
            whereClause.Items = queryList;