LINQ与SQL的交互、延迟加载与预加载

LINQ查询运算符可以处理内存中可查询的.NET类型实例。可查询的.NET类型是指那些实现IEnumerable接口或继承于IQueryable<T>泛型接口的类型。数组List、Dictionary及.NET Framework中的其他集合类型都是可查询的。

  XML和DataSet不能直接查询,因为二者都没实现IEnumerable接口。为此,在使用前,我们需要对它们进行特殊处理,该过程会调用DataSet上的AsEnumerable方法和XML文档的XDocument.Load方法时执行。

  LINQ与SqlCommand对象不同的是,在开始对数据进行操作时,查询才会隐式地执行。换言之,不必显式地执行命令,它会被延迟,直到遇到用于获取查询结果的代码。这种特性被称为查询的延迟执行。

  如果合作ToList或ToArray方法将数据加载到.NET对象中,查询便会立即执行,不会被延迟:

   
   
   
   
var data = (from c in dataContext.Customers where c.Country = " Spain " select c).ToList();

  最后注意,只有生成集合的查询会被延迟,若查询只获取某个标量,它会被立即执行。

与SQL Server的交互

  有一点需要铭记:Linq-to-SQL不是构建中间层的工具,请不要被它所用的对象所迷惑。它是构建数据访问层的有力工具。有了它,我们面对的将是对象和高级的查询语法,而不是结果集和T-SQL语句。换言之,Linq-to-SQL虽然能够访问数据库,但不会取代数据访问层。

数据上下文

  VS2008带一有款O/R设计器,能自动针对我们要连接的数据库的模型生成相关的类。该O/R设计器以连接字符串为输入便可以生成数据库的数据模型。该模型包含数据实体类和中央控制类(数据上下文),后者带有对表执行特定操作的方法。

  在VS2008中添加新项“LINQ to SQL类”选项用于将DBML文件添加到ASP.NET项目的App_Code文件夹中。DBML文件是一种XML文档,其中注册了物理数据库的连接字符串、要引入到内存模型的表以及相关的实体类。

  DBML文件配有两个配套文件,分别用于保存数据上下文的布局和存储数据上下文类和实体类的源代码。

  数据模型中的类都会被标记为partial,这表明我们可以在同一项目中对该类进行扩展。

  Linq-to-SQL实体类的实例用作数据传输对象。这些对象中包含数据,但没有行为,甚至连基本的输入输出行为也没有。实例对象的保存和加载由数据上下文层负责。

  数据上下文类是Linq-to-SQL数据模型的入口点,针对给定连接。它代表一个入口,我们可以通过它连接到数据库,这种类继承于System.Data.Linq.DataContext。对于添加到当前模型中的每个表,我们都会找到相应的属性。

  LINQ的Table类型代表数据表,并为LINQ方法查询其中内容添加了几个方法:Where、Distinct、First、GroupBy等。

  实体类中包含映射到源数据库的列,并提供了get和set方法。如下所示:

   
   
   
   
[Column(Storage = " _CustomerID " , DbType = " NChar(5) NOT NULL " ,
CanBeNull = false , IsPrimaryKey = true )] public string CustomerID { get { return this ._CustomerID; } set { if (( this ._CustomerID != value)) { this .OnCustomerIDChanging(value); this .SendPropertyChanging(); this ._CustomerID = value; this .SendPropertyChanged( " CustomerID " ); this .OnCustomerIDChanged(); } } }

自动生成代码中的扩展性方法

  每个实体类中的属性都配有一对这种原型方法:

   
   
   
   
partial void OnCustomerIDChanging( string value); partial void OnCustomerIDChanged();
  此外,实体类还有三个一般用途的扩展性方法:
   
   
   
   
partial void OnLoaded(); partial void OnValidate(); partial void OnCreated();

  扩展性方法的主要目的是,使开发者能够在该对象生命周期的特定时刻执行自己编写的代码。

  扩展性方法利用了分部方法(partial method)的机制,这里.NET 3.5的新特性。

  分部方法被定义在实体类的partial类中,对这种方法的调用会被自动置于生成的代码中。如果该方法的主体部分没有被实际定义,编译器会跳过该方法的调用。要为这种方法定义内容,我们应该添加一个分部类,再定义该分部方法,如下所示:

   
   
   
   
