深入SQL LINQ
7.1 将对象映射到关系数据
在上一章中,我们使用自定义属性建立映射信息,此后.NET Framework将会为我们管理语言转换(C#到SQL)。相对手动指定映射关系,Visual Studio 2008提供了三个机制帮助我们建立对象关系映射。所有的映射方法如下:
n 内联在类中的属性
n 外部的XML文件
n SqlMetal命令行工具
n SQL LINQ图形化设计工具
本节中,我们会探讨这四中方法各自的优缺点。通过理解每个方法,可以更有效更快速的创建一个应用程序。下面就让我们看一下如何使用内联属性提供更多的功能。
7.1.1 使用内联属性
从手写代码开始能让我们更好的理解如何使用那些自动化工具。上一章,我们介绍了三个自定义属性:Table, Column, 和 Association。除了这三个属性,还有一些不常用的属性。表7.1列出了这些属性。
表 7.1 System.Data.Linq.Mapping 命名空间中提供的映射属性
属性 | 描述 |
Association | 在类之间设置主外键关联 |
Column | 在表的列和类的属性之间建立关联 |
Database | 由CreateDatabase使用,用来指定数据库名称 |
Function | 用来映射一个自定义函数或者存储过程到方法上 |
InheritanceMapping | 用于映射到多态对象,将在第8章讨论 |
Parameter | 表示一个存储过程或者自定义函数的参数 |
Provider | 表示用于执行查询的类型,因为SQL LINQ局限于SQL Server,这指定了目标SQL Server的版本 |
ResultType | 表示存储过程或者自定义函数的返回值类型 |
Table | 表示类映射到数据库的表名称 |
我们将会在第八章着重讲解存储过程,函数和继承。现在,我们要关注Table Column和Association属性。
Table 属性
Table属性在类和数据库表之间架起一座桥梁。为了指示Author类映射到数据库中的Author表,如下使用Table属性。
[Table()]
public class Author
表属性有一个Name参数,如果数据库表的名称和类名称不相同,可以用它来指定数据库表的名称。如下:
[Table(Name="dbo.Authors")]
Column 属性
Column属性可能是使用最频繁的属性。该属性在类属性和列之间建立映射。Name参数的用法和Table属性相同。还可以指定表7.2中任何参数来添加映射功能。
表 7.2 Column属性使用的参数
参数名称 | 描述 | |
AutoSync | 一个枚举值,指示SQL LINQ如何在Create和Update方法后处理数据值,这对那些拥有默认值的列非常有用。 | |
CanBeNull | 指示数据库列的值可否为空值,空值与空字符串是不同的。 | |
DbType | 该属性值用于使用DataContext创建列时指定列的数据类型,有效的值可能会是NVarChar(50) Not Null Default('')。 |
|
Expression | 该属性只有在使用CreateDatabase 方法时有用,用来指定如何计算数据库中的计算列。 |
|
IsDbGenerated | 用来指示是否是数据库为该属性产生了值,该参数用来标识那些自动标识列。当调用了SubmitChanges 后,对象的属性值将会自动同步。 |
|
IsDiscriminator | 用来标识当前列是否标识一个特殊的类型,我们将会在8.3.3中讲述。 |
|
IsPrimaryKey | 标识对应的类属性是不是对应数据库中主键列,SQL LINQ要求,每个类必须有一个主键属性,用来标识对象和实现跟踪服务。对于多主键表,分别设置列属性即可。 |
|
IsVersion | 在表示时间戳的列上使用此属性。列值在每次行更新的时候就会变化,对于优化并发检查,这非常有用。 |
|
Name | 用来标识要映射的列的名称 |
|
Storage | 将数据库值直接存储在私有字段中,指定私有存储字段的名称。 |
|
UpdateCheck | 当处理优化查询的时候,指定SQL LINQ如何使用该列。默认情况下,所有映射的列都将用来控制并发检查。如果使用时间戳或者其它技术来控制并发。可以使用此参数来对更新和删除的方法进行优化。具体每个值的含义,请查阅MSDN。 |
|
使用这些属性,就可以建立完整的对象关系映射。如果想动态产生数据库,需要指定DbType参数。最严格的参数要数IsPrimaryKey了,因为每个映射类必需要有一个属性要使用此参数。对于CanBeNull参数,指定了类属性是否必须有一个实际值。如果没有提供给该类属性任何值,那么就会抛出运行时异常。举例如下:
private System.Guid _ID;
[Column(Storage = "_ID", Name = "ID", DbType = "UniqueIdentifier NOT NULL",IsPrimaryKey = true, CanBeNull = false)]
public System.Guid ID { get { return _ID;} set{ _ID = value;} }
[Column(Name = "LastName", DbType = "VarChar(50) NOT NULL", CanBeNull = false, UpdateCheck=UpdateCheck.Never)]
public string LastName { get; set; }
[Column(Name = "FirstName", DbType = "VarChar(30) NOT NULL", CanBeNull = false, UpdateCheck=UpdateCheck.Never)]
public string FirstName { get; set; }
[Column(Name = "WebSite", DbType = "VarChar(200)", UpdateCheck=UpdateCheck.Never)]
public string WebSite { get; set; }
示例中我们没有使用UpdateCheck参数,因为我们使用了timestamp列。在SQL Server中,一个TimeStamp列会在记录被改变的时候改变。可以对TimeStamp列建立类属性映射如下:
[Column(Name="TimeStamp", DbType="rowversion NOT NULL", IsDbGenerated=true, IsVersion=true, CanBeNull=false, UpdateCheck=UpdateCheck.Always)]
public byte[] TimeStamp { get; set; }
在更新的时候,我们检查数据库的值是否在我们上次访问之后被改变过。通过使用上次访问时获取的ID和timestamp值,我们就可以确定本条记录是否被更新过。所以通过使用timestamp值来检查并发冲突,可以提高并发检查的性能,因为不用再去比较所有列的值了。
Association 属性
Association属性用来建立类之间的关系。Association属性接受的参数如表7.3所示:
表 7.3 属性的可用参数
参数名 | 描述 |
DeleteRule | 指示关系的级联删除策略 |
DeleteOnNull | 当对其外键成员均不可以为 null 的一对一关联设置时,如果该关联设置为 null,则删除对象。
|
IsForeignKey | 表示数据库关系的关联中作为外键的成员。 |
IsUnique | 指示外键上的唯一约束,用于1:1关系。此参数并不常用,因为大多数关系不是1:1的关系 |
Name | 指定外键的名称,用于动态创建数据库 |
OtherKey | 表示关联的另一端上作为键值的、目标实体类的一个或多个成员。 |
Storage | 指定存储关联的内部成员 |
ThisKey | 指定关联的此端上的键值的此实体类成员 |
使用这些参数,可以为Author和BookAuthor类之间建立关联如下:
private EntitySet<BookAuthor> _BookAuthors;
[Association(Name="FK_BookAuthor_Author", Storage="_BookAuthors", OtherKey="Author", ThisKey="ID")]
public EntitySet<BookAuthor> BookAuthors
{
get
{
return this._BookAuthors;
}
set
{
this._BookAuthors.Assign(value);
}
}
Author类的主键是ID属性(ThisKey),BookAuthor类的关联键是Author属性(OtherKey)。然后将集合存储(Storage)在名为_BookAuthors的字段中,字段类型为EntitySet<BookAuthor>。如果希望从属性元数据产生数据库,那么就需要指定外键的名称为FK_BookAuthor_Author (Name)。
目前为止,我们学习了三个属性Table,Column和Association,之后就可以让SQL LINQ来处理那些数据访问代码了。但是使用自定义属性进行映射是把双刃剑。在业务代码中充斥着属性定义,使得代码难以阅读和维护。
使用自定义属性进行映射,需要在编译时就完全确定下来。如果映射信息发生变化,就需要重新编译程序。为了处理这种情况,SQL LINQ提供了另一种映射方法-使用外部的XML文件。
7.1.2使用外部XML文件进行映射
使用XML文件进行映射,需要在初始化DataContext的时候指定XML映射文件的路径。XML文件可以动态改变,而无需重新编译文件。此外,那些自定义属性也不会出现在业务类的定义中了。由此,我们可以专注与业务需求,同时XML映射文件可以让我们将映射信息保持在一个集中的为止,使得维护映射信息变得容易。
不需要学习所有XML映射的属性集合。XML映射元素看起来跟使用自定义属性类似。需要维护的代码减少了。如下,可以使用列表7.1所示XML映射Author类到数据表上。
列表7.1 Author类的XML映射文件
<?xml version="1.0" encoding="utf-16"?>
<Database Name="lia"
xmlns="http://schemas.microsoft.com/linqtosql/mapping/2007">
<Table Name="Author">
<Type Name="LinqInAction.LinqBooks.Common.Author">
<Column Name="ID" Member="ID" Storage="_Id"
DbType="UniqueIdentifier NOT NULL" IsPrimaryKey="True" />
<Column Name="LastName" Member="LastName"
DbType="VarChar(50) NOT NULL" CanBeNull="False" />
<Column Name="FirstName" Member="FirstName" DbType="VarChar(30)" />
<Column Name="WebSite" Member="WebSite" DbType="VarChar(200)" />
<Column Name="TimeStamp" Member="TimeStamp" DbType="rowversion NOT NULL" CanBeNull="False" IsDbGenerated="True" IsVersion="True" AutoSync="Always" />
</Type>
</ Table >
</Database>
映射文件中元素属性与我们使用的自定义属性的参数含义是一样的。我们还需要使用Type和Member信息来指示映射的类和属性。
XML文件的根元素是Database元素。这里指定了要映射的数据库的名称。Database元素可能有多个Table元素,每个Table元素包含一个Type元素,它指示了当前表要映射的对象Type。Type元素可能包含多个Column元素和Association元素。在Column和Association元素的属性上有一个在自定义属性没有的参数:Member。
为了使用新的映射文件,需要指定DataContext使用映射文件。如列表7.2所示:
列表 7.2 使用XML映射文件
XmlMappingSource map =
XmlMappingSource.FromXml(File.ReadAllText(@"lia.xml"));
DataContext dataContext = new DataContext(liaConnectionString, map);
Table<Author> authors = dc.GetTable<Author>();
我们可以使用DataContext的以下方法:FromXml, FromUrl, From- Stream, or FromReader来加载映射文件。一旦加载完成, 就可以像第七章讲的那样使用LINQ查询技术了。
使用映射文件使得我们可以集中关联映射关系,还可以在某些情况下,改变映射文件而不必重新编译程序。但是使用DataContext加载映射文件会消耗额外的解析时间。所以需要进行测试来找到适合你的方法,每种方法都有它的优点和缺点。
两种方法的都有的缺点是它们都需要手动维护映射信息,不过Visual Studio提供了两种方式可以自动产生映射信息:命令行的SqlMetal工具和SQL LINQ设计器。如果你是个受虐狂,喜欢自己建立映射信息,那么你可以跳过下节。否则就让我们探讨一下,如何使用这些工具吧。
7.1.3使用SqlMetal
指定了数据库,SqlMetal工具可以产生对应的业务类,基本的语法是:SqlMetal
[switches] [input file]。下面就对本书使用的数据库产生映射信息。
打开Visual Studio 2008命令提示符,使用此命令窗口是因为它设置了必要的路径信息。为了产生类,需要输入如下命令。确保输入了正确的数据库文件路径。
SqlMetal /server:./sqlexpress
/namespace:LinqInAction.LinqBooks.Common /code:Common.cs
/language:csharp "C:/projects/Lia/lia.mdf"
使用这个命令,我们指定从LIA数据库产生一系列C#类(使用language开关)。Code开关指定这些业务类将会写入到Common.cs文件中,并确在LinqInAction.LinqBooks.Common命名空间下。SqlMetal还有更多的开关用来指定更高级的功能。要显示如何使用这些开关,可以使用如下命令:
SqlMetal /?
下面的命令不但产生了业务类,还产生了访问存储过程的代码:
SqlMetal /database:lia.mdf /Namespace:LinqInAction /code:Common.cs
/language:csharp /sprocs
使用如下命令产生XML映射文件:
SqlMetal /database:lia.mdf /xml:LiaMetadata.xml
产生映射文件后,可以对其进行修改,使其能够与业务类进行对应。如果映射文件修改完毕。就可以使用如下命令产生业务类,而不用再次访问数据库。
SqlMetal /namespace:LinqInAction /code:Common.cs
/language:csharp LiaMetadata.xml
使用Visual Studio打开产生的Common.cs,文件被分成如下几个部分:
n DataContext
n 每个表插入,更新,删除的Partial方法的声明
n 重载的构造函数
n Table访问器
n 存储过程和函数实现
n Table类
n 改变通知时间参数
n 私有字段
n 改变通知的Partial方法声明
n 构造函数
n 带有属性映射的Public属性
n 关联表的属性
n 改变通知事件
n 有存储过程和自定义函数返回的对象类型定义
注意 Partial 方法是一种新语言特性,它允许你插入方法存根,产生的代码可以根据是否实现了Partial方法进行选择性调用。
Common.cs文件包含了大量的类定义。第一个类封装了DataContext,用来连接数据。同时包含访问各个表的方法和其它数据库对象。
如果我们要把整个数据库模型映射信息产生到一个类文件中,SqlMetal做的不错。还可以把SqlMetal集成到MSBuild中。但是SqlMetal不能对要产生的元素进行选择。它只能一次产生数据库模型的所有映射。不过使用Visual Studio 2008的设计工具可以弥补SqlMetal的缺陷。
7.1.4 SQL LINQ设计器
为了帮助开发人员进行可视化的数据映射。Visual Studio提供了集成的设计器。它允许开发人员进行对象拖拽以及对象映射进行可视化管理。也许有些高级的开发人员不喜欢使用设计器,但是设计器不仅能让你看到模型的快照,而且还能帮助你学习新技术。如果不确定如何映射特定的数据关系,可以使用设计工具。
下面就让我们一起学习一下如何使用设计器,首先,右击解决方案管理器的项目节点,选择添加,选择New Item。从提供的模版列表中,选择名为LINQ to SQL Classes的项。改变名称为Lia.dbml,点击添加按钮。将会得到一个空白的设计界面。
如果还没有一个数据连接,那么需要在服务资源管理器中添加一个数据连接。建立了数据连接节点后。选择所有的表节点,然后将它们拖放到设计器中。从工具箱中,可以拖放新的类和关联。通过选择项并按下Delete键,可以实现删除操作。还可以任意拖动实体,从而可以更好的展示它们之间的关系。如图7.1所示:
图7.1 SQL LINQ设计器
一旦设计好了模型视图并保存。那么相关的类就会自动产生。设计器包含三个文件:一个XML元数据文件(Lia.dbml),该文件指定了类是如何产生的。另一个XML文件包含设计器的布局信息(Lia.dbml.layout)。产生的类文件在一个单独的文件中(Lia.designer.cs)。任何对代码后置文件的修改都会被针对设计器的修改覆盖。应当尽量使用设计器或者属性窗口进行映射信息的修改。
7.2 转换查询表达式为SQL
到目前为止,我们已经学会如何使用SQL LINQ。如果要真正的理解SQL LINQ,还需要进行更深入的学习。因为LINQ查询是建立在类型扩展和对 IEnumerable<T>的实现上。我们要做的就是让EntitySet和Table实现IEnumerable<T>。实际上,EntitySet<T> and Table<T>确实实现了IEnumerable<T>,不过它们实际上是实现了一个扩展了IEnumerable<T>接口的IQueryable<T>接口。
7.2.1 IQueryable
SQL LINQ的一个最大的优势是能够将查询表达式转换到其它格式。为了实现这个特性。对象需要暴露查询之外的信息。所有的Object LINQ查询表达式都是构建在对IEnumerable<T>的扩展之上的。然而,IEnumerable<T>只提供了对数据的迭代功能。它并不包含可以供我们进行查询转换的信息。.NET Framework 3.5提供了IQueryable接口,它提供了转换查询必要的信息。图7.2显示了IQueryable和IEnumerable之间的关系。
图 7.2 IQueryable 的对象模型
IQueryable接口要求实现类除了提供IEnumerable接口的实现以外,还要提供三方面信息:包含的ElementType,表示要执行操作的Expression,实现IQueryProvider 泛型接口的Provider。IQueryable支持为其它数据源提供额外的提供者。提供者从Expression获取信息,然后将其转换为合适的结果表达式。转换是在CreateQuery方法中实现的。Execute方法执行了产生的查询。为了帮助理解它们之间的不同, 考虑如下查询:
var query = books.Where(book => book.Price>30);
如果books对象只实现了IEnumerable<T>,编译器将会把它编译为标准的静态方法调用,如下所示:
IEnumerable<Book> query = System.Linq.Enumerable
.Where<Book>(delegate(Book book){return book.Price > 30M;});
而如果books对象实现了IQueryable<T>,将会创建一个表达式树来保留步骤。如列表7.3所示:
列表 7.3 表达为表达式的查询
LinqBooksDataContext context = new LinqBooksDataContext();
var bookParam = Expression.Parameter(typeof(Book), "book");
var query =context.Books.Where<Book>(
Expression.Lambda<Func<Book, bool>>
(Expression.GreaterThan(
Expression.Property(
bookParam, typeof(Book).GetProperty("Price")),
Expression.Constant(30M, typeof(decimal?))),
new ParameterExpression[] { bookParam }));
通过保留查询的步骤,IQueryable的Provider的实现者可以将表达式转换为数据源可以理解的语言结构。同时,我们可以可以对查询进行组合以添加更多的功能。最后进行转换然后一次性获取查询结果。
7.2.2 表达式树
表达式树提供了SQL LINQ工作必要的信息。SQL LINQ获取现有表达式,然后将其转换为数据库可以理解的语法表示(SQL)。一直以来,人们都在致力于在不同数据源上使用同一个查询,即使做到这点,这些解决方案还是简单的将一个字符串转换到另一种字符串表示。SQL LINQ与这些解决方案不同。通过保留表达式树,我们可以组合查询,保持强类型,提供IDE集成并保留必要的元数据。下面让对前面的IQueryable示例进行分析:
LinqBooksDataContext context = new LinqBooksDataContext();
var bookParam = Expression.Parameter(typeof(Book), "book");
var query =
context.Books.Where<Book>(Expression.Lambda<Func<Book, bool>>
(Expression.GreaterThan(
Expression.Property(bookParam, typeof(Book).GetProperty("Price")),
Expression.Constant(30M, typeof(decimal?))),
new ParameterExpression[] { bookParam }));
通过高亮显示表达式类型,可以发现如下表达式类型:Lambda, GreaterThan, Property, Parameter, Constant。每种表达式类型又可以进行分解,比如GreaterThan实际上一个BinaryExpression。该表达式需要两个表达式作为参数,左边的表达式和右边的表达式。通过比较两个表达式,我们可以获取比较的结果。图7.3显示了整个表达式树的图形化表示。
图 7.3 book查询的ExpressionTree 的图形化展示
在图中,可以比编译后的表达式代码看的更清晰。在顶部,Where MethodCallExpression分为两个部分。ConstantExpression包含原始数据,而UnaryExpression包含一个我们要调用的函数。因为DataContext维护了包含映射信息的元数据。我们可以将对象展示转换为数据库可以理解的方式。
继续往下看,在解析GreaterThan BinaryExpression的时候,插入了额外的节点。当在CLR类型上应用GreaterThan操作符的时候,需要对相似的类型进行比较。因此,为了将一个ConstantExpression与潜在的book的price比较。我们需要将ConstantExpression转换为一个可空的Decimal类型。然而, 这种额外的转换在处理SQL语句的时候是不需要的。
那么,SQL LINQ是如何使用这些信息,然后对其进行转换呢?当我们第一次在实现IQueryable<T>对象上进行遍历的时候。整个Expression被传递给它的Provider。提供者使用Visitor模式对每个自己能够识别的表达式节点进行处理,如Where和GreaterThan。此外,它自下而上访问表达式树,从而标识那些不需要处理的节点,提供者构造了一棵更匹配SQL实现的表达式树。
一旦表达式解析完毕,提供者使用合适的映射信息将对象转换到对应的SQL语句。随后SQL语句被发送到数据库。返回的结果将通过映射信息转换到业务对象。
如果你想继续深入研究有关表达式树的知识,SQL LINQ的架构者之一Matt Warren对此进行了详细的讲述:http://blogs.msdn.com/mattwar/archive/2007/07/30/linq-building-an-iqueryable-provider-part-i.aspx。
目前为止,我们向你展示了如何使用SQL LINQ进行数据和类的映射,同时也展示了SQL LINQ如何将查询转换为数据库可以理解的语法的。但是SQL LINQ不只是提供了数据查询的功能,它还提供了跟踪数据变化的功能。
7.3 实体对象生命周期
DataContext在实体生命周期中起着重要的作用。它负责管理数据库连接。此外还要进行表达式转换和数据及对象的转换。如果我们只关心数据浏览,那么实体生命周期可能在我们获取到对象的时候就结束了。
因为应用程序使用了查询结果,并且对其进行修改。我们需要一种机制跟踪变化,直到我们不再需要这些数据。图7.4阐述了DataContext提供的服务。
图7.4 维护生命周期的DataContext 服务
生命周期开始于第一次读取数据库。在将结果对象传给应用程序代码之前,DataContext保留这些对象的引用。SQL LINQ使用标识管理服务跟踪这些对象,而标识就是我们在设计的时候标记的Identity属性。
每次从数据库进行查询的时候,DataContext通过标识管理服务进行检查,看是否已经存在一个相同标识的对象。如果有,DataContext将会返回那些保留在高速缓存中的值,而不是进行重新映射。通过保留原始值,我们可以允许客户在自己的拷贝上进行改变,而不需要关心其它用户的改变。直到提交改变的时候,我们都不需要担心并发性问题。
如果使用标识属性(IsPrimaryKey=true)进行单个对象查询(Where子句中有且只能有ID=2这样格式),那么SQL LINQ将会现在缓存中进行查找,如果没有找到,那么再从数据库查询。但是有一个例外,在使用Single方法的时候,不会使用缓存数据,而是总是到数据库进行查询。通过设置DataContext. ObjectTrackingEnabled=false可以禁用缓存能。(本段为我通过实际对SQL LINQ的使用得知,原著与此不同。)
7.3.1 跟踪变化
当我们改变了对象内容,DataContext维护了对象的原始值和新值,所以我们可以进行优化,只更新那些被修改的值。在列表7.4中,我们建立了两个不同的DataContext对象,每个都有自己的对象标识管理和变化跟踪。
列表7.4 标识管理和变化跟踪
LinqBooksDataContext context1 = new LinqBooksDataContext();
LinqBooksDataContext context2 = new LinqBooksDataContext();
context1.Log = Console.Out;
context2.Log = Console.Out;
Guid Id = new Guid("92f10ca6-7970-473d-9a25-1ff6cab8f682");
Subject editingSubject =
context1.Subjects.Where(s => s.ID == Id).SingleOrDefault();
ObjectDumper.Write(editingSubject); ObjectDumper.Write(context2.Subjects.Where(s => s.ID == Id));
editingSubject.Description = @"Testing update"; E
ObjectDumper.Write(context1.Subjects.Where(s => s.ID == Id));
ObjectDumper.Write(context2.Subjects.Where(s => s.ID == Id));
如列表7.3中做的一样,通过建立两个DataContext对象模拟两个独立的用户。本例中,我们根据一个指定的Guid获取了一个subject对象。为了阐述标识管理,我们使用两个DataContext对象从数据库中获取了同一条记录。
我们从第一个DataContext对象中获取了editingSubject对象,并同时将其值和第二个DataContext中获取的同一个Guid的subject显示出来。如表7.4,输出值是相同的。之后,我们修改了editingSubject的description属性,我们并没有将变化提交到数据库。所以,context2对象并不知道这个变化。当我们重新请求原始查询的时候,context1上返回的是新的description值,而context2仍然返回原始值。因为每个DataContext对象表示不同的用户,所以这意味着两个用户有着不同的数据展示。如果现在检查数据,那么数据库中仍然是原始值。
表 7.4 变化前后记录的状态
动作 | Context1 | Context2 | Database |
初始查询返回的结果 | Original | Original | Original |
改变值并重新查询 | Changed | Original | Original |
实际上,第二次从context1和context2查询的对象都是来自标识跟踪服务缓存的结果,因为我们没有修改context2获取的对象,所以,看上去它和数据库的值是一样的。除了跟踪映射属性值的变化,跟踪服务还能跟踪对象关联的变化。在进行数据库提交的时候,这些变化会一起提交到数据库。
7.3.2 提交变化
目前为止,我们对对象的修改都停留在内存中,为了保存修改,只需要简单调用DataContext. SubmitChanges方法。一旦调用了此方法,DataContext对象就会比较原始值和修改的值,如果它们不同,那么就会产生必要的SQL语句,并在数据库执行。假定更新的过程当中,没有发现冲突,那么DataContext对象将会更新数据,刷新变化列表。如果更新过程中出现了问题,那么根据选定的并发管理选项处理修改的数据。列表7.5是对前面示例的扩展。
列表7.5 使用标识和变化跟踪管理提交变化
LinqBooksDataContext context1 = new LinqBooksDataContext();
LinqBooksDataContext context2 = new LinqBooksDataContext();
Guid Id = new Guid("92f10ca6-7970-473d-9a25-1ff6cab8f682");
Subject editingSubject =
context1.Subjects.Where(s => s.ID == Id).SingleOrDefault();
Console.WriteLine("Before Change:");
ObjectDumper.Write(editingSubject); ObjectDumper.Write(context2.Subjects.Where(s => s.ID == Id));
editingSubject.Description = @"Testing update";
Console.WriteLine("After Change:");
ObjectDumper.Write(context1.Subjects.Where(s => s.ID == Id));
ObjectDumper.Write(context2.Subjects.Where(s => s.ID == Id));
context1.SubmitChanges();
Console.WriteLine("After Submit Changes:");
ObjectDumper.Write(context1.Subjects.Where(s => s.ID == Id));
ObjectDumper.Write(context2.Subjects.Where(s => s.ID == Id));
LinqBooksDataContext context3 = new LinqBooksDataContext();
ObjectDumper.Write(context3.Subjects.Where(s => s.ID == Id));
输出结果如下:
Before Change:
ID=92f10ca6-7970-473d-9a25-1ff6cab8f682
Name=Novel
Description=Initial Value
ObjectId=448c7362-ca4e-4199-9e4f-0a0d029b9c8d
ID=92f10ca6-7970-473d-9a25-1ff6cab8f682
Name=Novel
Description=Initial Value
ObjectId=5040810a-eca9-4850-bcf6-09e42837fe92
After Change:
ID=92f10ca6-7970-473d-9a25-1ff6cab8f682
Name=Novel
Description=Testing Update
ObjectId=448c7362-ca4e-4199-9e4f-0a0d029b9c8d
ID=92f10ca6-7970-473d-9a25-1ff6cab8f682
Name=Novel
Description=Initial Value
ObjectId=5040810a-eca9-4850-bcf6-09e42837fe92
After Submit Changes:
Id=92f10ca6-7970-473d-9a25-1ff6cab8f682
Name=Novel
Description=Testing update
ObjectId=bc2d5231-ed4e-4447-9027-a7f42face624
Id=92f10ca6-7970-473d-9a25-1ff6cab8f682
Name=Novel
Description=Original Value
ObjectId=18792750-c170-4d62-9a97-3a7444514b0b
Id=92f10ca6-7970-473d-9a25-1ff6cab8f682
Name=Novel
Description=Testing update
ObjectId=207eb678-0c29-479b-b844-3aa28d9572ac
列表7.5在修改了对象之后,在context1上调用了SubmitChanges。之后,重新在context1 和context2上执行同一个查询,此外,还创建了第三个DataContext对象来重新获取数据库值。表7.5总结了提交前后结果的变化。
表 7.5 每个DataContext保持的记录在提交前后值的变化
Action | Description1 | Description2 | Description3 | Id1 | Id2 | Id3 |
Initial fetch | Original | Original | n/a | Guid1 | Guid2 | n/a |
After change | Changed | Original | n/a | Guid1 | Guid2 | n/a |
After commit | Changed | Original | Changed | Guid1 | Guid2 | Guid3 |
为了详细标识对象,我们添加了一个名为ObjectId的Guid属性。该值作为Subject构造函数的参数传入的。从表中可以看到,context1和context2的每次查询返回的都是同一个对象,由此可以得到标识服务的所起的作用。
了解DataContext如何处理你的对象是很重要的。DataContext应该是一个短命对象。我们需要了解DataContext如何使用标识服务和管理变化,以避免意外的情况发生。当我们获取数据的时候,可以在获取数据以后将DataContext对象丢弃,在这种情况下,我们可以将ObjectTrackingEnabled属性设置为false来优化查询。这会提高性能,但是禁用了更新变化的功能。
如果需要更新数据,那么需要注意DataContext的作用域。在Windows程序中,可以接受保留一个DataContext对象来跟踪变化。但是跟踪大量对象的变化会严重影响性能。应该使用如下SQL LINQ的设计模式:Query – Report – Edit – Submit – Dispose。一旦我们不需要维护对象变化的时候,就应该清理DataContext对象,然后创建一个新的DataContext对象。
7.3.3 操作断开连接的数据
在有些情况下,不赞成在断开连接的环境中保持DataContext。这种情况发生在ASP.NET页面,web service等技术的数据更新中。此时,很难做到保持一个DataContext对象。因为对象需要与DataContext分离,所以我们不能再依赖标识管理服务和变化跟踪服务。传送到客户端的都是一些简单对象,在断开连接的模型中管理变化是一个巨大的挑战。
为了支持断开连接的模型,SQL LINQ提供了两种替代的方法来提交变化。如果你需要在表中添加一行,你可以调用DataContext.InsertOnSubmit方法。变化跟踪服务并不跟踪新对象,它只针对那些改变的已有对象。因此InsertOnSubmit工作良好,因为我们不用担心新对象会与现有数据冲突。
然而,如果我们需要改变一个现有对象,我们就需要把变化关联到已有对象上。将对象变化附加到特定的DataContext对象上会有一些可能的选择。推荐使用Attach方法。示例7.6使用Attach方法将对象与DataContext对象相关联。
列表 7.6 在断开连接的环境中更新记录
public void UpdateSubject(Subject cachedSubject)
{
LinqBooksDataContext context = new LinqBooksDataContext();
context.Subjects.Attach(cachedSubject);
cachedSubject.Name = @"Testing update";
context.SubmitChanges();
}
在本例中,参数是一个已经存在的未改变的对象。这个对象可能存储在ASP.NET Session中。我们使用Attach方法将对象连接到DataContext对象上。一旦成功连接,DataContext对象就可以使用变化跟踪服务和标识管理服务对这个对象的变化进行监视。所以,要记住,必须在改变对象之前把对象关联到DataContext对象,否则变化跟踪服务将会无法检测对象变化。
如果你试图关联一个已经更新的对象,除非对象有一些特殊的特点,否则不能简单的把一个已经改变的对象关联到DataContext对象上。如果对象中有一个对应的TimeStamp标识的属性,那么你可以使用Attach方法的一个重载建立关联。如下:
context.Authors.Attach(cachedAuthor, True)
第二个参数表示要附加的对象是脏对象,并强制DataContext将其加入到变化对象的列表当中。如果你没有在数据库中加入Timestamp列的自由。你需要设置类属性的UpdateCheck,使其不对属性值进行检查。这两种情况中,所有的属性都将被更新到数据库,而不管它们有没有变化。
如果你有一个原始对象的拷贝,就可以同时使用对象的原始版本和变化版本:
context.Subjects.Attach(changedSubject, originalSubject);
在这种情况下,Update子句中只有那些变化的列,而不是更新所有列。原始值将被用来检查并发冲突。如果没有原始对象,可以重新从数据库中获取。如列表7.7所示:
列表 7.7 更新已经改变的断开连接的对象
public static void UpdateSubject(Subject changingSubject)
{
LinqBooksDataContext context = new LinqBooksDataContext();
Subject existingSubject = context.Subjects
.Where(s => s.ID == changingSubject.ID)
.FirstOrDefault<Subject>();
existingSubject.Name = changingSubject.Name;
existingSubject.Description = changingSubject.Description;
context.SubmitChanges();
}
如果对象已经被更新,简单的将对象附加到DataContext上将会失败。因为没有被标记为已更新的值,因为变化跟踪服务将会无视那些变化。这里,我们再次从数据库获取了同一个ID的对象。然后进行更新。
因为在提取原始数据之后和更新之前,同一个记录可能已经被更新。为了管理并发跟踪,最好的选择使提供一个数据版本列。
DataContext类的对象标识服务和变化跟踪服务在对象生命周期中扮演了至关重要的角色。如果我们只是简单的读取数据,可以将DataContext. ObjectTrackingEnabled属性设置为false,从而禁用这些服务。然而,如果我们需要跟踪变化并允许更新变化,那么,跟踪对象变化就是必须的。
7.4 摘要
表面上,SQL LINQ允许我们很容易的对数据库进行查询和更新。在背后,它提供了强大的映射结构,表达式解析和实体关联机制。你可以在不知道它们背后的原理就可以使用它们。但是,如果你知道的越多,就可以很好的驾驭它,使其不能出现不希望的结果。此外,了解DataContext如何管理对象和变化跟踪也很重要,这能让你确保更新了正确的信息。
到此为止,我们讲述了SQL LINQ的核心概念并探讨了其背后的工作原理。下一章,我们将探讨SQL LINQ一些更加高级的功能。