每周源代码52 – 你继续使用LINQ,我不认为它的意义与你想的一样

[原文发表地址] The Weekly Source Code 52 - You keep using that LINQ, I dunna think it means what you think it means.

[原文发表时间] 2010-06-17 09:11 pm

 

请记住优秀的开发者不只是编写源代码,它们还要这些代码。光是写很多的诗并不足以使你成为一个伟大的诗人。同时还要阅读和吸收。请在我的博客上查看源代码这一项,一共有(到今天为止)15页的关于源代码的帖子供你查看。

 

最近,我的朋友Jonathan Carter(我叫他OData Dude)与他的一个合作伙伴正在研究一些在SQL 查询中使用LINQ时发生的非常奇怪的问题。请记住每一个抽象有时会有漏洞,但是抽象的意义在于“提高等级”,因此你不用担心。

自来水工程很棒是因为它简化掉了送水的过程。我所知道的是,每当我打开水龙头的时候,就好像有一个带着水桶的家伙冲到了我家里。只要可以得到水,这对我来说没什么关系。然而,那个家伙有时候会发生一些错误,这时我就不知道我的水怎么样了。同样的问题也发生于JC 和他的这个合作伙伴身上。

在这个例子中,我们使用的是AdventureWorks Sample Database来说明这一点。这是这个合作伙伴发给我们的一些示例代码来重现这些怪事。
protected virtual Customer GetByPrimaryKey

(Func<customer, bool> keySelection)

{

    AdventureWorksDataContext context = new AdventureWorksDataContext();

 

    return (from r in context.Customers select r).SingleOrDefault(keySelection);

}

 

[TestMethod]

public void CustomerQuery_Test_01()

{

Customer customer = GetByPrimaryKey

(c => c.CustomerID == 2);

}

 

[TestMethod]

public void CustomerQuery_Test_02()

{

    AdventureWorksDataContext context = new AdventureWorksDataContext();

    Customer customer = (from r in context.Customers select r).SingleOrDefault(c => c.CustomerID == 2);

}

 

CustomerQuery_Test_01调用GetByPrimaryKey方法。这个方法以Func作为参数。它实际上是把一个lamdba表达式传递给GetByPrimaryKey函数。这使得该方法可重用,同时对他的DAL(数据访问层)来说也是一些好的辅助函数的基础。他把查询分到了两块地方。看上去很合理,是吗?

 

然而,如果你在Visual Studio里运行这个程序——在这个例子中,我会使用Intellitrace功能来查看实际上被执行的SQL,尽管你也可以使用SQL Profiler来查看—我们会看到:

Description: Wrong SQL in the Watch Window

 

这里是文本中的查询语句:

 

SELECT [t0].[CustomerID], [t0].[NameStyle], [t0].[Title],

   [t0].[FirstName], [t0].[MiddleName], [t0].[LastName],

   [t0].[Suffix], [t0].[CompanyName], [t0].[SalesPerson],

   [t0].[EmailAddress], [t0].[Phone], [t0].[PasswordHash],

   [t0].[PasswordSalt], [t0].[rowguid], [t0].[ModifiedDate]

FROM [SalesLT].[Customer] AS [t0]

WHERE [t0].[CustomerID] = @p0

 

恩,WHERE子句在哪儿呢?从LINQ 到 SQL会毁掉我的劳动成果并使我丢掉我的工作吗?微软很烂吗?让我们看一看第二个查询语句,它在CustomerQuery_Test_02()中被调用:

 

好的,WHERE就在那,但是为什么第二个LINQ 查询会产生WHERE子句而第一个却没不会呢?它们看上去基本上是同一种代码方式,但只有一个是不完整的。

 

第一个查询很明显把所有的数据行都返回给了调用者,然后调用者必须应用LINQ 操作在内存里做WHERE工作。第二个查询使用SQL Server(正如它应该做的)做筛选,然后以较少的数据的方式返回。

 

总得来说是这样。请记住LINQ关心两件事情IEnumerable和 IQueryable。前一个允许你在一个集合上使用foreach语句,另一个包含各种你感兴趣的东西,它允许你查询那些东西。人们基于这些进行创建,从LINQ 到 SQL, LINQ 到 XML,LINQ 到YoMomma,等等。

 

当你在使用的物件是IQueryable时;也就是说,来源是IQueryable时,你需要确保实际上你是在使用针对于IQueryable的操作,否则你可能会得到一个不合要求的结果,就像这个使用IEnumerable的数据库案例一样。你并不想从数据库中向一个调用者返回超出它真正需要的数据

通过JC的这个例子,我要强调的是

使用IQueryable 的 SingleOrDefault版本,它以一个lambda表达式作为参数,它实际上接收的参数是一个Expression> ,而使用IEnumerable 的 SingleOrDefault版本,它是以一个Func作为参数的. 因此,在下面的代码中,对SingleOrDefault的调用在处理查询时是假设它是LINQ 到 Objects的, 所以先通过L2S(LINQ 到 SQL)执行查询,然后在返回的结果集中实现SingleOrDefault. 如果他们将GetByPrimaryKey 的参数改成Expression>, 它就可以如所期望的那样工作了.

什么是Func, Expression又是什么呢?Func<>(读作"Funk")代表一个泛型委托。就像:

 

Func<int,int,double> divide=(x,y)=>(double)x/(double)y;

Console.WriteLine(divide(2,3));

 

而Expression<> 是一个函数定义, 可以在运行时被编译和调用。例如

 

Expression<Func<int,int,double>> divideBody=(x,y)=>(double)x/(double)y;

Func<int,int,double> divide2=divideBody.Compile();

write(divide2(2,3));

 

因此,他的这个伙伴并不需要一个Func(一个以customer为参数并返回一个bool值的Func),它们需要的是一个Func类型的可编译的Expression,该Func以customer为参数并返回一个bool值。当然为了使用Expression,我必须得添加“using System.Linq.Expressions;”

 

protected virtual Customer GetByPrimaryKey(Expression<Func<customer,bool>> keySelection)

{

    AdventureWorksDataContext context = new AdventureWorksDataContext();

 

    return (from r in context.Customers select r).SingleOrDefault(keySelection);

 

}

 

[TestMethod]

public void CustomerQuery_Test_01()

{

Customer customer = GetByPrimaryKey

(c => c.CustomerID == 2);

}

 

[TestMethod]

public void CustomerQuery_Test_02()

{

    AdventureWorksDataContext context = new AdventureWorksDataContext();

    Customer customer = (from r in context.Customers select r).SingleOrDefault(c => c.CustomerID == 2);

}

 

只是改变了那一行,将GetByPrimaryKey改成以Expression>为参数,并且我也得到了期望的SQL:

 

Description: Corrected SQL in the Watch Window

 

某个名人曾经说过,“我的代码没有错误,它完全按照我编写的那样运行”。

 

抽象的各个层次都是错综复杂的,因此在你把某个东西投入生产之前,你应当总是先确保自己的假设是正确的并且时刻注意那些凭借你的DAL(数据访问层)获得的generated/created/executed SQL语句。什么都不要相信,除了profiler之外。