我一直使用Linq To SQL,以前一直没有问题,但前两天发生了一件怪事。先写个示例代码有助于理解。
2 /// <param name="title">文章标题包含的内容。</param>
3 /// <param name="keyword">文章关键字包含的内容。</param>
4 /// <param name="pageIndex">从0开始的页码。</param>
5 /// <param name="pageSize">每页包含的记录数。</param>
6 /// <param name="pageCount">输出总页数。</param>
7 /// <param name="total">输出匹配记录的总数目。</param>
8 /// <return>返回匹配文章的数组。</return>
9 public static Article[] QueryArticles(string title, string keyword, int pageIndex, int pageSize, out int pageCount, out int total)
10 {
11 using (FormusDataContext dc = new ForumsDataContext())
12 {
13 Dictionary<string, object> args = new Dictionary<string, object>();
14 args["Title"] = title;
15 args["Keyword"] = keyword;
16 var q = ParseQuery(args, dc.Articles);
17
18 total = q.Count();
19 pageCount = (int)Math.Ceiling((double)total / (double)pageSize);
20
21 q = q.OrderByDescending(o => o.PostTime).Skip(pageIndex * pageSize).Take(pageSize);
22 return q.ToArray();
23 }
24 }
25
26 // 根据参数字典生成查询。
27 private static IEnumerable<Article> ParseQuery(Dictionary<string, object> args, IEnumerable<Article> source)
28 {
29 var q = source;
30
31 if (args.ContainsKey("Title"))
32 {
33 string value = (string)args["Title"];
34 q = q.Where(o => o.Title.Contains(value));
35 }
36
37 if (args.ContainsKey("Keyword"))
38 {
39 string value = (string)args["Keyword"];
40 if (!string.IsNullOrEmpty(keyword))
41 {
42 q = q.Where(o => o.Keywords.Contains(value));
43 }
44 }
45
46 // 解析其它参数
47
48 return q;
49 }
粗粗一看,这段程序并没有什么大问题,它的主要功能是提供了一个文章搜索的功能,通过指定文章的标题和关键字来检索数据。可在实际运行时,我发现如果提供了keyword这个参数时,程序就会在第18行抛出一个NullReferenceException,而错误是由Linq中的o.Keywords.Contains(value)引起的,因为Keywords这个字段在数据库里是可能有空值的。起始我很纳闷,q.Count()应该是由Linq To SQL生成一条SQL语句在数据库里执行,怎么会在C#的代码里出现这个错误呢。通过一番猜想和试验,我找到了问题的所在。
罪魁祸首就是ParseQuery这个方法的参数和返回值使用了IEnumerable泛型类,这样整个查询连接起来就是一个混合体:
dc.Articles.Where(o => o.Keyword.Contains("...")).Count()
接下来会发生什么呢?很明显,整条语句的前半部分dc.Articles会通过Linq To SQL来执行,它会把数据库中表的所有内容都读出来,剩下的一部分会通过Linq To Object来执行,仅仅对已生成的对象统计一个数量。而因为Linq To Object是在C#代码中执行的,就会发生之前所说的问题了。而即使不出现这样的错误,这种情况也是不能容忍的,因为每次执行都会把所有的数据都读出来,那个效率可想而知了。
要解决这个问题很简单,把ParseQuery方法的参数和返回值改成IQueryable<Article>就可以了。在此,也要提醒自己和广大的同行们,在使用扩展方法时应该注意,对于在有继承关系的类型上定义了相同名称和参数的扩展方法,必须准确的明白自己要调用的是哪个类型的扩展方法,而在调用时应该显式地将变量声明为相应的类型,而不应该随意地声明为某个父类型。
PS:以上代码是随手敲的,不保证代码可以通过编译。