public partial class Customer { partial void OnCustomerIDChanging( string value) { if (value.Equals( " KOEN2 " )) { throw new InvalidExpressionException( " ... " ); } } }

  分部方法有一些限制:只能返回空(void),不能有输出参数,隐式地作为私有成员,不能被声明为虚方法,不能创建它的委托。

  扩展性方法非常适合进行验证。如果对某个属性值做验证,可使用OnXxxChanging/OnXxxChanged方法对。如果要对某个实体的状态做全局验证,我们可以用OnValidate方法。

  OnValidate方法会在实体被保存到数据库前被调用。OnXxxChanging/OnXxxChanged方法分别在相应属性被赋值的前后被调用。

数据的查询

  LINQ查询针对的是内存中的可查询对象,如果最终的数据存储是SQL Server数据库,我们需要事先获取一个可查询对象--数据上下文。

  由于请求期间不会自动保存数据上下文对象,因而每次请求都需要创建它。我们可以考虑对该对象进行缓存,这由开发者来决定。但数据上下文是一种轻型的对象,创建它不会带来严重的性能问题。

  考虑以下查询:

   
   
   
   
var data = (from c in dataContext.Customers select c.Country).Distinct();

  该语句会被转化为以下T-SQL语句:

   
   
   
   
select distinct [ t0 ] . [ Country ] from [ dbo ] . [ Customers ] as [ t0 ]

  接下来,在需要查询结果时,该命令会被运行,并将响应赋给data变量。

  dataContext.Customers何时被填充?用一句话概括:在需要时填充。

  考虑以下代码:

   
   
   
   
Response.Write(dataContext.Customers. Count ());

  假设有91个用户在用户表中。Customers在到达该指令前是空的。然而,dataContext.Customers不是一个纯粹的IEnumerable集体,因此Count方法不会简单地对系统内存中的元素数目进行统计。这是Linq-to-Sql与Linq-to-Objects的主要不同。

  那么在输出前,会有91个元素添加到该集合中吗?不是的,但输出仍会是91。在SQL Profiler中跟踪可发现,每次调用Count都会执行这样的T-SQL命令:

   
   
   
   
Select Count ( * ) From customers
  集合不会被填充,但每次调用Count都会统计对象的总数。这意味着以下代码会造成三次与数据库的往返交互:
   
   
   
   
Response.Write(dataContext.Customers. Count ());
Response.Write(dataContext.Customers. Count ());
Response.Write(dataContext.Customers. Count ());
  为完全填充该集合,我们需要使用foreach语句,或使用转换方法(如ToArray或ToList)。如下所示:
   
   
   
   
foreach (Customer c in dataContext.Customers) { Respose.Write(c.CustomerID + " , " ); }

  上面的代码会从数据库中将数据取出存入Customers后再进行遍历,而不是与数据库往返91次交互。

层次型结果

  在Linq-to-SQL中,表间关系是通过自动向集合中添加属性来实现的。例如,可向实体类Customer添加Orders属性,其中存储与当前客户相关的所有订单。Orders集合是一个填充Order对象的LINQ Table。

  考虑下面的查询语句:

   
   
   
   
var data = from c in dataContext.Customers group c.ContactName by new { City = c.City, Region = c.Region } into g where g.Count() > 1 select g;

  该查询翻译成自然语言后的意思是,按城市和地区对联系人进行分组。返回给出变量的结果是什么样呢?

  最后的结构不是序列,也不是表格式的,而层次型的。第一层包含执行分组操作所使用的键--城市和区域。第二层包含城市和区域中相应的联系人。这种数据格式不能通过网格控件来显示,而需要自行编写代码,如下所示:

   
   
   
   
foreach (var contacts in data) { Response.Write(contacts.Key.City + " , " + contacts.Key.Region); Response.Write( " <hr/> " ); foreach (var contact in contacts) { Response.Write(contact); Response.Write( " <br/> " ); } Response.Write( " <br/> " ); Response.Write( " <br/> " ); }

  在分组查询的结果中,每个元素都有Key属性,该属性代表当前组别的键。如果使用复合键,那么Key属性为一个对象。结果集中的每个元素也是列表,每个列表中的对象包含group关键字后面列出的所有属性。

  假设我们有两个关联的表,分别为Customers和Orders。考虑下面的查询:

   
   
   
   
var data = from c in dataContext.Customers where c.Country == " Spain " select new { c.CompanyName, c.Orders };

  Orders属性是一组Order对象,代表被选择的客户下的所有订单。表格式数据绑定控件无法绑定这种数据。我们要自己编写代码生成用户界面:

   
   
   
   
