转自:InfoQ,作者:吴磊。
信息系统的本质是对信息的输入、查询、计算、存储和输出操作,这就像金融的本质是价值的跨时空交换一样,虽然信息系统有各种各样的“衍生品”但都离 不开信息,离不开信息的表现形式:结构化与非结构化数据,而结构化数据几乎已经成为了上世纪90年代以来信息系统的必需品,譬如数据库对象、XML标签化 数据、实体类对象,我们都可以认为是结构化数据。特别是在企业级应用中对结构化数据的处理能力往往是技术选型的关键指标,本期我们就来深入探讨一下 Silverlight的数据查询能力。
笔者在《Silverlight之重》一文中曾提到Silverlight集成了LINQ等高级查询语言,具有多样化的数据处理与查询方式,使得开发者在数据处理上有更多选择。下面我们就通过一个案例来了解一下Silverlight在客户端强大的数据处理能力。
案例演示地址:http://space.silverlightchina.net/xpeter/Demo/SLQueryTest.html
源代码地址:http://space.silverlightchina.net/xpeter/Demo/code/SLQueryTest.rar
案例需求描述与分析
案例需求:“在大量的实体对象中找出满足条件的对象”。
这个需求描述只有一句话,很像现实项目中需求提出者言简意赅的口吻。但在这句话背后需要程序设计人员做的事情很多,首先需要构造出大量的实体对象数据,这需要我们建立承接结构化数据的是实体类,再通过数据生成器类来完成大量数据的产生工作;其次我们需要设计并实现Silverlight的查询类,最后我们需要将结果集输出到页面上。通过提炼名词,我们设计出如下类图:
在程序启动后App类构造了一个MainPage实例,同时自动构造10万条客户数据,数据构造完成后,用户可以通过各种查询方式查找满足条件的客 户记录,并显示在“查询结果”的DataGrid控件中,查询耗时信息将展现在“查询效率”文本框中(在本案例中,查询条件默认为客户姓名包含某字符串的 方式)。
实体类与产生器
实体类TestModel是按客户模型进行设计,包括姓名、性别、生日、年龄等属性。
数据产生器类DataGenerator类通过CreateByCnt<T>方法产生数据。数据产生器在企业级开发中常常用在大数据量性能测试与渗透测试中,本案例主要使用它来模拟真实的客户数据。
查询方式实现类
为了达到组件化复用和界面与逻辑分离的目的,笔者将Silverlight各种查询方式的具体实现放在QueryWorker类中,下面是Silverlight在客户端适用的几种轻量级查询方式:
-
- 直接查询法
通过For或者Foreach的循环直接查找对象集合,找出满足姓名包含传入字符要求的记录,并将其添加到结果集。该方法适用于大多数在开发时明确查找条件的应用,本案例中实现方法DirectQuery的代码如下:
publicvoid DirectQuery(string querystr, List<TestModel> data, refList<object> result){ foreach (var t in data) { if (t.Name.Contains(querystr)) { result.Add(t); } } //通知完成查找 InvokeQueryComplete(EventArgs.Empty); }
-
- 反射查询法
当查找条件中实体属性需要在运行时才能确定时,直接查找法就显得不够灵活了,这就需要通过C#反射方法获得运行时指定的实体属性信息 PropertyInfo,再通过GetValue方法检查该属性值是否满足包含传入字符要求。本案例中实现方法ReflectQuery的代码如下:
publicvoid ReflectQuery(string querystr, List<TestModel> data, refList<object> result) { //获取指定属性信息 PropertyInfo vPropertyInfo = typeof(TestModel).GetProperty("Name"); foreach (var t in data) { //判断属性值是否满足查找条件 if (vPropertyInfo.GetValue(t, null).ToString().Contains(querystr)) { result.Add(t); } } //通知完成查找 InvokeQueryComplete(EventArgs.Empty); }
-
- LINQ查询法
LINQ是.Net框架下特有的声明式语言,开发者可以通过这种类似SQL的语言快速构建数据逻辑,而避免了原有面向对象操作中的复杂过 程,笔者认为LINQ语言的表达式分为三个层次:第一层次是与SQL类似的LINQ表达式;第二层次为Lambda表达式;第三层次是基于 Expression类的表达式树,这是LINQ的最里层,也是LINQ实现动态的核心。本案例中实现方法LinqQuery的代码如下:
publicvoid LinqQuery(string querystr, List<TestModel> data, refList<object> result) { //定义延迟执行的Linq查询表达式 var linqquery = from t in data where t.Name.Contains(querystr) select t; //ToList方法使得查询被执行,从而获得结果 result = linqquery.Cast<object>().ToList(); //通知完成查找 InvokeQueryComplete(EventArgs.Empty); }
如果使用Lambda表达式,可以写成data.Where(t => t.Name.Contains(querystr))的筛选条件,与直接写查询表达式的含义一致。后面要介绍的两种查询方法实际都是LINQ的不同实现 方式,只是在灵活性上要比直接写表达式更胜一筹。
-
- 表达式树查询法
相较于LINQ查询法,表达式树查询更为复杂但灵活性更强。表达式树可以在运行时动态构建查询语句,它也是动态LINQ的实现基础,下面我就来看一下本案例中表达式树查询实现方法ExpressionQuery的代码:
publicvoid ExpressionQuery(string querystr, List<TestModel> data, refList<object> result) { IQueryable<TestModel> custs = data.AsQueryable(); //构造参数表达式it ParameterExpression it = Expression.Parameter(typeof(TestModel), "it"); //构造待筛选字符querystr的常量表达式 Expression funparam = Expression.Constant(querystr); //获得it参数的Name属性信息,实现Lambda表达式:it.Name Expression name = Expression.Property(it, typeof(TestModel).GetProperty("Name")); //获取字符串类的Contains方法信息 MethodInfo containsfun = typeof(string).GetMethod("Contains", newType[1] { typeof(string) }); //调用Contains方法,实现Lambda表达式:it.Name.Contains(querystr) Expression filter = Expression.Call(name, containsfun, newExpression[1] { funparam }); //构造Lambda表达式:it=>it.Name.Contains(querystr) Expression pred = Expression.Lambda(filter, it); //调用Where方法,实现Lambda表达式:custs.Where(it=>it.Name.Contains(querystr)) Expression expr = Expression.Call(typeof(Queryable), "Where", newType[] { typeof(TestModel) }, Expression.Constant(custs), pred); //形成延迟查询接口query IQueryable<TestModel> query = custs.Provider.CreateQuery<TestModel>(expr); //调用查询接口的GetEnumerator方法获得迭代器 IEnumerator Enumerator = query.GetEnumerator(); //调用迭代器的MoveNext方法执行查询结果 while (Enumerator.MoveNext()) { var o = Enumerator.Current; result.Add(o); } //通知完成查找 InvokeQueryComplete(EventArgs.Empty); }
这里可能会引起读者的疑惑,为什么只需要一行代码就可以完成的查询要用这么复杂的方式去编写?再回顾一次代码其实不难发现查询所用到的参 数、属性和方法都是以字符串形式“传入”表达式的,正是利用这一点就可以实现运行时动态查询。实际上,表达式定义语句的执行是在运行时才完成的,这就相当 于把编码过程后置到运行时,类似于JavaScript的Eval方法是在运行时再次调用一次解释器对传入字符串进行解释执行一样,这就是在运行时实现动 态的关键。
-
- 动态LINQ查询法
其实要实现动态查询并不需要开发人员去设计复杂的表达式树,从.Net 3.5起,微软就提供动态LINQ查询类库Dynamic Query Library。通过这个类库开发者可以将字符串形式的Lambda表达式作为参数传入Where方法,当然这样做的代价是传入字符串的语法安全风险。正 因如此,微软并没有将动态LINQ查询类库预置到Silverlight基础类库中。考虑到这一点对现实开发的意义重大,笔者自行封装了动态LINQ框 架,并提供了带有智能感知功能的动态LINQ语句输入框,这样就允许用户在Silverlight应用的运行时编写LINQ查询语句,并动态执行查询。
本案例中动态LINQ的实现方法DynamicLinqQuery代码如下:
publicvoid DynamicLinqQuery(string querystr, List<TestModel> data, refList<object> result) { try { var qtms = data.AsQueryable(); //将传入查询语句直接传给Where方法 var dynamicquery = qtms.Where(querystr); result = dynamicquery.Cast<object>().ToList(); } catch (Exception e)//动态Where子句存在语法风险 { //通知查找出错 _errorMessage = "你的查询语句遇到以下问题:" + e.Message; InvokeQueryError(EventArgs.Empty); } //通知完成查找 InvokeQueryComplete(EventArgs.Empty); }
现在查询类的所有方法都已经准备好,现在需要在UI层调用查询方法并展现查询结果了。
查询调用与结果反馈
由于所有查询方法都统一了入参结构,所以在调用上几乎都是相同的,这里笔者就以动态LINQ查询为例介绍UI层的查询调用与结果反馈的代码实现。
本案例中UI层的MainPage页面承担了所有交互任务,该页面包含一个查找类的实例queryworker。由于查找类在完成查找任务后异步触发完成事件,因此在构造MainPage页面时需要委托queryworker的查询完成事件,代码如下:
queryworker.OnQueryComplete += (sender1, e1) => Dispatcher.BeginInvoke(queryworker_OnQueryComplete);
在页面加载完成后查询按钮点击事件将统一委托给DoQuery方法:
btnDLinq.Click += newRoutedEventHandler(DoQuery);
在DoQuery方法中将根据不同的按钮,调用不同的查询方法。其中,动态LINQ查询的调用代码如下:
querystr = isiQuery.Words; queryworker.DynamicLinqQuery(querystr, testdata, ref queryresults);
接下来queryworker实例在完成动态查询后就会触发通知事件来回调上文中的接收方法queryworker_OnQueryComplete。该方法负责记录耗时信息,并将结果显示在DataGrid控件中,其代码如下:
void queryworker_OnQueryComplete() { long usetime = Environment.TickCount - StartTickCount; tbr.Text = "->" + string.Format("在{0}万条数据中,{1}找到{2}条记录,用时:{3}毫秒\n", testdata.Count / 10000.0, currquery, queryresults.Count, usetime) + tbr.Text.Replace("->", ""); tbRsCnt.Text = string.Format("共{0}条记录", queryresults.Count); dg.ItemsSource = queryresults; }
虽然在DataGrid数据绑定中只需要指定数据源,但在页面定义中还需要对数据进行翻译处理。比如:本案例中的性别都是以代码形式存在于数据实例 中,但在DataGrid中却显示为图片,这正是通过Silverlight提供的模板列DataGridTemplateColumn和绑定数据中的转 换器Converter来实现的,这里笔者不再展开介绍,读者可以自行研究本案例源代码。
Silverlight查询性能对比
上图是百万条数据在相同的查询条件下的性能对比,测试平台是普通的2G双核笔记本,操作系统为Win7,浏览器为IE9。从结果来看,除了效率比较 低下的反射方式,其他查询方法均在0.3秒内完成对百万数据的查询,性能相当不错。而其中表达式树查询法仅用时218毫秒,最为高效。这正是因为表达式树 已经是LINQ最核心的层次,不需要过多转换,所以在执行上更快。更为可喜的是即便使用灵活性最强的动态LINQ在性能损耗上也并不明显,所以在 Silverlight企业级应用中使用动态LINQ是完全可行的。事实上,笔者在目前开发的项目中经常使用动态LINQ来实现复杂的查询,甚至是规则引 擎。
Silverlight查询性能展望
前不久的Mix11大会上,微软已经展现了Silverlight5强大的3D渲染能力和更加丰富的商业应用支持,但并没有表示对LINQ的扩展。 其实更多的Silverlight企业级应用开发者希望能够在未来的版本中加入类似PLINQ的并行框架,这样就可以利用并行进一步提升客户端的查询效 率。笔者相信随着Windows Phone 7、Windows 8这些微软核心竞争力产品采用Silverlight作为“瘦UI”的展现层技术,并行框架被放进Silverlight基础框架的日子不会太远。