千里之行,始于足下
初识LINQ
软件是简单的,它包括两个物件:代码和数据。但是编写软件却不是那么简单,其中最主要的一个任务就是编写处理数据的代码。
我们可以从大量的编程语言中选择一种来编写代码。为应用程序选择编程语言取决于业务环境,开发者的喜好,开发小组的技能和操作系统,或者是公司的政策。
无论你选择了何种语言,你都需要处理数据,这些数据可能保存在磁盘文件上,数据库的表中或者来自Web的XML文档中,或者是混合在一起的不同来源的数据。归根结底,管理数据是每个软件项目都需要做的工作。
因为处理数据是如此一项普遍的任务,所以我们希望.NET Framework平台能够提供一种快捷的处理方式。.NET确实提供了处理数据的支持。你将会看到,可是它没有完成这种任务:语言和数据的深度集成。而这正是LINQ to Objects, LINQ to XML, and LINQ to SQL支持的目标。
我们在本书中展示的技术是一种编写代码的新方式。本书以程序员对程序员的方式编写,所以不要害怕:我们不会让你等的太久,你马上就会写出LINQ版的“Hello World”来。在本章中,我们将会介绍一点简单的LINQ代码,通过这些代码,你会对本书其他的部分有所了解。我们的目标是,当你看完本书时,你将会有能力解决真正的问题,而且确信与LINQ一起工作是多么的愉快!
本章的目的是让给你概览LINQ技术,是你确信使用LINQ的理由。我们将一LINQ技术概要和LINQ工具包作为开始,包括LINQ to Objects, LINQ to XML, and LINQ to SQL。然后我们将回顾LINQ的发展历史来彻底了解我们为什么需要LINQ以及LINQ来自何方。本章的下一部分我们将会为你开始编写LINQ代码做一些指导。
1.1 LINQ是何方神圣?
假如你在使用.NET编写一个应用程序,需要保存一个对象到数据库中、查询数据库,把结果转换为业务对象。问题在于,大多数情况下,至少在与关系数据库的交互中,我们无法很好的应对这种需求。面向对象的数据库做了一些好的尝试,它更接近面向对象的编程平台和命令式编程语言,如C#和VB.NET。然而,好多年过去了,关系数据库仍然应用的如此普遍,我们仍然要我们的程序的数据访问和持久化而挣扎。
LINQ本来的目的是为了处理在使用数据库和.NET编程语言遇到的概念和技术难题。使用LINQ,Microsoft的目的是为了提供一个解决对象关系映射问题的方案,同时简化对象和数据源的交互。LINQ最终发展成为一个通用语言集成查询的工具包。它可以访问来自内存的对象(LINQ to Objects)、数据库(LINQ to SQL),XML文档(LINQ to XML)、文件系统或者任何其他数据来源。
在深入了解LINQ之前,我们将会提供一个LINQ的概览,同时介绍LINQ如何扩展了编程语言。
1.1.1 LINQ鸟瞰
万事俱备,只欠东风,LINQ就是数据王国和面向对象的编程语言之间的东风(这个比喻也许并不恰当)。LINQ统一了数据访问,无论是何种数据源,LINQ允许从不同的数据源以相同的方式提取数据,允许查询和集合运算符,就像SQL语句一样。但是LINQ直接作为一系列扩展直接集成在了.NET语言中,如C#,VB.NET。LINQ表示Language-INtegrated Query。
LINQ出现之前,在使用通用语言如C# 、VB.NET编写应用程序时,我们需要穿梭于不同的语言之间,如SQL、XML或者XPath,以及不同的技术之间,如ADO.NET或者System.Xml,这些方法有很多缺陷。LINQ将这些世界粘合在一起。它使我们免于编写不同世界之间通道上的冗余代码。LINQ将会简化很多转换:XML到Object的转换,Object到关系数据的转换,关系数据到XML的转换。
LINQ的一个关键特点是:它被设计用来提供一个一致的编程模型而不受不同数据源的影响。LINQ具有一致的语法和概念,一旦你学会了使用LINQ操作一个数组或者集合,你同时也掌握了利用LINQ操作数据库和XML的概念。
LINQ另一个重要的特点是:当你使用LINQ的时候, 你工作在一个强类型的世界中。由此获得好处包含编译时检查和VS的智能感知特性等。
LINQ将会对你处理应用程序和组件中的数据产生重大影响。你会发现LINQ在向声明式变成模型方向迈进了一步。也许在不远的将来,你会感到疑惑, 你为什么会习惯写如此多的冗余代码。
LINQ有一个二元性,你可以认为LINQ由两个部件构成:一组操作的数据的工具的集合和一组编程扩展的集合。
你将会首先看到LINQ作为工具集如何操作对象、XML文档、关系数据库或者其他类型的数据。然后你将会看到LINQ也是一种编程语言的扩展,如C# 和VB.NET。
1.1.2 LINQ 作为工具集
LINQ为我们编程操作数据提供的很多的可能。本书中,我们将会详述它的三个功能:LINQ to Objects, LINQ to SQL, and LINQ to XML,LINQ的这三种提供者组成了一个工具家庭, 它可以被单独使用,也可以一起使用构建强大的解决方案。
LINQ对新的数据源是开放的。三个主要的LINQ提供者是构建在通用LINQ基础之上的。这个基础包括一系列的构建块,包括查询运算符,查询表达式和表达式树。其中表达式树允许LINQ工具集可被扩展。
不同的LINQ可以被创建用来提供访问不同的数据源。软件供应商可以发布LINQ的实现,你还可以创建自己的实现,12章中覆盖了LINQ的可扩展性。LINQ可以操作大量的数据源,包括文件系统,活动目录,WMI,Windows日志,或者其他任何数据源或者API。相信你可以从LINQ的这种功能重受益匪浅。实际上除了LINQ to Objects, LINQ to SQL, and LINQ to XML之外, Microsoft已经提供了很多其他的LINQ的Provider,其中两个就是LINQ to DataSet 和 LINQ to Entities。
图1.1 LINQ构建块,LINQ提供者和可以使用LINQ查询的数据源。
我们将会在本书的第二和第三部分介绍这些工具,目前让我们着眼大局。
图1.1显示了LINQ构建块和工具集之间的关系。图1.1中的LINQ 提供者不是独立的工具,他们可以直接应用于你的编程语言中。这是可能的,因为LINQ框架来自一系列的语言扩展。这是LINQ的另一个方面,我们将会在下一章中讲述。
1.1.3 LINQ 作为语言扩展
LINQ允许你通过针对各种数据源编写查询来进行信息访问,而不是语法糖,简单的让你在C#代码中包含SQL语句。LINQ提供了SQL提供的同样的功能,但是却实现在你选择的编程语言中,这样的做的好处是,LINQ提供了声明性的方法让你写出更简明的代码来。
列表 1.1 显示了使用LINQ编写的C#代码
var contacts = from customer in db.Customers
where customer.Name.StartsWith("A") && customer.Orders.Count > 0
orderby customer.Name
select new { customer.Name, customer.Phone };
var xml =new XElement("contacts", from contact in contacts
select new XElement("contact",
new XAttribute("name", contact.Name),
new XAttribute("phone", contact.Phone)
)
);
该列表展示你为了从数据库提取数据和从其创建XML文档所必须的代码。想象如果没有LINQ,我们如何做到同样的事情?现在我们可以看到, 使用LINQ将会使事情变得简单且更加自然。很快,你将会看到更多的LINQ查询,但是现在让我们将注意力转移到语言方面。在列表中我们使用from, where, orderby, 和 select关键字,显然C#已经为为了使用LINQ做了扩展。
图1.2展示了典型的用于与对象,XML,数据表通信的语言集成的查询
图中的查询是以C#展示的,而不是在一个新语言中。LINQ不是一种新的语言。它集成到C#和VB.NET中。此外LINQ还可以避免使你的编程语言与SQL、XSL或者其他针对特定数据的语言纠缠在一起。LINQ可以使用简答的语法在不同的数据源之间进行查询。可以把LINQ想象为一个遥控器,有时你用它来查询一个数据库,有时你用它来查询一个XML文档。但是你在你喜欢的语言里面完成了这一切,而不是切换到其他语言,例如SQL或者XQuery。
图 1.2 LINQ作为语言扩展以及作为访问各种数据源的方法
现在你对LINQ有了一些基本的了解。接下来让我来讨论一下LINQ的动机,然后我们将回顾一下它的设计目标和一点历史。
1.2 我们需要 LINQ?
我们刚刚对LINQ做了一个简单的介绍。最大的问题是:我们为什么需要LINQ呢?原来的方式为有什么缺陷?LINQ只是让我们工作在编程语言,关系数据和XML时变得更方便吗?
LINQ产生基于这样一个简单的事实:大部分应用程序都是围绕数据访问和关系数据库开发的。因此,为了编写程序,单单学此C#是不够的,你仍然需要学习其他各种语言,如SQL或者是其他API来构成你整个应用程序。
让我们来看看使用.NET APIs编写的标准的数据访问代码。这能让我们看出使用这种方式编写代码所遇到的普遍问题,同时我们将分析处理其他数据如XML时遇到的同样的问题。你会看到,LINQ解决了数据源和编程语言之间阻抗错配的问题。最后,我们会看到LINQ是如何解决这种问题的。
1.2.1 普遍问题
我们的应用程序需要.NET Framework提供一种能够让我们访问数据库数据的方法。这是由ADO.NET做到的,它提供了一系列的API来访问数据库,并把获取的数据显示在内存中。这些API包含一些类如:SqlConnection, SqlCommand, SqlReader, DataSet, 和 DataTable。问题是这些类让你明确的与关系数据工作,而且C#这样的语言却使用面向对象的泛型。
现在面向对象的泛型是软件的开发的主流模型,开发人员需要花大量的时间工作在对象和数据的映射上,结果造成我们花大量时间写了很多模式化代码。现在好了,LINQ帮我们铲除了这种负担,让我们的工作变得更有效率。而且不只是生产率的问题,也是质量的问题。编写冗长的零碎的铅块代码会导致隐藏的缺陷,有时会导致性能问题。
列表1.2我们如何使用.NET传统代码访问一个数据库。你会看到LINQ的出现是为了拯救我们。
列表 1.2 典型的.NET 数据访问代码
using (SqlConnection connection = new SqlConnection("..."))
{
connection.Open();
SqlCommand command = connection.CreateCommand();
command.CommandText =@"SELECT Name, Country
FROM Customers
WHERE City = @City";
command.Parameters.AddWithValue("@City", "Paris");
using (SqlDataReader reader = command.ExecuteReader())
{
while (reader.Read())
{
string name = reader.GetString(0);
string country = reader.GetString(1);
...
}
}
}
快速浏览一下这段代码,我们会看到这种编程模型的缺陷如下:
n 尽管我们只是执行简单的任务,却必须执行很多的步骤和编写冗余的代码
n 查询是作为一个字符串展示的,这意味着我们失去了所有的编译时检查。如果字符串里面包含了无效的SQL查询是将会怎么样呢?
n 参数和结果都是弱类型,列类型是我们期望的吗?参数的数目匹配吗?参数的名称匹配吗?
n 我们写的代码只能应用在SQL Server之上,而不能被应用于其他服务器。我们必须针对不同的数据库编写不同的SQL语句。
有一些其他的解决方案,我们可以使用代码生成工具和其他一些对象-关系映射工具来完成这个任务,问题是这些工具有他们各自的局限性,例如:如果它们被用来处理数据库,大多数情况下,它们将不能处理其他数据源,如XML文档。而且,语言提供商不能把映射工具的查询和访问特性集成到他们的语言中。
LINQ产生的动机来源于两点:微软还没有一个数据映射方案,使用LINQ,它可以将查询集成到他的编程语言中。这可以消除我们刚才提到的缺陷。而且通过使用LINQ,我们可以直接在编程语言中编写如下查询来访问任何数据源。
列表1.3 简单的查询表达式
from customer in customers
where customer.Name.StartsWith("A") && customer.Orders.Count > 0
orderby customer.Name
select new { customer.Name, customer.Orders }
在本查询中,数据可以在内存中,数据库中,XML文档中,或者其他地方,语法基本相似但是并不是完全相同,这种查询可以用于多种类型的数据和不同的数据源,这要归功于LINQ的可扩展性。例如,在将来我们可以看到LINQ可以查询文件系统或者调用Web服务。
1.2.2 处理阻抗错配
让我们继续关注为什么我们需要LINQ。事实上,现在的应用程序都需要同时处理关系数据,SQL,XML文档,XPath等等,这意味着两件事情:
n 我们需要与每种技术打交道。
n 把这些技术组织在一起来构建一个丰富和一致的解决方案
问题在于OOP,关系数据模型和XML等技术并不是为它们在一起工作而设计的。它们表示不同的泛型,他们之间并不能很好的协作。
什么是阻抗错配?
应用程序操作数据来完成工作任务, 但是转换一个对象到其他表示,如关系数据库,经常需要写冗长的代码。
LINQ处理的普遍问题是:数据!=对象,特别的,对于LINQ到SQL:关系数据 != 对象。同样的原理可以应用于LINQ到XML:XML data != Objects,同样:XML数据 != 关系数据,我们已经用过术语 阻抗错配了。它一般用于描述系统之间的不兼容性,一个系统的输出并不能直接作为另一个系统的输入。虽然该属于来自电器工程,但是它同样适用于物理、计算机科学以及信息学等领域。
对象关系映射
如果我们以面向对象的泛型和关系泛型作为示例,阻抗错配存在于许多层次上。如下所述。
关系数据库和面向对象的语言并不共享相同的基本数据类型。例如,数据库中的字符串通常有限定的长度。而在C#中却没有。如果你将一个150个字符的字符串保存到一个只能容纳100个字符的列中,将会引发问题。另一个简单的举例是大多数数据库没有Boolean类型,而编程语言中却普遍使用了Boolean类型。
OOP和关系理论来自不同的数据模型。基于性能问题和原有的特性,关系数据库通常被正规化,正规化是消除冗余,更有效的组织数据,提高数据一致性的过程。正规化的结果是使数据专门用于关系数据模型。这妨碍了表和记录与对象和集合之间的直接映射。关系数据对表和关系进行正规化,而对象模型使用继承,组合和复杂引用表示。一个普遍的问题是关系数据库并不能理解继承,在关系数据库和类层次之间进行映射需要我们“变一个魔术”。
封装,对象是自包含的,它包含数据和行为。在数据库中,数据并不具备行为,我们只能通过存储过程和等操纵数据,也就是说,数据和行为是分离的。
面向对象的类层次和关系数据库之间的不同是造成这种错配的根本原因。我们可以说关系数据库来自火星,而对象来自金星。
如图1.3所示,我们有一个对象模型,需要把它转化为关系模型。
关系数据库不支持继承和组合等概念,这意味着在这两种模型中我们不能以相同的方式展现数据。可以看到,这些对象可以映射为数据库中的一个表。
即使我们想把一个对象模型持久化到一个数据库中。我们也不能使用直接映射的方法。例如,为了提高性能或者避免重复,这将会为我们创建表提供便利,但是结果是我们就不能把从数据库获取的数据简单的转换为对象模型。
我们可能会通过设计来减少两个世界的错配,但是我们永远不能消除这种错配,因为两种泛型有本质的不同,我们甚至没有选择的机会。通常数据库模式已经定义好,而其他情况下有人已经定义好了对象模型。
图1.3 简单对象如何映射到数据库模型。映射的复杂度会根据两种模型之间的不同而不同
更复杂的问题出现在集成多个数据源的访问。当使用面向对象的语言编程的时候,我们通常使用对象模型来展示我们的业务域,而不是直接绑定到关系结构上。而我们需要让对象模型和关系模型一起工作。这不是一项简单的工作,因为.NET包含类,业务,规则,复杂的关系和继承,而关系数据源却包含表,行,列和主键以及外键。
一个典型的解决方案是在面向对象的语言和关系数据库之间建立一个对象-关系映射。也就是说要把关系数据模型映射到我们的对象模型,或者反之。映射可以这样定义,它是一种让对象和其之间的关系保存于一个持久的数据存储中的行为,当前是关系数据库。
关系数据库不会自动映射到对象模型,对象-关系映射器是处理阻抗错配的自动化解决方案。长话短说:我们通过我们的类,数据库和映射配置提供一个对象-关系映射,而映射器为我们做了余下的工作。它产生SQL查询,把从数据库中获取的数据填充到我们的对象。或者将对象中的数据保存的数据库中等等。
你或许会想到,没有解决方案是完美的,对象-关系映射器也不例外,下面是它主要的缺陷:
n 在使用这个工具之前,我们必须要很好的了解他,而且要避免引起性能问题。
n 我们仍然需要熟知关系数据库的知识。
n 映射工具通常没有手写的数据访问代码有效率
n 并不是所有的工具都支持编译时验证
目前.NET有很多对象-关系映射工具。你可以选择开源的,免费的或者商业产品,例如列表1.4显示了NHibernate的映射配置文件,它们的字段,关系和集成都在XML文件中做了定义。
列表1.4 NHibernate用于映射Cat类和CATS表的映射文件
<?xml version="1.0" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.0" namespace="Eg" assembly="Eg">
<class name="Cat" table="CATS" discriminator-value="C">
<id name="Id" column="uid" type="Int64">
<generator class="hilo"/>
</id>
<discriminator column="subclass" type="Char"/>
<property name="Birthdate" type="Date"/>
<property name="Color" not-null="true"/>
<property name="Sex" not-null="true" update="false"/>
<property name="Weight"/>
<many-to-one name="Mate" column="mate_id"/>
<set name="Kittens">
<key column="mother_id"/>
<one-to-many class="Cat"/>
</set>
<subclass name="DomesticCat" discriminator-value="D">
<property name="Name" type="String"/>
</subclass>
</class>
<class name="Dog">
<!-- mapping for Dog could go here -->
</class>
</hibernate-mapping>
下面让我们继续关注LINQ能解决的其他问题。
对象-XML映射
同对象-关系的阻抗错配一样,相同的错配也存在于对象和XML之间,例如,W3C定义的XML模式规范同.NET Framework之间没有一一的类型对应关系,然而在.NET程序中使用XML并不是什么大问题,因为我们有可以处理XML的API,他们在System.Xml命名空间下。而且它们还支持对象的序列化和反序列化。但是,当我们要对XML文档做一个简单的操作的时候,我们仍然需要编写许多冗赘的代码。
由于处理XML在当前的软件开发中已经变得如此普遍,所有我们必须采取措施来提高我们操作XML时的工作效率。
当你看到这些技术领域的时候,你会发现他们之间有着多么惊人的差异,主要体现在以下几个方面:
n 关系数据库基于关系代数理论,它们都是关于表,行,列和SQL
n XML都是关于文档,元素,属性,层次结构和XPath
n 面向对象的编程语言构建在包含类,方法,属性,继承和循环的世界中
许多概念都是特定域专有的,而且与其它域的概念没有直接的映射关系。图1.4显示了几种领域之间概念的比较。
程序员往往为了让这些不同领域的技术在一起协作而做大量的工作。每种数据类型的不同的API也会花去开发人员大量的学习时间。最让人头疼的是那些错误的SQL语句或XML标记以及那些只能在运行时才能检查的内容。.NET语言为开发者提供了智能感知,强类型代码和编译时检查。当我们在程序中包含SQL语句和XML片段时,这些编译器提供的功能都无能为力。
图1.4 .NET 程序和数据源来自不同的世界,它们有着不同的概念
一个成功的解决方案需要在不同的技术之间架起一座桥梁,而且需要解决对象持久化,消除阻抗错配,降低资源消耗问题。我们必须在.NET和数据源元素之间解决如下问题:
n 完全不同的技术
n 不同的技能集合
n 为每种技术划分不同的职责和权力
n 不同的模型和设计原理
为了降低阻抗错配,微软已经做了一些努力,如:SQLXML4.0将SQL与XSD联系在一起,System.Xml将XML,XML DOM,XSL,XPath和CLR组织在一起。ADO.NET将在SQL和CLR数据之间建立起一座桥梁。所有这些努力证明,数据集成式必要的。然而这些努力没有一个共同的基础,这样很难将它们结合在一起使用。而LINQ为处理阻抗错配问题提供了一个公共的基础。
1.2.3 LINQ 雪中送炭
为了使对象和关系数据库在一起成功使用,你需要理解两种泛型,还有他们之间的不同,然后在对他们的理解之上做一个明智的折中。LINQ的主要目标就是消除至少是降低你对这些限制的担心。
阻抗错配使你必须从两端中选择一个主要的一端。使用LINQ微软选择了编程语言端,因为他可以比较容易的调整C#语言,而调整SQL和XML则不是那么容易。这样就可以深度集成数据查询和操作语言到编程语言中。
LINQ清除了对象,数据库和XML之间的障碍。它使我们使用同样的语言集成工具工作在不同的泛型之间。例如,我们可以在一个查询中同时使用XML数据和关系数据库中的数据。
一行代码胜千言,让我们看看LINQ的强大功力吧。列表1.5显示了使用LINQ用一个查询就完成了从数据库获取数据然后创建一个XML文档的功能。
列表 1.5 在同一个查询中使用关系数据和XML
var database = new RssDB("server=.; initial catalog=RssDB");
XElement rss =
new XElement("rss",
new XAttribute("version", "2.0"),
new XElement("channel",
new XElement("title", "LINQ in Action RSS Feed"),
new XElement("link", "http://LinqInAction.net"),
new XElement("description", "The RSS feed for this book"),
from post in database.Posts
orderby post.CreationDate descending
select new XElement("item",
new XElement("title", post.Title),
new XElement("link", "posts.aspx?id="+post.ID),
new XElement("description", post.Description),
from category in post.Categories
select new XElement("category", category.Description)
)
)
);
我们不会详述这段代码是如何工作的。重要的是我们认识到LINQ让我们与关系数据库和XML一起工作时更轻松。如果你以前做过类似的工作,相比LINQ出现之前,你会发现这段代码更简洁,可读性更好。
下面让我们回顾一下LINQ的成长的烦恼吧。
1.3 LINQ的原始设计目标
彻底了解微软使用LINQ要完成的目标是很重要的。本章将为你呈现LINQ项目的设计目标。了解LINQ发展的根源和理解相关的其他项目也是一件很有意思的事情,我们将会花费一点时间来了解LINQ的发展历史,了解一下它是如何产生的。
LINQ并不是微软最近的项目,它继承许多其它研究和开发工作的成果。我们将会讨论LINQ和微软其它项目之间的关系。如你所知,LINQ替代了一些项目如ObjectSpaces, WinFS和在.NET Framework提供对XQuery的支持。
1.3.1 LINQ 项目的目标
为了让你更加了解LINQ的功能,表 1.1 回顾了微软为LINQ项目制定的目标。
LINQ的第一个特性是处理多数据类型和数据源,LINQ支持通用数据集合,数据库,实体,XML等数据源的查询。LINQ有强大的扩展性, 开发者可以很容易让它与其它数据源和其它提供者集成。
表1.1 LINQ的动机和设计目标
目标 | 动机 |
集成对象,关系数据和XML | 在不同的数据源之间统一查询语法,以避免针对数据源使用不同的语言,为处理不同类型的数据提供单一的处理模型,而不管他们在内存中如何表示 |
使C# 和 VB具有SQL和XQuery的能力 | 在编程语言中集成查询能力 |
对语言提供可扩展模型 | 允许其他编程语言提供实现 |
对多数据源提供可扩展模型 | 允许访问其他数据源,而不只是允许访问关系数据库和XML文档 允许其他框架使用LINQ支持自己的需求 |
类型安全 | 编译时检查,避免以前只能在运行时发现的错误。编译器会在查询中捕捉错误 |
丰富的智能感知支持(使用强类型) | 帮助开发者使用新语法提高他们编写查询的生产率。 编辑器会指导你编写查询。 |
调试支持 | 允许开发者单步调试LINQ查询,并提供丰富的调试信息 |
在C# 1.0 和 2.0, VB.NET 7.0 and 8.0的基础上编译 | 重用以前版本的语言提供的丰富特性 |
可运行在.NET 2.0 CLR | 避免产生对新的运行时的需求,产生不必要的开发争论 |
保持100%的向后兼容 | 可以在已经存在的Web和Windows程序中使用标准和泛型集合,以及数据绑定功能。 |
LINQ另一个主要的特性是它是强类型的,这表示我们获取以下好处:
n 我们的查询获取了编译时类型检查,SQL语句则不行,我们只能在运行时才能发现SQL的错误,这意味着我们在开发时就保证我们的代码是正确的。这很重要,因为这能让我们避免因为键盘引发的错误。
n 我们在写LINQ查询的时候获得智能感知功能,这不仅能让我们输入更快,而且在面对复杂集合和数据对象模型时工作的更加容易。
下面让我们看看LINQ的诞生历史。
1.3.2 一点历史
LINQ是微软长期研究的结果,编程语言的发展和一些数据方法的开发导致LINQ的产生,LINQ to XML来自XLinq,LINQ to SQL来自DLinq。
C (读作 “c-omega”) 是一个微软研究院在很多领域扩展C#语言的一个项目。
n 为异步并发性扩展的控制流程
n 为XML和数据库操作进行的数据类型扩展。
LINQ中的一些技术已经在C中出现,C项目就是用来完成集成查询,在C#中支持LINQ,XQuery等等功能。
C在2004年发布了预览版,几个月过后,C#的主要设计师Anders Hejlsberg宣布微软将会将很多知识应用到C#和其他编程语言中。Anders说,他在过去的这些年中一直在深入研究C#和数据之间的阻抗错配。这包括数据库和SQL,也包括XML,XQuery。
C扩展了.NET类型系统和C#语言,这是整合SQL样式的查询,查询结果集和XML内容作为C语言的第一步。C引入了流类型,这与System.Collections.Generic. IEnumerable<T>类型有些类似。C同样定义了匿名结构,与C#3.0中的匿名类型相似。此外C还支持嵌入式的XML。
ObjectSpaces
LINQ to SQL是微软在对象-关系映射上的第一个尝试。另一个与LINQ有着很大关系的项目就是ObjectSpaces项目。
ObjectSpaces在2001发布了预览版。ObjectSpaces是数据访问API的集合,他允许把数据当作对象访问,独立于内在数据存储。ObjectSpaces引入了OPath,专有对象查询语言。2004年,微软宣布ObjectSpaces将会从属于WinFS项目,将会推迟到Orcas(.NET2.0和VS2005的下一个版本)之后发布,自从微软宣布不会将WinFS集成到Windows Vista第一版之中,这样每个人都知道ObjectSpaces项目将可能永远都不会再重现江湖了。
XQuery 实现
与ObjectSpaces所遇到的情形一样,同一时间微软开始在XQuery处理器上的工作。.NET Framework 2.0中包含了一个预览版。但最终决定不把它包含在最终版之中。XQuery的问题是,它是一种不同那个的语言,我们需要额外的学习才能使用它来处理XML。
为什么这些项目都有始无终?为什么微软停止了对这些的技术的研究工作?在PDC2005上,当LINQ项目被宣布时,谜团终于揭开了。
LINQ由Anders Hejlsberg设计,用来处理编程语言和其他技术之间的阻抗错配。这就是为什么微软钟情LINQ而放弃其他单体项目的原因。
如你所见,LINQ有着复杂的历史,它的出现得益于之前所做的研究和开发工作。在我们更深入的探讨LINQ的使用之前, 还是让我们先写下第一行LINQ代码吧。
1.4 LINQ to Objects第一步:查询内存对象
介绍完LINQ的历史之后,你肯定急切的想完全的学习LINQ,但是我想先通过一段代码了解一下LINQ提供的功能会更好。
1.4.1 做好准备
看代码之前,让我花些时间看看运行这些代码的前提条件。
编译器和.NET Framework 支持以及相关软件
LINQ是Orcas风暴的一部分,他包含在Visual Studio 2008 和the .NET Framework 3.5中。这个版本的框架包含了额外的库和更新的库,以及C#和VB.NET的新编译器,但是与.NET Framework 2.0保持兼容。
LINQ特性是由编译器和库提供的,理解这点很重要,虽然C#和VB.NET语言被增强,并且在.NET Framework加入一些新的库,.NET 运行时并没有变化,C#3.0和VB.NET 9.0需要新的编译器,但是运行时仍然只需要未修改的2.0版。这意味着使用LINQ的应用程序可以运行在.NET 2.0的运行时上。
在写本书时,最新发布的Silverlight 运行时支持LINQ或者LINQ to XML,或者至少他们的一个子集。它们在System.Linq and System.Xml.Linq namespaces命名空间中。
所有本书的代码示例基于Visual Studio 2008 and .NET 3.5 RTM(发布于2007.11.19)
要运行本书示例,需要安装如下组件:
至少有一个以下版本的Visual Studio:
n Visual C# 2008 Express Edition
n Visual Basic 2008 Express Edition
n Visual Web Developer 2008 Express Edition
n Visual Studio 2008 Standard Edition or higher
如果要运行LINQ到SQL,需要安装如下组件:
n SQL Server 2005 Express Edition or SQL Server 2005 Compact Edition (included with most versions of Visual Studio)
n SQL Server 2005
n SQL Server 2000a
n A later version of SQL Server9
这就是所有需要用到的软件。下面让我们复习一下本书用到的编程语言。
关于编程语言
本书中,我们假定你知道C#的语法,而且知道一点VB.NET的语法,为了保持简单,当我们介绍示例的时候,我们会有详细的注解。
1.4.2 Hello LINQ to Objects
你可能对这些新概念和语法有一些了解,不要害怕,我们最终的目标就是掌握这些技术,我们会详述LINQ的每个细节和新语言的扩展。
列表1.6显示了我们第一个LINQ示例,使用C#
列表 1.6 C#中的Hello LINQ
using System;
using System.Linq;
static class HelloWorld
{
static void Main()
{
string[] words = { "hello", "wonderful", "linq", "beautiful", "world" };
var shortWords = from word in words
where word.Length <= 5 select word;
foreach (var word in shortWords)
Console.WriteLine(word);
}
}
如果你编译并运行这些代码,你将会看到如下输出:
hello linq world
从这些结果可以很明显的看出,我们从单词列表中只选择那些长度小于等于5的字符串。
使用如下代码,我们可以完成同样的事情。
列表1.8 老版本的Hello LINQ
using System;
static class HelloWorld
{
static void Main()
{
string[] words = new string[] {"hello", "wonderful", "linq", "beautiful", "world" };
foreach (string word in words)
{
if (word.Length <= 5)
Console.WriteLine(word);
}
}
}
注意到,这种老款的代码比LINQ版的代码更简单。好,不要放弃,LINQ不只是能做到我们在示例中做到这些。如果继续读下去,你会发现LINQ的强大功力。
为了让你有继续读下去的动力,我们使用分组和排序改进我们的示例,这样应该会让你对LINQ的强大功力有一个认识了。
为了得到这种结果
Words of length 9
beautiful
wonderful
Words of length 5
hello world
Words of length 4
linq
我们可以使用列表 1.9.的代码
列表1.9 带有分组和排序的Hello LINQ
using System;
using System.Linq;
static class HelloWorld
{
static void Main()
{
string[] words = { "hello", "wonderful", "linq", "beautiful", "world" };
var groups = from word in words orderby word ascending
group word by word.Length into lengthGroups
orderby lengthGroups.Key descending
select new {Length=lengthGroups.Key, Words=lengthGroups};
foreach (var group in groups)
{
Console.WriteLine("Words of length " + group.Length);
foreach (string word in group.Words)
Console.WriteLine(" " + word);
}
}
}
在前面的例子中,我们使用查询表达了这样一种信息:对一个单词列表按按字母降序排序并按照他们的长度分组。
如果你有时间,你可以不使用LINQ做到这一点。你会发现事情不会比LINQ更简单。这个例子中LINQ显示出这样一种有点,我们可以使用查询声明式的表达我们想要查询,否则我们需要写很多复杂的代码。
我们不会立即详述你刚才看到的代码,如果你对SQL很熟悉,你很可能了解了这段代码的作用。除了像SQL查询一样,LINQ同样提供了很多方法,如Sum, Min, Max, Average等,这让我可以执行一些复杂的集合操作。
例如,下面我们将每个订单的下单量求和得到总量:
decimal totalAmount = orders.Sum(order => order.Amount);
如果你还没有使用过C#3.0,这种语法确实会让你感到困惑。你会想:“这种奇怪的箭头是什么?”。稍后我们将详述这些代码,届时你将会完全理解。
下面让我们介绍一下XML LINQ和LINQ TO SQL。使你对它们有一个大概的了解。
1.5 LINQ to XML第一步:查询 XML documents
XML LINQ利用LINQ框架在编程语言中提供了XML查询和转换能力。你可以认为XML LINQ是一个功能齐全的XML API,它重新设计了System.Xml命名空间的类并且加入了一些关键特性。LINQ to XML为编辑XML文档和元素树提供了一种更简单的方法。这表示我们可以使用LINQ操作XML以非常容易的方式完成XML处理任务。
我们将会比较XML API和XML LINQ,看看XML LINQ的强大之处,
1.5.1 为什么我们需要 LINQ to XML
XML无所不在,我们使用C#操作它进行数据交换,存储配置信息,保存临时数据,产生Web页面和报表等等。
知道现在,大多数编程语言都不能很好的支持他,我们一般使用API来处理XML数据,这些API包含XmlDocument, XmlReader, XPathNavigator, XslTransform for XSLT, SAX, XQuery implementations。问题是这些API没有很好集成到编程语言中,通常我们需要写很多晦涩的代码来完成一个简单的任务,下面先让我们看看XML LINQ提供了哪些功能。
XML LINQ扩展了LINQ语言集成查询的特性。它在编程语言中提供了XPath和XQuery的强大功能,而且还具备类型安全检查和智能感知特性。
表1.2比较了XML LINQ和 XML DOM的特性
表1.2 比较LINQ to XML 和XML DOM可以发信LINQ to XML 更有价值
LINQ to XML 特性 | XML DOM 特性 | |
元素为中心 | 文档为中心 | |
声明模型 | 命令模型 | |
XML LINQ代码显示的结构接近文档本身的层次结构 | 代码和文档结构之间没有相似性 | |
语言集成的查询 | 没有集成查询 | |
在一个指令中就可以创建元素和属性,文本节点只是一个字符串 | 基本的构造需要大量代码 |
|
简单的XML命名空间支持 | 需要管理前缀和命名空间 |
|
更快更小 | 重量级且耗费内存 |
|
流能力 | 所有数据都需要加载到内存 |
|
对称的元素和属性API | 多种不同的方法 |
|
DOM是低层次的,需要很多代码来完成我们需要完成的任务,XML LINQ提供了更高级别的语法让我们更简单的做简单的事情。
XML LINQ使用元素-中心的方法,相比DOM中文档-中心的方法而言,我们可以更容易的处理XML片段,而不必创建一个完整的XML文档。
.NET Framework 提供了 XmlReader 和 XmlWriter类用来在低层次上处理XML文本。XML LINQ在内部使用了这两个类。这样做的好处是使XML LINQ保留了与XmlReader 和XmlWriter的兼容性。
XML LINQ是创建XML文档更加直接,查询更加简单。对XML文档的查询更加自然而不用写很多循环指令。同样,作为LINQ家族的一员,当我们合并不同数据源的时候也会变得简单。
使用XML LINQ,微软的目标是用它来完成80%的任务。这些任务包括XML格式化和通用处理。其他情况下,开发者可能将会继续使用其他API,虽然XML LINQ汲取了XSLT,XPath,XQuery的灵感,这些技术在自己特定的领域很好的完成了工作任务。在这些特定的领域,XML LINQ无法完成所有的任务,但是它与其他XML API兼容,这允许我们可以同时使用XML LINQ和其他API来完成任务。
现在,让我们看看XML LINQ到底有什么不同。
1.5.2 Hello LINQ to XML
在我们的第一个例子中,我们选择并保存了一个Book对象的集合到XML中,下面是Book类的C#定义:
class Book
{
public string Publisher;
public string Title;
public int Year;
public Book(string title, string publisher, int year)
{
Title = title;
Publisher = publisher;
Year = year;
}
}
我们有如下Book:
Book[] books = new Book[] {
new Book("Ajax in Action", "Manning", 2005),
new Book("Windows Forms in Action", "Manning", 2006),
new Book("RSS and Atom in Action", "Manning", 2006)
};
下面是我们想获得的2006年出版的书籍XML表示结果:
<books>
<book title="Windows Forms in Action">
<publisher>Manning</publisher>
</book>
<book title="RSS and Atom in Action">
<publisher>Manning</publisher>
</book>
</books>
使用XML LINQ,用列表1.11.中的代码可以做到
列表 1.11 Hello LINQ to XML
using System;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
class Book
{
public string Publisher;
public string Title;
public int Year;
public Book(string title, string publisher, int year)
{
Title = title;
Publisher = publisher;
Year = year;
}
}
static class HelloLinqToXml
{
static void Main()
{
Book[] books = new Book[]
{ new Book("Ajax in Action", "Manning", 2005),
new Book("Windows Forms in Action", "Manning", 2006),
new Book("RSS and Atom in Action", "Manning", 2006)
};
XElement xml = new XElement("books",
from book in books
where book.Year == 2006
select new XElement("book",
new XAttribute("title", book.Title),
new XElement("publisher", book.Publisher)));
Console.WriteLine(xml);
}
}
作为对比,列表1.13显示了我们使用XML DOM来完成相同的功能是所需编写的代码
列表 1.13老版本的Hello LINQ to XML
using System;
using System.Xml;
class Book
{
public string Title;
public string Publisher;
public int Year;
public Book(string title, string publisher, int year)
{
Title = title;
Publisher = publisher;
Year = year;
}
}
static class HelloLinqToXml
{
static void Main()
{
Book[] books = new Book[] {
new Book("Ajax in Action", "Manning", 2005),
new Book("Windows Forms in Action", "Manning", 2006),
new Book("RSS and Atom in Action", "Manning", 2006) };
XmlDocument doc = new XmlDocument();
XmlElement root = doc.CreateElement("books");
foreach (Book book in books)
{
if (book.Year == 2006)
{
XmlElement element = doc.CreateElement("book");
element.SetAttribute("title", book.Title);
XmlElement publisher = doc.CreateElement("publisher");
publisher.InnerText = book.Publisher;
element.AppendChild(publisher);
root.AppendChild(element);
}
}
doc.AppendChild(root);
doc.Save(Console.Out);
}
}
如你所见,XML LINQ比DOM表达的更形象。用来获取我们想要的XML 片段的代码跟XML片段本身相像。这就是我们所说的WYSIWYM,What You See Is What You Mean。
微软把这种方法命名为功能构建模式。它允许我们以一种反映我们要构建的XML文档结构的方式构建我们的代码。
下面,是到了我们介绍LINQ三部曲最后一部,LINQ to SQL的时候了。
1.6 LINQ to SQL第一步:查询关系数据库
LINQ的目标是使查询成为编程语言的一部分。LINQ TO SQL,如DLINQ一样,使你能够使用XML LINQ查询一样的语法查询关系数据库。
1.6.1 LINQ TO SQL特性概要
使用LINQ的扩展性,LINQ TO SQL提供了语言继承的数据访问。它构建在ADO.NET和对象映射上。
LINQ TO SQL使用编码在自定义属性或者定义在XML文档中的映射信息。使用这些信息,把对象持久化到数据库中工作可以自动完成。一个表可以映射为一个类,表的列可以映射到类的属性上。表之间的关系可以表示为其他属性。
LINQ TO SQL自动跟踪对象变化,并使用自动产生的SQL查询或者存储过程来更新数据库。所以大多数时候,我们不需要自己编写SQL查询语句。下面让我看看LINQ TO SQL的代码。
1.6.2 Hello LINQ to SQL
如你在Hello LINQ示例中看到的,我们可以对一个对象集合进行查询。下面的代码根据城市信息查找联系方式:
from contact in contacts
where contact.City == "Paris"
select contact;
我们要感谢LINQ TO SQL,从数据库中做获取相同的数据与以上代码基本相同。
from contact in db.GetTable<Contact>()
where contact.City == "Paris"
select contact;
这个查询基于数据库的一个联系方式表。注意到这两种查询之间的区别是多么微小。只有集合对象不同,查询语法完全相同。这表明我们可以使用同一种方法处理不同类型的数据。这就是LINQ的伟大之处。
如果你知道,关系数据库唯一能理解的语言就是SQL,也许你能猜到LINQ查询一定是在某个时候转换为SQL查询。这就是LINQ TO SQL的核心。在第一个例子中,集合实在内存中被查询,而在第二段代码中,查询用来产生SQL语句,产生的SQL语句将被送往数据库服务器。在这个过程中,真正的处理发生在数据库服务器上。但是这些查询有着很好强类型支持,而SQL查询只能在字符串中表示,而且不能在编译时检查。
我们将在以后详细讨论LINQ TO SQL。现在首先让我们详解这个简单的例子,你一定在想在我们的LINQ TO SQL查询中db.GetTable<Contact>()表示什么?
实体类
构建LINQ TO SQL程序的第一步是创建表示应用程序数据的实体类
在我们的示例中,我要定义一个名为Contact的类。并把它关联到Northwind数据库中的Contacts表。为了做到这一点,我需要在类上应用自定义属性:
[Table(Name="Contacts")]
class Contact
{
public int ContactID;
public string Name;
public string City;
}
Table属性在System.Data.Linq.Mapping命名空间中定义。它有一个Name属性,用来定义数据库表的名称。
为了把实体类管理到表上,我们需要知识类的每个属性或字段关联到表的列上。这是由Colume属性完成的。
[Table(Name="Contacts")]
class Contact
{
[Column(IsPrimaryKey=true)]
public int ContactID { get; set; }
[Column(Name="ContactName"]
public string Name { get; set; }
[Column]
public string City { get; set; }
}
Column属性也是System.Data.Linq.Mapping命名空间的一部分。该命名空间有大量可用的属性用来定制类和表之间精确的映射。我们使用IsPrimaryKey属性表示表的ContactID列示这个表的主键。我们声明了ContactName列映射到Name属性,我们没有特别指定其他列于属性之间的映射,因为LINQ TO SQL会根据属性类型自动推断。
DataContext
我们要准备的下一个事情就是使用System.Data.Linq.DataContext对象。DataContext对象会针对数据库产生SQL查询并且从结果生成对象集合。
本书中,我们将使用基于Northwnd.mdf数据库的代码示例。这个数据库在Data目录中,那么创建DataContext对象的代码将如下所示:
string path = Path.GetFullPath(@"../../../../Data/northwnd.mdf");
DataContext db = new DataContext(path);
DataContext类的构造函数需要一个连接字符串参数。因为我们使用的是SQL Server 2005 Express Edition,所以一个数据库文件的路径能达到相同的目的。
DataContext提供对数据库表的访问。下面是我们如何访问Contacts表并把它映射到Contact类的代码:
Table<Contact> contacts = db.GetTable<Contact>();
DataContext.GetTable是一个泛型方法,它允许我们可以与强类型对象一起工作。
完整的代码示例如列表1.15所示
列表1.15 Hello LINQ to SQL 完整源代码
using System;
using System.Linq;
using System.Data.Linq;
using System.Data.Linq.Mapping;
static class HelloLinqToSql
{
[Table(Name="Contacts")]
class Contact
{
[Column(IsPrimaryKey=true)]
public int ContactID { get; set; }
[Column(Name="ContactName")]
public string Name { get; set; }
[Column]
public string City { get; set; }
}
static void Main()
{
String path=System.IO.Path.GetFullPath(@"../../../../Data/northwnd.mdf");
DataContext db = new DataContext(path);
var contacts =from contact in db.GetTable<Contact>()
where contact.City == "Paris"
select contact;
foreach (var contact in contacts)
Console.WriteLine("Bonjour "+contact.Name);
}
}
执行这段代码将会产生如下输出:
Bonjour Marie Bertrand Bonjour Dominique Perrier Bonjour Guylène Nodier
下面是传送到数据库的SQL语句:
SELECT [t0].[ContactID], [t0].[ContactName] AS [Name], [t0].[City]
FROM [Contacts] AS [t0]
WHERE [t0].[City] = @p0
使用LINQ我们可以很简单的使用强类型来访问一个数据库,这是一个简单的示例,但是却能很好的让你看到LINQ TO SQL是如何访问数据库的。
让我们总结一下LINQ TO SQL为我们做了哪些事情:
n 打开一个数据库连接
n 生成SQL查询
n 对指定的数据库执行SQL查询
n 根据表结果创建对象集合
作为一个练习,你可以不使用LINQ TO SQL来做到这一点。例如,你可以使用一个DataReader。下面是两者之间的比较:
n 查询用SQL字符串编写
n 没有编译时检查
n 参数松散绑定
n 弱类型结果集
n 需要写更多的代码
n 需要更多的知识
对于简单的用例,标准的数据访问代码降低了我们的生产率。相反,LINQ TO SQL不会阻碍我们,在结束介绍LINQ TO SQL之前,让我们复习一下它的特性。
1.6.3 近看LINQ TO SQL
可以看到LINQ TO SQL可以根据数据库动态的产生SQL查询。这也许并不适用于所有的情形,不过LINQ TO SQL同样支持普通的SQL查询和存储过程,这样我们就可以在LINQ框架的庇护下使用我们手写的SQL代码。
在我们的示例中,我们使用自定义属性进行信息映射。如果你不喜欢在你的业务中把映射信息硬编码,你可以使用外部的XML映射文件做到相同的事情。
为了更好的理解LINQ TO SQL的工作方式,我们创建了类并提供了映射信息。实际上,我们会用图形化的设计器来产生这些代码。
1.7 总结
本章展示了LINQ技术背后的动机,你也走完了对象LINQ,LINQ to XML ,LINQ TO SQL的第一步。
虽然我们只是概要的讲述了LINQ技术。但我们希望你能对LINQ提供的功能有一个大概的了解。如你所见,LINQ不只是将SQL或者XML快速加入到我们的代码中,它能做到远不止这些,LINQ应用程序访问数据提供了一种新的方法,然而如果没有编程语言一系列的新特性,LINQ也做不到这些。下一章我们将会复习C#语言支持语言集成的查询的新特性。
为了简单起见,在后续的章节中, 我们会将Linq to Object, Linq to XML, Linq to SQL分别称之为Object Linq,XML Linq,SQL LINQ。