DLinq实现了对数据库表相关联的实体集合的标准查询。这一章我们将阐述DLinq查询的细节。
执行查询
不论您写的是一个高级查询表达式(query expression)还是构建了一个简单的查询,这些查询都还不是最终的SQL命令,它不会被立即执行。它仅仅是对查询的一个描述。例如,下面例子中的局部变量”q”中只是一个查询表达式,而不是表示查询的最终结果。
var q =
from c in db.Customers
where c.City == "London"
select c;
foreach (Customer c in q)
Console.WriteLine(c.CompanyName);
局部变量‘q’的实际类型是Query<Customer>。直到我们枚举查询结果的时候,这个查询才被真正执行。在这个例子中,foreach语句将引起查询的执行。
一个查询对象(Query)类似于ADO.NET的命令对象(Command)。创建了这么一个对象并不表明查询就被执行了。一个命令对象(Command)只是拥有了一个描述查询的文本。同样地,一个查询对象(Query)也只是拥有一个封装了查询文本的Expression数据结构。我们可以调用命令对象(Command)的ExecuteReader()来执行查询并返回查询结果DataReader。我们同样可以调用的查询对象(Query)的GetEnumerator()方法来执行查询并返回查询结果IEnumerator<Customer>。
因此,下面例子中的查询将被执行两次,因为这个查询调用了两次GetEnumerator()。
var q =
from c in db.Customers
where c.City == "London"
select c;
// Execute first time
foreach (Customer c in q)
Console.WriteLine(c.CompanyName);
// Execute second time
foreach (Customer c in q)
Console.WriteLine(c.CompanyName);
这也就是所谓的延迟加载。ADO.NET的命令对象(Command)也可能拥有一个查询然后被再次执行。
当然,应用程序开发者往往需要确切地知道一个查询在哪里以及什么时候被执行。一个应用程序如果只是因为查看查询结果而导致同样的查询被多次执行,这是我们所不愿意看到的。例如,您可能希望某个查询的结果绑定到DataGrid控件,而屏幕每刷新一次这个控件就执行了一次查询,这当然是我们所不愿意看到的。
为了避免这种情况,我们可以把查询结果转化为一个标准的集合对象。方法很简单,就是调用查询对象的ToList() or ToArray()方法就可以把结果转化为一个List 或者Array对象。
var q =
from c in db.Customers
where c.City == "London"
select c;
// Execute once using ToList() or ToArray()
var list = q.ToList();
foreach (Customer c in list)
Console.WriteLine(c.CompanyName);
foreach (Customer c in list)
Console.WriteLine(c.CompanyName);
延迟执行的一个好处就是我们可以执行一个分段查询。我们可以先构建查询的一部分,然后赋值给另外一个查询对象并添加更多的查询操作。
var q =
from c in db.Customers
where c.City == "London"
select c;
if (orderByLocation) {
q =
from c in q
orderby c.Country, c.City
select c;
}
elseif (orderByName) {
q =
from c in q
orderby c.ContactName
select c;
}
foreach (Customer c in q)
Console.WriteLine(c.CompanyName);
在这个例子中我们新建了一个查询”q”用于查询所在城市为London的所有的Customer。然后我们根据程序的某个状态为这个查询添加了排序的功能。通过延迟执行,我们可以根据程序的需要构建出我们需要的查询,而没必要去冒操作字符串的风险。
对象标识
每个运行时的对象都有一个唯一标识。如果两个变量引用了同一个对象,那它们指向的就是同一个对象实例。修改其中一个变量的值同样也会影响到另外一个变量。但数据库表格中的行并一定有一个唯一的标识。尽管数据库表格中的行的确拥有一个主键,而且这个主键也有可能是唯一的,但两行也有可能拥有相同的标识。但这也只是限制了数据库表格的内容。通过调用远程命令来交换数据,结果也是一样。
尽管如此,这种情况很少见。在大多数情况下,应用程序从数据库中读取数据然后对这些数据进行操作。DLinq支持这种数据操作模型。但当同一个数据库行被应用程序两次读取出来的时候,我们不能假设它们拥有同样的数据。如果对同一行执行了两次查询,我们得到的将是两行数据,每行都拥有各自的数据。
但是对于对象来说,情况又会有所不同。如果您调用DataContext获取数据,您希望同一个查询返回的是同一个对象的实例。因为应用程序中的对象有其特殊的意义,所以您希望能够像操作普通对象那样来操作它们。您希望对象是可以被继承的,您当然也希望执行同一个查询返回的是同一个实例。
基于此,我们用DataContext来管理对象的标识。任何时候,我们从数据库中读取一行数据,创建一个新的对象,然后DataContext就把主键和这个对象记录到一个标识表中。当从数据库中读取已经被DataContext记录的某行数据时,DataContext就会返回已经存在了对象实例。通过这种方式DataContext将数据库中主键的概念转化为了编程语言中实例的概念。应用程序只会看到第一次从数据库中读取出来的对象状态。如果从数据库最新获取的数据和对象不一样,读取来的数据会被丢弃。
您可能会感到疑惑,为什么应用程序要丢弃读取出来的数据?因为DLinq既可以保证应用程序变量的完整性,同时也能够支持乐观的数据更新。因此只有应用程序中变化了的实体才可以被得到更新,应用程序的意图也就变得很单纯明了了。如果内存中的对象在外部发生了变化,只有SubmitChanges()被调用的时候它们才能被更新。更多内容请参考第四章的内容,同步变化。
当然,如果通过查询得到的对象已经存在于内存中,那么这个查询将不会被执行。标识表其实就是缓存了已经获取到的所有的实体。
关系
就如在快速一览里面看到的一样,在实体类中定义了对另外一个对象或者对象集合的应用,也就是生命了实体类所映射的表格之间的关联关系。当您要查询相关联的对象时,您只需要简单地使用“dot”操作。这些简单的访问操作被翻译成复杂的等价SQL连接或者子查询,而且还可以让您很方便地看到查询的结果。例如,在下面的查询中,我们从orders导航到customers,最后得到所在城市为London的客户的所有订单。
var q =
from o in db.Orders
where o.Customer.City == "London"
select o;
如果两个实体类的关联并不存在,那就需要您手动来编写类似SQL的一个连接查询。
var q =
from c in db.Customers
join o in db.Orders on c.CustomerID equals o.CustomerID
where c.City == "London"
select o;
您可以很方便地使用’.’语法来实现数据的导航。尽管如此,这并不是连接存在的根本原因。关系的存在是因为我们更趋向使用继承、图来表示特定的领域对象模型。而实体之间也存在着引用关系。数据库中表与表之间通过外键来实现连接的方式也可以用实体之间的引用关系来进行表达,而且还可以使数据导航更加方便,这种一致的确是一件很让人欣慰的事情。
所以从这个方面来讲,关系属性存在的意义更重要地在于查询的结果而不是查询本身。通过Customer类定义,我们可容易判别customer拥有多个order。所以,当您获取特定Customer实体的Orders属性时,如您所期望的那样,您将得到一个order集合,因而这种类定义方式的确反映了程序实际的需求。即便您不是想要获取order,您也可以通过这种方式了解customer有多个order。同时也您会期望程序中定义的对象模型能够反映数据库的最终状态,而且可以立即获取到特定的实体。
DLinq实现了一种叫做延迟加载(deferred loading)的技术,这样您通过查询看到的只是数据库内容的一个表象。当您真正执行查询的时候,您才能得到您您想要得到的实体。而与此同时,与之相关联的实体也并不会立即获取到。尽管如此,相关联的实体没有被加载时是不可见的,而一旦您试图去访问相关联的实体,您才能获到它们。
var q =
from o in db.Orders
where o.ShipVia == 3
select o;
foreach (Order o in q) {
if (o.Freight > 200)
SendCustomerNotification(o.Customer);
ProcessOrder(o);
}
例如,您想要查询到某些order,然后在某个特定的时候给这些订单的拥有者customer发送邮件。您没必要返回每个order的拥有者customer。延迟加载可以让您获您所需,从而减少不必要的开销。
当然,事物往往是双方面的。您可能需要在您的应用程序中同时查看到customer和所有的order。也就是说,您想同时得到两部分数据。您不想在您在应用程序中每次只访问一个customer的所有order,您知道这样会降低程序的效率。您想一次性地获取到所有的customer以及所有的order。
var q =
from c in db.Customers
where c.City == "London"
select c;
foreach (Customer c in q) {
foreach (Order o in c.Orders) {
ProcessCustomerOrder(o);
}
}
的确,您可以通过对orders和customers表进行连接查询,一次性地获取到所有的数据。然而返回结果不是实体,而实体拥有的标识,可以在程序中被修改,可以被修改和持久化。但通过查询得到的数据并不能很好地被修改和持久化。更糟糕的是,您将得到一大堆重复无用的数据。
事实上,您真正想得到的只是符合程序需要的相关实体的集合。所以任何时候您都不想得到太少或者太多的实体,这和您的期望是相悖的。
为了一次性地获取到所有的实体,您可以使用DLinq在某个作用域内“立即加载”您想要得到的实体。DLinq定义了一个新的查询操作符Including(),这样在您执行完某个查询之后可以立即获取到的关联的实体。
var q = (
from c in db.Customers
where c.City == “London”
select c)
.Including(c => c.Orders);
每次使用Including()操作符都将产生一个表达式,这个表达式引用了单个关联属性。这个查询操作并不会改变查询的本意,除非您想获取到额外的数据。您可以在Including()操作符之后使用Including()或者其他查询操作符。您没必要将Including()放到查询的最后面,尽管这可以提供程序的可读性。您也可以在Including()操作表达式中嵌套使用Including()。Including()是一个唯一允许立即引用关系属性的操作符。
var q = (
from c in db.Customers
where c.City == “London”
select c)
.Including(c => c.Orders.Including(o => o.OrderDetails));
在上面的例子中,我们就嵌套使用了Including()操作符一次性地获取了所有的数据:Customer集合、Order集合以及Order Detail集合。请注意,其他的关系,如Order-Detail,并不会被立即加载。
发射查询
到目前为止,我们仅仅知道了怎样通过查询来获取和数据库表格相关联的实体。我们的目标不只如此。查询语言的优雅之处在于您可以任意组合查询来获取到您所想要的信息。但是,当您执行某个查询的时候,没有变化跟踪服务,也没有实体标识管理。您仅仅能获取到自己想要的数据而已。
例如,您只是想简单地获取到所在城市为London的所有Customer的所在的公司名称。在这种情况下,您完全没必要因为只是要获取到公司的名称而返回整个的Customer实体。这个时候,您可以从您的查询中发射出公司名称。
var q =
from c in db.Customers
where c.City == "London"
select c.CompanyName;
在这个例子中,查询‘q’返回的是一个字符串数组。
如果您不只是想要得到一个简单的名称,同时您也不希望获取到整个的Customer实体,那么您可以选择返回一个实体集合的子集。
var q =
from c in db.Customers
where c.City == "London"
selectnew { c.CompanyName, c.Phone };
在上面的例子中,我们使用了“匿名对象初始化“来构建了一个由Company Name和Phone Number组合的结构。您或许会觉得您根本不知道返回的数据类型,但是C#支持”隐式类型局部变量声明“,所以您没必要显示指定返回的类型。
var q =
from c in db.Customers
where c.City == "London"
select new { c.CompanyName, c.Phone };
foreach(var c in q)
Console.WriteLine(“{0}, {1}”, c.CompanyName, c.Phone);
如果您想立即是返回的数据,“匿名类型”是一个很好的选择,它能够根据查询结果自动地构建类定义。
您也可以组合查询返回的实体,尽管您很少会需要这样做。
var q =
from c in db.Customers
from o in c.Orders
where c.City == “London”
select new { c, o };
这个查询返回的是Customer和Order实体对的序列。
您也可能需要在查询表达式的中间使用发射。那么您可以将这些数据发射到新的实体中,然后对这些发射得到的实体进行后续的查询。
var q =
from c in db.Customers
where c.City == “London”
select new {Name = c.ContactName, c.Phone} into x
orderby x.Name
select x;
在这个地方,您要慎用参数化构造函数。这虽然在技术上来讲上没有问题的,但是DLinq无法知道构造函数所有的元数据,所以无法正确地推测构造函数的使用。
var q =
from c in db.Customers
where c.City == “London”
select new MyType(c.ContactName, c.Phone) into x
orderby x.Name
select x;
因为DLinq会把查询转化为纯SQL语句,所以程序内部定义的对象类型在数据库系统中并不存在。事实上,所有的对象类型都是在获取完数据之后才被构建出来的。在构建返回的实体的时候,我们并没有使用实际的构造函数,而是根据SQL返回的数据列来构建实体。因为查询转换器无法知道构造函数的基本结构,所以不可能根据MyType的Name来反射到实际的构造函数。
相反,最好的实践的就是使用“对象初始化器“来封装预测的结果。
var q =
from c in db.Customers
where c.City == “London”
select new MyType { Name = c.ContactName, HomePhone = c.Phone } into x
orderby x.Name
select x;
使用参数化构造函数最保险的方法就是将之放到最终的查询视图中。
var e =
new XElement(“results”,
from c in db.Customers
where c.City == “London”
select new XElement(“customer”,
new XElement(“name”, c.ContactName),
new XElement(“phone”, c.Phone)
)
);
如果您愿意,您还可以使用嵌套构造函数来创建实体对象,如上面例子中使用查询结构构走XML对象,只需要将实体的构造放在查询最末的位置。
还有,即便DLinq可以认识构造函数,但还是不能认识本地方法。如果您需要在查询的最后面调用本地方法,那么DLinq会强制这么做。DLinq无法认识本地方法,所以在将查询转化为SQL的时候不能调用本地方法。有一个例外,您可以调用只使用查询变量作为参数的本地方法。在这种情况下,本地方法的调用将直接转化为SQL语句的参数。
还有,您或许需要在查询(转化)中加入程序逻辑。为了要调用本地方法,您需要发射两次。第一次发射返回您所要引用的数据值,第二次发射执行SQL转换。在这两次发射之间,调用了ToSequence()操作将DLinq查询转化成了本地可执行的代码。
var q =d
from c in db.Customers
where c.City == “London”
select new { c.ContactName, c.Phone };
var q2 =
from c in q.ToSequence()
select new MyType {
Name = DoNameProcessing(c.ContactName),
Phone = DoPhoneProcessing(c.Phone)
};
请注意,ToSequence()操作符,不同于ToList()和ToArray(),它不会导致个查询立即被执行,查询仍旧会被延后执行。ToSequence()操作几乎不会改变查询的静态类型,即将Query<T>转化为IEnumerable<T>,而是让编译器觉得后续的查询将在本地被执行。
SQL转化
事实上,DLinq不会在内存中执行查询,但是数据库可以。DLinq只会将查询转化为等价的SQL查询,然后让数据库服务器去执行。因为DLinq查询是延迟加载的,所以它能够在执行之前检查整个查询,即便这个查询由多个部分组成。
因为关系数据库服务器并不能真正执行IL,即便SQL Server 2005集成了CLR,这些查询也不会转化为IL,让数据库服务器来执行。Dlinq查询被转化为了纯文本的参数化SQL查询。
当然,集成了CLR 的SQL,甚至是T-SQL,都不能执行您应用程序中的本地方法。因此,DLinq查询中对本地方法的调用需要转化为等价的SQL可执行的过程或者方法。
.NET Framework中的大部分方法和操作都能够直接转化为SQL。一些可以转化为相近的方法。DLinq禁止使用不能转化为SQL的本地方法或者过程,并会抛出一个运行时异常。在以后节中,我们详细描述了.NET Framework中可以转化为SQL的所有的方法。