技术开发 频道

用LINQ to SQL进行数据层访问


 【IT168 技术分析】Orcas的SQL LINQ和它的Object/Relational Designer让ADO.NET开发人员可以结合O/R图,对象保持,和LINQ在SQL服务器自动生成的数据接入层的查询请求。
    技术工具箱:VB.NET, C#,SQL Server 2005 SP2,其它:Visual Studio"Orcas" Beta 1或更新版本 。
   
    面向对象的概念稳固建立,是现今的软件抽象模型,但是如果打赌目前有90%或更多的.NET发展项目都连接到关系数据库,多半不会输。 

    把.NET的“简单老式的CLR对象”链接到关系表的行会产生编程中断,这经常会引起阻抗不匹配。差不多有50个专用和开源的插件,库,工具可供.NET开发人员用来桥接对象—关系。但是在Orcas之前,微软不会提供对象/关系图(O/RM)的工具。Redmond第一次尝试提供一个O/RM平台的努力主要以ObjectSpaces的形式在Whidbey(对 2005 Beta 1)中出现。但随后就随着WinFS的退位而销声匿迹了。根据Whidbey的参与者引起的骚动来看,当ObjectSpaces消失之后,面向数据的.NET开发人员显然会是O/RM的狂热分子。

    LINQ to SQL(以前叫DLing)是微软用O/RM数据接入层(DAL)来填充ObjectSpaces的第一个努力。DAL综合了对象持续管理和独立语言查询(LINQ)技术.设立DAL的目的是把数据存储和其它的应用层分开。DAL处理创建,找回,更新,和删除(CRUD)操作。仅仅几行代码就能为你的表示层和内存中的对象或,必要时,下层数据存储的LINQ加强查询提供事务对象。LINQ也掌控数据标志,以避免内存中对象实例和数据跟踪的重复,这样就可以持续保证数据存储表中实例的改变。

    传统的ADO.NET SqlCommand和OleDbCommand对象依赖于查询字符串在数据库上执行CRUD操作。查询字符串有两个致命弱点:第一,要求开发者学习另一门编程语言(SQL),这门语言有很多独特的地方。第二,SQL命令是后期绑定的,所以无法被编译器的语法检测检查出来,也不能获得智能感应的支持。LINQ通过向C# 3.0和VB 9.0添加一种强类型的,类似于SQL的查询语法来克服这些问题。LINQ为查询内存中的对象(LINQ to 对象),XML文档(LINQ to文档),SQL数据库(LINQ to SQL),以及Orcas的实体框架和实体数据模型定义的数据对象(LINQ to 实体)提供完全的智能感应,类型安全,和静态类型检查。LINQ将在下一个版本的C#,VB中综合关键字,从而为查询提供最好的结果。

    Orcas Beta 1包括一个较早,但是可以操作的LINQ to SQL的应用,以及一个更新过的图形O/R Designer,本文将深入讨论这个。在把LINQ to SQL作为一个O/RM工具进行全面的评价之前,你得知道LINQ to SQL只能连接到SQL Server 200x,MSDE 2000,或SQL Server Express。不支持SQL Server CE或任何其它的后端数据库。LINQ to SQL直接由数据库的元数据产生类,所以映射仅限于每个类模型的表。继承的模型也受到了严格限制;单一表的映射需要继承结构被存在一张表上,这张表得指派一列来存储用来区别子类的鉴别值。

    写代码或设立XML文件来定义O/RM是令人畏惧的过程,但是LINQ to SQL会替你完成其中的大部分工作。你可以用命令行工具SQLMetal.exe或图形化O/R Designer,从已有的SQL服务器数据库创建一系列类。开始一个新的Windows表格或Web站点项目并增加Ling[sic]到SQL文件对象,这些动作会增加一个默认的,带有指向DataClasses.dbml.diagram XML文件和DataClasses.designer.vb 或cs 文件的条目的DataClasses.dbml节点。后面的文件包含一个框架DataClassesDataContext局部类,这个类带有两个默认的构造器。默认情况下,所有的WinForm工程都有一个System.Xml.Linq的LINQ to XML引用;添加Linq to SQL文件项将把System.Data.Linq引用增加到WinForm和Web站点工程中。基础LINQ查询的System.Linq名称空间是System.Core.dll组件的一部分,因此,你不必添加一个指向它的引用。

