信息系统的本质是对信息的输入、查询、计算、存储和输出操作,这就像金融的本质是价值的跨时空交换一样,虽然信息系统有各种各样的“衍生品”但都离不开信息,离不开信息的表现形式:结构化与非结构化数据,而结构化数据几乎已经成为了上世纪 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
查询方式实现类
为了达到组件化复用和界面与逻辑分离的目的,笔者将 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 基础框架的日子不会太远。
本期通过一个实例向大家介绍了 Silverlight 轻量级查询的实际性能,希望对正在采用或者将要采用 Silverlight 的项目开发者有所帮助。
如果您对该作者《Silverlight 轻舞飞扬》系列感兴趣,你可以点击下面的链接: http://www.infoq.com/cn/silverlight-column 。
感谢张凯峰对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。
评论