本书翻译目的为个人学习和知识共享,其版权属原作者所有,如有侵权,请告知本人,本人将立即对发帖采取处理。
允许转载,但转载时请注明本版权声明信息,禁止用于商业用途!
博客园:韩现龙
LINQ是如何工作的
假如你已经明白了将语法集成到一种语言中的概念,你可能就想看看它是如何工作的。当你写如下的代码时:
Customer[] Customers = GetCustomers(); var query = from c in Customers where c.Country == "Italy" select c;
编译器生成这些代码:
Customer[] Customers = GetCustomers(); IEnumerable<Customer> query = Customers .Where( c => c.Country == "Italy" );
从现在开始,为了简洁起见,我们将略过Customer的声明部分。当查询变得长的时候,就像下面这样:
var query = from c in Customers where c.Country == "Italy" orderby c.Name select new { c.Name, c.City };
生成的代码也会变长:
var query = Customers .Where( c => c.Country == "Italy" ); .OrderBy( c => c.Name ) .Select( c => new { c.Name, c.City } );
就像你看到的那样,代码显然调用了前者调用返回的对象实例的成员。在主语言(这里指C#)中,这种行为叫做“扩展方法(extension methods)”。查询事例中的Where,OrderBy,和Select方法依赖于Customers对象和在用using引入的命名空间。扩展方法(extension methods)是LINQ中用相同的语法对不同的数据域进行操作的一个基本的语法特征。
更多信息 扩展方法看起来像是对一个类的扩展(即本例中的Customer类),但实际上是一个接受一个类的实例作为第一个参数的一个外部方法。var关键字用来声明查询使用的从初始的参数中推断变体类型,在这个事例中返回的是一个IEnumerable<T>类型。更详细的描述和其他语言的扩展在第2章,“C# 语言特性” 和第3章,“Visual Basic 9.0语言特性”中。
另外重要的概念是对数据操作的时机。总的来说,LINQ查询在获取查询结果之前是不会执行的,因为它描述的一组在需要执行时才会执行的操作。获取查询结果的操作才真正使查询执行。可以在下面这个foreach循环中说明这一点:
var query = from c in Customers ... foreach ( string name in query ) ...
也有方法对LINQ查询的结构进行遍历,生成在内存中的数据的持久复制品。例如,ToList方法能够生成一个强类型的List<T>集合:
var query = from c in Customers ... List<Customer> customers = query.ToList();
当LINQ查询操作关系型数据库的时候(比如Microsoft SQL Server),它会生成相应的SQL语句,而不是操作内存中的数据表的复制品的表达式。因此,在上面两个例子中的Customers是一个Table<Customer>类型(关系型数据库中的一个物理表)或者是一个ObjectQuery(Customer)类型(映射关系型数据库的一个实体),相应的SQL语句只在调用了ToList方法和foreach循环执行时才会被发送到数据库。LINQ查询在执行的时候以不同的方式生成。
关系模型和层次/图表模型
可能LINQ给你的第一印象是另外的一种SQL语句。这种语言和SQL语句的相似性源于LINQ查询能够描述实体间的关系,比如SQL的连接(join):
var query = from c in Customers join o in Orders on c.CustomerID equals o.CustomerID select new { c.CustomerID, c.CompanyName, o.OrderID };
这和在关系模型中查询数据的方法是相似的。然而,LINQ不仅仅是像关系模型一样的单一的数据域。在层次模型中,假设每个客户有他自己的订单,并且每个订单有它自己的产品列表,在LINQ中,我们可以获得一个将每个客户作为排序条件的产品列表:
var query = from c in Customers
from o in c.Orders
select new { c.Name, o.Quantity, o.Product.ProductName };
上面的查询没有连接(join)。Customers和Orders之间有关系是通过第二个from关键字来表现的,该关键用c.Orders来说明Products是通过Order实例的Products成员来描述的。该查询的结果就是用o.Product.ProductName为每个订单行投影出来产品名称。
层次关系表现为通过引用其他对象的类型定义。为了支持前面的查询,我们需要有和Listing 1-1相同的几个类:
Listing 1-1: 简单关系下的类型声明
public class Customer { public string Name; public string City; public Order[] Orders; } public struct Order { public int Quantity; public Product Product; } public class Product { public int IdProduct; public decimal Price; public string ProductName; }
然而,我们很有可能想用同一个Product类实例作为同一个产品的不同的Orders。我们可能也想不通过访问Customer这个类就去对Orders或者Products过滤。在Listing 1-2中是一个普通的场景:
Listing 1-2: 用双向选择的关系进行类型定义
public class Customer {
public string Name;
public string City;
public Order[] Orders;
}
public struct Order {
public int Quantity;
public Product Product;
public Customer Customer;
}
public class Product {
public int IdProduct;
public decimal Price;
public string ProductName;
public Order[] Orders;
}
通过如下的定义就有了所有产品的数组:
Product[] products;
我们可以对对象图表进行查询,在订单列表中查询出ID为3的产品来:
var query = from p in products where p.IdProduct == 3 from o in p.Orders select o;
用同一种查询语言我们查询了不同的的数据模型。当在实体之间没有定义在查询中使用的关系时,你可以像SQL语句中那样通过LINQ语法中支持的子查询来连接各个实体。
如果在数据模型中有实体之间的关系,你依然可以在LINQ查询是显示地使用关系――比如说,当你想加强一些条件,或者当你只想简单的将两个没有本质上关系的实体关联起来。例如,假设你查询出住在同一城市的客户和供应商。数据模型中可能没有提供出在这些属性之间显示的关系,但是你依然可以按如下的方式进行查询:
var query = from c in Customers join s in Suppliers on c.City equals s.City select new { c.City, c.Name, SupplierName = s.Name };
然后查询返回的结果如下:
City=Torino Name=Marco SupplierName=Trucker
City=Dallas Name=James SupplierName=FastDelivery
City=Dallas Name=James SupplierName=Horizon
City=Seattle Name=Frank SupplierName=WayFaster
如果你有使用SQL查询的经验,可能你就会认为查询的结果总是一个像上面那样的的重复许多次某些列的数据的“矩形(rectangular)”的表。然而,通常一个查询包括多对一或一对多的关系。使用LINQ你可以返回层次或者图表的对象集合:
var query = from c in Customers join s in Suppliers on c.City equals s.City into customerSuppliers select new { c.City, c.Name, customerSuppliers };
最后一句查询返回每个客户对象的行,每行中又包括和该客户在同一城市的供应商列表。这个结果可以被再次查询,就像LINQ中其他一切对象图表一样。下面是这个层次化(hierarchized)的结果:
City=Torino Name=Marco customerSuppliers=... customerSuppliers: Name=Trucker City=Torino City=Dallas Name=James customerSuppliers=... customerSuppliers: Name=FastDelivery City=Dallas customerSuppliers: Name=Horizon City=Dallas City=Seattle Name=Frank customerSuppliers=... customerSuppliers: Name=WayFaster City=Seattle
如果你想获得客户列表,为客户列表中的每个客户提供他订购了至少一次的产品列表,提供在同一个城市中的供应商列表,你可以按如下的方式写查询语句:
var query = from c in Customers select new { c.City, c.Name, Products = (from o in c.Orders select new { o.Product.IdProduct, o.Product.Price }).Distinct(), CustomerSuppliers = from s in Suppliers where s.City == c.City select s };
看一下两个客户的结果,了解一下上面的LINQ查询是的数据是如何返回的:
City=Torino Name=Marco Products=... CustomerSuppliers=... Products: IdProduct=1 Price=10 Products: IdProduct=3 Price=30 CustomerSuppliers: Name=Trucker City=Torino City=Dallas Name=James Products=... CustomerSuppliers=... Products: IdProduct=3 Price=30 CustomerSuppliers: Name=FastDelivery City=Dallas CustomerSuppliers: Name=Horizon City=Dallas
这种类型的结果用一个或者多个SQL查询实现起来可能会很困难,因为它需要对查询结果进行分析来构建需要的对象图表。LINQ提供了一个从一个模型向另一个模型中移动数据和用不同的方式获取相同的结果的简便方法。
LINQ需要你按照实体的方式来描述你的数据,这个实体也是语言中的类型。当你构造一个LINQ查询时,通常是对类的实体进行的一组操作。这些对象可能是数据的真正容器,或者是对你将要操作的外部数据的一个简要描述(以元数据的方式)。仅当LINQ对包括了数据库中的图表和关系的映射的一组类型进行操作时,它才通过一个SQL命令发送到数据库中。当你定义了实体类之后,你就可以使用我们刚才描述的两种方式了(联结和实体关系导航)。在SQL命令中的对这些操作的转换正是LINQ引擎的责任。
小提示:你可以用像SQLMetal或者用在Microsoft Visual Studio中的LINQ to SQL设计器的代码生成工具来生成实体类。
在 Listing 1-3中,你可以看到一个映射了名称为Products的关系表的Product类,其中包括和公共数据成员相关的五列。
Listing 1-3: 映射数据表的类声明
[Table("Products")] public class Product { [Column(IsPrimaryKey=true)] public int IdProduct; [Column(Name="UnitPrice")] public decimal Price; [Column()] public string ProductName; [Column()] public bool Taxable; [Column()] public decimal Tax; }
当你对描述外部数据的实体(比如说数据库中的数据表)进行操作时,你可以创建这种类型的类,并且操作内存中的对象,就好像数据表中的数据被加载到的内存中一样。当你调用SubmitChanges时,这些更改将会通过SQL命令被提交到数据库中。见Listing 1-4:
Listing 1-4: 调用SubmitChanges方法更新数据库Database update calling the SubmitChanges method
var taxableProducts =
from p in db.Products
where p.Taxable == true
select p;
foreach( Product product in taxableProducts ) {
RecalculateTaxes( product );
}
db.SubmitChanges();
刚才例子中的Product代表了在外部数据库中Products表中的一行记录。当SubmitChanges方法被调用时,所有已经发生变化的对象生成一个SQL命令去更新数据表中的相应的行。
更多信息 在第5章“LINQ to ADO.NET”中对匹配数据库中的表和关系的类实体有更深的讲解。
XML操作
LINQ有一组不同的类和扩展支持对XML数据的操作。我们将根据下面的情景创建一些例子。假设你的客户能够发送像Listing 1-5描述的OODERS.XML文件存储的订单。
Listing 1-5: 订单XML文件片断
<?xml version="1.0" encoding="utf-8" ?> <orders xmlns="http://schemas.devleap.com/Orders"> <order idCustomer="ALFKI" idProduct="1" quantity="10" price="20.59"/> <order idCustomer="ANATR" idProduct="5" quantity="20" price="12.99"/> <order idCustomer="KOENE" idProduct="7" quantity="15" price="35.50"/> </orders>用标准的Microsoft .NET 2.0中的System.Xml类,你可以用DOM加载该文件或者用XmlReader来转换它的内容。无论使用哪种方案,你必须考虑到结点,结点类型,XML命名空间及和XML世界中相关的其他一切东西。许多开发人员不喜欢使用XML就是因为它需要另一种数据结构域中的知识,并且这种知识使用它自己的语法。
如果需要取出包括它们订购量的所有产品,你可以用XmlReader通过转换这个订单文件来实现这一目的,见 Listing 1-6。
Listing 1-6: 用XmlReader读取订单的XML文件
String nsUri = "http://schemas.devleap.com/Orders";
XmlReader xmlOrders = XmlReader.Create( "Orders.xml" );
List<Order> orders = new List<Order>();
Order order = null;
while (xmlOrders.Read()) {
switch (xmlOrders.NodeType) {
case XmlNodeType.Element:
if ((xmlOrders.Name == "order") &&
(xmlOrders.NamespaceURI == nsUri)) {
order = new Order();
order.CustomerID = xmlOrders.GetAttribute( "idCustomer" );
order.Product = new Product();
order.Product.IdProduct =
Int32.Parse( xmlOrders.GetAttribute( "idProduct" ) );
order.Product.Price =
Decimal.Parse( xmlOrders.GetAttribute( "price" ) );
order.Quantity =
Int32.Parse( xmlOrders.GetAttribute( "quantity" ) );
orders.Add( order );
}
break;
}
}
你也可以用下面的XQuery来查询结点:
for $order in document("Orders.xml")/orders/order
return $order
然而,XQuery也需要学习另外一种语言和语法。而且,为了能在我们的代码中使用,前面的XQuery事例还需转换为Order的实例。最后,对于许多开发人员来说它并不十分的直接。正如我们所说,LINQ提供了适用于所有数据源的查询引擎,甚至是一个XML文档。通过使用LINQ查询,你可以更轻松并且使用更通用的编程语言来实现同样的效果。Listing 1-7展示了对orders文件用LINQ to XML查询的例子:
Listing 1-7: 使用LINQ to XML读取XML文件
XDocument xmlOrders = XDocument.Load( "Orders.xml" ); XNamespace ns = "http://schemas.devleap.com/Orders"; var orders = from o in xmlOrders.Root.Elements( ns + "order" ) select new Order { CustomerID = (String)o.Attribute( "idCustomer" ), Product = new Product { IdProduct = (Int32)o.Attribute("idProduct"), Price = (Decimal)o.Attribute("price") }, Quantity = (Int32)o.Attribute("quantity") };
使用Microsoft Visual Basic 9.0的新的语法,你可以在代码中使用像和XPath一样的语法一样直接操作XML节点,如Listing 1-8。
Listing 1-8: 使用LINQ to XML和Visual Basic 9.0语法查询XML文件
Imports <xmlns:o="http://schemas.devleap.com/Orders"> ' ... Dim xmlOrders As XDocument = XDocument.Load("Orders.xml") Dim orders = _ From o In xmlOrders.<o:orders>.<o:order> _ Select New Order With { .CustomerID = o.@idCustomer,_ .Product = New Product With { .IdProduct = o.@idProduct, .Price = o.@price}, _ .Quantity = o.@quantity}
LINQ to XML查询的结果可以被显示地将Order实体的列表加载到一个客户的Orders属性中,使用LINQ to SQL来将更改保存至物理的数据库层:
customer.Orders.AddRange( From o In xmlOrders.<o:orders>.<o:order> _ Where o.@idCustomer = customer.CustomerID _ Select New Order With { .CustomerID = o.@idCustomer, _ .Product = New Product With { .IdProduct = o.@idProduct, .Price = o.@price}, _ .Quantity = o.@quantity})
如果你需要生成一个从你的客户订单开始的ORDERS.XML文件,你可以至少综合利用Visual Basic 9.0 XML的语法来定义输出的XML的架构。如Listing 1-9。
Listing 1-9:使用Visual Basic 9.0的XML语句创建orders的XML
Dim xmlOrders = <o:orders>
<%= From o In orders _
Select <o:order idCustomer=<%= o.CustomerID %>
idProduct=<%= o.Product.IdProduct %>
quantity=<%= o.Quantity %>
price=<%= o.Product.Price %>/> %>
</o:orders>
你可以欣赏这个解决方案的强大功能,它在没有失去键入代码的稳定性发问下,保持了XML语法,并且将一组通过LINQ to SQL查询出来的实体转换为一个XML InfoSet。
更多信息 在第六章“LINQ to XML”中对LINQ to XML的语法和它潜在的功能有更多的讲解。
译注:
本篇专用词汇:
[1]扩展方法: extension methods
[2]关系模型: Relational Model[3]层次/图表模型: Hierarchical/Graph Model
[4]双向选择的关系:two-way relationships
[5]XML操作: XML Manipulation
上一篇:微软免费图书《Introducing Microsoft LINQ》翻译--LINQ概述
下一篇:微软免费图书《Introducing Microsoft LINQ》翻译-Chapter1:语言集成(CDPlayer译)