用Designer组装DataContext

    点击DataClasses.dbml节点,打开一个空的O/R Designer界面,这个界面有一个空的主区域和Method窗格。然后从存储数据的服务器或数据库资源管理器节点拖动表格到主窗口中,从而添加实体类和表示独立实体间关系的虚线。实体类之间的联系表示一个数据存储中的双向外部键关系。把第一个表格拖到设计器表面,从而添加一个默认无参数的New构造器。这个构造器是关于Data Context和它到AppName.exe.config或Web.config文件的<connectionStrings>元素的连接字符串。默认情况下,LINQ to SQL会自动把复数形式的表格名改为单数的实体类名字,但你可以在entity-class编辑这个名称。只需要几分钟就可以添加从原始Northwind样本数据库衍生的八个实体类。

    现在来为父/子/孙子窗体建立一个WinForm数据源,按F5构建并编译你的项目,选择Data,然后选Add New Data Source启动DataSource配置向导。接下来,点击Next,在Choose Data Source Type对话框中选择Object,点Next,展开ProjectName节点,并选择databinding分层结构的最上面一个成员,本例中是Customer。最后,点击Finish。在DataSources窗口中,整个实体的层次结构由Customer实体和它们的关系链以对象图的形式表示出来。ASP.NET团队承诺在Orcas Beta 2.0版本中的Web工程会有一个LINQDataSource控件。

    你可以在设计器中改变表,列和关系属性,但如果只是用为查询和更新而动态生成的T-SQL声明来进行简单测试的话,通常默认设置就可以满足要求了。如果表格包含一个时间戳列,是因为DataContext用它来进行并发冲突检测优化。添加时间戳列将把其它所有列的UpdateCheck属性设置为Never。这样,在更新和删除时就不用将现在的数据库值和原始值进行比较(时间戳除外)。 

    DataContext是LINQ to SQL层次结构中的顶层对象;它管理数据库的SqlClient连接,并为每一个数据存储表维护一个System.Data.Linq.Table(T的)实体 (Listing 1) 。DataContext同样会跟踪所有在内存中的实体(除非你明确地禁止实体跟踪),掌控数据存储表的更新,并使并发冲突的优化能够进行。表格(T的)的类实现了IEnumerable(T的)和LINQ-specific IQueryable(T的)的交互界面。这个类可以作为DataGridView和其它编辑控件的DataSource,或者是作为BindingSource组件的DataSource。LINQ to SQL把表示一对多的外部键关系的联合物化为一个EntitySet(T的),这是一个表格(T的)的扩展实现。这意味着你可以用对DataSets用的相同技术,通过Data Source窗口对象图来产生父/子/[孙子]窗体。 

   让我们用Northwind数据库作为例子来实验一下这个技术吧。从Data Source窗口把Customer节点以CustomerDataGridView或Details View的形式拖到窗体中。这样就添加了CustomerBindingSource和CustomerBindingNavigator,可以把它们作为父实体进行编辑。接下来,拖动Customer.Order_s联合节点到窗体中,从而创建同步的Order_sDataGridView/BindingSource。最后,拖动Customer.Orders.Order_Details节点到窗体中,从而添加Order_DetailsDataGridView/BindingSource。Order_s和Order_Details节点表示EntitySets,所以复数形式的名字比较合适。Order_实体有一个下划线后缀,这是因为Order标志符可能会和操作者的LINQ Order冲突,所以.NET的Pluralizer为它保留了下划线。 

    如果实体包含时间戳列,你必须从DataGridView控件里把这些列移去,因为这个控件会把时间戳的Byte()数据类型误认为是图像,当你的代码试图去填充它的时候就会抛出一个错误。OrdersDataGridView包括很多列,分别代表EntityRef(Customer的),EntityRef(Employee的),以及EntityRef(Shipper的),这些表示了联合的一个方面。类似地,   OrderDetailsDataGridView有EntityRef(Order_的)和Entity Ref(Product的)列。把这些列从DataGridViews移除也是个不错的练习。

    把下面的代码添加到你的WinForm类,以此启动一个DataContext会话,并填充窗体的文本框和DataGridViews:
Imports System.Data 
Imports System.Data.Linq
Imports System.Linq

Public Class frmLINQ2SQL
Private dcNwind As LINQ2SQLDataContext