StringBuilder sb = new StringBuilder(); foreach (var c in data) { sb.AppendFormat( " <b>{0}</b><hr/> " , c.CompanyName); foreach (Order o in c.Orders) { sb.AppendFormat( " {0}<br/> " , o.OrderID); } sb.Append( " <br/><br/> " ); } Label1.Text = sb.ToString();

  通过Table类的SelectMany方法,相同的数据也能以表格式的格式来表达,但不可避免地造成数据冗余。代码如下:

   
   
   
   
var data = (from c in dataContext.Customers where c.Country == " Spain " select c).SelectMany(c => c.Orders);

  这种情况下,结果是表格式数据,每条记录代表一个订单。

延迟加载与预获取

  Linq-to-SQL在获取对象方面有一种延滞性,不会同时自动获取所有相关对象。例如,假设我们要加载客户:

   
   
   
   
var data = from c in dataContext.Customers select c;
  Customers与Orders、Orders与Order Details、Order Details与Products,以及Products与Categories间都存在关系,是否表明我们为了一条查询而要将整个Northwind数据库加载到内存中?当然不是。在默认情况下,仅当十分必要时,数据才会被加载。下面的查询会获取定单,并只在必要时才访问客户信息:
   
   
   
   
var data = from o in dataContext.Orders where o.ShipVia == 3 select o; foreach (var o in data) { // 此处仅当满足条件时,才会与SQL数据库交互获取Customer信息 if (o.Freight > 300 ) SendCustomerNotification(o.Customer); ProcessOrder(o); }

  foreach语句会触发Orders表的查询。每次访问Customer属性,都会为获取相关数据而与SQL进行一次往返交互。

  延迟加载功能可通过数据上下文类的DeferredLoadingEenabled属性来控制。若将其设为false,那么它会指示Linq-to-SQL不延迟一对一和一对多关系的加载。这样,开发者需要自行负责所需数据的物理加载。如下所示:

   
   
   
   
// 使用ToList方法显式加载数据 var orders = (from o in dataContext.Orders where o.Customer.City == " Lond " select o).ToList();
//关闭延迟加载 dataContext.DeferedLoadingEnabled = false ; StringBuilder sb = new StringBuilder(); foreach (var c in customers) { sb.AppendFormat( " Customer ID: {0}<br/> " , c.CustomerID); sb.AppendFormat( " Orders: {0}<br/> " , c.Orders.Count()); } Label1.Text = sb.ToString();

  如果延迟加载被关闭而又不使用显式加载,那么c.Orders集合会一直为空(也就是集合中的元素个数为0),Count也只能返回0。

数据的预加载

  数据上下文类有一个叫LoadOptions的属性,用于基于现有的一对多和一对一关系强制提前加载数据。LoadOptions属性中包含的是DataLoadOptions类的实例。示例:

NorthwindDataContext dataContext= new NorthwindDataContext();

   
   
   
   
DataLoadOptions options = new DataLoadOptions(); options.LoadWith < Customer > (c => c.Orders); dataContext.LoadOptions = options;

  LoadWith方法用于通知特定类型(此处为Customer)实体预加载关系(子记录)。该方法主要通知Linq-to-SQL运行库,在加载每个客户信息时,预加载其订单。如果延迟加载被关闭,那么我们可以通过LoadWith确保在访问连接到关系的属性(如Customer实体的Orders和Order实体的Order_Details)时相关数据能够就绪。

  此例中,每个Customer实体被加载时,都会执行一条Select语句来填充它的Orders属性。更值得注意的是,不论我们是否使用订单记录,这个过程都会发生。

  DataLoadOptions类中除LoadWith外,还有AssociateWith方法。AssociateWith方法用于指定特定关系的筛选条件或排序方式。不论关系是立即加载还是延迟加载,都会应用筛选条件和排序方式。

Linq-to-SQL查询的限制

  Linq-to-SQL能够将标准的LINQ运算符转化为T-SQL语句。注意,LINQ运算符针对的是元素的“有序序列”,而T-SQL语言处理的是无序的值集合。我们通过LINQ的Order By子句设置的排序操作,只会在获取数据后执行。也就是说,我们开始得到的总是无序的集合,之后才会按我们请求的方式进行排序。排序方式不会暗含在序列中。因此,要通过元素的值为确定某个元素,而不是它在序列中的位置

你可能感兴趣的:(LINQ与SQL的交互、延迟加载与预加载)