代码改变世界

[转载]充分发挥 Visual Studio“Orcas”中查询的强大功能

2008-08-29 03:12  G yc {Son of VB.NET}  阅读(369)  评论(0编辑  收藏  举报

         原文地址:http://msdn.microsoft.com/msdnmag/issues/07/06/VBLINQ/Default.aspx?loc=zh

         作者:Ting Liang  Kit George 


目   录

 


 


       长期以来,开发人员一直需要能够以其代码使用数据查询技术,这与他们在 SQL 查询中可能需要的十分相似。现在,随着 Microsoft® Visual Studio® 的新版本(代号为“Orcas”)即将发布,这种工具也将面市。Visual Studio 的下一个版本包含了一组新的语言功能 (LINQ),统称为语言集成查询 (LINQ),它将查询功能直接添加到 Visual Basic® 和 Visual C#®。
 

        有了 LINQ,您可以直接使用 Visual Basic 编写数据访问代码。您可以使用编译时语法检查和架构检查,以及更好的工具支持(如 IntelliSense®)来编写您的查询。您不必再使用字符串来编写查询,也不必甚至等到运行时才能知道其构成是否正确。有了 LINQ,数据访问代码便可以从底层存储工具中提取出来。这意味着,您可以使用同一个代码构造来查询关系数据库、XML 和内存中的对象,而且您可以轻松地联接跨不同源域的信息。例如,您可以创建一个存储在 XML 中的客户名称列表和存储在数据库表中的客户名称列表之间的组合。此外,LINQ 允许延迟执行,这样您就可以将查询分成几个逻辑部分,并且仍能实现只要运行一次最终结果的性能优势。最后,Visual Basic/XML 集成几乎可以彻底消除您编写的代码和您正试图表示或操作的文档之间的概念性障碍。现在,让我们开始学习如何编写一些查询。


LINQ 语法

    利用 LINQ 您可以查询不同的源,例如 Microsoft .NET Framework 对象集合、关系数据库或 XML 数据源。在 Visual Basic 中,LINQ 查询的一般格式如下所示:

From ... < Where ... Order By ... Select ... >


    当然,如果您是 SQL 程序员,您会想知道为什么查询是以 From 开始,而不是以 Select 开始。原因很简单:即 IntelliSense。通过首先指出数据源,IntelliSense 可以显示集合中对象的类型成员的正确列表。当您键入 Select 子句时,这一点很重要。在下面的部分,我们会看一些标准的 LINQ 子句,并且看一下它们是如何相互构建的,以致可以创建强大、灵活的查询。我们会依次看下列各个子句:From 子句、Select 子句、使用查询结果、Where 子句和 Order By 子句。在下列示例中,我们有时会引用一个 Customer 或一个 Order。对于这些示例,设想它们已在图1 中定义。每个 Customer 都通过 Orders 属性具有对其订单的集合的引用。这允许两个实体之间存在简单关系。该关联由每个 Order 中的 CustomerID 外键形成。除了这些类型定义外,还有存储在变量客户中的 Customers 集合。

 

From 子句

    From 子句是 LINQ 的核心,因为每个查询都需要它。查询必须始终以 From 子句开头,它是必须要明确提供的唯一的子句。From 子句的基本模式如下:

From <query variable> In <datasource>


    从本质上而言,From 表示遍历一组数据的行为。要实现此操作,它需要与源中的每个数据项相对应的变量,这与用于 For Each 语句的迭代变量非常相似。在 From 语句后面的子句中,可以引用该查询变量来对数据进行筛选、排序或其他操作。下面是 From 子句一个非常简单的示例:

Dim numbers() As Integer = {1, 7, 4, 2, 91, 12, 23, 37, 42}

Dim allNumbers = From number In numbers

For Each num In allNumbers

    Console.Write(num & " ")

Next

    在此示例中,查询会返回类型为 IEnumerable(Of Integer) 的集合。关于此查询,有几个地方值得注意。首先,Select 是隐式的,这也就是您在示例中的看不到它的原因。Select 语句是可选的;如果您一个也不提供,那么即会返回作用域中的变量集。其次,该查询不是真的很有用。您可以删除该查询,然后只要遍历使用 For Each 循环的那些数字变量。LINQ 的真正功能还尚不明显,但是我们来看一下下面的查询:

From <query variable> As Type In <datasource>
Dim checkboxes = From checkbox  As Checkbox In groupJobType.Controls

    From 语句的变体允许您指定遵循该查询变量的类型语句。如果您想从特定源检索类型,并且在检索它们的时候重新将它们键入到更具体(也可能不那么具体)的类型中,这就很有用。我们恰好知道,groupJobType 的 GroupBox 中的所有控件都是 Checkbox 控件,因此,我们可以在检索它们的时候安全地将它们键入 Checkbox。

    还有使用 From 关键字两次的 From 子句的另一个变体:

From <query variable> In <datasource> Let <variable> = <value>

 

Dim evens = From Number In numbers Let IsEven = (Number Mod 2 = 0)

 

注:原文这里如下,但正式版中格式有所变动2个语句变成一个,并且Let为显示

From <query variable> In <datasource> From <let variable> = <value>

From <query variable> In <datasource>, <let variable> = <value>

Dim evens = From Number In numbers From IsEven = (Number Mod 2 = 0)
Dim evens = From Number In numbers, IsEven = (Number Mod 2 = 0)

 

 

    该查询语法实际上就是一个 From 子句,它后面的部分称为 From 的“Let”部分。实际上,我们是在定义另一个变量,并将它赋予指定的值。在这种情况下,我们是在名为“IsEven”的结果集上定义另一个值,即布尔值 — 不论我们选择的数字是否为偶数,都是这个结果。这个过程必然意味着所返回的对象发生了变化:它不再只是 Integers 的 IEnumerable,因为返回的可枚举的结果中的每个项目都包含数字和布尔值。我们会看一下如何使用下面的查询结果。请注意,在通用语法表达式中,只要有可能省略 From 关键字的第二次使用,就可以使用逗号代替;上面的两个示例都是相同的。

    Let 变量和标准查询变量的区别在于,对 Let 变量始终指定 using = 值,而查询变量的后面则是 In 关键字。然而,Visual Basic 团队正在积极地研究公开 Let 变量的方法,因此,现在有一个好机会,那就是用于 Let 的支持的语法会从现在到产品以最终形式发布的这段时间内发生变化。

 

From <queryVar1> In <datasource1>, <queryVar2> In <datasource2>, ...
Dim customerOrders = From cust In customers, ord In cust.Orders

    该语法允许您指定多个数据源。在上面的示例中,我们获得了客户和订单的集合;实际上,这是对这两个源的一个隐式 Join。很多时候,这些源不会这么容易就关联起来(您的第一个查询变量上不会有第二个查询变量的集合),因此您必须使用 Where 子句将它们关联起来。但是在这种情况下,我们的 Orders 集合实际上都位于每个 cust 实例上;因此我们仅获得了应用于给定客户的订单。

 

Select 子句

    到目前为止,您已了解如何指定数据源。请注意,在每个示例中,我们都已省略了 Select 关键字。这仅意味着我们使用了默认的 Select 行为,就是要选择指定的每个查询或 Let 变量(逻辑上等同于 Select *)。但是我们也有能力指定 Select 子句,并且我们在执行时获得了高得多的灵活性:

Select <varA>, <varB>, ...

Dim allNumbers = From number In numbers _

                 Select number

 

Dim evens = From Number In numbers Let IsEven = (Number Mod 2 = 0) _

            Select Number, IsEven

 

Dim customerOrders = From cust In customers, ord In cust.Orders _

                     Select cust, ord

    这是最简单的 Select。比较一下前面的查询和此处 From 部分中的同等查询。使用显式 Select 子句的这三个示例中的每一个查询,都将具有和 Select 子句为隐式子句的先前查询一样的行为。

使用 Select 会更有趣,但是,当您对这个主题尝试变体时:

Dim customerInfos1 = From cust In customers _

                     Select cust.FirstName, cust.LastName

 

Dim customerInfos2 = From cust In customers, ord In cust.Orders _

                     Select cust.LastName, ord.OrderID, ord.OrderDate

 

    首先要注意的是新的通用语法并未指定(该示例上方没有注释)。这是因为,如果您考虑了这些示例,则前一个代码就会是同一个样式:毕竟 FirstName 和 LastName 只是变量。但是与先前的示例不同,我们废弃了查询变量,并只返回了几个属性。这是一个非常强大的功能。

LINQ 另一个强大的功能就是:当您不需要重命名字段的时候,Visual Basic 会根据属性的名称来推断这些名称。但是,有时候您可能希望重命名字段,或者合并数据,如下所示:

Select <aliasA> = <varA>, <aliasB> = <varB>, ...

Dim customerInfos1 = From cust In customers, ord In cust.Orders _

                     Select [Date] = ord.OrderDate, _

Name = cust.FirstName & cust.LastName

 

    在某些情况下(例如 Date = ord.OrderDate),您可能会由于个人偏好而这样做。在另一种情况下(例如 Name = cust.FirstName & cust.LastName),您实际上会想创建一个新的数据段 — 一个字段组合。还有使用别名的第三种情况。设想您从恰好具有相同名称但实际不同的源中选择了两个字段。名称推断会对产生的类型为这些变量指定相同的属性名称,由于冲突,所以无法编译。您可以对一个(或两个)冲突的字段名称使用别名,以解决该冲突。

    关于 Select 子句,值得注意的最后一点就是,它可以重新确定变量的作用域,因此查询中的任何子句仅可以看到 Select 公开的内容。我们会在讨论 Where 子句的时候更详细地讨论这一点。

 

使用查询结果

    现在,让我们看一下如何使用查询结果。要执行这个操作,您需要了解在执行查询时,这个查询是由什么组成的:

Dim allNumbers = From number In numbers _

                 Select number

 

    任何查询的结果始终都会是 IEnumerable(Of T)。这是查询最可靠、最强大的一个方面,因为这意味着您可以一直遍历数据。有时候您可能知道 of T 的类型是什么,但有时候可能又不知道,例如在匿名类型的情况下。匿名类型对于 LINQ 是新类型,用来表示具有多个返回字段列的查询的任何结果。匿名类型是一种您无法以代码直接命名或参考,但是编译器可以将它视为其他命名类型的类型(编译器为该类型指定了名称,您只是不知道是什么名称,或者您不需要知道是什么名称)。匿名类型的字段名称可以直接以代码指定,也可以由查询表达式的编译器自动推断。例如,使用匿名类型会启用类似您之前看到的代码,{Num, IsEven} 和 {cust, ord} 的配对是有效的新类型,每个类型都包含两个数据段(包含作为各自可访问的字段名称的 Num、IsEven 和 Cust、Ord):

Dim customerInfos1 = From cust In customers _

                     Select cust.FirstName, cust.LastName

 

Dim customerInfos2 = From cust In customers, ord In cust.Orders _

                     Select cust.LastName, ord.OrderID, ord.OrderDate

 

 

    正如您能看到的一样,匿名类型允许您在 Select 子句指定不同数量的字段,而无需您预先指定表示每个特定字段组合的准确类型。虽然我们说每个查询都是一个 IEnumerable(Of T),但是您可能已经注意到,实际上没有一个示例为代表该查询的变量指定了类型。您可能会担心该变量的类型会变成 System.Object,并且您正在失去流程中的所有强类型化支持。其实并非如此。有一个称为类型推断的新编译器功能,这意味着该编译器可以根据其初始化值,推断出声明没有类型说明符的变量的类型(如果您在代码顶部包括了 Option Infer On 语句)。例如,下列变量就是根据初始化值以静态的方式确定类型的: 

Dim x = 5 ' x is typed as an Integer

Dim y = DateTime.Now ' x is typed as a DateTime

Dim z = GetCustomer() ' assuming GetCustomer returns a customer

' type, z is typed as a Customer

 

x = "hello" ' COMPILE-TIME ERROR: x can only store Integers

     表示查询的变量也可以采用相同的方式被强类型化。这使您可以真正地对那个变量直接使用成员,而不必担心那些后期绑定的成员。另外,您会保留强类型化的其他优点,例如 IntelliSense 支持。当声明新变量没有类型说明符时,在 For Each 语句中会出现同一种类型推断:

' assuming allNumbers is a collection of Integers,

' the type of num is inferred to be an Integer

For Each num In allNumbers

    通常,使用查询时您可能最难知道的事情便是正被返回的对象是哪种特定种类。这就是您必须信任查询的原因。实际上,不同种类的对象都可以用来存储查询结果。值得注意的是,它们将始终为 IEnumerable(Of T),因此您可以使用 For Each 或将它们传递给接受 IEnumerable 对象的 API,来遍历它们。尽管如此,但还有一条经验法则,它将帮助您分辨返回的对象,这条经验非常重要(您随即就会明白原因)。当返回的结果由单个命名类型组成时,则查询会返回 IEnumerable(Of <your type>)。当返回的结果由多个类型组成,则查询会返回 IEnumerable(Of <anonymous type>)。因此在上面的示例中,正好返回一个来自 Integers 数组的整数,所以您将获得一个 IEnumerable(Of Integer)。

    当查询结果为单个类型的 IEnumerable 时,使用这个结果就会非常容易。您只需要使用 For Each 子句:

For Each num In allNumbers

    Console.WriteLine(num)

Next

    既然我们有了单个类型的 IEnumerable,就应该不会再有什么出乎我们的意料了。但是,现在让我们看一下涉及匿名类型的情况:

Dim evens = From Number In numbers Let IsEven = (Number Mod 2 = 0) _

            Select Number, IsEven

 

For Each numInfo In evens

    Console.WriteLine("Is " & numInfo.Number & " even? " & _

    numInfo.IsEven)

Next

    由于查询会返回内部具有两个数据段的对象,因此变量“evens”是 IEnumerable(Of <anonymous type>)。我们得出的每个实体都具有名为 Num 和 IsEven 的字段;因此,当我们遍历偶数集合时,我们需要废弃所有数据段以检索它们(例如 numInfo.Num)。有几个地方值得注意。首先,我们将这个迭代变量命名为 numInfo。当遍历匿名类型的集合时,使用通用名称来指示它具有多个数据段是一个不错的方法。其次,我们对示例中的查询变量使用了大写,因此当我们在 For Each 循环内部废弃和使用它们时,它们看似标准属性。我们不需要对上一个示例中的“number”这么做,因为那时还尚未引入匿名类型。为了说明这个操作的简易性,让我们再看一个示例:

Dim customerInfos = From cust In customers, ord In cust.Orders _

                    Select Name = cust.FirstName & cust.LastName, _

                    [Date] = ord.OrderDate

 

For Each custInfo In customerInfos

    Console.WriteLine("Customer Name: " & custInfo.Name & _

                      " Order Date: " & custInfo.Date)

Next

    它真是再简单不过了。但是,有时候您可能想分别检索您的名和姓两个字段。上面的变体会保留 Customers 和 Orders 中的所有数据(以后我们想使用其他字段),但是对这个特定的迭代不会获得同样的效果。

Dim customerInfos = From cust In customers, ord In cust.Orders _

                    Select Customer = cust, Order = ord

 

For Each custInfo In customerInfos

    Console.WriteLine("Customer Name: " & _

    custInfo.Customer.LastName & ", " & _

    custInfo.Customer.FirstName)

    Console.WriteLine("Order Date: " & custInfo.Order.OrderDate)

 

Next

    同样,一切都在意料之中。通过将每个完整的对象纳入我们的结果,我们必须废弃这些对象,以获得我们想使用的特定字段。请注意,我们会在 For Each 循环内部将名和姓连接起来。我们会教您一个更深一层的小技巧。如果您要考虑查询时可用的成员,您可以做一些相当酷的操作。例如,请考虑下面这个代码:

Dim customerInfos_2 = From cust In customers, ord In cust.Orders _

                      Select Customer = cust, Order = ord

Console.WriteLine("There are " & customerInfos.Count & _

                  " customer orders")

    这个示例对查询结果直接使用了 Count,以查看有多少项结果。我们不会进入您可在集合找到的所有成员,但是请不要忘记试试您可以对结果集执行何种操作。同时也请注意,这些成员对由查询产生的所有集合都是可用的。如果您猜测到这意味着这些成员必须要在 IEnumerable(Of T) 上,您就猜对了;甚至连它们的实现也必须在 IEnumerable(Of T) 上进行。这是一个被称作扩展方法的新功能。关于如何执行查询,有一个概念您必须了解,那就是延迟执行。在延迟执行中,直到它被实际使用时才会尝试检索查询的数据。这一点很重要,因为它允许您在不同的部分编写查询,而不需要多次调用底层数据源,同时还存在巨大的性能优势,特别是在根据数据库访问信息时(在这种情况下应特别避免多次调用)。这意味着,查询的声明实际上不会检索这些数据;只执行需要数据的操作会导致检索这些数据。您将可能执行的操作最常见的就是遍历查询。但是其他可能的操作还包括通过显示数据或尝试使用一种方法(如 Count)来查找查询的条目数。图 2 显示了执行数据检索的地方。

 

使用 Where 子句筛选

    Where 子句通过允许根据特定条件筛选使您的查询变得更加强大。Where 子句的用法与 If 语句非常相似。即在 If 语句中可能出现的情况,在 Where 子句中也可能出现:

Where <condition>

Dim evens = From num In numbers _

            Where num Mod 2 = 0 _

            Select num

    请注意这个集合称为“evens”,与上一示例中的一样。与上一个示例不同的是,它仅包含偶数数字。当您遍历结果时,您只能得到偶数结果:

Dim mids = From num In numbers _

           Where num > 10 And num < 50 _

           Select num

    Where 子句的优点就在于它很简单。您的 <condition> 可以是您选择的任何条件。让我们看一下它如何处理更棘手的查询:

Dim seattleCustomers = From cust In customers, ord In cust.Orders _

                       Where cust.City = "Seattle" _

                       Select Customer = cust, Order = ord

 

Dim seattle2003Orders = From cust In customers, ord In cust.Orders _

                        Where cust.City = "Seattle" _

                        And ord.OrderDate.Year = 2003 _

                        Select Customer = cust, Order = ord

    有时候您可能会想要按一个字段来进行筛选,并选择一个(或多个)不同的字段。这非常简单:

Dim dallas2005Orders = From cust In customers, ord In cust.Orders _

                       Where cust.City = "Dallas" _

                       And ord.OrderDate.Year = 2005 _

                       Select cust.CustomerID, ord.OrderID

    一旦您声明了 From 子句,则其他子句的顺序就是可选的,您可以按照您喜欢的任何顺序使用它们:

Dim seattle2003Orders = From cust In customers, ord In cust.Orders _

                        Select Customer = cust, Order = ord _

                        Where Customer.City = "Seattle" _

                        And Order.OrderDate.Year = 2003          

    请比较一下这个示例和上述相同名称的示例。请注意,我们已将 Select 子句移到了 Where 子句之前,这是完全可以接受的。但是也请注意,Where 子句必须进行细微的更改,现在提到的是 Customer 和 Order,而不是 cust 和 ord。请特别注意,Select 子句是重新确定作用域的子句,这样在 Select 子句之后的任何子句就仅可以看到 Select 公开的内容,这意味着该代码将无效:

' This won’t compile

Dim dallas2005Orders = From cust In customers, ord In cust.Orders _

                       Select cust.CustomerID, ord.OrderID _

                       Where cust.City = "Dallas" _

                       And ord.OrderDate.Year = 2005

    我们在 Where 子句内部提到了 ord 和 cust,但是现在 Where 在 Select 的后面,因此这两个变量就不见了。Select 之后,我们所留下的是具有 CustomerID 和 OrderID 这两个属性的匿名类型。该代码可以被更改,如下所示:

Dim dallas2005Orders = From cust In customers, ord In cust.Orders _

                       Select cust.CustomerID, ord.OrderID, _

                              cust.City, ord.OrderDate _

                       Where City = "Dallas" _

                       And OrderDate.Year = 2005

    请注意许多子句,包括 Where 和 Order By,都不会重新确定可用变量的作用域。

 

使用 Order By 排序

    Order By 是任何查询的基本子句之一。Order By 将查询结果按指定的一个或多个字段进行排序:

Order By <query variable>

    这是最简单的 Order By。如果没有指定任何顺序方向,则假定您是想按升序的顺序查看结果。下面是一个使用中的子句:

Dim nums = From num In numbers _

           Order By num _

           Select num

    Order By 的下一个变体几乎与第一个变体完全相同。它假设您是想要 Order By 变量内部的一个字段,而不是变量本身:

Order By <field1>, <field2>, ...

Dim customerInfos1 = From cust In customers, ord In cust.Orders _

                     Order By cust.City _

                     Select Customer = cust, Order = ord

 

Dim customerInfos2 = From cust In customers, ord In cust.Orders _

                     Order By cust.City, ord.OrderDate _

                     Select Customer = cust, Order = ord

    最好选择一个或多个字段。此外,因为 Order By 不会重新确定可用变量的作用域,所以下列 Select 子句可以使用前一作用域中可用的任何变量:

Order By <var1> [Ascending/Descending], _
              <var2> [Ascending/Descending], ...

Dim customerInfos = From cust In customers, ord In cust.Orders _

                    Order By cust.City Descending, ord.OrderDate _

                    Select Customer = cust, Order = ord

    这充分使用了 Order By。我们忽略了第二列的方向,但是它仍包含在第一列,这意味着我们先按城市以降序排序,然后再按订单日期以升序排序。当然,您可以将 Order By 和 Where 结合起来,我们以几个示例结束对该子句的介绍:

Dim orderedEvens = From num In numbers _

                   Where num Mod 2 = 0 _

                   Order By num _

                   Select num

 

Dim seattleCustomers = From cust In customers, ord In cust.Orders _

                       Order By cust.City Descending, ord.OrderDate _

                       Where cust.City = "Dallas" _

                       And ord.OrderDate.Year = 2005 _

                       Select Name = cust.LastName & ", " & _

                       cust.FirstName, ord.OrderDate

 

更多子句

    其他可用的子句在其他文章中讨论,但是仍支持许多标准查询功能,因为本文还包含一些类似的子句。例如,支持联接、聚合和分组功能。您在上面的示例中已经看到隐式联接,而显式联接可以通过使用 Join 关键字执行。可以使用 Aggregate 子句支持聚合信息功能,而分组功能则可使用 Group By 子句和预先提供的聚合方法(如 Sum 或 Average)之组合来予以支持(尽管您始终可以创建和使用自己的组合)。诸如插入、更新和删除等标准查询功能均可获得支持,只是它们并非直接来自查询语法。

 

摘要

    LINQ 提供了基本查询功能,包括指定源 (From)、识别要返回的数据 (Select)、筛选 (Where) 和排序 (Order By)。同时还提供可以对您的数据集进行分组、联接或聚合的高级查询功能。有了这些功能,又能得到其他功能(如匿名类型或类型推断)的支持,您便完全可以直接使用 Visual Basic 代码来编写类似 SQL 的查询。

 

 

附件

     图1

 

 

 

 

Public customers As List(Of Customer)

Public orders As List(Of Order)

 

 

Public Class Customer

    Public Property CustomerID() As Integer

        Get

            

        End Get

        Set(ByVal value As Integer)

 

        End Set

    End Property

    Public Property FirstName() As String

        Get

 

        End Get

        Set(ByVal value As String)

 

        End Set

    End Property

    Public Property LastName() As String

        Get

 

        End Get

        Set(ByVal value As String)

 

        End Set

    End Property

    Public Property City() As String

        Get

 

        End Get

        Set(ByVal value As String)

 

        End Set

    End Property

    Public Property Orders() As List(Of Order)

        Get

    

        End Get

        Set(ByVal value As List(Of Order))

 

        End Set

    End Property

 

End Class

 

Public Class Order

    Public Property OrderID() As Integer

        Get

 

        End Get

        Set(ByVal value As Integer)

 

        End Set

    End Property

    Public Property CustomerID() As Integer

        Get

 

        End Get

        Set(ByVal value As Integer)

 

        End Set

    End Property

    Public Property OrderDate() As DateTime

        Get

 

        End Get

        Set(ByVal value As DateTime)

 

        End Set

    End Property

 

End Class

     图2

'Figure 2 在 For Each 子句中检索到的数据

Dim seattleCustomers = From cust In customers _

                       Where cust.City = "Seattle"

 

Dim recentOrders = From ord In orders _

                   Where ord.OrderDate.Year = 2007

 

Dim custInfos = From cust In seattleCustomers, ord In recentOrders _

                Where cust.CustomerID = ord.OrderID _

                Select Name = cust.LastName & ", " & cust.FirstName, _

                       cust.CustomerID, ord.OrderDate _

                Order By OrderDate Descending, Name

 

' None of the above queries are run until this point.

' This means that the information is only retrieved once.

For Each custInfo In custInfos

' take action on the data here

Next