Private Sub frmLINQ2SQL_Load(ByVal sender _
As System.Object, ByVal e _
As System.EventArgs) _
Handles MyBase.Load
dcNwind = New NwindDataContext
CustomerBindingSource.DataSource = _
dcNwind.Customer
End Sub
End Class
   
    LINQ2SQLDataContext假设你已经把LINQ to SQL设计器文件命名为LINQ2SQL.dbml。

    按F5构建并运行项目,然后检查OrdersDataGridView是否和Customer数据同步了,以及OrderDetailsDataGridView是否按照你选择的顺序显示了排列项。

探究设计器生成的类

    要学会并理解O/RM的处理过程,至少得研究LINQ2SQL.designer.vb文件或Northwind数据库的cs文件中自动生成的2000行实体类代码的一部分。为了简洁,很多例子中都使用了Shippers表的代码(Listing 2)。LIINQ to SQL的O/RM默认采用基于属性的映射;这段摘录的TableAttribute装饰把Shipper表映射到Shipper实体类:
<Global.System.Data.Linq.Table(Name:= _ 
"dbo.Shippers")> _
Partial Public Class Shipper
Implements Global.System.Data.Linq._
INotifyPropertyChanging, _
Global.System.ComponentModel._
INotifyPropertyChanged
...
End Class
   
    在设计器中,表名是作为Source属性值出现的。为了支持DataContext的数据跟踪特性,类实现了System.Data.Linq.INotifyPropertyChanging和System.ComponentModel.INotifyPropertyChanged接口。支持INotifyPropertyChanged接口就不必在object和databound控件之间加入BindingSource了。虽然如此,微软仍然建议添加BindingSource。

    O/R设计器在普通的列成员中加入了三个默认的ColumnAttribute名字/类型对:Storage(私有成员名字),Name(数据库的列名称),以及DBType(SQL Server数据类型):
<Global.System.Data.Linq.Column(Storage:= _ 
"_Phone", Name:="Phone", _
DBType:="NVarChar(24)")> _
Public Property Phone() As String
Get
Return Me._Phone
End Get
Set
If ((Me._Phone Is value) _
= false) Then
Me.OnPropertyChanging("Phone")
Me._Phone = value
Me.OnPropertyChanged("Phone")
End If
End Set
End Property

    好几个其它的Column属性限定了primary-key,non-nullable, auto-increment, timestamp 列, 以及没有作为属性表示出来的entity properties (Table 1)。OnPropertyChanging("PropertyName")将引发Property-Changing事件,该事件把原始的属性值存储到DataContext中,用来和数据库的值进行比较,以此来管理乐观并发。OnPropertyChanged("PropertyName")方法会引起PropertyChanged事件,从而跟踪目前的属性值。

    Association把EntityRef(T的)属性和EntitySet(T的)收藏连接起来定义在多对一的表格关系中的对象对等性。Association的属性要比列的属性少,但是要比你在Designer属性页(Table 2)中设置的要多很多。Association的OnProperty-Changing代码要比列的更复杂,这是因为处理过程要求移除原来的并添加一个新的实例,而不是仅仅改变外部键的值(Listing 3)。

    大多数O/RM工具都支持三种继承映射策略:每个类层次结构一个表,每个之类一个表,以及每个实体类一个表。LINQ to SQL仅支持每个类层次结构一张表的策略,这种策略需要一个鉴别器列来指定子类。下面的摘录用Shipper-Type鉴别器S和Truck, Rail, Air, 以及 Ocean图表类型来指定Shipper基类:
<Table(Name:="dbo.Shippers", ...)>
<InheritanceMapping(Code:="S", _
Type:=GetType(Shipper), IsDefault=True)> _
<InheritanceMapping(Code:="T", _
Type:=GetType(Truck))> _
<InheritanceMapping(Code:="R", _
Type:=GetType(Rail))> _
<InheritanceMapping(Code:="A", _
Type:=GetType(Air))> _
<InheritanceMapping(Code:="O", _
Type:=GetType(Ocean))> _
Partial Public Class Shipper
,,,
<Column(Name:="ShipperType", ... _
IsDiscriminator:=True)> _
Public Property ShipperType() _
As String
...
End Property
End Class
    在这段摘录的代码中,每个类一张表的映射方式需要五张表(基础类和另外的四个子类);每个实体类一张表,则需要四张表。如果域模型要求以上两种映射策略中的一种,可以尝试Entity Framework和它的Entity Data Model,或者主流的.NET O/RM工具之一。在LINQ to NHibernate提供者的支持下,工作可以继续进行。

    如果需要实体类的自由属性POCO,必须使用SqlMetal.exe命令行工具生成一个ClassName.vb或.cs文件,以及一个带有这种指令的Mapping.xml文件:
