引言
说明
由于博客园是个技术社区,所以我得显得严谨点,这里留下几点说明,我会在接下来的几篇文章中(如果有的话)重复这个说明。
其一,这篇(或者系列,如果有的话)文章是为了和大家一起入门(注意不是指导)。所以所编写的代码仅仅是示例的,或者说是处于编写中(完善中)的。
其二,至于为什么在学习的过程中就着手写这些文章,那是因为我深深觉得作为入门,这些内容还是容易的,但是常常让人却而退步。比如在一周之前,我还问博客园中的另一位博主,请求资料。那个时候我还觉得非常困难,非常苦恼。但是,经过一些摸索,一些文章的指导之后,却轻轻叩开了LINQ的门,一窥其瑰丽了。
其三,其实网上并不是没有LINQ的教程(指编写Provider)。但是“会”和不会往往隔了一点顿悟。就像“水门事件”一样。所以作为初学者来和大家一起探讨可以让彼此更同步。
其四,这真的是一个非常有挑战,非常有趣的内容。我接触了之后就忍不住和大家一起分享,邀大家一起参与冒险。
最后,这里列出所有我参考的,觉得有价值的资源。
其一,MSDN的博客: http://blogs.msdn.com/b/mattwar/archive/2007/07/30/linq-building-an-iqueryable-provider-part-i.aspx
这系列文章直接和本系列文章相关。07年的帖子,13年才发现,真该面壁思过。
其二,http://weblogs.asp.net/mehfuzh/archive/2007/10/04/writing-custom-linq-provider.aspx
待会会在文章中引用到这个博主写的一个非常短小的Provider示例。
其三,博客园中某个博主的作品http://www.cnblogs.com/Terrylee/category/48778.html
大神的文章读起来有点累,所以这系列我访问了好几次,愣是没看懂怎么回事,不过里面有张图挺不错。
接上文
在上一篇文章中,我们成功的将一个lambda表达式转换为被DirectorySearcher接受的Filter字符串。煽情点说,我们已经构建了LINQ查询和LDAP查询的桥梁。“这具有重大意义!”,好比听懂了方言。其实在上一篇中我提到,最后一篇的内容不会特别多。但是如果直接附在上一篇的话,就会使那篇显得有点长(当然这只是一个借口,很大程度上是我的强迫症在作怪),所以我要紧牙关写了第三篇。同时我在编码的过程中,脑海中浮现了一些问题和想法,想和大家分享。
另外,这篇文章算得上是“暂时告别篇”。和园友留言交流的时候我多次表明了自己的决心,所以我会长期跟进这个系列,但是不会像这三篇文章那么连续了。这三篇文章算是补上LINQ Provider实现这方面的“空白”,希望能帮助到喜欢和想要了解LINQ的朋友们。另外,再强调一次,http://blogs.msdn.com/b/mattwar/archive/2007/07/30/linq-building-an-iqueryable-provider-part-i.aspx,这个帖子其实已经包含了绝大多数“实现LINQ Provider”的内容,非常有价值。
Hello World
接下来我们就要完成这个Hello World并使之运行了,虽然我们已经耗费了很长的时间,但是值得。还记得高一的电脑课上,第一次输入VB代码的时候因为拼写错误抓狂了半天(也是高中唯一一次编码)。在第一篇中,我们把7个需要实现的方法缩减为1个。现在就去实现那个方法。从第二篇到这一篇,我已经对部分代码做了较大程度的更改。比如,为User类型提供了构造函数以保存结果。为Provider类型和Context类型提供了Username,Password,Path等属性以方便访问AD,同时还为他们提供了PropertiesToLoad只读属性用来加快LDAP搜索(关于这点,可以参考我的另外一篇文章:http://www.cnblogs.com/lightluomeng/archive/2013/01/18/2867019.html),另外为User提供了GetDirectory方法来增强实用性。下面是User类型,贴出来作为示例。
[Category("User")] public class User { public User(SearchResult result) { this.result = result; } private SearchResult result; /*模型类型,用来构造查询,和存储结果*/ [Property("userPrincipalName")] public string UserPrincipalName { get { return this["userPrincipalName"][0].ToString(); } } [Property("cn")] public string Name { get { if (this["cn"].Count == 0) throw new ArgumentException("未定义加载的属性!参数:cn。"); else return this["cn"][0].ToString(); } }
这些工作都是为了实现抽象类(见第一篇),QueryProvider的Excute方法准备的。这个方法的实现目前是这样的。
protected override object excute(System.Linq.Expressions.Expression expression) {/*最核心的部分,将表达式目录树转换为目标查询,执行查询并返回结果*/ string queryString = translator.Translate(expression); /*加载属性*/ DirectoryEntry root = new DirectoryEntry(Path, Username, Password); DirectorySearcher searcher = new DirectorySearcher(root); if (PropertiesToLoad.Count == 0) {//如果没有指定要加载哪些属性的话,则根据表达式计算需要加载的属性的最小集合 string ptm = @"[\w]+(?=[=<>])"; var matches = Regex .Matches(queryString, ptm) .Cast<Match>() .Select(i => i.Value); foreach (var i in matches) searcher.PropertiesToLoad.Add(i); } else { foreach (var i in PropertiesToLoad) searcher.PropertiesToLoad.Add(i); } searcher.Filter = queryString; searcher.SearchScope = SearchScope.Subtree; searcher.PageSize = 10; var result = searcher.FindAll(); return searcher.FindAll() .Cast<SearchResult>() //.ToArray() .Select(i => new User(i)); //foreach (var item in searcher.FindAll()) //{ // yield return (new User(item)); //} }
如果没有指定PropertiesToLoad的话,为了快速搜索,就根据表达式的值去指定PropertiesToLoad,不管何时,加载全部属性都是不明智的,对于一个需要详细信息的单个条目,宁愿调用GetDirecotryEntry方法来获取DirectoryEntry。还是要声明一下,上面的代码是出于“生长中”的,最明显的是,我把返回结果直接封装成了User,实际上应该根据lambda的ElementType来生成结果。或许还有朋友注意到了我注释掉的代码,这个我的几个想法有关,容后考虑。现在,暂且欣赏我们的成果吧。下面是调用示例。
class Program { static void Main(string[] args) { Context context = new Context("LDAP://searchAD", @"search\Ouwsearch", "OuweiSoft0123"); context.PropertiesToLoad.Add("cn"); context.PropertiesToLoad.Add("userPrincipalName"); //var query1 = context.Users.Where(i => i.UserPrincipalName.StartsWith("c")); /*注意upn可是带了完整域名的(@)*/ var query1 = from u in context.Users where u.UserPrincipalName.Contains("xc") select u; var query2 = context.Groups; var query3 = context.Users; foreach (var u in query1) { Console.Write("{0},", u.Name); } /*因为只是胚胎代码,这个查询甚至都无法输出呢*/ //foreach (var g in query2) //{ // //Console.Write("{0},", //} Console.ReadLine(); }
以及靓照一张:
很开心,但又是最淡淡的开心。因为这一切并不是“你突然掌握了一种魔术”,而是“按照计划,达到了预期”。
一些想法
或许你注意到了代码中注释掉的ToArray(在Excute方法实现中),这正是我关心的地方。从延时查询说吧。
延时查询——缓存和流式
“LINQ中的缓存和流式都属于延时查询”(《深入C#》P229)。书中选用了排序这个操作作为比较,因为排序总是要访问了所有元素之后才能得出结果。然而对于数据库而言,排序简直是家常便饭,LINQ to SQL也不可能真正的做到“每次请求一个元素,然后提交给迭代器”(那样的请求是要多频繁啊)。但是如果指定语义为“SELECT * FROM TABLE”的查询的话呢?难道真的要返回所有记录?可不可以内置一种方式,只有在需要的时候才加载?不然的话,Excute方法一被调用,就要处理所有记录并转化,代价太高了。我的想法是在枚举器中进行处理。
对于这一点我进行了测试,编写一个查询,使用foreach进行迭代,在foreach方法块的第一个花括号设置断点。程序执行到断点后就去修改SQL表中的值。这里是简单的查询迭代。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace Sample { class Program { static void Main(string[] args) {//<--断点 var context = new QueryDataContext(); var query = context.Master; foreach (var i in query) { Console.WriteLine(i.Name); } Console.ReadLine(); } }
我一共有2W条记录,修改了最后一条,结果输出的竟然是“修改过的值”!由此可见,MS进行了优化,防止一次性返回太多的数据,从而提升性能。但是,我认为绝对不是逐行求值,那将是灾难。所以呢,我尝试着去修靠前的几条值来验证。结果如何呢?有趣的事情发生了,我得到了这个提示。经过尝试,我发现这个锁定区域是30左右(我的环境中),也就是说,前30条被锁定无法更改。所以我修改了第35条,然后输出验证,发现第35条的值已经被更改。然后我尝试修改第一条,通过了;修改第36-65条(大概这个区间),又提示如下。所以呢,数据库的返回也是分段的,而不是一股脑的,当然作为程序员的我们倒不需要太在意(除了,在实现LINQ to SQL的Excute方法中千万不要ToArray!)不过很有意思是吧?
LDAP中也同样存在这个问题,请看下文。
在我们的例子中
LDAP查询支持分页,但是不像数据库哪样明确。DirecotrySearcher可以设置PageSize属性,但是没用PageIndex。设置好PageSize属性之后,进行查询输输出,在控制台中,可以看到输出一段,停一小会(非常微妙,有时无法看出),在输出一段...这样的循环直到输出完毕。这也是一种流式(不是一个一个元素,而是一段一段序列)。流式的好处是相应速度快(第一次加载),而且能够控制流量——只加载必要的信息。但是调用了.ToArray之后,就等于加载所有数据进行处理。为了弄清楚这个,先写一段纯粹的LDAP查询做测试,在测试环境中导入了1000条记录,以延长时间。我们只需要弄清楚,调用ToArray和不调用ToArray的区别,就能初步说明问题。这里是测试代码。
static void Main(string[] args) { DirectoryEntry entry = new DirectoryEntry("LDAP://dc.lsow.ow", @"lsow\exadmin", "1qaz@WSXEx"); DirectorySearcher searcher1 = new DirectorySearcher(entry); searcher1.SearchScope = SearchScope.Subtree; searcher1.Filter = "(objectclass=user)"; DirectorySearcher searcher2 = new DirectorySearcher(entry); searcher2.SearchScope = SearchScope.Subtree; searcher2.Filter = "(objectclass=user)"; DirectorySearcher searcher3 = new DirectorySearcher(entry); searcher3.SearchScope = SearchScope.Subtree; searcher3.Filter = "(objectclass=user)"; PrintTime(); var result1 = searcher1.FindAll(); PrintTime(); var result2 = searcher2.FindAll().Cast<SearchResult>(); PrintTime(); var result3 = searcher3.FindAll().Cast<SearchResult>().ToArray(); PrintTime(); foreach (var i in result1) { Console.Write("."); } PrintTime(); foreach (var i in result2) { Console.Write("."); } PrintTime(); foreach (var i in result3) { Console.Write("."); } PrintTime(); Console.ReadLine(); }
比较让我惊讶的是Cast方法调用的那一段...也太省力了点。我刚才说“一段一段”这个效果有点微妙,但在这个示例中就非常明显了(注意对比第三段的ToArray版本的),是不是很好玩?:)现在我要做一些转换,使用一个方法将Cast而且执行了Select的结果以object返回,然后再重新转换为IEnumerable接口的对象,再使用foreach进行迭代。如果还是有“分段”的效果的话,我心里的石头就落下了。
static object getObjectEnumerable(IEnumerable<SearchResult> source) { return source.Select(i => new User(i)); }
方法很简单,对应的用是:
foreach (var i in (IEnumerable<SearchResult>) getObjectEnumerable<SearchResult>( result2)) { Console.Write("."); }
这其实是过度担心的做法,前面的例子已经说明了,调用ToArray和不调用ToArray的区别了。不过还是请大家亲手试试。LINQ的延时查询非常完美的支持了“分段求值”(大家或许还注意到我注释掉的那个yield return,我害怕Cast<>和Select破坏了原有的枚举,现在看来是担心过度了)。
备注
我按照原样将我的想法和验证步骤(当然如果我验证过程中出错,我自动纠正,只提供我认为“正确”的做法,以缩减篇幅)写在这里,欢迎各位朋友一起讨论。
结语
OK,这个系列到这里暂停了。2号就准备回家,宽带被我妈迁到她工作的地方了,没打算迁回家。原本还纠结假期怎么发博客呢,现在总算不用担心了。如大家所见,我比较快的给这个系列开完了头,但是接下来的事情就不那么容易了,我们要实现更多的操作符/更丰富的查询,还要进行优化。我会坚持更新文章,同时希望对这方面感兴趣的朋友也推出自己的文章,方便大家学习讨论。:)