SqlMetal /server:oakleaf-vs20\SQLEXPRESS 
/database:NwindLINQ
/map:\LINQ2SQLTest\NwindLINQ.xml
/language:vb /namespace:NwindLINQ
/code:\LINQ2SQLTest\NwindLINQ.vb /pluralize
   
    然后用使用了连接字符串和映射文件规范的过载构造器来建立DataContext(本例中是dcNwind):
Private Sub frmLINQ2SQL_Load(ByVal sender _ 
As System.Object, ByVal e _
As System.EventArgs) _
Handles MyBase.Load
Dim strFile As String = _
"\LINQ2SQL\NwindLINQ.xml"
Dim strConn As String = _
"Data Source=.\SQLEXPRESS;Initial " + _
Catalog=NwindLINQ;" + _
Integrated Security=SSPI"
Dim xmsNwind As XmlMappingSource = _
XmlMappingSource. _
FromXml(File.ReadAllText(strFile))
dcNwind = New NwindLINQ(strConn, xmsNwind)
CustomerBindingSource.DataSource = _
dcNwind.Customers
End Sub

T-SQL更新替换为存储过程

    O/RM工具选择了动态生成的SQL语句而不是查询的存储过程,甚至没有选择更新数据存储,关于这点存在着很大的争议。默认情况下,LINQ to SQL从LINQ的From … In … Where … Order By … Select查询创建的表示树产生SELECT语句。类似地,添加、更新、或移除一个实体实例,并调用DataContext.SubmitChanges()方法会向数据库发送T-SQL INSERT, UPDATE, or DELETE语句。可以从DataContext.Log产生一个TextWriter来写日志文件,或者是一个StringBuilder来存储或显示LINQ to SQL发送到SQL Server 200x实例的命令。下面的代码向文本框txtSQL写入内容:
Private sbLog As New StringBuilder 
Private swLog _
As New StringWriter(sbLog)
...

dcNwind = New NwindLINQDataContext
dcNwind.Log = swLog
...

txtSQL.Text = sbLog.ToString()
    不需用SQL Profiler来检查LINQ to SQL发出的命令,这对运行SQL Server Express的程序开发人员来说是一个福音,并且对那些拥有全套SQL Server客户端工具的用户而言,非常节省时间。

    LINQ to SQL的O/R Designer让我们可以用方便的存储过程来替换SELECT, INSERT, UPDATE, 和DELETE T-SQL批处理。从Server Explorer拖动SELECT 存储过程节点到Designer的表面,在Methods边窗添加StoredProcName(ParameterList),局部公有类 StoredProcName实体类,和DataContext的公有函数StoredProcName。这个函数返回StoredProcName实体,比如uspGetCustomersByCountry。添加一个采用了IMultipleResults接口,但是向局部类文件返回一个单一结果的重新命名的函数。这样就可以返回事先定义好的,兼容的实体,而不是根据存储过程命名的实体。

    要注册INSERT, UPDATE, 或DELETE存储过程,只需从Server Explorer拖动相应的节点到Designer表面。在这个例子中,你必须右键点击entity-class,得到相应的表,并选择Configure Behavior,打开相同名字的对话框,在其中你就可以让储存过程和一个已经存在的实体类联系起来。选Accept或选择对话框里Class列表中的实体类,然后在Behavior列表中选择Insert,Update,或Delete。选择Customize选项,打开已注册存储过程列表,并选择实体和行为恰当的一个,从而加入Method Arguments和相应的Class Properties列表。更新的时候,选择PropertyName(原来的)找到要更新的行的主键。

     PropertyName(当前的)项目指定更新的值。点Apply或OK生成Sub InsertEntityName,Sub UpdateEntityName,或Sub DeleteEntityName重载方法和相应的执行存储过程的函数StoredProcName。在代码调用DataContext.SubmitChanges()方法之前,这些方法是不会被执行的。(Beta 1 介绍了使用存储过程来更新时的几个问题。但是这些问题在CTP或Beta 2.0中都会得到修正。可以在这篇博客里找到关于这些问题的描述http://tinyurl.com/2o23ch)。

    可以通过用时间戳列(用于处理并发冲突)来代替原值参数,从而简化存储过程的代码。例如:更新Order_Detail的过程更新了增加了时间戳列的Order_Details:
Private Sub UpdateOrder_Detail(ByVal current As _ 
Order_Detail, ByVal original _
As Order_Detail)
Me.uspUpdateOrder_Detail(original.ProductID, _
current.UnitPrice, current. _
Quantity, current.Discount, _
original.OrderID, original.TimeStamp)
End Sub

    DataContext向UpdateEntityName方法提供当前和原始的值作为参数。InsertEntityName方法只有一个当前的参数,而DeleteEntityName 方法有一个原始的参数。
   
    这是通过映射来执行存储过程的uspUpdateOrder_Detail函数的代码:
<DebuggerNonUserCodeAttribute(), _ 
StoredProcedure(Name:= _
"dbo.uspUpdateOrder_Detail")> _
Public Function uspUpdateOrder_Detail( _
<Parameter(Name:="@ProductID")> _
ByVal ProductID _
As Global.System.Nullable(Of Integer), _
<Parameter(Name:="@UnitPrice")> ByVal _
UnitPrice As Global.System.Nullable(Of _
Decimal), <Parameter(Name:="@Quantity")> _
ByVal Quantity As Global.System.Nullable(Of _
Short), <Parameter(Name:="@Discount")> _
ByVal Discount As Global.System.Nullable(Of _
Single), <Parameter(Name:="@OrderID")> _
ByVal OrderID As Global.System.Nullable(Of _
Integer), <Parameter(Name:="@TimeStamp")> _
ByVal TimeStamp() As Byte) As Integer

Dim result As Provider.IExecuteResults = _
Me.ExecuteMethodCall(Me, CType( _
MethodInfo.GetCurrentMethod, _
MethodInfo), ProductID, _
UnitPrice, Quantity, Discount, _
OrderID, TimeStamp)

Return CType( _
result.ReturnValue,Integer)
End Function
   
    请注意,为简洁起见,我删除了Global.System.Diagnostics, Global.System.Data.Linq, 和Global.System.Reflection的名称空间前缀。

解决并发冲突

    如果执行INSERT, UPDATE, 或DELETE语句,或者执行存储过程影响到的行数为0, 则DataAdapters会抛出DbConcurrencyException。异常的Row属性会返回引起第一个异常的DataRow对象。解决这个冲突问题通常需要编写相当数量的事务逻辑代码,并且不容易测试。在很多情况下,当试图更新相关记录的时候,都要处理很多被抛出的异常。
   
      在执行LINQ to SQL的DataContext.Submit-Changes(ConflictMode.ContinueOnConflict)重载的时候,DataContext.ChangeConflicts属性会返回一个Change- ConflictsCollection(ObjectChangeConflict的),其中每一个发生了并发冲突的对象都有一个对应的成员。ObjectChangeConflict的成员包含一个MemberConflicts(MemberChangeConflict的)集合,其中包含有冲突的成员。对象成员ChangeConflict有OriginalValue, Current-Value, 和DatabaseValue属性,同样也有重载后的Resolve方法来以参数的形式接受Refresh-Mode.Keep-Changes,RefreshMode.KeepCurrent-Values,或RefreshMode.OverwriteCurrent-Values。这些嵌套的集合允许事务-规则代码或用户能够用一对嵌套的For Each … Next循环(产生一个简单的消息框)来解决多冲突问题。本文的LINQ2SQL.sln例子工程包含了一个ProcessChanges过程,其中就有会产生这种消息框的冲突解决代码。

     一些β测试员把LINQ to SQL的实体类称作是“荣耀类型的Data-Sets”,我把这叫做假赞美,真诅咒。LINQ to SQL的O/R Designer和DataSet 设计器有一些相似之处。从这些实体类创建的Object Data Sources提供了和DataSets类似的数据绑定能力。然而,LINQ to SQL的内在哲学已经从操作高速缓存的DataRows集合转变到支持LINQ查询的内存中的用户-业务对象集合。下载LINQ2SQL.sln工程和它的NwindLINQ.mdf数据库。在以下几个方面和BindingNavigators进行性能比较:Tables跳转,LINQ查询,或SELECT存储过程的结果,以及在多个对象实例上INSERT,UPATE,DELETE操作的执行时间。通过比较反衬出和Data-Sets相比,LINQ to SQL编码技术的简单。甚至在这个早期的β阶段我就认为LINQ to SQL会赢得SQL Server 200x用户。
0
相关文章