环境:

本地:Windows 2003+VS2008,IBM CSDK 3.50

远程:Red Hat Enterprise Linux AS release 4 (Nahant Update 5)+Informix Dynamic Server 2000 Version 9.21.UC2

现象:

创建ODBC数据源时,提示

  1. “---------------------------IBM Informix ODBC Error Message:---------------------------Test connection was NOT successful.[Informix][Informix ODBC Driver][Informix]Client host or user (informix@zzy-flyinweb) is not trusted by the server.-----------------------” 

解决办法:

在远程服务器上创建/etc/hosts.equiv 文件,内容如下:

[root@datacenter ~]# cat /etc/hosts.equiv
zzy-flyinweb
59.57.251.62

现象:

报错:System.Data.Odbc.OdbcException: ERROR [42000] [Informix][Informix ODBC Driver][Informix]A syntax error has occurred.

解决办法:

请检查SQL语句是还正确

特别注意:VS2008在自动生成的SQL代码中,会将字段名及表名加[]号括起,而这可能会引起语法错误(不知是不是Informix的原因,去掉相关的中括号即可)

另外,OLEDB方式一直没有通过,原因未知

本日志由 flyinweb 于 2009-06-23 17:21:22 发表到 DotNet专栏 中,目前已经被浏览 280 次,评论 0 次;

作者添加了以下标签: asp.netINFORMIXODBCOLEDB

06 Oct 2005

Delve into the details of using ADO.Net and Informix® Dynamic Server beyond the basic connection string. This article covers stored procedures and parameterized SQL. Then it presents a model (with code) for generating your own strongly typed DataSets.
Introduction

With its massive .NET framework, Microsoft® introduced a new data access technology called ADO.NET. In this article, we will examine how to use the ADO.NET driver for Informix that is included with the IBM Client SDK version 2.90. The sample code included is written in C#.

本日志由 flyinweb 于 2009-06-23 15:07:21 发表到 DotNet专栏 中,目前已经被浏览 337 次,评论 0 次;

作者添加了以下标签: ADO.NETinformix

首页只显示了部分日志内容,要查看日志的全部内容请阅读全文

概述

TreeView是一个重要的控件,无论是在VB.NET,C# 还是VB、Delphi等各种语言中,都充当了导航器的作用。在实际工作中,很多情况下需要将TreeView与数据库进行连接,以填充其节点。在Windows Form和Web Form中,我们可以用TreeView来显示树形结构,如显示目录树、显示地区、分类显示商品等。可以说,在大部分软件的开发中,TreeView都是一个不可缺少的展示控件。因此,树形结构的设计就成了软件开发人员一个永恒的话题。
树形结构的展示方式
树形结构的展示一般来讲有三种方式:
1.界面设计时在TreeView设计器或者代码中直接填充TreeView控件。
2.从XML文件中建立树形结构。
3.从数据库中得到数据,建立树形结构。
第一种方式是最简单的,这种方式主要用于树形结构一般没有变化的应用程序,在设计时就固定一颗树。当然,在设计时固定了树的结构,以后要想修改、增加、删除树的节点,就必须修改源程序。所有不利于扩展。
第二种方式从XML文件中提取,由于XML本身就是树形结构的,微软提供的文档对象模型DOM 可以方便的读取、操作和修改 XML 文档。在.NET中,应用System.Xml类可以方便地将XML文件加载到TreeView控件中,微软的MSDN也提供了实例,此处就不再多说。
第三种方式,树形结构的数据,从数据库中获得。一般来讲,我们的应用程序多数是基于数据库的。采用这种方式,增加、修改、删除一颗树的节点很方便,只要操作数据库中的数据就可以了。而且,这种方式可以和数据库中的其它表做关联、查询和汇总,通过设计视图或存储过程,很容易查询出你想要的相关数据。下面,我们主要讨论这种方式的设计和实现。

数据库设计

首先,我们在SQL SERVER 2000里建立一个表tbTree,表的结构设计如下:
列名    数据类型    描述        长度    主键
ID    Int    节点编号        4    是
ParentID    Int    父节点编号        4    
ConText    Nvarchar    我们要显示的节点内容    50    

在SQL SERVER 2000中建表的脚本:

  1. CREATE TABLE [dbo].[tbTree] (  
  2.     [ID] [int] IDENTITY (1, 1) NOT NULL ,  
  3.     [Context] [nvarchar] (50) COLLATE Chinese_PRC_CI_AS NULL ,  
  4.     [ParentID] [intNULL   
  5. ON [PRIMARY


在表中添加如下记录:

  1. SET IDENTITY_INSERT tbtree ON 
  2. insert tbtree (ID,Context,ParentID)  values ( 1,'中国',0)  
  3. insert tbtree (ID,Context,ParentID)  values ( 2,'北京',11)  
  4. insert tbtree (ID,Context,ParentID)  values ( 3,'天津',11)  
  5. insert tbtree (ID,Context,ParentID)  values ( 4,'河北省',1)  
  6. insert tbtree (ID,Context,ParentID)  values ( 5,'广东省',1)  
  7. insert tbtree (ID,Context,ParentID)  values ( 6,'广州',5)  
  8. insert tbtree (ID,Context,ParentID)  values ( 7,'四川省',1)  
  9. insert tbtree (ID,Context,ParentID)  values ( 8,'成都',7)  
  10. insert tbtree (ID,Context,ParentID)  values ( 9,'深圳',5)  
  11. insert tbtree (ID,Context,ParentID)  values ( 10,'石家庄',4)  
  12. insert tbtree (ID,Context,ParentID)  values ( 11,'辽宁省',1)  
  13. insert tbtree (ID,Context,ParentID)  values ( 12,'大连',11)  
  14. insert tbtree (ID,Context,ParentID)  values ( 13,'上海',1)  
  15. insert tbtree (ID,Context,ParentID)  values ( 14,'天河软件园',6)  
  16. insert tbtree (ID,Context,ParentID)  values ( 15,'汕头',5)  
  17. SET IDENTITY_INSERT tbtree off 


按此在新窗口打开图片
下载地址
http://msdn.microsoft.com/downloads/samples/internet/ASP_DOT_NET_ServerControls/WebControls/default.asp
安装后,通过“自定义工具箱”->“.net框架组件”把TreeView添加到工具箱里。
新建一个项目,选择Visual Basic.Net 工程Asp.net Web应用程序,在页面上拖画一个TreeView控件。

Html页:

  1. <%@ Register TagPrefix="iewc" Namespace="Microsoft.Web.UI.WebControls" Assembly="Microsoft.Web.UI.WebControls, Version=1.0.2.226, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %> 
  2. <%@ Page Language="vb" AutoEventWireup="false" Codebehind="WebForm1.aspx.vb" Inherits="Tree.WebForm1"%> 
  3. <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> 
  4. <HTML> 
  5. <HEAD> 
  6. <title>WebForm1</title> 
  7. <meta name="GENERATOR" content="Microsoft Visual Studio .NET 7.0"> 
  8. <meta name="CODE_LANGUAGE" content="Visual Basic 7.0"> 
  9. <meta name="vs_defaultClientScript" content="JavaScript"> 
  10. <meta name="vs_targetSchema" content="http://schemas.microsoft.com/intellisense/ie5"> 
  11. </HEAD> 
  12. <body MS_POSITIONING="GridLayout"> 
  13. <form id="Form1" method="post" runat="server"> 
  14. <FONT face="宋体"> 
  15. <iewc:TreeView id="TreeView1" style="Z-INDEX: 101; LEFT: 39px; TOP: 68px" runat="server"></iewc:TreeView></FONT> 
  16. </form> 
  17. </body> 
  18. </HTML> 


后台代码:

  1.     Private Sub Page_Load(ByVal sender As System.ObjectByVal e As System.EventArgs) Handles MyBase.Load  
  2.         Dim ds As New DataSet()  
  3.         Dim CN As New SqlConnection()  
  4.         Try 
  5.             '初始化连接字符串  
  6.             CN.ConnectionString =   
  7.             "data source=pmserver;initial catalog=Benchmark;persist security info=False;user id=sa;Password=sa;" 
  8.             CN.Open()  
  9.             Dim adp As SqlDataAdapter = New SqlDataAdapter("select * from tbTree", CN)  
  10.             adp.Fill(ds)  
  11.             Me.ViewState("ds") = ds  
  12.         Catch ex As Exception  
  13. #If DEBUG Then  
  14.             Session("Error") = ex.ToString()  
  15.             Response.Redirect("error.aspx")        '̀跳转程序的公共错误处理页面  
  16. #End If  
  17.         Finally 
  18.             '关闭连接  
  19.             CN.Close()  
  20.         End Try 
  21.         '调用递归函数,完成树形结构的生成  
  22.         AddTree(0, Nothing)  
  23.     End Sub 
  24.  
  25.     '递归添加树的节点  
  26.     Private Sub AddTree(ByVal ParentID As IntegerByVal pNode As TreeNode)  
  27.         Dim ds As DataSet  
  28.         ds = Me.ViewState("ds")  
  29.         Dim dvTree As New DataView()  
  30.         dvTree = New DataView(ds.Tables(0))  
  31.         '过滤ParentID,得到当前的所有子节点  
  32.         dvTree.RowFilter = "PARENTID = " + ParentID.ToString  
  33.  
  34.         Dim Row As DataRowView  
  35.         For Each Row In dvTree  
  36.             Dim Node As New TreeNode()  
  37.             If pNode Is Nothing Then  '判断是否根节点  
  38.                 '添加根节点  
  39.                 Node.Text = Row("ConText").ToString()  
  40.                 TreeView1.Nodes.Add(Node)  
  41.                 Node.Expanded = True 
  42.                 '再次递归  
  43.                 AddTree(Int32.Parse(Row("ID").ToString()), Node)  
  44.             Else 
  45.                 '̀添加当前节点的子节点  
  46.                 Node.Text = Row("ConText").ToString()  
  47.                 pNode.Nodes.Add(Node)  
  48.                 Node.Expanded = True 
  49.                 '再次递归  
  50.                 AddTree(Int32.Parse(Row("ID").ToString()), Node)  
  51.             End If 
  52.         Next 
  53.     End Sub 


C#版本:

  1. using System;  
  2. using System.Collections;  
  3. using System.ComponentModel;  
  4. using System.Data;  
  5. using System.Drawing;  
  6. using System.Web;  
  7. using System.Web.SessionState;  
  8. using System.Web.UI;  
  9. using System.Web.UI.WebControls;  
  10. using System.Web.UI.HtmlControls;  
  11. using Microsoft.Web.UI.WebControls;  
  12. using System.Data.SqlClient;  
  13. namespace TreeCS  
  14. {  
  15.     ///   
  16.     /// WebForm1 的摘要说明  
  17.     ///   
  18.     public class WebForm1 : System.Web.UI.Page  
  19.     {  
  20.         protected Microsoft.Web.UI.WebControls.TreeView TreeView1;  
  21.       
  22.         private void Page_Load(object sender, System.EventArgs e)  
  23.         {  
  24.             // 定义数据库连接  
  25.             SqlConnection CN = new SqlConnection();  
  26.             try   
  27.             {  
  28.                 //初始化连接字符串  
  29.                 CN.ConnectionString=  
  30.                 "data source=pmserver;initial catalog=Benchmark;persist security info=False;user id=sa;Password=sa;";  
  31.                 CN.Open();  
  32.  
  33.                 SqlDataAdapter adp = new SqlDataAdapter("select * from tbTree",CN);  
  34.                 DataSet ds=new DataSet();  
  35.                 adp.Fill(ds);  
  36.                 this.ViewState["ds"]=ds;   
  37.             }  
  38.             catch (Exception ex)  
  39.             {  
  40.                 Session["Error"] = ex.ToString();  
  41.                 Response.Redirect("error.aspx");       //̀跳转程序的公共错误处理页面  
  42.             }  
  43.             finally   
  44.             {  
  45.                 CN.Close();  
  46.             }  
  47.             //调用递归函数,完成树形结构的生成  
  48.             AddTree(0, (TreeNode)null);  
  49.         }  
  50.  
  51.         //递归添加树的节点  
  52.         public void AddTree(int ParentID,TreeNode pNode)   
  53.         {  
  54.             DataSet ds=(DataSet) this.ViewState["ds"];   
  55.             DataView dvTree = new DataView(ds.Tables[0]);  
  56.             //过滤ParentID,得到当前的所有子节点  
  57.             dvTree.RowFilter =  "[PARENTID] = " + ParentID;  
  58.  
  59.             foreach(DataRowView Row in dvTree)   
  60.             {  
  61.                 TreeNode Node=new TreeNode() ;  
  62.                 if(pNode == null)   
  63.                 {    //添加根节点  
  64.                     Node.Text = Row["ConText"].ToString();  
  65.                     TreeView1.Nodes.Add(Node);  
  66.                     Node.Expanded=true;  
  67.                     AddTree(Int32.Parse(Row["ID"].ToString()), Node);    //再次递归  
  68.                 }   
  69.                 else   
  70.                 {   //̀添加当前节点的子节点  
  71.                     Node.Text = Row["ConText"].ToString();  
  72.                     pNode.Nodes.Add(Node);  
  73.                     Node.Expanded = true;  
  74.                     AddTree(Int32.Parse(Row["ID"].ToString()),Node);     //再次递归  
  75.                 }  
  76.             }                     
  77.         }              
  78.  
  79.         #region Web Form Designer generated code  
  80.         override protected void OnInit(EventArgs e)  
  81.         {  
  82.             //  
  83.             // CODEGEN该调用是 ASP.NET Web 窗体设计器所必需的。  
  84.             //  
  85.             InitializeComponent();  
  86.             base.OnInit(e);  
  87.         }  
  88.           
  89.         /// <summary>  
  90.         ///设计器支持所需的方法 - 不要使用代码编辑器修改  
  91.         /// 此方法的内容  
  92.         /// </summary>  
  93.         private void InitializeComponent()  
  94.         {      
  95.             this.Load += new System.EventHandler(this.Page_Load);  
  96.  
  97.         }  
  98.         #endregion  
  99.     }  


后记:请读者自行修改程序中的连接字符串设置。
附:相关微软MSDN文档,包括在VB6和.NET中从XML建立树形结构 
http://support.microsoft.com/default.aspx?kbid=311318
http://support.microsoft.com/default.aspx?kbid=308063
http://support.microsoft.com/default.aspx?kbid=317597
http://support.microsoft.com/default.aspx?kbid=244954

本日志由 flyinweb 于 2009-06-21 20:51:26 发表到 DotNet专栏 中,目前已经被浏览 144 次,评论 0 次;

作者添加了以下标签: 树形结构

WEB项目开发中,经常会遇到这种情况,就是当用户点击页面上的Button时,系统需要弹出一个窗口。这个窗口可能是标准对话框,也可能是一个新的页面。开始的时候我不知道如何实现这样的功能,按照我以前的编程习惯,我认为应该有一个类似于ShowMessage的方法,但是,可惜这种方法在WEB下是没有的。通过在网上查阅资料,发现一般的做法是在Button的OnClick事件中写下如下代码:

  1. private void Button1_Click(object sender, System.EventArgs e)  
  2. {  
  3.      string strScript = "<script language=javascript>\n";  
  4.      strScript += "window.alert(" + "\"hello\"" + ");";  
  5.      strScript += "";  
  6.      Response.Write(strScript);  


      以上代码的效果就是当用户点击Button1按钮,将会弹出一个对话框。这种做法,其实就是在你的.cs文件中内嵌一段javascript脚本文件。但是这样的代码,或许会感觉很不舒服,这么多的""很容易晕!如果需要弹出一个页面,并且需要传递参数,那样编写出的代码感觉会更晕!
      考虑能不能把关于脚本部分都放在.aspx文件里,.cs里直接引用函数名称即可。事实证明是可以的,请看下面的实现方法:
1.在aspx的之前添加这段代码

  1. <script language="jscript">   
  2.      function showmessagebox()  
  3.     {  
  4.          window.alert("hello");  
  5.      }  
  6. </script> 

2.在cs文件的Page_Load事件里添加以下代码

  1. private void Page_Load(object sender, System.EventArgs e)  
  2. {  
  3.       // 在此处放置用户代码以初始化页面  
  4.       this.Button1.Attributes.Add("onclick","javascript:showmessagebox();");  


3.现在当你点击页面上Button1按钮,所产生效果与先前那种方法类似,但是整个系统代码看起来舒服了许多。
[总结]:使用这样的方法来编写程序,不会使你的程序运行的更快、更稳定....它所起到的作用,只是让你的代码阅读起来更方便,便于与人交流。利用这种思想,可以举一反三,尽量不要在cs文件里写大量的javascript脚本文件。

本日志由 flyinweb 于 2009-06-21 20:48:12 发表到 DotNet专栏 中,目前已经被浏览 139 次,评论 0 次;

作者添加了以下标签: 代码分离

.NET 数据访问体系结构指南
发布日期 : 6/17/2004 | 更新日期 : 6/17/2004

相关链接

patterns & practices Index

Application Architecture for .NET:Designing Applications and Services

Alex Mackman、Chris Brooks、Steve Busby、Ed Jezierski、Jason Hogg、Roberta Leibovitz (Modeled Computation) 和 Colin Campbell (Modeled Computation)

Microsoft Corporation

摘要:本文提供有关在基于 .NET 的多层应用程序中实现基于 ADO.NET 的数据访问层的指南。它集中讨论一系列常见的数据访问任务和方案,并介绍了可帮助您选择最适合的方法和技术的指南。

简介

如果您要为基于 .NET 的应用程序设计数据访问层,应该使用 Microsoft?ADO.NET 作为数据访问模型。ADO.NET 功能丰富,支持松耦合的多层 Web 应用程序和 Web 服务的数据访问要求。像其他功能丰富的对象模型一样,ADO.NET 提供了多种方法来解决特定问题。

《.NET 数据访问体系结构指南》提供的信息可帮助您选择最适合的数据访问方法。它通过描述一系列广泛的常见数据访问方案、提供性能提示以及推荐最佳实施策略来达到此目的。本指南还提供了对常见问题的解答,这些问题包括:哪里是存储数据库连接字符串的最佳场所?如何实现连接池?如何处理事务?如何实现分页以使用户能够在大量记录中滚动?

本指南集中讨论如何使用 ADO.NET 并通过 SQL Server .NET 数据提供程序(ADO.NET 随附的两个数据提供程序之一)来访问 Microsoft SQL?Server?2000。在适当的时候,本指南将明确指出您在使用 OLE DB .NET 数据提供程序来访问其他支持 OLE DB 的数据源时应该了解的差异。

有关使用本文中讨论的指南和最佳实施策略开发的数据访问组件的具体实现,请参阅 Data Access Application Block。Data Access Application Block 包含该实现的源代码,您可以在基于 .NET 的应用程序中直接使用这些代码。

《.NET 数据访问体系结构指南》分为以下几个部分:

*
本页内容

ADO.NET 简介ADO.NET 简介
管理数据库连接管理数据库连接
错误处理错误处理
性能性能
通过防火墙进行连接通过防火墙进行连接
处理 BLOB处理 BLOB
通过数据集执行数据库更新通过数据集执行数据库更新
使用强类型数据集对象使用强类型数据集对象
处理空数据字段处理空数据字段
事务处理事务处理
数据分页数据分页
附录附录

本文档的目标读者

本文档为需要生成基于 .NET 的应用程序的应用程序设计人员和企业开发人员提供指南。如果您负责设计和开发基于 .NET 的多层应用程序的数据层,请阅读本文档。

预备知识

要使用本指南来生成基于 .NET 的应用程序,您必须具有使用 ActiveX?Data Objects (ADO) 和/或 OLE DB 开发数据访问代码以及 SQL Server 方面的经验。您必须了解如何为 .NET 平台开发托管代码,并且必须了解 ADO.NET 数据访问模型引入的根本性的变化。有关 .NET 开发的详细信息,请参阅 http://msdn.microsoft.com/net

新增功能

本文档已经进行了更新,以便包含有关执行数据集更新、使用类型化 DataSet 以及使用空数据字段的部分。

如正文中所指出的那样,本指南中的一些内容特别适用于 Microsoft Visual Studio?2003 开发系统和 .NET 框架 SDK 1.1 版。

下载《.NET 数据访问体系结构指南》

单击可从 MS.com 下载中心下载《.NET 数据访问体系结构指南》

ADO.NET 简介

ADO.NET 是基于 .NET 的应用程序的数据访问模型。可以使用它来访问关系数据库系统(如 SQL Server 2000、Oracle)和其他许多具有 OLE DB 或 ODBC 提供程序的数据源。在某种程度上,ADO.NET 代表 ADO 技术的最新进展。不过,ADO.NET 引入了一些重大变化和革新,旨在解决 Web 应用程序的松耦合特性以及在本质上互不关联的特性。有关 ADO 与 ADO.NET 的比较,请参阅 MSDN 文章“ADO.NET for the ADO Programmer”,网址为:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dndotnet/html/adonetprogmsdn.asp

ADO.NET 引入的主要变化之一是用 DataTableDataSetDataAdapterDataReader 对象的组合取代了 ADO Recordset 对象。DataTable 表示单个表中行的集合,在这一方面类似于 RecordsetDataSet 表示 DataTable 对象的集合,同时包括将各种表绑定在一起的关系和约束。实际上,DataSet 是带有内置 XML 支持的、内存中的关系结构。

DataSet 的主要特性之一是它不了解可能用来填充它的基础数据源。它是一个不连续的、独立的实体,用于表示数据集合,并且可以通过多层应用程序的不同层在组件之间传递。它还可以作为 XML 数据流进行序列化,这使其非常适合于在不同种类的平台之间进行数据传输。ADO.NET 使用 DataAdapter 对象将数据传送到 DataSet 和基础数据源,或者从数据源传出。DataAdapter 对象还提供以前与 Recordset 关联的增强的批量更新功能。

图 1 显示了完整的 DataSet 对象模型。

1.1.DataSet 对象模型

.NET 数据提供程序

ADO.NET 依赖于 .NET 数据提供程序的服务。这些提供程序提供对基础数据源的访问,并且包括四个主要对象(ConnectionCommandDataReaderDataAdapter)。

目前,ADO.NET 随附了两类提供程序:Bridge 提供程序和 Native 提供程序。通过 Bridge 提供程序(如那些为 OLE DB 和 ODBC 提供的提供程序),可以使用为以前的数据访问技术设计的数据库。Native 提供程序(如 SQL Server 和 Oracle 提供程序)通常能够提供性能方面的改善,部分原因在于少了一个抽象层。

  • SQL Server .NET 数据提供程序。这是一个用于 Microsoft SQL Server 7.0 和更高版本数据库的提供程序。它被进行了优化以便访问 SQL Server,并且它通过使用 SQL Server 的本机数据传输协议来直接与 SQL Server 进行通讯。

    当您连接到 SQL Server 7.0 或 SQL Server 2000 时,请始终使用该提供程序。

  • Oracle .NET 数据提供程序。用于 Oracle 的 .NET 框架数据提供程序通过 Oracle 客户端连接软件支持对 Oracle 数据源的数据访问。该数据提供程序支持 Oracle 客户端软件版本 8.1.7 及更高版本。

  • OLE DB .NET 数据提供程序。这是一个用于 OLE DB 数据源的托管提供程序。它的效率要比 SQL Server .NET 数据提供程序稍微低一些,因为它在与数据库通讯时通过 OLE DB 层进行调用。请注意,该提供程序不支持用于开放式数据库连接 (ODBC) 的 OLE DB 提供程序 MSDASQL。对于 ODBC 数据源,请改为使用 ODBC .NET 数据提供程序(稍后将加以介绍)。有关与 ADO.NET 兼容的 OLE DB 提供程序的列表,请参阅 http://msdn.microsoft.com/library/en-us/cpguidnf/html/cpconadonetproviders.asp

其他目前正处于测试阶段的 .NET 数据提供程序包括:

  • ODBC .NET 数据提供程序。用于 ODBC 的 .NET 框架数据提供程序使用本机 ODBC 驱动程序管理器 (DM) 来支持借助于 COM 互操作性进行的数据访问。

  • 用于从SQL Server 2000 检索 XML 的托管提供程序。XML for SQL Server Web update 2(目前正处于测试阶段)包含一个托管提供程序,专门用于从 SQL Server 2000 中检索 XML。有关此更新的详细信息,请参阅 http://msdn.microsoft.com/library/default.asp?url=/nhp/default.asp?contentid=28001300

有关不同数据提供程序的详细概述,请参阅《.NET 框架开发人员指南》中的“.NET 框架数据提供程序”,网址为:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpconadonetproviders.asp

命名空间组织结构

与各个 .NET 数据提供程序相关联的类型(类、结构、枚举等)位于其各自的命名空间中:

  • System.Data.SqlClient。包含 SQL Server .NET 数据提供程序类型。

  • System.Data.OracleClient。包含 Oracle .NET 数据提供程序。

  • System.Data.OleDb。包含 OLE DB .NET 数据提供程序类型。

  • System.Data.Odbc。包含 ODBC .NET 数据提供程序类型。

  • System.Data。包含独立于提供程序的类型,如 DataSetDataTable

在各自的关联命名空间内,每个提供程序都提供了对 ConnectionCommandDataReaderDataAdapter 对象的实现。SqlClient 实现的前缀为“Sql”,而 OleDb 实现的前缀为“OleDb”。例如,Connection 对象的 SqlClient 实现是 SqlConnection,而 OleDb 实现则为 OleDbConnection。同样,DataAdapter 对象的两个实现分别为 SqlDataAdapterOleDbDataAdapter

在本指南中,所用示例取自 SQL Server 对象模型。尽管此处未加说明,Oracle/OLEDB 和 ODBC 中提供了类似的功能。

一般编程

如果您可能要面向不同的数据源,并且需要将您的代码从一个数据源移至另一个数据源,请考虑编程以支持 System.Data 命名空间中的 IDbConnectionIDbCommandIDataReaderIDbDataAdapter 接口。ConnectionCommandDataReaderDataAdapter 对象的所有实现都必须支持这些接口。

有关实现 .NET 数据提供程序的详细信息,请参阅 http://msdn.microsoft.com/library/en-us/cpguidnf/html/cpconimplementingnetdataprovider.asp

还应该注意,如果应用程序使用单对象模型来访问多个数据库,则 OLE DB 和 ODBC 桥接提供程序都可以使用。在此情况下,需要考虑与应用程序的性能需要相比,要求应用程序具有多大的灵活性,以及在何种程度上需要数据库特有的功能。

图 2 阐明了数据访问栈,并说明了 ADO.NET 与其他数据访问技术(包括 ADO 和 OLE DB)之间的关系。它还说明了 ADO.NET 模型内的两个托管提供程序和主要对象。

1.2.数据访问栈

有关 ADO 向 ADO.NET 的演变的详细信息,请参阅文章“Introducing ADO+:Data Access Services for the Microsoft .NET Framework”(MSDN Magazine,2000 年 11 月号),网址为:http://msdn.microsoft.com/msdnmag/issues/1100/adoplus/default.aspx

存储过程与直接 SQL

本文档中显示的大多数代码片段使用 SqlCommand 对象来调用存储过程,以执行数据库操作。在某些情况下,您将不会看到 SqlCommand 对象,因为存储过程名被直接传递给 SqlDataAdapter 对象。在内部,这仍然会导致创建 SqlCommand 对象。

您应该使用存储过程而不是嵌入的 SQL 语句,原因如下:

  • 存储过程通常可以改善性能,因为数据库能够优化存储过程使用的数据访问计划,并且能够缓存该计划以供将来重用。

  • 可以在数据库内分别设置各个存储过程的安全保护。客户端不必对基础表拥有访问权限,就可以获得执行存储过程的权限。

  • 存储过程可以简化维护工作,因为修改存储过程通常要比更改已部署组件中的硬编码 SQL 语句容易。

  • 存储过程为基础数据库架构增加了额外的抽象级别。存储过程的客户端与存储过程的实现细节是彼此隔离的,与基础架构也是彼此隔离的。

  • 存储过程可以减少网络流量,因为可以批量执行 SQL 语句,而不是从客户端发送多个请求。

SQL Server 联机文档强烈建议您不要使用“sp_”作为名称前缀来创建任何存储过程,因为此类名称已经被指定给系统存储过程。SQL Server 始终按以下顺序来查找以 sp_ 开头的存储过程:

  1. 在主数据库中查找存储过程。

  2. 基于所提供的任何限定符(数据库名或所有者)来查找存储过程。

  3. 使用 dbo 作为所有者来查找存储过程(如果未指定所有者)。

属性与构造函数参数

可以通过构造函数参数来设置 ADO.NET 对象的特定属性值,也可以直接设置属性值。例如,下面的代码片段在功能上是等效的。

// Use constructor arguments to configure command object
SqlCommand cmd = new SqlCommand( "SELECT * FROM PRODUCTS", conn );
// The above line is functionally equivalent to the following
// three lines which set properties explicitly
sqlCommand cmd = new SqlCommand();
cmd.Connection = conn;
cmd.CommandText = "SELECT * FROM PRODUCTS";

从性能角度看,这两种方法之间的差异是微不足道的,因为针对 .NET 对象设置和获取属性要比针对 COM 对象执行类似操作更为高效。

选择哪种方法取决于个人喜好和编码风格。不过,对属性进行明确设置确实能够使代码更易理解(尤其是当您不熟悉 ADO.NET 对象模型时)和调试。

过去,Microsoft Visual Basic? 开发系统的开发人员被告诫避免使用“Dim x As New…”构造来创建对象。在 COM 领域里,上述代码会导致 COM 对象创建过程出现短路现象,从而带来一些微小和重大的错误。然而,在 .NET 领域里,这不再是一个问题。

管理数据库连接

数据库连接代表一种关键的、昂贵的和有限的资源,尤其是在多层 Web 应用程序中。正确地管理连接是十分必要的,因为您采取的方法可能显著影响应用程序的总体可伸缩性。同时,还要认真考虑在何处存储连接字符串。需要使用可配置的且安全的位置。

在管理数据库连接和连接字符串时,应该努力做到:

  • 通过在多个客户端中多路复用数据库连接池,帮助实现应用程序的可伸缩性。

  • 采用可配置的、高性能的连接池策略。

  • 在访问 SQL?Server 时使用 Windows 身份验证。

  • 在中间层避免模拟。

  • 安全地存储连接字符串。

  • 尽量晚地打开数据库连接,尽量早地将其关闭。

本节讨论连接池,并且帮助您选择适当的连接池策略。本节还将考虑应该如何管理、存储和操纵数据库连接字符串。最后,本节将给出两种编码模式,可用来帮助确保连接被可靠地关闭,并被返回到连接池。

使用连接池

通过数据库连接池,应用程序可以重用池中现有的连接,而不必反复与数据库建立新的连接。该技术可显著提高应用程序的可伸缩性,因为有限数量的数据库连接可以为数量大得多的客户端提供服务。同时,由于可以节省建立新连接所需的大量时间,该技术还能够改善性能。

像 ODBC 和 OLE DB 这样的数据访问技术提供了多种形式的连接池,可以在不同程度上进行配置。这两种方法对于数据库客户端应用程序而言在很大程度上是透明的。OLE DB 连接池经常被称为会话或资源池。

有关 Microsoft Data Access Components (MDAC) 内部的池机制的一般性讨论,请参阅“Pooling in the Microsoft Data Access Components”,网址为 http://msdn.microsoft.com/library/en-us/dnmdac/html/pooling2.asp

ADO.NET 数据提供程序提供了透明的连接池,这些连接池的确切技术细节对于各个提供程序而言是不同的。本节针对下列提供程序来讨论连接池:

  • SQL Server .NET 数据提供程序

  • Oracle .NET 数据提供程序

  • OLE DB .NET 数据提供程序

  • ODBC .NET 数据提供程序

SQL Server .NET 数据提供程序的池机制

如果您使用的是 SQL Server .NET 数据提供程序,请使用该提供程序提供的连接池支持。这是一种由该提供程序在内部实现的支持事务处理并且非常高效的机制,它存在于托管代码中。池是以每个应用程序的域为基础创建的,并且在应用程序域卸载之前不会销毁。

可以透明地使用这种形式的连接池,但应该知道池的管理方式以及可用来微调连接池的各种配置选项。

在许多情况下,对于您的应用程序而言,SQL Server .NET 数据提供程序的默认连接池设置可能已经足够了。在开发和测试基于 .NET 的应用程序的过程中,建议您对规划通信模式进行模拟,以确定是否需要修改连接池大小。

需要生成可伸缩的高性能应用程序的开发人员应该最大限度地减少使用连接的时间,只在检索或更新数据时才使连接保持打开状态。连接关闭时,将被返回到连接池,并可供重用。在此情况下,到数据库的实际连接不会被切断;不过,如果连接池被禁用,则到数据库的实际连接将被关闭。

开发人员应该十分小心,不要依赖垃圾回收器来释放连接,因为当引用离开作用范围时,连接未必能够关闭。这是连接泄漏的一种常见根源,当请求新连接时,这会导致连接异常。

配置 SQL Server .NET 数据提供程序连接池

可以使用一组名称-值对(通过连接字符串提供)来配置连接池。例如,可以配置是否启用连接池(默认情况下启用)、池的最大容量和最小容量,以及要打开连接的排队请求可以阻塞的时间长度。下面是一个示例连接字符串,用于配置池的最大容量和最小容量。

"Server=(local); Integrated Security=SSPI; Database=Northwind; 
Max Pool Size=75; Min Pool Size=5"

打开连接并创建池以后,会将多个连接添加到池中,以便将连接数量提高到所配置的最小数量。随后,可以继续向该池中添加连接,直至达到所配置的最大池数量。当达到最大数量时,要打开连接的新请求将排队等待一段可配置的时间。

选择池大小

对于管理成千上万个客户端的并发请求的大规模系统而言,能够建立最大值阈值是非常重要的。您需要监控连接池和应用程序的性能,以便确定系统的最佳池大小。最佳大小还取决于用来运行 SQL Server 的硬件。

在部署过程中,您可能希望减小默认的最大池大小(目前为 100)以帮助查找连接泄漏。

如果您设立最小池大小,将会产生一点性能开销,因为在开始时对池进行填充以使其达到该尺寸,尽管头几个进行连接的客户端会因此受益。注意,创建新连接的过程是按顺序执行的,这意味着在最初填充池时,服务器不会被同时到来的大量请求所淹没。

有关监控连接池的详细信息,请参阅本文档中的监控连接池一节。有关连接池连接字符串关键字的完整列表,请参阅《.NET 框架开发人员指南》中的“Connection Pooling for the .NET Framework Data Provider for SQL Server”一节,网址为:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpconconnectionpoolingforsqlservernetdataprovider.asp

更多信息

当使用 SQL Server .NET 数据提供程序连接池时,请注意以下几个方面:

  • 连接是通过连接字符串上的完全匹配算法进行池化的。池机制甚至对名称-值对之间的空格也敏感。例如,下面的两个连接字符串将导致两个独立的池,因为第二个连接字符串包含额外的空格字符。

    SqlConnection conn = new SqlConnection(
             "Integrated Security=SSPI;Database=Northwind");
    conn.Open(); // Pool A is created
    SqlConmection conn = new SqlConnection(
             "Integrated Security=SSPI ; Database=Northwind");
    conn.Open(); // Pool B is created (extra spaces in string)
    
  • 连接池被划分为多个事务专有池和一个与当前尚未在事务中登记的连接对应的池。对于与特定事务上下文关联的线程,会返回相应池(该池包含在该事务中登记的连接)的连接。这就使得使用已登记的连接成为一个透明的过程。

OLE DB .NET 数据提供程序的池机制

OLE DB .NET 数据提供程序通过使用基础 OLE DB 资源池来池化连接。有多个用于配置资源池的选择:

  • 可以使用连接字符串来配置、启用或禁用资源池。

  • 可以使用注册表。

  • 可以用编程方式配置资源池。

为避免出现与注册表相关的部署问题,请不要使用注册表来配置 OLE DB 资源池。

有关 OLE DB 资源池的详细信息,请参阅《OLE DB 程序员参考》第 19 章“OLE DB Services”中的“Resource Pooling”,网址为:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/oledb/htm/olprcore_chapter19.asp

使用池对象管理连接池

作为 Windows DNA 开发人员,鼓励您禁用 OLE DB 资源池和/或 ODBC 连接池,并使用 COM+ 对象池作为池化数据库连接的技术。这有两个主要原因:

  • 池大小和阈值可以明确的配置(在 COM+ 目录中)。

  • 性能得到改善。池对象方法的性能比本机池高 50%。

然而,因为 SQL Server .NET 数据提供程序在内部使用池机制,所以您不再需要开发自己的对象池机制(在使用该提供程序时)。因此,您可以避免执行与手动事务登记相关联的复杂任务。

如果您使用的是 OLE DB .NET 数据提供程序,您可能需要考虑使用 COM+ 对象池,以便充分利用卓越的配置和改善的性能。如果您为此目的开发池对象,则必须禁用 OLE DB 资源池和自动事务登记(例如,通过在连接字符串中包含 "OLE DB Services=-4")。您必须在自己的池对象实现中处理事务登记。

监控连接池

要对应用程序使用连接池的情况进行监控,可以使用 SQL Server 随附的事件探查器工具,或者使用 Microsoft Windows? 2000 操作系统随附的性能监视器工具。

使用 SQL Server 事件探查器监控连接池

  1. 单击 Start,指向 Programs,指向 MicrosoftSQLServer,然后单击 Profiler 以启动事件探查器。

  2. File 菜单上,指向 New,然后单击 Trace

  3. 提供连接详细信息,然后单击 OK

  4. Trace Properties 对话框中,单击 Events 选项卡。

  5. Selected event classes 列表中,确保 Audit Login Audit Logout 事件显示在 Security Audit 下面。要使跟踪变得更为清晰,请从该列表中删除所有其他事件。

  6. 单击 Run 以启动跟踪。当连接建立时,您将看到 Audit Login 事件;当连接关闭时,您将看到 Audit Logout 事件。

使用性能监视器监控连接池

  1. 单击 Start,指向 Programs,指向 Administrative Tools,然后单击 Performance 以启动性能监视器。

  2. 右键单击图形背景,然后单击 AddCounters

  3. Performance object 下拉列表中,单击 SQL Server:General Statistics

  4. 在显示的列表中,单击 User Connections

  5. 单击 Add,然后单击 Close

管理安全性

尽管数据库连接池提高了应用程序的总体可伸缩性,但这意味着您不再能够在数据库级别管理安全性。这是因为,要支持连接池,连接字符串必须完全相同。如果您需要跟踪每个用户的数据库操作,请考虑添加一个参数,以便能够传递用户标识并在数据库中手动记录用户操作。您需要将该参数添加到每个操作中。

使用 Windows 身份验证

在连接到 SQL Server 时,应该使用 Windows 身份验证,因为它提供了许多好处:

  1. 安全性更易于管理,因为您使用单一 (Windows) 安全模型,而不是独立的 SQL Server 安全模型。

  2. 可避免将用户名和密码嵌入到连接字符串中。

  3. 不会以明文方式通过网络传递用户名和密码。

  4. 通过采用密码到期期限、最小长度以及在多次无效登录请求后锁定帐户,改善了登录安全性。

更多信息

在使用 Windows 身份验证来访问 SQL Server 时,请遵循以下指导原则:

  • 考虑性能折衷。性能测试已经表明,在使用 Windows 身份验证打开池数据库连接时,速度要比使用 SQL Server 身份验证慢。.NET 运行库 1.1 版已经降低了 SQL Server 安全性能超过 Windows 身份验证的幅度,但 SQL Server 身份验证仍然要快一些。

    然而,尽管 Windows 身份验证的开销仍然比较高,但与执行命令或存储过程所花的时间相比,性能上的降低相对而言是可以忽略的。因此,在大多数情况下,使用 Windows 身份验证在安全方面的好处胜过了性能略有下降所带来的坏处。在做决定之前,请对应用程序的性能要求进行评估。

  • 在中间层避免模拟。Windows 身份验证要求有 Windows 帐户以便进行数据库访问。尽管在中间层使用模拟似乎是合理的,但请避免这样做,因为这样会使连接池失效,并且对应用程序可伸缩性产生严重的影响。

    要解决该问题,请考虑模拟有限数量的 Windows 帐户(而不是已验证身份的用户),每个帐户都代表一个特定的角色。

    例如,您可以使用以下方法:

    1. 创建两个 Windows 帐户,一个用于读操作,一个用于写操作。(或者,您可能需要单独的帐户来镜像应用程序专有的角色。例如,您可能需要将一个帐户用于 Internet 用户,将另一个帐户用于内部操作员和/或管理员。)

    2. 将各个帐户映射到 SQL Server 数据库角色,并且为每个角色设立必要的数据库权限。

    3. 在数据访问层使用应用程序逻辑,确定在执行数据库操作之前要模拟的 Windows 帐户。

      每个帐户都必须是域帐户,并且 Internet 信息服务 (IIS) 和 SQL Server 位于同一域中或位于可信域中。或者,您可以在每台计算机上创建匹配帐户(具有相同的帐户名和密码)。

  • 对网络库使用TCP/IP。SQL Server 7.0 及更高版本为所有网络库提供 Windows 身份验证支持。使用 TCP/IP 可获得配置、性能和可伸缩性方面的好处。有关使用 TCP/IP 的详细信息,请参阅本文档中的通过防火墙连接一节。

有关开发安全的 ASP.NET 和 Web 应用程序的一般性指导,请参阅下面的 Microsoft patterns & practices 指南:

存储连接字符串

要存储数据库连接字符串,可以有多种选择,这些选择具有不同级别的灵活性和安全性。尽管在源代码中对连接字符串进行硬编码可提供最佳性能,但文件系统缓存可确保在外部将该字符串存储到文件系统中所带来的性能下降是微不足道的。几乎在所有情况下,人们都首选外部连接字符串所提供的额外的灵活性(它支持管理员配置)。

当您选择连接字符串存储方法时,需要注意的两个最重要的事项是安全性和配置简易性,然后紧跟着的是性能。

可以选择下列位置来存储数据库连接字符串:

  • 在应用程序配置文件中;例如,ASP.NET Web 应用程序的 Web.config

  • 在通用数据链接 (UDL) 文件中(仅由 OLE DB .NET 数据提供程序支持)

  • 在 Windows 注册表中

  • 在自定义文件中

  • 在 COM+ 目录中,方法是使用构建字符串(仅适用于服务组件)

通过使用 Windows 身份验证来访问 SQL Server,可以避免将用户名和密码存储在连接字符串中。如果您的安全要求需要采取更严格的措施,请考虑以加密格式存储连接字符串。

对于 ASP.NET Web 应用程序而言,在 Web.config 文件内以加密格式存储连接字符串,代表着一种安全的、可配置的解决方案。

可以在连接字符串中将 Persist Security Info 命名值设置为 false,以禁止通过 SqlConnectionOleDbConnection 对象的 ConnectionString 属性返回对安全敏感的细节(如密码)。

下面几小节讨论了如何使用各种选择来存储连接字符串,并介绍了各种方法的相对优点和缺点。这些内容有助于您根据自己特定的应用程序方案做出明智的选择。

通过“配置应用程序管理”块,可以管理从数据库连接到复杂层次结构数据的各种配置设置。有关详细信息,请参阅 http://msdn.microsoft.com/practices

使用 XML 应用程序配置文件

可以使用 <appSettings> 元素在应用程序配置文件的自定义设置节中存储数据库连接字符串。该元素支持任意的密钥-值对,如以下代码片段所示:

<configuration>
 <appSettings>
  <add key="DBConnStr"
     value="server=(local);Integrated Security=SSPI;database=northwind"/>
 </appSettings>
</configuration>

<appSettings> 元素出现在 <configuration> 元素下面,并且不是紧跟在 <system.web> 的后面。

优点

  • 易于部署。连接字符串是通过定期 .NET xcopy 部署与配置文件一起部署的。

  • 易于以编程方式访问。通过 ConfigurationSettings 类的 AppSettings 属性,可以在运行时方便地读取已配置的数据库连接字符串。

  • 支持动态更新(仅限于ASP.NET)。如果管理员在 Web.config 文件中更新连接字符串,当下一次访问该字符串时(对于无状态组件而言,这可能是客户端下一次使用该组件进行数据访问请求),所做更改将生效。

缺点

  • 安全性。尽管 ASP.NET Internet 服务器应用程序编程接口 (ISAPI) 动态链接库 (DLL) 禁止客户端直接访问带有 .config 文件扩展名的文件,并且可以使用 NTFS 权限进一步限制访问,您可能仍然希望避免以明文形式在前端 Web 服务器上存储这些详细信息。为获得额外的安全性,请以加密格式在配置文件中存储连接字符串。

更多信息

  • 可以使用 System.Configuration.ConfigurationSettings 类的静态 AppSettings 属性来检索自定义应用程序设置。以下代码片段对此进行了说明,该代码片段采用了前面例举的名为 DBConnStr 的自定义密钥:

    using System.Configuration;
    private string GetDBaseConnectionString()
    {
      return ConfigurationSettings.AppSettings["DBConnStr"];
    }
    
  • 有关配置 .NET 框架应用程序的详细信息,请参阅 http://msdn.microsoft.com/library/en-us/cpguidnf/html/cpconconfiguringnetframeworkapplications.asp

使用 UDL 文件

OLE DB .NET 数据提供程序在其连接字符串中支持通用数据链接 (UDL) 文件名。您可以通过使用 OleDbConnection 对象的构建参数来传递连接字符串,或者通过使用该对象的 ConnectionString 属性来设置连接字符串。

SQL Server .NET 数据提供程序在其连接字符串中不支持 UDL 文件。因此,只有在您使用 OLE DB .NET 数据提供程序时,才可使用该方法。

对于 OLE DB 提供程序,要通过连接字符串引用 UDL 文件,请使用“File Name=name.udl”。

优点

  • 标准方法。您可能已在使用 UDL 文件进行连接字符串管理。

缺点

  • 性能。每次打开连接时,都要读取和分析含有 UDL 的连接字符串。

  • 安全性。UDL 文件以纯文本形式存储。您可以通过使用 NTFS 文件权限来确保这些文件的安全,但这样做可能带来与 .config 文件相同的问题。

  • SqlClient 不支持 UDL 文件。SQL Server .NET 数据提供程序不支持该方法。该提供程序用于访问 SQL Server 7.0 及更高版本。

更多信息

  • 要支持管理,请确保管理员对 UDL 文件具有读/写访问权限,并且用于运行应用程序的身份具有读访问权限。对于 ASP.NET Web 应用程序,应用程序辅助进程在默认情况下通过使用 SYSTEM 帐户来运行,尽管您可以通过使用计算机全局配置文件 (Machine.config) 的 <processModel> 元素来覆盖这一配置。您还可以通过使用 Web.config 文件的 <identity> 元素进行模拟(可随意使用指定帐户)。

  • 对于 Web 应用程序,请确保不要将 UDL 文件放在虚拟目录中,否则会使该文件可通过 Web 下载。

  • 有关上述功能及其他与安全相关的 ASP.NET 功能的详细信息,请参阅“Authentication in ASP.NET:.NET Security Guidance”,网址为 http://msdn.microsoft.com/library/en-us/dnbda/html/authaspdotnet.asp

使用 Windows 注册表

您还可以在 Windows 注册表中使用自定义键来存储连接字符串,尽管由于部署问题不鼓励这样做。

优点

  • 安全性。可以通过使用访问控制列表 (ACL) 来管理对选定注册表键的访问。要获得更高级别的安全性,请考虑将数据加密。

  • 易于以编程方式访问。可以使用 .NET 类来支持从注册表中读取字符串。

缺点

部署。必须与应用程序一起部署相关注册表设置,这在一定程度上抵消了 xcopy 部署的优点。

使用自定义文件

可使用自定义文件来存储连接字符串。然而,该技术没有任何优点,建议不要使用该技术。

优点

  • 无。

缺点

  • 额外的编码。该方法需要完成额外的编码工作,并迫使您显式处理并发问题。

  • 部署。该文件必须与其他 ASP.NET 应用程序文件一起复制。应避免将该文件放在 ASP.NET 应用程序目录或子目录中,以防止它通过 Web 被下载。

使用构建参数和 COM+ 目录

可以在 COM+ 目录中存储数据库连接字符串,并通过对象构建字符串将其自动传递给您的对象。COM+ 将在实例化该对象后立即调用该对象的 Construct 方法,同时提供所配置的构建字符串。

该方法仅对服务组件有效。仅当您的托管组件使用其他服务(如分布式事务处理支持或对象池)时,才应考虑该方法。

优点

  • 管理。管理员可以通过使用“组件服务”MMC 管理单元方便地配置连接字符串。

缺点

  • 安全性。COM+ 目录被视为不安全的存储区域(尽管您可以通过 COM+ 角色来限制访问权限),因而不得用来以明文形式保存连接字符串。

  • 部署。COM+ 目录中的项必须与基于 .NET 的应用程序一起部署。如果您正在使用其他企业服务,如分布式事务处理或对象池,则在该目录中存储数据库连接字符串不会带来任何额外的部署开销,因为必须部署 COM+ 目录以支持那些其他服务。

  • 组件必须接受服务。您只能对服务组件使用构建字符串。不应简单地从 ServicedComponent 中派生组件的类(使您的组件接受服务)来启用构建字符串。

    重要说明:确保连接字符串的安全性至关重要。对于 SQL 身份验证,连接字符串包含用户名和密码。如果攻击者利用 Web 服务器上的源代码漏洞,并且获取了访问配置存储的权限,则数据库将容易受到攻击。要避免这一问题,应该将连接字符串加密。有关可用于加密明文连接字符串的不同方法的说明,请参阅“Improving Web Application Security:Threats and Countermeasures”(该文章将发布在 http://www.microsoft.com/practices)。

更多信息

连接使用模式

无论您使用哪种 .NET 数据提供程序,您都必须始终遵循下列原则:

  • 尽可能晚地打开数据库连接。

  • 以尽可能短的时间使用连接。

  • 尽可能早地关闭连接。在通过 CloseDispose 方法关闭连接之前,不会将连接返回到池中。即使您检测到连接已经进入断开状态,也应该关闭该连接。这可以确保将该连接返回到池中,并将其标记为无效连接。对象池程序定期扫描池,查找那些已被标记为无效的对象。

要保证在方法返回之前关闭连接,请考虑使用下面的两个代码示例中阐明的方法之一。第一个方法使用 finally 块。第二个方法使用 C# using 语句,它确保调用对象的 Dispose 方法。

以下代码确保 finally 块关闭连接。注意,该方法对 Visual Basic .NET 和 C# 都有效,因为 Visual Basic .NET 支持结构化异常处理。

public void DoSomeWork()
{
  SqlConnection conn = new SqlConnection(connectionString);
  SqlCommand cmd = new SqlCommand("CommandProc", conn );
  cmd.CommandType = CommandType.StoredProcedure;
  try
  {
    conn.Open();
    cmd.ExecuteNonQuery();
  }
  catch (Exception e)
  {
    // Handle and log error
  }
  finally
  {
    conn.Close();
  }
}
  

以下代码显示了一个备选方法,该方法使用了 C# using 语句。注意,Visual Basic .NET 不提供 using 语句或任何等效功能。

public void DoSomeWork()
{
  // using guarantees that Dispose is called on conn, which will
  // close the connection.
  using (SqlConnection conn = new SqlConnection(connectionString))
  {
    SqlCommand cmd = new SqlCommand("CommandProc", conn);
    fcmd.CommandType = CommandType.StoredProcedure;
    conn.Open();
    cmd.ExecuteQuery();
  }
}
  

您还可以将该方法应用于其他必须关闭才能使用当前连接执行其他任何任务的对象,例如 SqlDataReaderOleDbDataReader

错误处理

ADO.NET 错误是通过 .NET 框架所固有的基础结构化异常处理支持来产生和处理的。因此,在数据访问代码内部处理错误的方法与在应用程序的其他地方一样。可以通过标准的 .NET 异常处理语法和技术来检测和处理异常。

本节向您说明如何开发健壮的数据访问代码,并解释如何处理数据访问错误。同时,本节还提供了与 SQL Server .NET 数据提供程序相关的具体异常处理指导。

.NET 异常

.NET 数据提供程序将数据库特有的错误状况转换为标准异常类型,供您在自己的数据访问代码中进行处理。数据库特有的错误细节是通过相关异常对象的属性提供给您的。

所有 .NET 异常类型归根结底都是从 System 命名空间中的 Exception 类派生出来的。.NET 数据提供程序引发提供程序特有的异常类型。例如,每当 SQL Server 返回错误状况时,SQL Server .NET 数据提供程序都会引发 SqlException 对象。类似地,OLE DB .NET 数据提供程序引发 OleDbException 类型的异常,它含有基础 OLE DB 提供程序所公开的细节。

图 3 显示了 .NET 数据提供程序异常层次。注意,OleDbException 类是从 ExternalException(它是所有 COM Interop 异常的基类)派生的。该对象的 ErrorCode 属性存储了由 OLE DB 产生的 COM HRESULT。

1.3..NET 数据提供程序异常层次

捕获和处理 .NET 异常

要处理数据访问异常状况,请将数据访问代码放在 try 块内部,并使用 catch 块通过适当的筛选器来捕获产生的任何异常。例如,当使用 SQL Server .NET 数据提供程序来编写数据访问代码时,应该捕获 SqlException 类型的异常,如以下代码所示:

try
{
  // Data access code
}
catch (SqlException sqlex) // more specific
{
}
catch (Exception ex) // less specific
{
}
  

如果您提供多个具有不同筛选条件的 catch 语句,请记住将这些语句按其类型的特殊程度从高到低排序。这样,对于任何给定的异常类型,都会执行最特殊类型的 catch 块。

SqlException 类公开了含有异常状况细节的属性。这些属性包括:

  • Message 属性,它包含描述错误的文本。

  • Number 属性,它包含唯一标识错误类型的错误号。

  • State 属性,它包含有关错误的调用状态的附加信息。它通常用于指示特定错误状况的具体出现位置。例如,如果单个存储过程能够在多行产生同一错误,则应该使用状态来标识错误的具体出现位置。

  • Errors 集合,它包含有关 SQL Server 产生的错误的详细错误信息。Errors 集合将始终至少包含一个 SqlError 类型的对象。

以下代码片段阐明了如何使用 SQL Server .NET 数据提供程序来处理 SQL Server 错误状况:

using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
// Method exposed by a Data Access Layer (DAL) Component
public string GetProductName( int ProductID )
{
  SqlConnection conn = null;
  // Enclose all data access code within a try block
  try
  {
    conn = new SqlConnection(
        "server=(local);Integrated Security=SSPI;database=northwind");
    conn.Open();
    SqlCommand cmd = new SqlCommand("LookupProductName", conn );
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.Parameters.Add("@ProductID", ProductID );
    SqlParameter paramPN = 
         cmd.Parameters.Add("@ProductName", SqlDbType.VarChar, 40 );
    paramPN.Direction = ParameterDirection.Output;
    cmd.ExecuteNonQuery();
    // The finally code is executed before the method returns
    return paramPN.Value.ToString();  
  }
  catch (SqlException sqlex)
  {
    // Handle data access exception condition
    // Log specific exception details
    LogException(sqlex);
    // Wrap the current exception in a more relevant
    // outer exception and re-throw the new exception
    throw new DALException(
                  "Unknown ProductID: " + ProductID.ToString(), sqlex );
  }
  catch (Exception ex)
  {
    // Handle generic exception condition . . .
    throw ex;
  }
  finally
  {
    if(conn != null) conn.Close(); // Ensures connection is closed
  }
}
// Helper routine that logs SqlException details to the 
// Application event log
private void LogException( SqlException sqlex )
{
  EventLog el = new EventLog();
  el.Source = "CustomAppLog";
  string strMessage;
  strMessage = "Exception Number : " + sqlex.Number + 
               "(" + sqlex.Message + ") has occurred";
  el.WriteEntry( strMessage );
  foreach (SqlError sqle in sqlex.Errors)
  {
    strMessage = "Message: " + sqle.Message +
                 " Number: " + sqle.Number +
                 " Procedure: " + sqle.Procedure +
                 " Server: " + sqle.Server +
                 " Source: " + sqle.Source +
                 " State: " + sqle.State +
                 " Severity: " + sqle.Class +
                 " LineNumber: " + sqle.LineNumber;
    el.WriteEntry( strMessage );
  }
}

SqlException catch 块内部,代码首先使用 LogException 辅助函数记录异常细节。该函数使用 foreach 语句来枚举 Errors 集合内部的特定于提供程序的细节,并将错误细节记录到错误日志中。catch 块中的代码随后将 SQL Server 特有的异常包装在 DALException 类型的异常内部,后者对于 GetProductName 方法的调用方更有意义。异常处理程序使用 throw 关键字将该异常传播回调用方。

更多信息

从存储过程中产生错误

Transact-SQL (T-SQL) 提供了一个 RAISERROR函数(请注意拼写),可用来产生自定义错误并将其返回到客户端。对于 ADO.NET 客户端,SQL Server .NET 数据提供程序会截获这些数据库错误,并将其转换为 SqlError 对象。

使用 RAISERROR 函数的最简单方法是将消息正文包含为第一个参数,然后指定严重度和状态参数,如以下代码片段所示。

RAISERROR( 'Unknown Product ID: %s', 16, 1, @ProductID )

在该示例中,使用了一个替代参数将当前产品 ID 作为错误消息正文的一部分返回。第二个参数是消息严重度,第三个参数是消息状态。

更多信息

  • 要避免对消息正文进行硬编码,可以使用 sp_addmessage 系统存储过程或使用 SQL Server 企业管理器,将您自己的消息添加到 sysmessages 表中。然后,您可以使用传递给 RAISERROR 函数的 ID 引用该消息。您定义的消息 ID 必须大于 50,000,如以下代码片段所示。

    RAISERROR( 50001, 16, 1, @ProductID )
    
  • 有关 RAISERROR 函数的详细信息,请在 SQL Server 联机图书索引中查找 RAISERROR。

适当地使用严重度级别

请认真选择错误严重度级别,并注意各个级别的影响。错误严重度级别的范围为 0 到 25,用于表示 SQL Server 2000 已经遇到的问题类型。在客户端代码中,可以通过检查 SqlError 对象的 Class 属性获得错误的严重度,该对象位于 SqlException 类的 Errors 集合中。表 1 指出了各种严重度级别的影响和含义。

1. 错误严重度级别 — 影响和含义

严重度级别

是否关闭连接

产生 SqlException

含义

10 及更低

指示性消息,不一定代表错误状况。

11 - 16

可由用户纠正的错误,例如,通过修正的输入数据重试操作。

17 - 19

资源或系统错误。

20 - 25

致命系统错误(包括硬件错误)。客户端的连接被终止。

控制自动事务处理

对于所遇到的任何严重度超过 10 的错误,SQL Server .NET 数据提供程序都会引发 SqlException。当自动 (COM+) 事务处理中的某个组件检测到 SqlException 时,该组件必须确保它赞成中止该事务处理。这可能是也可能不是自动过程,具体取决于是否用 AutoComplete 属性对该方法进行了标记。

有关在自动事务处理的上下文中处理 SqlException 的详细信息,请参阅本文档中的确定事务处理结果一节。

检索指示性消息

严重度级别 10 及更低级别用于表示指示性消息,并且不会导致引发 SqlException

检索指示性消息:

  • 创建一个事件处理程序,并预订由 SqlConnection 对象公开的 InfoMessage 事件。该事件的委托如以下代码片段中所示。

    public delegate void SqlInfoMessageEventHandler( object sender, 
                                                         SqlInfoMessageEventArgs e );
    

消息数据可通过传递给事件处理程序的 SqlInfoMessageEventArgs 对象获得。该对象公开了一个 Errors 属性,该属性包含一组 SqlError 对象 — 每条指示性消息对应一个对象。以下代码片段阐明了如何注册一个用于记录指示性消息的事件处理程序。

public string GetProductName( int ProductID )
{
  SqlConnection conn = null;
  try
  {
    conn = new SqlConnection(
        "server=(local);Integrated Security=SSPI;database=northwind");
    // Register a message event handler
    conn.InfoMessage += new SqlInfoMessageEventHandler( MessageEventHandler );
    conn.Open();
    // Setup command object and execute it
    . . .
  }
  catch (SqlException sqlex)
  {
    // log and handle exception
    . . .
  }
  finally
  {
    if(conn != null) conn.Close();
  }
}
// message event handler
void MessageEventHandler( object sender, SqlInfoMessageEventArgs e )
{
  foreach( SqlError sqle in e.Errors )
  {
    // Log SqlError properties
    . . .
  }
}

性能

本节介绍一些常见的数据访问方案,对于每种方案,都提供了就 ADO.NET 数据访问代码而言性能最高、可伸缩性最强的解决方案的有关详细信息。在适当的地方,对性能、功能和开发工作进行了比较。本节考虑了下列功能方案:

  • 检索结果集并对检索到的行进行迭代处理。

  • 检索具有指定主键的单个行。

  • 从指定行检索单个项。

  • 检查是否存在具有特定主键的行。这是单项查找方案的变种,此时返回一个简单的 Boolean 类型值就足够了。

检索多行

在该方案中,您希望检索一组表格形式的数据,并对检索到的行进行迭代以执行操作。例如,您可能希望检索一组数据,以不连续的方式处理这些数据,并将其作为 XML 文档传递给客户端应用程序(可能是通过 Web 服务传递)。或者,您还可能希望以 HTML 表的形式显示这些数据。

要帮助确定最适当的数据访问方法,请考虑您是需要(不连续的)DataSet 对象所提供的额外的灵活性,还是需要 SqlDataReader 对象(它非常适合于企业对用户 (B2C) Web 应用程序中的数据展示)所提供的原始性能。图 4 显示了这两种基本方案。

用于在内部填充 DataSetSqlDataAdapter 使用 SqlDataReader 来访问数据。

1.4.多行数据访问方案

比较几种选择

当从数据源中检索多个行时,您具有下列选择:

  • 使用 SqlDataAdapter 对象生成 DataSetDataTable

  • 使用 SqlDataReader 提供只读的、只进的数据流。

  • 使用 XmlReader 提供 XML 数据的只读、只进数据流。

选择 SqlDataReader 还是 DataSet/DataTable,从根本上说是一个注重性能还是注重功能的问题。SqlDataReader 可提供最佳性能;DataSet 可提供额外的功能和灵活性。

数据绑定

上述所有三个对象都可以充当数据绑定控件的数据源,尽管 DataSetDataTable 可以充当比 SqlDataReader 更多种控件的数据源。这是因为 DataSetDataTable 实现了 IListSource(产生 IList),而 SqlDataReader 则实现了 IEnumerable。一些支持数据绑定的 WinForm 控件需要实现了 IList 的数据源。

这一差异的原因在于为其设计各种对象类型的方案的类型。DataSet(它包含 DataTable)是一种丰富的、不连续的结构,同时适用于 Web 和桌面 (WinForm) 这两种方案。另一方面,数据读取器对需要执行优化的只进数据访问的 Web 应用程序进行了优化。

请检查您希望绑定到的特定控件类型的数据源要求。

在应用程序层之间传递数据

DataSet 为可以根据需要作为 XML 进行操作的数据提供了关系视图,并且使您可以在应用程序层和组件之间传递不连续的缓存数据副本。然而,SqlDataReader 提供了最佳性能,因为它避免了与 DataSet 的创建关联的性能和内存开销。请记住,DataSet 对象的创建可导致多个子对象(包括 DataTableDataRowDataColumn 对象)以及用作这些子对象的容器的集合对象的创建。

使用数据集

在下列情况下,请使用由 SqlDataAdapter 对象填充的 DataSet

  • 您需要不连续的内存驻留型数据缓存,以便可以将其传递给应用程序内的其他组件或层。

  • 您需要数据在内存中的关系视图,以便进行 XML 或非 XML 操作。

  • 您要处理从多个数据源(如多个数据库、表或文件)中检索到的数据。

  • 您希望更新检索到的全部或部分行,并且使用 SqlDataAdapter 的批量更新功能。

  • 您希望对其执行数据绑定的控件需要支持 IList 的数据源。

    有关详细信息,请参阅 MSDN 网站上的“Designing Data Tier Components and Passing Data Through Tiers”,网址为 http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/BOAGag.asp

更多信息

如果您使用 SqlDataAdapter 来生成 DataSetDataTable,请注意下列事项:

  • 不需要显式打开或关闭数据库连接。SqlDataAdapterFill 方法可打开数据库连接,然后在其返回之前关闭该连接。如果连接已经打开,Fill 将使该连接保持打开。

  • 如果您需要将该连接用于其他目的,请考虑在调用 Fill 方法之前打开它。这样,您可以避免不必要的打开/关闭操作,从而提高性能。

  • 尽管您可以反复使用同一 SqlCommand 对象来多次执行同一命令,请不要重用同一 SqlCommand 对象来执行不同的命令。

  • 有关说明如何使用 SqlDataAdapter 来填充 DataSetDataTable 的代码示例,请参阅附录中的如何使用 SqlDataAdapter 来检索多个行。

使用 SqlDataReader

在下列情况下,请使用通过调用 SqlCommand 对象的 ExecuteReader 方法得到的 SqlDataReader

  • 您要处理大量的数据 — 多得难以在单个缓存中进行维护。

  • 您希望减少应用程序的内存使用量。

  • 您希望避免与 DataSet 相关联的对象创建开销。

  • 您希望用对实现了 IEnumerable 的数据源提供支持的控件执行数据绑定。

  • 您希望简化和优化数据访问。

  • 您要读取的行包含二进制大对象 (BLOB) 列。您可以使用 SqlDataReader 在可管理的块区中将 BLOB 数据从数据库中提取出来,而不是一次将其全部提取出来。有关处理 BLOB 数据的详细信息,请参阅本文档中的处理 BLOB 一节。

更多信息

如果您使用 SqlDataReader,请注意以下事项:

  • 当数据读取器处于活动状态时,到数据库的基础连接将保持打开状态,并且无法用于任何其他目的。尽可能早地调用 SqlDataReader 上的 Close

  • 每个连接只能有一个数据读取器。

  • 您可以在使用完数据读取器后显式关闭连接,或者通过将 CommandBehavior.CloseConnection 枚举值传递给 ExecuteReader 方法,将连接的生存期与 SqlDataReader 对象联系起来。这表示在关闭 SqlDataReader 后,应该关闭连接。

  • 在使用读取器访问数据时,如果知道列的基础数据类型,应使用类型化的访问器方法(如 GetInt32GetString),这是因为它们可减少读取列数据时需要执行的类型转换的数量。

  • 要避免将不需要的数据从服务器提取到客户端,如果您希望关闭读取器并丢弃其余任何结果,应在调用读取器上的 Close 之前,调用命令对象的 Cancel 方法。Cancel 确保在服务器上丢弃结果,而不会将结果无谓地提取到客户端上。否则,调用数据读取器上的 Close 会导致读取器无谓地提取其余结果以清空数据流。

  • 如果您希望获得输出或者从存储过程返回的返回值,并且您使用的是 SqlCommand 对象的 ExecuteReader 方法,则必须在输出和返回值可用之前调用读取器上的 Close 方法。

  • 有关说明如何使用 SqlDataReader 的代码示例,请参阅附录中的如何使用 SqlDataReader 来检索多个行。

使用 XmlReader

在以下情况下,请使用通过调用 SqlCommand 对象的 ExecuteXmlReader 方法得到的 XmlReader

  • 您希望将检索到的数据作为 XML 处理,但您不希望创建 DataSet 带来性能开销,并且不需要不连续的数据缓存。

  • 您希望利用 SQL Server 2000 FOR XML 子句的功能,该子句可用来以灵活的方式从数据库中检索 XML 片段(即不带根元素的 XML 文档)。例如,使用该方法可以指定准确的元素名称,还可以指定是否应该使用以元素或属性为中心的架构,是否应该通过 XML 数据返回架构,等等。

更多信息

如果您使用 XmlReader,请注意以下事项:

  • 在您从 XmlReader 中读取数据时,连接必须保持打开。SqlCommand 对象的 ExecuteXmlReader 方法目前不支持 CommandBehavior.CloseConnection 枚举值,因此您在使用完读取器后必须显式关闭连接。

  • 有关说明如何使用 XmlReader 的代码示例,请参阅附录中的如何使用 XmlReader 来检索多个行。

检索单个行

在该方案中,您希望从数据源中检索包含指定列集的单行数据。例如,您有一个客户 ID 并希望查找相关的客户详细信息,或者您有一个产品 ID 并希望检索产品信息。

比较几种选择

如果您希望使用从数据源中检索到的单行执行数据绑定,可以使用 SqlDataAdapter,以与前面讨论的“多行检索和迭代”方案中介绍的相同方式来填充 DataSetDataTable。然而,除非您特别需要 DataSet/DataTable 功能,否则应该避免创建这些对象。

如果您需要检索单个行,请使用下列选择之一:

  • 使用存储过程输出参数。

  • 使用 SqlDataReader 对象。

这两个选择都可以避免由于在服务器上创建结果集和在客户端上创建 DataSet 而带来的不必要的开销。各个方法的相对性能取决于压力级别以及是否启用了数据库连接池。当启用了数据库连接池时,性能测试表明,存储过程方法在高压力条件(同时存在 200 个以上的连接)下的性能比 SqlDataReader 方法高将近 30%。

使用存储过程输出参数

当您希望从已经启用连接池的多层 Web 应用程序中检索单个行时,请使用存储过程输出参数。

更多信息

有关说明如何使用存储过程输出参数的代码示例,请参阅附录中的如何使用存储过程输出参数来检索单个行。

使用 SqlDataReader

在以下情况下,请使用 SqlDataReader

  • 除了数据值以外,您还需要元数据。您可以使用数据读取器的 GetSchemaTable 方法获得列元数据。

  • 您未使用连接池。如果禁用了连接池,SqlDataReader 在所有压力条件下都是一个不错的选择;性能测试已经表明,在存在 200 个浏览器连接的条件下,它的性能比存储过程方法高 20% 左右。

更多信息

如果您使用 SqlDataReader,请注意以下事项:

  • 如果您知道查询仅返回单个行,请在调用 SqlCommand 对象的 ExecuteReader 方法时使用 CommandBehavior.SingleRow 枚举值。一些提供程序(如 OLE DB .NET 数据提供程序)使用这一提示来优化性能。例如,该提供程序通过使用 IRow 接口(如果该接口可用)而不是开销更大的 IRowset 来执行绑定。该方法对 SQL Server .NET 数据提供程序没有影响。

  • 如果您的 SQL Server 命令包含输出参数或返回值,在关闭 DataReader 之前它们将不可用。

  • 在使用 SqlDataReader 对象时,请始终通过 SqlDataReader 对象的类型化访问器方法(例如 GetStringGetDecimal)来检索输出参数。这可避免不必要的类型转换。

  • .NET 框架 1.1 版包含一个名为 HasRows 的附加 DataReader 属性,使用该属性,可以在从 DataReader 中读取之前确定它是否已经返回结果。

  • 有关说明如何使用 SqlDataReader 对象检索单个行的代码示例,请参阅附录中的如何使用 SqlDataReader 来检索单个行。

检索单个项

在该方案中,您希望检索单个数据项。例如,您可能希望查找单个产品名(给定了产品 ID)或单个客户信用等级(给定了客户名称)。在此类方案中,您通常不希望在检索单个项时产生 DataSet 的开销,甚至不希望产生 DataTable 的开销。

您可能还希望简单地检查一下数据库中是否存在特定行。例如,当新用户在网站上注册时,您需要检查选择的用户名是否已存在。这是单项查找的特殊情况,但在此情况下,简单的 Boolean 类型返回值就足够了。

比较几种选择

当您从数据源中检索单个数据项时,请考虑下列选择:

  • 通过存储过程使用 SqlCommand 对象的 ExecuteScalar 方法。

  • 使用存储过程输出或返回参数。

  • 使用 SqlDataReader 对象。

ExecuteScalar 方法直接返回数据项,因为它专用于只返回单个值的查询。它所需要的代码比存储过程输出参数和 SqlDataReader 方法需要的代码都要少。

从性能角度而言,应该使用存储过程输出或返回参数,因为测试表明,存储过程方法在低压力条件和高压力条件下(从同时存在的浏览器连接少于 100 个到同时存在 200 个浏览器连接)都能够提供一致的性能。

更多信息

在检索单个项时,请注意以下事项:

  • 如果查询通常返回多个列和/或行,则通过 ExecuteQuery 执行该查询将仅返回第一行的第一列。

  • 有关说明如何使用 ExecuteScalar 的代码示例,请参阅附录中的如何使用 ExecuteScalar 来检索单个项。

  • 有关说明如何使用存储过程输出或返回参数来检索单个项的代码示例,请参阅附录中的如何使用存储过程输出或返回参数来检索单个项。

  • 有关说明如何使用 SqlDataReader 对象检索单个项的代码示例,请参阅附录中的如何使用 SqlDataReader 来检索单个项。

通过防火墙进行连接

您将经常需要配置 Internet 应用程序以通过防火墙连接到 SQL Server。例如,许多 Web 应用程序及其防火墙的关键体系结构组件是外围网络(也称为 DMZ,即非管制区),它用于将前端 Web 服务器与内部网络隔离。

通过防火墙连接到 SQL Server 时,要求对防火墙、客户端和服务器进行特殊的配置。SQL Server 提供了客户端网络实用工具和服务器网络实用工具程序来帮助进行配置。

选择网络库

在通过防火墙进行连接时,可使用 SQL Server TCP/IP 网络库来简化配置。这是 SQL Server 2000 安装的默认配置。如果您使用的是较低版本的 SQL Server,请确保已经分别通过使用客户端网络实用工具和服务器网络实用工具,在客户端和服务器上将 TCP/IP 配置为默认网络库。

除了配置方面的好处以外,使用 TCP/IP 库结果还意味着您可以:

  • 由于在处理大量数据时的性能改善以及可伸缩性得到提高而受益。

  • 避免其他与命名管道关联的安全问题。

您必须将客户端计算机和服务器计算机配置为使用 TCP/IP。因为大多数防火墙都对允许通信的端口集进行限制,所以您还必须对 SQL Server 使用的端口号给予认真的考虑。

配置服务器

SQL Server 的默认实例在端口 1433 上侦听。同时,还使用了 UDP 端口 1434,以使 SQL 客户端能够定位其网络中的其他 SQL 服务器。然而,SQL?Server?2000 的命名实例在首次启动时动态分配端口号。网络管理员不希望在防火墙上打开一系列端口号;因此,在使用带防火墙的 SQL Server 命名实例时,请使用服务器网络实用工具来配置该实例,以使其在特定端口号上侦听。然后,您的管理员可以配置防火墙,以允许通信到达该服务器实例正在侦听的特定 IP 地址和端口号。

客户端网络库使用的源端口是在范围 1024 - 5000 中动态分配的。对于 TCP/IP 客户端应用程序,这是标准做法,但这意味着您的防火墙必须允许来自该范围内任意端口的通信。有关 SQL Server 使用的端口的详细信息,请参阅 Microsoft 知识库文章 287932“INF:TCP Ports Needed for Communication to SQL Server Through a Firewall”。

动态发现命名实例

如果您更改 SQL Server 所侦听的默认端口号,请配置客户端以连接到此端口。有关详细信息,请参阅本文档中的配置客户端一节。

在为 SQL Server 2000 的默认实例更改端口号后,如果未能修改客户端,将导致连接错误。如果有多个 SQL Server 实例,最新版本的 MDAC 数据访问栈 (2.6) 将使用动态发现,并且使用用户数据文报协议 (UDP) 协商(通过 UDP 端口 1434)来查找命名实例。尽管这可能在开发环境中有效,但却不太可能在实际应用环境中起作用,因为防火墙通常会阻塞 UDP 协商通信。

要避免这一问题,请始终配置客户端以连接到已配置的目标端口号。

配置客户端

应该配置客户端以使用 TCP/IP 网络库连接到 SQL?Server,并且应确保客户端库使用正确的目标端口号。

使用 TCP/IP 网络库

您可以使用 SQL Server 客户端网络实用工具来配置客户端。在某些安装中,客户端(例如,您的 Web 服务器)上可能尚未安装该实用工具。在此情况下,您可以执行下列操作之一:

  • 通过使用由连接字符串提供的名称-值对“Network Library=dbmssocn”指定网络库。字符串 "dbmssocn" 用于标识 TCP/IP(套接字)库。

    在使用 SQL Server .NET 数据提供程序时,网络库设置在默认情况下使用 "dbmssocn"。

  • 修改客户端计算机上的注册表,以便将 TCP/IP 设置为默认库。有关配置 SQL Server 网络库的详细信息,请参阅 HOWTO:Change SQL Server Default Network Library Without Using Using Client Network Utility (Q250550)

指定端口

如果您的 SQL Server 实例被配置为在除默认的 1433 以外的端口上侦听,可以通过以下方法指定要连接到端口号:

  • 使用客户端网络实用工具。

  • 指定端口号,并将“Server”或“Data Source”名称-值对提供给连接字符串。使用具有以下格式的字符串:

    "Data Source=ServerName, PortNumber"
    

    ServerName 可能是 IP 地址或域名系统 (DNS) 名称。要获得最佳性能,请使用 IP 地址以避免执行 DNS 查找。

分布式事务处理

如果您已经开发了使用 COM+ 分布式事务处理以及 Microsoft 分布式事务处理协调器 (DTC) 的服务的服务组件,您可能还需要配置您的防火墙以允许 DTC 通信在独立的 DTC 实例之间流动,以及在 DTC 和资源管理器(如 SQL Server)之间流动。

有关为 DTC 打开端口的详细信息,请参阅 INFO:Configuring Microsoft Distributed Transaction Coordinator (DTC) to Work Through a Firewall

处理 BLOB

现在,许多应用程序除了处理较为传统的字符和数值数据以外,还需要处理诸如图形和声音之类的数据格式,甚至需要处理更为复杂的数据格式,如视频。存在许多不同类型的图形、声音和视频格式。不过,从存储角度而言,它们都可以视为二进制数据块,通常称为二进制大对象,即 BLOB。

SQL Server 提供了 binaryvarbinaryimage 数据类型来存储 BLOB。尽管具有现在的名称,BLOB 数据还可以指基于文本的数据。例如,您可能希望存储可以与特定行关联的任意长的注释字段。为此,SQL Server 提供了 ntexttext 数据类型。

通常,对于小于 8 KB 的二进制数据,请使用 varbinary 数据类型。对于超过此大小的二进制数据,请使用 image。表 2 介绍了每个数据类型的主要功能。

2. 数据类型功能

数据类型

大小

说明

binary

从 1 到 8,000 字节。存储大小为指定长度加 4 字节。

固定长度二进制数据

varbinary

从 1 到 8,000 字节。存储大小为所提供数据的实际长度加 4 字节。

变长二进制数据

image

大小介于 0 到 2 GB 之间的变长二进制数据。

大型、变长二进制数据

text

大小介于 0 到 2 GB 之间的变长数据。

字符数据

ntext

大小介于 0 到 2 GB 之间的变长数据。

Unicode 字符数据

注 Microsoft? SQL Server 2000 能够在数据行中存储小型到中等大小的 textntextimage 值。该功能对于具有以下特征的表最为适用:表的 textntextimage 列中的数据通常作为一个单元读/写,并且大多数引用该表的语句都使用 textntextimage 数据。有关详细信息,请参阅 SQL Server 联机图书中的“text in row”主题。

将 BLOB 数据存储在何处

SQL Server 7.0 及更高版本已经改善了使用数据库中存储的 BLOB 数据时的性能。原因之一是数据库页大小已经增加到 8 KB。因此,小于 8 KB 的文本或图像数据不再需要存储在单独的二元树页结构中,而可以存储在一行中。这意味着读/写 textntextimage 数据可以像读/写字符和二进制字符串一样快。当数据大于 8 KB 时,将在行内维护一个指针,而将数据本身保存在单独数据页的二元树结构中 — 从而不可避免地影响性能。

有关将 textntextimage 数据强行存储在一行中的详细信息,请参阅 SQL Server 联机图书中的“Using Text and Image Data”主题。

处理 BLOB 数据的常用备选方法是将 BLOB 数据存储在文件系统中,并且在数据库列中存储一个指针(最好是统一资源定位符 [URL] 链接)以引用相应的文件。对于低于 SQL?Server 7.0 的版本而言,在数据库外部将 BLOB 数据存储到文件系统中可以改善性能。

然而,由于在 SQL Server 2000 中改善了 BLOB 支持,再加上 ADO.NET 对读/写 BLOB 数据的支持,因此在数据库中存储 BLOB 数据成为一种可行的方法。

在数据库中存储 BLOB 数据的优点

在数据库中存储 BLOB 数据可提供许多优点:

  • 更易于使 BLOB 数据与行中的其余项保持同步。

  • BLOB 数据通过数据库进行备份。拥有单个存储系统可以简化管理。

  • 可以通过 SQL Server 2000 中的 XML 支持访问 BLOB 数据,从而在 XML 流中返回 Base64 编码形式的数据。

  • 对于包含固定长度或可变长度字符(包含 Unicode)数据的列,可以执行 SQL Server 全文检索 (FTS) 操作。还可以针对 image 字段中包含的基于带格式文本的数据(例如,Microsoft Word 或 Microsoft Excel 文档)执行 FTS 操作。

在数据库中存储 BLOB 数据的缺点

请认真考虑哪些资源存储在文件系统中可能比存储在数据库中更好。通常通过 HTTP HREF 引用的图像就是很好的例子。这是因为:

  • 从数据库中检索图像会导致比使用文件系统更大的开销。

  • 数据库 SAN 上的磁盘存储通常比 Web 服务器场中使用的磁盘上的存储更为昂贵。

    通过精心设计元数据策略,可以消除在数据库中存储图像、电影甚至 Microsoft Office 文档之类资源的需要。元数据可以编入索引,并可以包含指向文件系统中存储的资源的指针。

将 BLOB 数据写入数据库

以下代码说明了如何使用 ADO.NET 将从文件中得到的二进制数据写入 SQL Server 中的 image 字段。

public void StorePicture( string filename )
{
  // Read the file into a byte array
  using(FileStream fs = new FileStream(filename, FileMode.Open, FileAccess.Read))
  {
    byte[] imageData = new Byte[fs.Length];
    fs.Read( imageData, 0, (int)fs.Length );
  }
  using( SqlConnection conn = new SqlConnection(connectionString) )
  {
    SqlCommand cmd = new SqlCommand("StorePicture", conn);
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.Parameters.Add("@filename", filename );
    cmd.Parameters["@filename"].Direction = ParameterDirection.Input;
    cmd.Parameters.Add("@blobdata", SqlDbType.Image);
    cmd.Parameters["@blobdata"].Direction = ParameterDirection.Input;
    // Store the byte array within the image field
    cmd.Parameters["@blobdata"].Value = imageData;
    conn.Open();
    cmd.ExecuteNonQuery();
  }
}

从数据库中读取 BLOB 数据

在通过 ExecuteReader 方法创建 SqlDataReader 对象以读取包含 BLOB 数据的行时,请使用 CommandBehavior.SequentialAccess 枚举值。如果不使用该枚举值,读取器会每次一行地将数据从服务器提取到客户端。如果行中包含 BLOB 列,可能需要占用大量内存。通过使用枚举值,可以进行更精确的控制,因为仅在 BLOB 数据被引用(例如,通过 GetBytes 方法,该方法可用于控制所读取的字节数)时才会进行提取。以下代码片段对此进行了说明。

// Assume previously established command and connection
// The command SELECTs the IMAGE column from the table
conn.Open();
using(SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess))
{
  reader.Read();
  // Get size of image dataa€“pass null as the byte array parameter
  long bytesize = reader.GetBytes(0, 0, null, 0, 0);
  // Allocate byte array to hold image data
  byte[] imageData = new byte[bytesize];
  long bytesread = 0;
  int curpos = 0;
  while (bytesread < bytesize)
  {
    // chunkSize is an arbitrary application defined value 
    bytesread += reader.GetBytes(0, curpos, imageData, curpos, chunkSize);
    curpos += chunkSize;
  }
}
// byte array 'imageData' now contains BLOB from database

使用 CommandBehavior.SequentialAccess 时,要求按照严格的顺序次序访问列数据。例如,如果 BLOB 数据位于第 3 列,并且您还需要第 1 列和第 2 列中的数据,则必须在读取第 3 列之前读取第 1 列和第 2 列。

通过数据集执行数据库更新

在引入 ADO.NET 以后,执行数据库更新的体系结构已经显著改变。ADO.NET 的目标是使得开发能够适应大型数据库和大量客户端的多层应用程序变得更加容易。这已经产生了一些重要的结果,尤其是:

  • ADO.NET 应用程序通常将客户端上的应用程序逻辑与中间层和数据库层上的业务和数据完整性计算分开。实际上,这意味着典型的应用程序将具有较多的批处理或事务处理性质,而在客户端应用程序和数据库之间具有较少的(但较大的)交互。

  • ADO.NET 应用程序对于究竟如何 处理更新具有更多的控制(与 ADO 及其前身比较)。

  • ADO.NET 允许应用程序通过存储在后端数据库中的存储过程来传播更改,而不是直接操纵数据库表中的行。这是推荐的实施策略。

更新使用模式

使用 ADO.NET 从 DataSet 中更新数据的过程可以按如下方式加以概述:

  1. 创建一个 DataAdapter 对象,并且使用数据库查询的结果填充 DataSet 对象。数据将在本地缓存。

  2. 对本地 DataSet 对象进行更改。这些更改可以包括对本地缓存的 DataSet 中的一个或多个表执行更新、删除和插入操作。

  3. 初始化 DataAdapter 的与更新相关的属性。此步骤配置处理更新、删除或插入的确切方式。因为有多种处理该问题的方法,下面的“初始化 DataAdapter 以进行更新”部分对推荐方法进行了讨论。

  4. 调用 DataAdapter Update 方法以提交挂起的更改。本地缓存的 DataSet 中的每个已更改的记录都将被处理。(未更改的记录将被 Update 方法自动忽略。)

  5. 处理由 DataAdapter Update 方法引发的异常。当无法在数据库中进行请求的更改时,将引发异常。

(还有另外一种执行更新的方法。可以使用 ExecuteNonQuery 方法直接执行 SQL 更新查询。当您希望以编程方式更新特定的行并且不使用 DataSet 对象时,适合使用该技术。)

初始化 DataAdapter 以进行更新

在 ADO.NET 中,必须添加您自己的代码来向 DataAdapter 对象提交数据库更新。有三种完成该任务的方法:

  • 可以提供您自己的更新逻辑。

  • 可以使用“数据适配器配置向导”生成更新逻辑。

  • 可以使用 CommandBuilder 对象生成更新逻辑。

建议您提供自己的更新逻辑。要节省时间,您可以使用“数据适配器配置向导”,但如果您确实使用了该向导,请尽量不要在运行时生成更新逻辑。除非迫不得已,否则请不要依赖于 CommandBuilder 对象,因为它会导致性能下降,并且您无法控制该对象生成的更新逻辑。此外,CommandBuilder 不会帮助您使用存储过程来提交更新。

对于动态生成数据访问逻辑的应用程序,如报告工具或数据提取工具,您可以使用 CommandBuilder。使用 CommandBuilder 可免除这些工具编写其自身的代码生成模块的需要。

使用存储过程

通过使用存储过程来进行更新,可以使数据库管理员实现比使用动态 SQL 时粒度更低的安全性,以及更完善的数据完整性检查。例如,存储过程除了执行请求的更新以外,还可以向审核日志中插入一项。因为数据库内部对存储过程执行脱机查询优化,所以存储过程还能够提供最佳性能。最后,因为存储过程在数据库结构和应用程序之间提供了隔离,所以更加易于维护。

因为使用存储过程的 ADO.NET 应用程序提供了许多好处,并且不比那些直接对数据库进行更改的应用程序更难实现,所以建议在几乎所有情况下都使用该方法。例外情况是当您必须使用多个后端或不支持存储过程的数据库(如 Microsoft Access)时。在这些情况下,请使用基于查询的更新。

管理并发

DataSet 对象的目的是鼓励对长期运行的活动(例如,当您远程处理数据时以及当用户与数据进行交互时)使用开放式并发。在从 DataSet 向数据库服务器提交更新时,有四种管理开放式并发的主要方法:

  • 仅包括主键列

  • 包括 WHERE 子句中的所有列

  • 包括唯一键列和时间戳列

  • 包括唯一键列和已修改的列

注意,后三种方法维护了数据完整性;第一种方法则没有。

仅包括主键列

该选项造成了这样一种情况,即最后的更新覆盖了所有以前的更改。CommandBuilder 不支持此选项,而“数据适配器配置向导”却支持。要使用此选项,请转至 AdvancedOptions 选项卡并清除 UseConcurrency 复选框。

该方法不是推荐的实施策略,因为它允许用户无意中改写其他用户的更改。损害其他用户更新的完整性永远是不可取的。(该技术仅适用于单用户数据库。)

包括 WHERE 子句中的所有列

使用该选项,可防止您改写由其他用户在您的代码取出行和您的代码提交该行中挂起的更改之间这段时间内进行的更改。该选项是“数据适配器配置向导”与 SqlCommandBuilder 生成的 SQL 代码二者的默认行为。

由于以下原因,该方法不是推荐的实施策略:

  • 如果向表中添加了额外列,查询将需要修改。

  • 通常,数据库不让您比较两个 BLOB 值,因为它们太大,以至于这样的比较效率很低。(像 CommandBuilder 与“数据适配器配置向导”这样的工具不应该在 WHERE 子句中包含 BLOB 列。)

  • 将表中的所有列与已更新行中的所有列进行比较时,会产生额外的开销。

包括唯一键列和时间戳列

使用该选项,数据库会在每次更新行后,将时间戳列更新为唯一值。(您必须在表中提供时间戳列。)目前,CommandBuilder 与“数据适配器配置向导”都不支持该选项。

包括唯一键列和已修改的列

通常,不推荐使用该选项,因为如果您的应用程序逻辑依赖于过期的数据字段甚至它不更新的字段,则可能会产生错误。例如,如果用户甲更改了订单数量,而用户乙更改了单价,则可能计算出错误的订单总值(数量乘以价格)。

正确地更新空字段

当数据库中的字段不包含数据值时,通常可以方便地将这些空字段视为包含特殊的空值。然而,这一心理却可能是编程错误的根源,因为数据库标准要求对空值进行特殊处理。

空字段的核心问题在于:当两个操作数都为空值或其中一个为空值时,普通的 SQL = 运算符总是返回 false。在 SQL 查询中,运算符 ISNULL 是检查是否存在空字段的唯一正确方法。

如果应用程序通过指定 WHERE 子句,使用上面介绍的技术来管理并发,则您必须在字段可能为空的任何地方包括显式的 IS NULL 表达式。例如,如果 OldLastName 为空,下面的查询将总是失败:

SET LastName = @NewLastName WHERE StudentID = @StudentID AND 
                                  LastName = @OldLastName

应该按如下方式重写该查询:

SET LastName = @NewLastName WHERE (StudentID = @StudentID) AND
                                  ((LastName = @OldLastName) OR
                                   (OldLastName IS NULL AND LastName IS NULL))

要了解如何编写上述种类的更新逻辑,一种好方法是阅读 CommandBuilder 工具生成的输出。

更多信息

有关数据库更新的完整论述,请参阅 David Sceppa 的《Microsoft ADO.NET》(Microsoft Press, 2002) 第 11 和 12 章。

使用强类型数据集对象

强类型 DataSet 对象将数据库表和列呈现为对象和属性。访问是按名称执行的,而不是通过对集合进行索引来执行的。这意味着您可以使用访问字段的方法来识别强类型和非类型化 DataSet 对象之间的区别:

string n1 = myDataSet.Tables["Students"].Rows[0]["StudentName"];  // untyped
string n2 = myDataSet.Students[0].StudentName;           // strongly typed

使用强类型 DataSet 对象有以下几点好处:

  • 访问字段所需的代码可读性更高、更加简洁。

  • Visual Studio .NET 代码编辑器中的智能感知功能可以在您键入时自动完成代码行。

  • 编译器可以捕获强类型 DataSet 类型不匹配错误。在编译时检测类型错误要比在运行时检测更好。

何时使用强类型数据集

强类型 DataSet 很有用,因为它们使应用程序开发变得更加容易并且更少出错误。对于多层应用程序的客户端而言尤其如此,在此类客户端上,重点在于需要进行多字段访问操作的图形用户界面和数据验证。

不过,如果数据库结构改变(例如,当字段名和表名被修改时),则强类型 DataSet 可能会很麻烦。在此情况下,必须重新生成类型化 DataSet 类,并且必须修改所有相关类。

可以在同一应用程序中使用强类型方法和非类型化方法。例如,一些开发人员在客户端使用强类型 DataSet,在服务器上使用非类型化记录。强类型 DataSet.Merge 方法可用来从非类型化 DataSet 中导入数据。

生成 DataSet 类

.NET 框架 SDK 和 Visual Studio.NET 都提供了实用工具,帮助您生成必要的 DataSet 子类。.NET 框架 SDK 涉及到使用命令行工具和编写代码。很显然,Visual Studio .NET 方法依赖于 Visual Studio .NET 开发环境,并且不要求您打开命令窗口。

无论如何生成 DataSet 类,都必须将新类部署到所有引用该类型化 DataSet 的层上。(这种情形不太常见,但如果通过使用远程处理技术在多个层中传递类型化 DataSet,则需要考虑这种情况。)

使用 .NET 框架实用工具

.NET 框架 SDK 包含一个称为 XML 架构定义工具的命令行实用工具,可帮助您基于 XML 架构 (.xsd) 文件生成类文件。请将该实用工具与 DataSet 对象的 WriteXmlSchema 方法结合使用,以将非类型化 DataSet 转化为强类型 DataSet

以下命令阐明了如何从 XML 架构文件生成类文件。打开一个命令窗口,并键入以下内容:

C:\>xsd MyNewClass.xsd /d

该命令中的第一个参数是 XML 架构文件的路径。第二个参数表示要创建的类派生于 DataSet 类。默认情况下,该工具会生成 Visual C# .NET 类文件,但它还可以通过添加适当的选项来生成 Visual Basic .NET 类文件。要列出该工具的可用选项,请键入以下内容:

xsd /?

在创建了新的类文件以后,请将其添加到项目中。现在,可以创建强类型 DataSet 类的实例,如下面的 Visual C# .NET 代码片段所示:

MyNewClass ds = new MyNewClass();

使用 Visual Studio .NET

要在 Visual Studio .NET 中生成强类型 DataSet,请右键单击窗体设计器窗口,然后单击 Generate Dataset。这将创建一个 .xsd(XML 架构定义)文件以及一个类文件,然后将其添加到项目中。在执行此操作之前,请确保已经将一个或多个 DataAdapter 添加到 Windows 窗体中。注意,类文件是隐藏的。要查看该文件,请单击位于解决方案资源管理器窗口工具栏中的 Show All Files 按钮。该类文件与 .xsd 文件相关联。

要向强类型 DataSet 添加关系,请通过双击解决方案资源管理器窗口中的架构文件打开 XML 架构设计器,然后右键单击您要向其添加约束的表。在快捷菜单上,单击 Add New Relation

在 Visual Studio .NET 中生成强类型 DataSet 的另一种方法是,右键单击项目资源管理器中的项目,选择 Add Files,然后选择 dataset。将创建一个新的 .xsd 文件。此时您可以使用服务器资源管理器连接到数据库,并将表拖到 xsd 文件上。

处理空数据字段

以下是几点帮助您在 .NET 数据体系结构中正确使用空字段值的提示:

  • 始终使用 System.DBNull 类设置空字段的值。不要使用由 C# 或 Visual Basic .NET 提供的空值。例如:

    rowStudents["Nickname"] = DBNull.Value   // correct!
    
  • 强类型 DataSet 包含两个针对 DataRow 执行操作的附加方法:一个用于检查列是否含有空值,另一个用于将列值设置为空。以下代码片段显示了这两个方法:

    If (tds.rowStudent[0].IsPhoneNoNull()) {a€_.}
    tds.rowStudent[0].SetPhoneNoNull()
    
  • 请始终使用 DataRow 类(或在上一个项目符号中给出的强类型等效类)的 IsNull 方法来测试数据库中的空值。该方法是测试数据库空值的唯一受支持的方式。

  • 如果数据字段可能包含空值,请确保在需要非空值的上下文中使用该值之前,对其进行测试(通过 IsNull 方法)。这方面的一个典型示例是可能为空的 Integer 值数据字段。注意,.NET 运行时 Integer 数据类型不包含空值。以下为一个示例:

    int i = rowStudent["ZipCode"];         // throws exception if null!
    
  • 使用强类型 DataSet .xsd 文件的 nullValue 批注来配置如何映射数据库中的空值。默认情况下会引发异常;然而,为了获得更高粒度的控制,可以将该类配置为使用指定的值(如 String.Empty)来替换空值。

事务处理

几乎所有更新数据源的、面向商业的应用程序都需要事务处理支持。事务处理用来确保一个或多个数据源中包含的系统状态的完整性,其方法是提供以下四项由著名的缩写词 ACID 表示的基本保证:原子性、一致性、隔离性和持久性。

例如,考虑一个处理采购订单的基于 Web 的零售应用程序。每个订单都需要三种截然不同的操作,这些操作涉及三种数据库更新:

  • 必须按订购数量降低存货水平。

  • 必须按采购数量借记客户的信用级别。

  • 必须将新订单添加到订单数据库中。

将上述三种截然不同的操作作为一个单位以原子方式执行是至关重要的。它们必须或者全部成功,或者全部失败:任何其他情况都将损害数据完整性。事务处理能够提供这一保证以及其他一些保证。

有关事务处理基本原理的详细信息,请参阅 http://msdn.microsoft.com/library/en-us/cpguidnf/html/cpcontransactionprocessingfundamentals.asp

有许多可以用来将事务管理集成到数据访问代码中的方法。每种方法都适用于两种基本编程模型中的一种:

  • 手动事务处理。直接在组件代码或存储过程中分别编写使用 ADO.NET 或 Transact-SQL 事务处理支持功能的代码。

  • 原子(COM+) 事务。向 .NET 类中添加声明性属性,从而指定对象在运行时的事务处理要求。该模型使您可以方便地将多个组件配置为在同一事务中执行工作。

这两种技术都可以用来执行本地事务(即针对单个资源管理器如 SQL Server 2000 执行的事务)或分布式事务(即针对远程计算机上的多个资源管理器执行的事务),尽管原子事务处理模型大大简化了分布式事务处理。

您可能倾向于使用原子 (COM+) 事务处理,以便从更容易的编程模型中受益。在有很多执行数据库更新的组件的系统中,这种编程模型的好处尤其明显。但是,在许多方案中,您应该避免这种事务处理模式所带来的额外开销和性能损失。

本节将提供一些指导,帮助您基于特定的应用程序方案来选择最合适的模型。

选择事务处理模型

在选择事务处理模型之前,您应该考虑是否真正需要事务处理。事务是服务器应用程序所使用的最昂贵的单一资源,如果不必要地加以使用,还会降低可伸缩性。考虑下列对事务的使用进行控制的准则:

  • 仅当需要在一组操作中获得锁并且需要贯彻 ACID 规则时,才执行事务处理。

  • 使事务处理时间仅可能地短,以最大限度减少占有数据库锁的时间。

  • 永远不要让客户端控制事务的生存期。

  • 不要对单个 SQL 语句使用事务。SQL Server 自动将每个语句作为单个事务运行。

原子事务处理与手动事务处理

尽管自动事务处理的编程模型在某种程度上得到了简化,尤其是在多个组件都执行数据库更新时,但手动执行本地事务处理总是要快很多,因为它们不需要与 Microsoft DTC 进行交互。即使您要针对单个本地资源管理器(如 SQL Server)使用原子事务,也是这样的(尽管可以减少性能损失),原因是手动的本地事务处理能够避免与 DTC 进行任何不必要的进程间通讯 (IPC)。

在以下情况下,请使用手动事务处理:

  • 针对单个数据库执行事务处理。

在以下情况下,请使用自动事务处理:

  • 要求单个事务跨越多个远程数据库。

  • 您要求单个事务包括多个资源管理器,例如,一个数据库和一个 Windows 2000 消息队列(以前称为 MSMQ)资源管理器。

    请避免混用事务处理模型。二者只能择其一。

在认为性能可以满足要求的应用程序场合中,选择使用自动事务处理(即使针对单个数据库)以简化编程模型是合理的。自动事务处理使得多个组件可以容易地执行属于同一事务的操作。

使用手动事务处理

使用手动事务处理时,可以直接在组件代码或存储过程中分别编写使用 ADO.NET 或 Transact-SQL 事务处理支持功能的代码。在大多数情况下,应该选择在存储过程中控制事务处理,因为该方法提供了优良的封装,并且从性能角度看也与使用 ADO.NET 代码执行事务处理相当。

通过 ADO.NET 执行手动事务处理

ADO.NET 支持一种事务对象,可以使用该对象来开始新的事务,然后对应该提交还是回滚该事务进行显式控制。该事务对象与单个数据库连接相关联,并且通过该连接对象的 BeginTransaction 方法获得。调用该方法并不暗示着随后的命令是在该事务的上下文中发出的。必须将各个命令与事务显式关联,方法是设置相应命令的 Transaction 属性。可以将多个命令对象与事务对象相关联,从而将针对单个数据库的多个操作组合到单个事务中。

有关使用 ADO.NET 事务处理代码的示例,请参阅附录中的如何编写 ADO.NET 手动事务处理代码。

更多信息

  • ADO.NET 手动事务处理的默认隔离级别是“提交读”,这意味着在读取数据时数据库拥有共享锁,但数据在事务处理结束之前可以更改。这有可能导致不可重复的读取,或者导致幻像数据。可以更改隔离级别,方法是将事务对象的 IsolationLevel 属性设置为 IsolationLevel 枚举类型所定义的枚举值之一。

  • 必须在经过认真考虑后,为事务选择适当的隔离级别。这涉及到在数据一致性和性能之间进行权衡。最高隔离级别 (Serialized) 提供绝对的数据一致性,但代价是降低系统总吞吐量。较低的隔离级别可使应用程序具有更好的可伸缩性,但同时会增加因为数据不一致导致出错的可能性。对于在大多数时间读取数据而很少写数据的系统而言,使用较低的隔离级别可能是适当的。

  • 有关选择适当事务隔离级别的有价值信息,请参阅 Microsoft Press? 书籍《Inside SQL Server 2000》(作者:Kalen Delaney)。

通过存储过程执行手动事务处理

还可以通过在存储过程中使用 Transact-SQL 语句来直接控制手动事务处理。例如,可以通过使用单个存储过程(该存储过程使用 Transact-SQL 事务处理语句,如 BEGIN TRANSACTIONEND TRANSACTIONROLLBACK TRANSACTION)来执行事务性操作。

更多信息

  • 如果需要,可以通过在存储过程中使用 SET TRANSACTION ISOLATION LEVEL 语句来控制事务隔离级别。Read Committed 是 SQL Server 的默认隔离级别。有关 SQL Server 隔离级别的详细信息,请参阅 SQL Server 联机图书“Accessing and Changing Relation Data”部分中的“Isolation Levels”。

  • 有关说明如何使用 Transact-SQL 事务处理语句来执行事务性更新的代码示例,请参阅附录中的如何使用 Transact-SQL 执行事务处理。

使用自动事务处理

自动事务处理简化了编程模型,因为它们不要求显式开始新事务或者显式提交或中止事务。然而,自动事务处理的最大优势是它们与 DTC 协同工作,这使得单个事务可以跨越多个分布式数据源。在大型分布式应用程序中,这一优势可能很有意义。尽管可以通过直接编写 DTC 来手动控制分布式事务,但自动事务处理大幅度地简化了该任务,并且非常适合于基于组件的系统。例如,很容易以声明方式配置多个组件来执行组成单个事务的工作。

自动事务处理依赖于 COM+ 提供的分布式事务处理支持功能,因此,只有服务组件(即从 ServicedComponent 类派生的组件)可以使用自动事务处理。

配置类以执行自动事务处理:

  • 从位于 System.EnterpriseServices 命名空间中的 ServicedComponent 类派生该类。

  • 通过使用 Transaction 属性来定义该类的事务处理要求。TransactionOption 枚举类型中提供的值确定了将如何在 COM+ 目录中配置该类。其他可以使用该属性建立的属性包括事务隔离级别和超时。

  • 要明确避免必须在事务处理结果中进行表决,可以使用 AutoComplete 属性来给方法加上批注。如果这些方法引发异常,事务将自动中止。注意,如果需要,您仍然可以直接对事务处理结果进行表决。有关详细信息,请参阅本文下面的确定事务处理结果一节。

更多信息

  • 有关 COM+ 自动事务处理的详细信息,请在 Platform SDK 文档中搜索“Automatic Transactions Through COM+”(通过 COM+ 执行自动事务处理)。

  • 有关事务性 .NET 类的示例,请参阅附录中的如何编写事务性 .NET 类。

配置事务隔离级别

COM+ 1.0 版(即在 Windows 2000 上运行的 COM+)的事务隔离级别是 Serialized。尽管这提供了最高程度的隔离,这样的保护是以牺牲性能为代价的。系统的总吞吐量会降低,因为涉及到的资源管理器(通常是数据库)必须在事务处理期间同时拥有读锁和写锁。在此过程中,所有其他事务都将被阻塞,这可能对应用程序的伸缩能力产生重大影响。

COM+ 1.5 版(它随附在 Microsoft Windows .NET 中)允许在 COM+ 目录中以组件为单位对事务隔离级别进行配置。与事务中的根组件相关联的设置确定了该事务的隔离级别。此外,属于同一事务流的内部子组件所具有的事务级别不得高于由根组件所定义的事务级别。如果不满足此项要求,当相关子组件被实例化时,将产生错误。

对于 .NET 托管类,Transaction 属性支持公共 Isolation 属性。可以使用该属性以声明方式指定特定的隔离级别,如下面的代码所示。

 [Transaction(TransactionOption.Supported, Isolation=TransactionIsolationLevel.ReadCommitted)]
public class Account : ServicedComponent
{
  . . .
}

更多信息

确定事务结果

自动事务处理的结果由事务中止标志以及一致性标志(它们位于单事务流中的所有事务性组件的上下文中)的状态控制。当事务流中的根组件被停用(并且控制被返回到调用方)时,将确定事务结果。图 5 对此进行了说明,该图显示了一个传统的银行资金转帐事务。

1.5.事务流和上下文

事务的结果是在根对象(在该示例中,为 Transfer 对象)被停用并且客户端的方法调用返回时确定的。如果任意上下文中的任意一致性标志被设置为 false,或者如果事务中止标志被设置为 true,则底层的物理 DTC 事务被中止。

可以用下列两种方式之一从 .NET 对象中控制事务结果:

  • 可以使用 AutoComplete 属性给方法加上批注,并且让 .NET 自动发出您的表决以控制事务的结果。使用该属性时,如果方法引发异常,一致性标志将被自动设置为 false(最终会导致事务中止)。如果方法返回且未引发异常,则一致性标志被设置为 true,这表示该组件同意提交事务。这一点不能保证,因为它依赖于同一事务流中其他对象的表决。

  • 可以调用 ContextUtil 类的静态 SetCompleteSetAbort 方法,这两个方法分别将一致性标志设置为 true 或 false。

严重度大于 10 的 SQL Server 错误会导致托管数据提供程序引发 SqlException 类型的异常。如果您的方法捕获并处理了该异常,请确保手动表决以中止该事务,或者(对于被标记为 [AutoComplete] 的方法)确保将该异常传播给调用方。

[AutoComplete] 方法

对于用 AutoComplete 属性标记的方法,请执行以下任意一种操作:

  • SqlException 沿调用堆栈向上传播。

  • SqlException 包装到外部异常中,并将后者传播给调用方。您可能希望将该异常包装在对调用方更有意义的异常类型中。

如果不传播该异常,将导致对象不会表决以中止该事务(尽管发生了数据库错误)。这意味着:由共享同一事务流的其他对象所进行的其他成功操作可能被提交。

下面的代码捕获了 SqlException,然后将其直接传播到调用方。该事务最终将中止,因为该对象的一致性标志将在其被停用时自动设置为 false。

 [AutoComplete]
void SomeMethod()
{
  try
  {
    // Open the connection, and perform database operation
    . . .
  }
  catch (SqlException sqlex )
  {
    LogException( sqlex ); // Log the exception details
    throw;                 // Rethrow the exception, causing the consistent 
                           // flag to be set to false.
  }
  finally
  {
    // Close the database connection
    . . .
  }
}

非 [AutoComplete] 方法

对于没有用 AutoComplete 属性进行标记的方法,必须执行以下操作:

  • catch 块中调用 ContextUtil.SetAbort,从而表决中止该事务。这会将一致性标志设置为 false。

  • 如果未发生异常,则调用 ContextUtil.SetComplete 以表决提交该事务。这会将一致性标志设置为 true(其默认状态)。

以下代码阐明了该方法。

void SomeOtherMethod()
{
  try
  {
    // Open the connection, and perform database operation
    . . .
    ContextUtil.SetComplete(); // Manually vote to commit the transaction
  }
  catch (SqlException sqlex)
  {
    LogException( sqlex );   // Log the exception details
    ContextUtil.SetAbort();  // Manually vote to abort the transaction
    // Exception is handled at this point and is not propagated to the caller
  }
  finally
  {
    // Close the database connection
    . . .
  }
}
  

如果有多个 catch 块,则在方法开头调用一次 ContextUtil.SetAbort,并且在 try 块结尾调用 ContextUtil.SetComplete 将会更加容易。这样,就不需要在每个 catch 块中重复调用 ContextUtil.SetAbort。这些方法所确定的一致性标志设置仅当方法返回时才有意义。

必须始终将异常(或被包装的异常)沿调用堆栈向上传播,因为这样可使调用代码知道该事务将失败。这使得调用代码可以进行优化。例如,在银行资金转帐方案中,如果借记操作已经失败,则转帐组件可以决定不执行贷记操作。

如果您将一致性标志设置为 false,然后返回并且不引发异常,则调用代码无法知道事务将要失败。尽管可以返回一个 Boolean 值或者设置一个 Boolean 输出参数,您应该始终如一并引发异常以表明发生了错误。这样会产生更加清晰、更加一致的代码,并且具有标准的错误处理方法。

数据分页

对数据进行分页是分布式应用程序中的共同要求。例如,可能要向用户展示一个书籍清单,此时,同时显示整个清单可能被禁止。用户将希望对数据执行熟悉的操作,如查看下一页或上一页数据,或者跳到该清单的开头或结尾。

本节将讨论用于实现此功能的选项,以及每个选项对可伸缩性和性能的影响。

比较几种选择

数据分页的选择包括:

  • 通过 SqlDataAdapterFill 方法,用查询的一系列结果填充 DataSet

  • 通过 COM 互操作性使用 ADO,并且使用服务器端游标

  • 使用存储过程来手动实现数据分页

数据分页的最佳选择取决于下面列出的因素:

  • 可伸缩性要求

  • 性能要求

  • 网络带宽

  • 数据库服务器内存和功能

  • 中间层服务器内存和功能

  • 希望进行分页的、由查询返回的行数

  • 数据页的大小

性能测试表明,使用存储过程的手动方法在广泛的压力级别下提供了最佳性能。不过,因为手动方法在服务器上完成其工作,所以如果网站的大部分功能都依赖于数据分页功能,则服务器压力级别可能成为一个重要问题。要确保该方法适合您的特定环境,应该针对您的特定要求测试所有选择。

下面讨论了各种选择。

使用 SqlDataAdapter 的 Fill 方法

如上所述,可通过 SqlDataAdapter 用数据库中的数据来填充 DataSet。重载的 Fill 方法之一(如以下代码所示)采用两个整数索引值作为参数。

public int Fill(
   DataSet dataSet,
   int startRecord,
   int maxRecords,
   string srcTable
);
  

startRecord 值表示起始记录的从零开始的索引。maxRecords 值表示要复制到新 DataSet 中的记录数(从 startRecord 开始)。

SqlDataAdapter 在内部使用 SqlDataReader 来执行查询并返回结果。SqlDataAdapter 读取结果并基于从 SqlDataReader 中读取的数据创建一个 DataSetSqlDataAdapter 通过 startRecordmaxRecords 将所有结果复制到刚生成的 DataSet 中,并且丢弃它不需要的结果。这意味着可能会将大量不需要的数据通过网络提取到数据访问客户端,而这正是该方法的主要缺点。

例如,如果有 1,000 条记录,并且只需要记录 900 到 950,则仍然会将前面的 899 条记录通过网络提取到客户端并丢弃。这一开销对于小型结果集而言可能很小,但当您对较大的数据集进行分页时,这一开销可能会很大。

使用 ADO

另一种实现分页的选择是使用基于 COM 的 ADO 进行分页。该选项背后的主要动机是获取对服务器端游标的访问,该游标通过 ADO Recordset 对象公开。可以将 Recordset 游标位置设置为 adUseServer。如果您的 OLE DB 提供程序支持它(SQLOLEDB 就支持),这将导致对服务器端游标的使用。然后,您可以使用该游标直接导航到起始记录,而无须将所有记录通过网络提取到数据访问客户端代码。

该方法有以下两个主要缺点:

  • 大多数情况下,您需要将在 Recordset 对象中返回的记录转换到 DataSet 中,以便在客户端托管代码中使用。尽管 OleDbDataAdapter 的确重载了 Fill 方法,采用 ADO Recordset 对象作为参数并将其转换到 DataSet 中,但没有用来指定起始和结束记录的工具。唯一现实的选择是移动到 Recordset 对象中的起始记录,依次处理各个记录,并将数据手动复制到手动生成的新 DataSet 中。这样做(特别是通过 COM Interop 调用的开销)有可能完全抵消不通过网络提取额外数据的优点,尤其是对小型 DataSet 而言。

  • 在从服务器提取所需数据花费的这段时间内,您需要使连接和服务器端游标保持打开状态。在数据库服务器上打开和维护时,游标通常是一种昂贵的资源。尽管该选择可以提高性能,它还可能由于在服务器上长期消耗宝贵的资源而降低可伸缩性。

使用手动实现

本节讨论的最后一个对数据进行分页的选择是,通过使用存储过程为应用程序手动实现分页功能。对于包含唯一键的表而言,可以相对容易地实现存储过程。对于不包含唯一键的表(您不应该具有许多这样的表)而言,该过程更复杂一些。

对包含唯一键的表进行分页

如果表包含唯一键,可以在 WHERE 子句中使用该键来创建从特定行开始的结果集。将这种方法与用于限制结果集大小的 SET ROWCOUNT 语句或 SQL Server TOP 语句结合起来,可以提供一种有效的分页机制。下面的存储过程代码阐明了这一方法:

CREATE PROCEDURE GetProductsPaged
@lastProductID int,
@pageSize int
AS
SET ROWCOUNT @pageSize
SELECT *
FROM Products
WHERE [standard search criteria]
AND ProductID > @lastProductID
ORDER BY [Criteria that leaves ProductID monotonically increasing]
GO

该存储过程的调用方简单地维护 lastProductID 值,并且在连续调用之间将其递增或递减所选的页面大小。

对不包含唯一键的表进行分页

如果您要对其进行分页的表不包含唯一键,可以考虑通过某种方法(例如,使用标识列)添加一个唯一键。这将使您能够实现前面讨论的分页解决方案。

对于不包含唯一键的表,只要您能够通过将属于结果集的其他两个或更多个字段组合起来生成唯一性,则仍然能够实现有效的分页解决方案。

例如,请考虑下表:

Col1

Col2

Col3

其他列§

A

1

W

§

A

1

X

§

A

1

Y

§

A

1

Z

§

A

2

W

§

A

2

X

§

B

1

W

§

B

1

X

§

对于该表,可以通过组合 Col1Col2Col3 来生成唯一性。因此,可以通过使用以下存储过程中阐明的方法来实现分页机制。

CREATE PROCEDURE RetrieveDataPaged
@lastKey char(40),
@pageSize int
AS
SET ROWCOUNT @pageSize
SELECT
Col1, Col2, Col3, Col4, Col1+Col2+Col3 As KeyField
FROM SampleTable
WHERE [Standard search criteria]
AND Col1+Col2+Col3 > @lastKey
ORDER BY Col1 ASC, Col2 ASC, Col3 ASC
GO

客户端保持由存储过程返回的 KeyField 列的最后值,并将其重新插入到存储过程中以控制表分页。

尽管手动实现增加了数据库服务器的负担,但它能够避免通过网络传递不必要的数据。性能测试表明,该方法能够在广泛的压力级别下正常工作。然而,根据与数据分页功能有关的网站工作量的不同,在服务器上执行手动分页可能会影响应用程序的可伸缩性。您应该在自己的环境中运行性能测试,以便找到适合于特定应用程序方案的最佳方法。

附录

如何启用 .NET 类的对象构建

可以使用企业 (COM+) 服务来启用 .NET 托管类,以便进行对象构建。

启用 .NET 托管类

  1. 从位于 System.EnterpriseServices 命名空间中的 ServicedComponent 类派生类。

    using System.EnterpriseServices;
    public class DataAccessComponent : ServicedComponent
    
  2. ConstructionEnabled 属性修饰该类,并根据需要指定默认的构建字符串。该默认值存放在 COM+ 目录中。管理员可以使用“组件服务”Microsoft 管理控制台 (MMC) 单元来维护该值。

     [ConstructionEnabled(Default="default DSN")]
    public class DataAccessComponent : ServicedComponent
    
  3. 提供虚拟 Construct 方法的重写实现。该方法在对象的语言特定构造函数之后调用。COM+ 目录中维护的构建字符串作为该方法的唯一参数提供。

    public override void Construct( string constructString )
    {
      // Construct method is called next after constructor.
      // The configured DSN is supplied as the single argument
    }
    
  4. 通过 AssemblyKey 文件或 AssemblyKeyName 属性对程序集进行签名,以便为其提供强名称。向 COM+ 服务注册的程序集必须具有强名称。有关强名称程序集的详细信息,请参阅 http://msdn.microsoft.com/library/en-us/cpguidnf/html/cpconworkingwithstrongly-namedassemblies.asp

     [assembly:AssemblyKeyFile("DataServices.snk")]
    
  5. 要支持动态(惰性)注册,请使用程序集级别属性 ApplicationNameApplicationActivation,分别指定用于容纳程序集组件的 COM+ 应用程序的名称以及该应用程序的激活类型。有关程序集注册的详细信息,请参阅 http://msdn.microsoft.com/library/en-us/cpguidnf/html/cpconregisteringservicedcomponents.asp

    // the ApplicationName attribute specifies the name of the
    // COM+ Application which will hold assembly components
    [assembly : ApplicationName("DataServices")]
    // the ApplicationActivation.ActivationOption attribute specifies 
    // where assembly components are loaded on activation
    // Library : components run in the creator's process
    // Server : components run in a system process, dllhost.exe
    [assembly: ApplicationActivation(ActivationOption.Library)]
    

以下代码片段显示了一个名为 DataAccessComponent 的服务组件,它使用 COM+ 构建字符串来获取数据库连接字符串。

using System;
using System.EnterpriseServices;
// the ApplicationName attribute specifies the name of the
// COM+ Application which will hold assembly components
[assembly : ApplicationName("DataServices")]
// the ApplicationActivation.ActivationOption attribute specifies 
// where assembly components are loaded on activation
// Library : components run in the creator's process
// Server : components run in a system process, dllhost.exe
[assembly: ApplicationActivation(ActivationOption.Library)]
// Sign the assembly. The snk key file is created using the 
// sn.exe utility
[assembly: AssemblyKeyFile("DataServices.snk")]
[ConstructionEnabled(Default="Default DSN")]
public class DataAccessComponent : ServicedComponent
{
    private string connectionString;
    public DataAccessComponent()
    {
      // constructor is called on instance creation
    }
    public override void Construct( string constructString )
    {
      // Construct method is called next after constructor.
      // The configured DSN is supplied as the single argument
      this.connectionString = constructString;
    }
}

如何使用 SqlDataAdapter 来检索多个行

以下代码阐明了如何使用 SqlDataAdapter 对象发出可生成 DataSetDataTable 的命令。它从 SQL Server Northwind 数据库中检索一组产品类别。

using System.Data;
using System.Data.SqlClient;
public DataTable RetrieveRowsWithDataTable()
{
  using ( SqlConnection conn = new SqlConnection(connectionString) )
  {
    conn.Open();
    SqlCommand cmd = new SqlCommand("DATRetrieveProducts", conn);
    cmd.CommandType = CommandType.StoredProcedure;
    SqlDataAdapter adapter = new SqlDataAdapter( cmd );
    DataTable dataTable = new DataTable("Products");
    adapter .Fill(dataTable);
    return dataTable;
  }
}

使用 SqlAdapter 生成 DataSet 或 DataTable

  1. 创建一个 SqlCommand 对象以调用该存储过程,并将其与一个 SqlConnection 对象(显示)或连接字符串(不显示)相关联。

  2. 创建一个新的 SqlDataAdapter 对象并将其与 SqlCommand 对象相关联。

  3. 创建一个 DataTable(也可以创建一个 DataSet)对象。使用构造函数参数来命名 DataTable。

  4. 调用 SqlDataAdapter 对象的 Fill 方法,用检索到的行填充 DataSet 或 DataTable。

如何使用 SqlDataReader 来检索多个行

以下代码片段阐明了可检索多个行的 SqlDataReader 方法。

using System.IO;
using System.Data;
using System.Data.SqlClient;
public SqlDataReader RetrieveRowsWithDataReader()
{
  SqlConnection conn = new SqlConnection(
         "server=(local);Integrated Security=SSPI;database=northwind");
  SqlCommand cmd = new SqlCommand("DATRetrieveProducts", conn );
  cmd.CommandType = CommandType.StoredProcedure;
  try
  {
    conn.Open();
    // Generate the reader. CommandBehavior.CloseConnection causes the
    // the connection to be closed when the reader object is closed
    return( cmd.ExecuteReader( CommandBehavior.CloseConnection ) );
  }
  catch
  {
    conn.Close();
    throw;
  }
}
// Display the product list using the console
private void DisplayProducts()
{
  SqlDataReader reader = RetrieveRowsWithDataReader();
  try
  {
    while (reader.Read())
    {
      Console.WriteLine("{0} {1} {2}", 
                        reader.GetInt32(0).ToString(), 
                        reader.GetString(1) );
    }
  }
  finally
  {
    reader.Close(); // Also closes the connection due to the
                    // CommandBehavior enum used when generating the reader
  }
}

使用 SqlDataReader 检索行

  1. 创建一个用来执行存储过程的 SqlCommand 对象,并将其与一个 SqlConnection 对象相关联。

  2. 打开连接。

  3. 通过调用 SqlCommand 对象的 ExecuteReader 方法创建一个 SqlDataReader 对象。

  4. 要从流中读取数据,请调用 SqlDataReader 对象的 Read 方法来检索行,并使用类型化访问器方法(如 GetInt32GetString 方法)来检索列值。

  5. 使用完读取器后,请调用其 Close 方法。

如何使用 XmlReader 检索多个行

可以使用 SqlCommand 对象来生成 XmlReader 对象,后者可提供对 XML 数据的基于流的只进访问。命令(通常为存储过程)必须产生基于 XML 的结果集,对于 SQL Server 2000 而言,该结果集通常包含一个带有有效 FOR XML 子句的 SELECT 语句。以下代码片段阐明了该方法:

public void RetrieveAndDisplayRowsWithXmlReader()
{
  using( SqlConnection conn = new SqlConnection(connectionString) )
  {;
    SqlCommand cmd = new SqlCommand("DATRetrieveProductsXML", conn );
    cmd.CommandType = CommandType.StoredProcedure;
try
  {
      conn.Open();
    XmlTextReader xreader = (XmlTextReader)cmd.ExecuteXmlReader();
    while ( xreader.Read() )
    {
      if ( xreader.Name == "PRODUCTS" ) 
      {
        string strOutput = xreader.GetAttribute("ProductID");
        strOutput += " ";
        strOutput += xreader.GetAttribute("ProductName");
        Console.WriteLine( strOutput );
      }
    }
    xreader.Close();  // XmlTextReader does not support IDisposable so it can't be
                      // used within a using keyword 
  }
}
  

上述代码使用了以下存储过程:

CREATE PROCEDURE DATRetrieveProductsXML
AS
SELECT * FROM PRODUCTS 
FOR XML AUTO
GO

使用 XmlReader 检索 XML 数据

  1. 创建一个 SqlCommand 对象来调用可生成 XML 结果集的存储过程(例如,在 SELECT 语句中使用 FOR XML 子句)。将该 SqlCommand 对象与某个连接相关联。

  2. 调用 SqlCommand 对象的 ExecuteXmlReader 方法,并且将结果分配给只进 XmlTextReader 对象。当您不需要对返回的数据进行任何基于 XML 的验证时,这是应该使用的最快类型的 XmlReader 对象。

  3. 使用 XmlTextReader 对象的 Read 方法来读取数据。

如何使用存储过程输出参数来检索单个行

借助于命名的输出参数,可以调用在单个行内返回检索到的数据项的存储过程。以下代码片段使用存储过程来检索 Northwind 数据库的 Products 表中包含的特定产品的产品名称和单价。

void GetProductDetails( int ProductID, 
                        out string ProductName, out decimal UnitPrice )
{
  using( SqlConnection conn = new SqlConnection(
        "server=(local);Integrated Security=SSPI;database=Northwind") )
  {
    // Set up the command object used to execute the stored proc
    SqlCommand cmd = new SqlCommand( "DATGetProductDetailsSPOutput", conn )
    cmd.CommandType = CommandType.StoredProcedure;
    // Establish stored proc parameters.
    //  @ProductID int INPUT
    //  @ProductName nvarchar(40) OUTPUT
    //  @UnitPrice money OUTPUT
    // Must explicitly set the direction of output parameters
    SqlParameter paramProdID = 
             cmd.Parameters.Add( "@ProductID", ProductID );
    paramProdID.Direction = ParameterDirection.Input;
    SqlParameter paramProdName = 
             cmd.Parameters.Add( "@ProductName", SqlDbType.VarChar, 40 );
    paramProdName.Direction = ParameterDirection.Output;
    SqlParameter paramUnitPrice = 
             cmd.Parameters.Add( "@UnitPrice", SqlDbType.Money );
    paramUnitPrice.Direction = ParameterDirection.Output;
    conn.Open();
    // Use ExecuteNonQuery to run the command. 
    // Although no rows are returned any mapped output parameters 
    // (and potentially return values) are populated 
    cmd.ExecuteNonQuery( );
    // Return output parameters from stored proc
    ProductName = paramProdName.Value.ToString();
    UnitPrice = (decimal)paramUnitPrice.Value; 
  }
}

使用存储过程输出参数来检索单个行

  1. 创建一个 SqlCommand 对象并将其与一个 SqlConnection 对象相关联。

  2. 通过调用 SqlCommandParameters 集合的 Add 方法来设置存储过程参数。默认情况下,参数都被假设为输入参数,因此必须显式设置任何输出参数的方向。

    一种良好的习惯做法是显式设置所有参数(包括输入参数)的方向。

  3. 打开连接。

  4. 调用 SqlCommand 对象的 ExecuteNonQuery 方法。这将填充输出参数(并可能填充返回值)。

  5. 通过使用 Value 属性,从适当的 SqlParameter 对象中检索输出参数。

  6. 关闭连接。

上述代码片段调用了以下存储过程。

CREATE PROCEDURE DATGetProductDetailsSPOutput
@ProductID int,
@ProductName nvarchar(40) OUTPUT,
@UnitPrice money OUTPUT
AS
SELECT @ProductName = ProductName, 
       @UnitPrice = UnitPrice 
FROM Products 
WHERE ProductID = @ProductID
GO

如何使用 SqlDataReader 来检索单个行

可以使用 SqlDataReader 对象来检索单个行,尤其是可以从返回的数据流中检索需要的列值。以下代码片段对此进行了说明。

void GetProductDetailsUsingReader( int ProductID, 
                        out string ProductName, out decimal UnitPrice )
{
  using( SqlConnection conn = new SqlConnection(
         "server=(local);Integrated Security=SSPI;database=Northwind") )
  {
    // Set up the command object used to execute the stored proc
    SqlCommand cmd = new SqlCommand( "DATGetProductDetailsReader", conn );
    cmd.CommandType = CommandType.StoredProcedure;
    // Establish stored proc parameters.
    //  @ProductID int INPUT
    SqlParameter paramProdID = cmd.Parameters.Add( "@ProductID", ProductID );
    paramProdID.Direction = ParameterDirection.Input;
    conn.Open();
    using( SqlDataReader reader = cmd.ExecuteReader() )
    {
      if( reader.Read() ) // Advance to the one and only row
      {
        // Return output parameters from returned data stream
        ProductName = reader.GetString(0);
        UnitPrice = reader.GetDecimal(1);
       }
    }
  }
}

使用 SqlDataReader 对象来返回单个行

  1. 建立 SqlCommand 对象。

  2. 打开连接。

  3. 调用 SqlDataReader 对象的 ExecuteReader 方法。

  4. 通过 SqlDataReader 对象的类型化访问器方法(在这里,为 GetStringGetDecimal)来检索输出参数。

上述代码片段调用了以下存储过程。

CREATE PROCEDURE DATGetProductDetailsReader
@ProductID int
AS
SELECT ProductName, UnitPrice FROM Products
WHERE ProductID = @ProductID
GO

如何使用 ExecuteScalar 来检索单个项

ExecuteScalar 方法专门适用于仅返回单个值的查询。如果查询返回多个列和/或行,ExecuteScalar 将只返回第一行的第一列。

以下代码说明了如何查找与特定产品 ID 相对应的产品名称:

void GetProductNameExecuteScalar( int ProductID, out string ProductName )
{
  using( SqlConnection conn = new SqlConnection(
         "server=(local);Integrated Security=SSPI;database=northwind") )
  {
    SqlCommand cmd = new SqlCommand("LookupProductNameScalar", conn );
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.Parameters.Add("@ProductID", ProductID );
    conn.Open();
    ProductName = (string)cmd.ExecuteScalar();
  }
}

使用 ExecuteScalar 来检索单个项

  1. 建立一个 SqlCommand 对象来调用存储过程。

  2. 打开连接。

  3. 调用 ExecuteScalar 方法。注意,该方法返回一个对象类型。它包含检索到的第一列的值,并且必须转化为适当的类型。

  4. 关闭连接。

上述代码使用了以下存储过程:

CREATE PROCEDURE LookupProductNameScalar
@ProductID int
AS
SELECT TOP 1 ProductName
FROM Products
WHERE ProductID = @ProductID
GO

如何使用存储过程输出或返回参数来检索单个项

可以使用存储过程输出或返回参数来查找单个值。以下代码阐明了输出参数的用法:

void GetProductNameUsingSPOutput( int ProductID, out string ProductName )
{
  using( SqlConnection conn = new SqlConnection(
        "server=(local);Integrated Security=SSPI;database=northwind") )
  {
    SqlCommand cmd = new SqlCommand("LookupProductNameSPOutput", conn );
    cmd.CommandType = CommandType.StoredProcedure;
    SqlParameter paramProdID = cmd.Parameters.Add("@ProductID", ProductID );
    ParamProdID.Direction = ParameterDirection.Input;
    SqlParameter paramPN = 
           cmd.Parameters.Add("@ProductName", SqlDbType.VarChar, 40 );
    paramPN.Direction = ParameterDirection.Output;
    conn.Open();
    cmd.ExecuteNonQuery();
    ProductName = paramPN.Value.ToString();  
  }
}

使用存储过程输出参数来检索单个值

  1. 建立一个 SqlCommand 对象来调用存储过程。

  2. 通过将 SqlParameters 添加到 SqlCommandParameters 集合中,设置任何输入参数和单个输出参数。

  3. 打开连接。

  4. 调用 SqlCommand 对象的 ExecuteNonQuery 方法。

  5. 关闭连接。

  6. 通过使用输出 SqlParameterValue 属性来检索输出值。

上述代码使用了以下存储过程。

CREATE PROCEDURE LookupProductNameSPOutput 
@ProductID int,
@ProductName nvarchar(40) OUTPUT
AS
SELECT @ProductName = ProductName
FROM Products
WHERE ProductID = @ProductID
GO

以下代码阐明了如何使用返回值来指明是否存在特定行。从编码角度来看,这类似于使用存储过程输出参数,不同之处在于必须将 SqlParameter 方向显式设置为 ParameterDirection.ReturnValue

bool CheckProduct( int ProductID )
{
  using( SqlConnection conn = new SqlConnection(
       "server=(local);Integrated Security=SSPI;database=northwind") )
  {
    SqlCommand cmd = new SqlCommand("CheckProductSP", conn );
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.Parameters.Add("@ProductID", ProductID );
    SqlParameter paramRet =  
           cmd.Parameters.Add("@ProductExists", SqlDbType.Int );
    paramRet.Direction = ParameterDirection.ReturnValue;
    conn.Open();
    cmd.ExecuteNonQuery();
  }
  return (int)paramRet.Value == 1;
}

通过使用存储过程返回值来检查是否存在特定行

  1. 建立一个 SqlCommand 对象来调用存储过程。

  2. 设置一个输入参数,该参数含有要访问的行的主键值。

  3. 设置单个返回值参数。将一个 SqlParameter 对象添加到 SqlCommandParameters 集合中,并将其方向设置为 ParameterDirection.ReturnValue

  4. 打开连接。

  5. 调用 SqlCommand 对象的 ExecuteNonQuery 方法。

  6. 关闭连接。

  7. 通过使用返回值 SqlParameterValue 属性来检索返回值。

上述代码使用了以下存储过程。

CREATE PROCEDURE CheckProductSP 
@ProductID int
AS
IF EXISTS( SELECT ProductID
           FROM Products
           WHERE ProductID = @ProductID )
  return 1
ELSE
  return 0
GO

如何使用 SqlDataReader 来检索单个项

可以使用 SqlDataReader 对象并通过调用命令对象的 ExecuteReader 方法来获取单个输出值。这要求编写稍微多一点的代码,因为必须调用 SqlDataReader Read 方法,然后通过该读取器的访问器方法之一来检索需要的值。以下代码阐明了 SqlDataReader 对象的用法。

bool CheckProductWithReader( int ProductID )
{
  using( SqlConnection conn = new SqlConnection(
         "server=(local);Integrated Security=SSPI;database=northwind") )
  {
    SqlCommand cmd = new SqlCommand("CheckProductExistsWithCount", conn );
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.Parameters.Add("@ProductID", ProductID );
    cmd.Parameters["@ProductID"].Direction = ParameterDirection.Input;
    conn.Open();
    using( SqlDataReader reader = cmd.ExecuteReader(
                                CommandBehavior.SingleResult ) )
    {
    if( reader.Read() )
    {
      return (reader.GetInt32(0) > 0);
    }
    return false;
  }
}

上述代码采用了以下存储过程。

CREATE PROCEDURE CheckProductExistsWithCount 
@ProductID int
AS
SELECT COUNT(*) FROM Products
WHERE ProductID = @ProductID
GO

如何编写 ADO.NET 手动事务处理代码

以下代码显示了如何充分利用 SQL Server .NET 数据提供程序所提供的事务处理支持,通过事务来保护资金转帐操作。该操作在同一数据库中的两个帐户之间转移资金。

public void TransferMoney( string toAccount, string fromAccount, decimal amount )
{
  using ( SqlConnection conn = new SqlConnection(
            "server=(local);Integrated Security=SSPI;database=SimpleBank" ) )
  {
    SqlCommand cmdCredit = new SqlCommand("Credit", conn );
    cmdCredit.CommandType = CommandType.StoredProcedure;
    cmdCredit.Parameters.Add( new SqlParameter("@AccountNo", toAccount) );
    cmdCredit.Parameters.Add( new SqlParameter("@Amount", amount ));
    SqlCommand cmdDebit = new SqlCommand("Debit", conn );
    cmdDebit.CommandType = CommandType.StoredProcedure;
    cmdDebit.Parameters.Add( new SqlParameter("@AccountNo", fromAccount) );
    cmdDebit.Parameters.Add( new SqlParameter("@Amount", amount ));
    conn.Open();
    // Start a new transaction
    using ( SqlTransaction trans = conn.BeginTransaction() )
    {
      // Associate the two command objects with the same transaction
      cmdCredit.Transaction = trans;
      cmdDebit.Transaction = trans;
      try
      {
        cmdCredit.ExecuteNonQuery();
        cmdDebit.ExecuteNonQuery();
        // Both commands (credit and debit) were successful
        trans.Commit();
      }
      catch( Exception ex )
      {
        // transaction failed
        trans.Rollback();
        // log exception details . . .
        throw ex;
      }
    }
  }
}

如何使用 Transact-SQL 执行事务处理

以下存储过程阐明了如何在 Transact-SQL 存储过程内部执行事务性资金转帐操作。

CREATE PROCEDURE MoneyTransfer
@FromAccount char(20),
@ToAccount char(20),
@Amount money
AS
BEGIN TRANSACTION
-- PERFORM DEBIT OPERATION
UPDATE Accounts
SET Balance = Balance - @Amount
WHERE AccountNumber = @FromAccount
IF @@RowCount = 0
BEGIN
  RAISERROR('Invalid From Account Number', 11, 1)
  GOTO ABORT
END
DECLARE @Balance money
SELECT @Balance = Balance FROM ACCOUNTS
WHERE AccountNumber = @FromAccount
IF @BALANCE < 0
BEGIN
  RAISERROR('Insufficient funds', 11, 1)
  GOTO ABORT
END
-- PERFORM CREDIT OPERATION
UPDATE Accounts 
SET Balance = Balance + @Amount 
WHERE AccountNumber = @ToAccount
IF @@RowCount = 0
BEGIN
  RAISERROR('Invalid To Account Number', 11, 1)
  GOTO ABORT
END
COMMIT TRANSACTION
RETURN 0
ABORT:
  ROLLBACK TRANSACTION
GO

该存储过程使用 BEGIN TRANSACTION、COMMIT TRANSACTION 和 ROLLBACK TRANSACTION 语句来手动控制该事务。

如何编写事务性 .NET 类

以下示例代码显示了三个服务性 .NET 托管类,这些类经过配置以执行自动事务处理。每个类都使用 Transaction 属性进行了批注,该属性的值确定是否应该启动新的事务流,或者该对象是否应该共享其直接调用方的事务流。这些组件协同工作来执行银行资金转帐任务。Transfer 类被使用 RequiresNew 事务属性进行了配置,而 DebitCredit 被使用 Required 进行了配置。结果,所有这三个对象在运行时都将共享同一事务。

using System;
using System.EnterpriseServices;
[Transaction(TransactionOption.RequiresNew)]
public class Transfer : ServicedComponent
{
  [AutoComplete]
  public void Transfer( string toAccount, 
                        string fromAccount, decimal amount )
  {
    try
    {
      // Perform the debit operation
      Debit debit = new Debit();
      debit.DebitAccount( fromAccount, amount );
      // Perform the credit operation
      Credit credit = new Credit();
      credit.CreditAccount( toAccount, amount );
    }
    catch( SqlException sqlex )
    {
      // Handle and log exception details
      // Wrap and propagate the exception
      throw new TransferException( "Transfer Failure", sqlex );    
    }
  }
}
[Transaction(TransactionOption.Required)]
public class Credit : ServicedComponent
{
  [AutoComplete]
  public void CreditAccount( string account, decimal amount )
  {
    try
    {
      using( SqlConnection conn = new SqlConnection(
              "Server=(local); Integrated Security=SSPI"; database="SimpleBank") )
      {
        SqlCommand cmd = new SqlCommand("Credit", conn );
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.Parameters.Add( new SqlParameter("@AccountNo", account) );
        cmd.Parameters.Add( new SqlParameter("@Amount", amount ));
        conn.Open();
        cmd.ExecuteNonQuery();
      }
    }
  }catch( SqlException sqlex ){
     // Log exception details here
     throw; // Propagate exception
  }
}
[Transaction(TransactionOption.Required)]
public class Debit : ServicedComponent
{
  public void DebitAccount( string account, decimal amount )
  {
    try
    {
      using( SqlConnection conn = new SqlConnection(
              "Server=(local); Integrated Security=SSPI"; database="SimpleBank") )
      {
        SqlCommand cmd = new SqlCommand("Debit", conn );
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.Parameters.Add( new SqlParameter("@AccountNo", account) );
        cmd.Parameters.Add( new SqlParameter("@Amount", amount ));
        conn.Open();
        cmd.ExecuteNonQuery();
      } 
    }
    catch (SqlException sqlex)
    {
      // Log exception details here
      throw; // Propagate exception back to caller
    }
  }
}

合作者

非常感谢下列投稿人和审阅者:

Bill Vaughn、Mike Pizzo、Pablo Castro、Doug Rothaus、Kevin White、Blaine Dokter、David Schleifer、Graeme Malcolm (Content Master)、Bernard Chen (Sapient)、Matt Drucker (Turner Broadcasting)、Steve Kirk、David Sceppa、Scott Densmore、Diego González (Lagash Systems)

本日志由 flyinweb 于 2009-06-20 08:35:56 发表到 DotNet专栏 中,目前已经被浏览 151 次,评论 0 次;

作者添加了以下标签: .NET 数据访问体系结构

.NET 数据访问架构指南

Alex Mackman, Chris Brooks, Steve Busby, 和 Ed Jezierski 
微软公司 
2001年10月 
概述:本文提供了在多层.NET应用程序中实施基于ADO.NET的数据访问层的指导原则。其重点是一组通用数据访问任务和方案,并指导你选择最合适的途径和技术(68张打印页)。 

目录 

ADO.NET简介 
管理数据库链接 
错误处理 
性能 
通过防火墙建立链接 
处理 BLOBs 
事务处理 
数据分页 
简介 

如果你在为.NET应用程序设计数据访问层,那么就应该把 Microsoft ADO.NET用作数据访问模型。ADO.NET扩展丰富,并且支持结合松散的数据访问需求、多层Web应用程序及Web服务。通常,它利用许多扩展丰富的对象模型, ADO.NET提供了多种方法用于解决一个特定问题。 

本文将指导你选择最合适的数据访问方法,其做法是详细列出大范围的通用数据访问方案,提供运用技巧,并且建议最优实践。本文还回答了其它经常问到的问题:何处最适合存放数据库链接字符串?应如何实现链接存储池?如何处理事务?如何实现分页以允许用户在许多记录中滚动? 

注意本文的重点是ADO.NET的使用:利用SQL Server .NETData Provider--随ADO.NET一起提供的两个供应器之一--访问Microsoft SQL Server 2000。本文在合适的地方,将突出显示在你使用OLE DB .NET数据供应器访问其它OLE DB敏感数据源时需要注意的所有差别。 

对于利用本文所讨论的指导原则和最优实践所开发的数据访问组件的具体实现,见(Data Access Application Block)数据访问应用程序块。注意,本实现的源代码是可以获得的,并且能直接用于你的.NET应用程序中。 

谁应当阅读本文? 

本文为希望构建.NET应用程序的应用程序设计师和企业开发人员提供了指导原则。如果你负责设计并开发多层.NET应用程序的数据层,那么请阅读本文。 

你首先需要知道什么? 

要利用本指南构建.NET应用程序,你必须有利用ActiveX数据对象(ADO)和/或 OLE DB开发数据访问代码的实际经验,及SQL Server经验。你也必须明白如何为.NET平台开发管理代码,并且也必须清楚ADO.NET数据访问模型引入的基本变化。有关.NET开发的更多信息,见http://msdn.microsoft.com/net 。 

ADO.NET简介 

ADO.NET是.NET应用程序的数据访问模型。它能用于访问关系型数据库系统,如SQL Server 2000,及很多其它已经配备了OLE DB供应器的数据源。在某种程度上,ADO.NET代表了最新版本的ADO技术。然而,ADO.NET引入了一些重大变化和革新,它们专门用于结构松散的、本质非链接的Web应用程序。关于ADO 与 ADO.NET的比较,见MSDN中的“用于ADO程序员的ADO.NET”一文。 

ADO.NET引入的一个重要变化是,用DataTable, DataSet, DataAdapter, 和 DataReader对象的组合代替了ADO Recordset对象。DataTable表示来自一个表的行集合,在这方面它与Recordset类似。DataSet表示DataTable对象的集合,及与其它表绑定在一起的关系和限制。实际上,DataSet是具有内置的扩展标记语言(XML)支持的内存中的关联结构。 

DataSet的一个主要特点是,它对底层的数据源一无所知,而这些数据源可能用于对其进行填充。这是一个分离的用于表示数据集合的独立实体,并且它可通过多层应用程序的不同层由一个组件传递到另一组件。它也可作为XML 数据流被序列化,因而非常适合于不同类型平台间的数据传输。ADO.NET使用DataAdapter对象为发送到和来自DataSet及底层数据源的数据建立通道。DataAdapter对象还支持增强的批更新特性,以前这是Recorder的相关功能。 

图1显示了完整的DataSet对象模型。 

按此在新窗口打开图片
图1 DataSet 对象模型 

.NET 数据供应器 

ADO.NET 依靠.NET 数据供应器的服务。 它们提供了对底层数据源的访问,包括四个主要对象(Connection, Command, DataReader,及DataAdapter),目前,ADO.NET只发行了两个供应器: 

SQL Server .NET 数据供应器。这是用于Microsoft SQL Server 7.0及其以后版本数据库的供应器,它优化了对SQL Server的访问,并利用 SQL Server内置的数据转换协议直接与SQL Server通信。 
当链接到SQL Server 7.0 或 SQL Server 2000时,总是要使用此供应器。 
OLE DB .NET 数据供应器。. 这是一个用于管理OLE DB 数据源的供应器。它的效率稍低于SQL Server .NET Data Provider,因为在与数据库通信时,它需通过OLE DB层进行呼叫。注意,此供应器不支持用于开放数据库链接(ODBC),MSDASQL的OLE DB供应器。对于ODBC数据源,应使用ODBC .NET数据供应器。有关与ADO.NET兼容的OLE DB供应器列表,见。 
目前测试版中的其它.NET数据供应器包括: 

ODBC .NET 数据供应器。目前Beta 1.0版可供下载。它提供了对ODBC驱动器的内置访问,其方式与OLE DB .NET数据供应器提供的对本地OLE DB供应器的访问方式相同。关于ODBC .NET及Beta版下载的更多信息见. 
用于从SQL Server 2000中得到XML的管理供应器。用于SQL Server Web升级2版的XML还包括了专用于从SQL Server 2000中得到XML的管理供应器。关于此升级版本的更多信息,见 . 
名称空间组织 

与每个.NET数据供应器相关的类型(类,结构,枚举,等等)位于它们各自的名称空间中: 

System.Data.SqlClient. 包含了 SQL Server .NET 数据供应器类型。 
System.Data.OleDb. 包含了 OLE DB .NET数据供应器类型。 
System.Data.Odbc. 包含了ODBC .NET数据供应器类型。 
System.Data. 包含了独立于供应器的类型,如DataSet及DataTable。 
在各自关联的名称空间中,每个供应器都提供了Connection, Command, DataReader, 及 DataAdapter对象的实现。SqlClient实现都有前缀"Sql";而OleDb实现前面都有前缀"OleDb"。例如,Connection对象的 SqlClient实现是SqlConnection。而OleDb实现是OleDbConnection。类似的,DataAdapter对象的两种实现是SqlDataAdapter 和OleDbDataAdapter。 

通用编程 

如果你很有可能以不同的数据源为目标,并希望将代码从一种数据源移植到另一数据源,那么可以考虑对System.Data名称空间中的IDbConnection, IDbCommand, IDataReader,和IDbDataAdapter接口进行编程。Connection, Command, DataReader, 及 DataAdapter对象的所有实现都必须支持这些接口。 

关于实现.NET数据供应器的更多信息,见http://msdn.microsoft.com/library/en-us/cpguide/html/cpconimplementingnetdataprovider.asp. 

图2显示了数据访问堆栈及ADO.NET如何与其它数据访问技术,包括ADO和OLE DB,联系起来。该图还显示了ADO.NET模型中的两个管理供应器和主要对象。 

按此在新窗口打开图片
图2 数据访问堆栈 

关于ADO到ADO.NET的演化,见MSDN杂志2000年11月期的文章“ADO+简介:用于微软.NET框架的数据访问服务”。 

存储过程与直接SQL的比较 

在本文剩余部分的大部分代码片段中,都使用了SqlCommand对象调用存储过程去执行数据库操作。在一些例子中,你见不到SqlCommand对象,因为存储过程名直接传递给了SqlDataAdapter对象,但这仍将导致SqlCommand对象的创建。 

使用存储过程而非SQL语句的原因是: 

存储过程通常会使性能增加,因为数据库可以优化过程使用的数据访问计划,并对其进行缓存以备将来重用。 
在数据库中,存储过程可分别得到保护。客户可以被给予执行某个存储过程的权限,但无权处理底层的表。 
存储过程将导致维护简单,因为在一个已部署组件内,修改存储过程通常要比修改硬编码的SQL语句简单。 
存储过程增加了一个从底层的数据库结构中提取出的层。存储过程的客户与存储过程的实现细节及底层结构被隔离开了。 
存储过程可以降低网络流量,因为SQL语句可以以批处理的方式执行,而不是从客户端发送多个请求。 
属性与构造函数的比较 

可以通过构造函数参数或直接设置属性来为ADO.NET对象设置具体的属性值。例如,下面的代码片段在功能上是等同的。 

  1. // Use constructor arguments to configure command object  
  2. SqlCommand cmd = new SqlCommand( "SELECT * FROM PRODUCTS", conn );  
  3.  
  4. // The above line is functionally equivalent to the following  
  5. // three lines which set properties explicitly  
  6. sqlCommand cmd = new SqlCommand();  
  7. cmd.Connection = conn;  
  8. cmd.CommandText = "SELECT * FROM PRODUCTS"


从性能角度来说,两种方法的差别可以忽略,因为设置或获得.NET对象的属性比对COM对象执行类似操作要有效得多。 

所作出的选择只是个人爱好和编码风格而已。然而,明确地设置属性的确使代码易于理解(特别是当你不熟悉ADO.NET对象模型时),便于调试。 

注意 过去,VB开发人员被建议避免使用"Dim x As New…"结构创建对象。在COM环境中,这些代码将导致COM对象创建过程的“短路”,产生一些奇妙的和不怎么奇妙的错误。然而,在.NET环境中,这已不再是一个问题。 

管理数据库链接 

数据库链接是一种危险的、昂贵的、有限的资源,特别是在多层Web应用程序中。你必须正确管理你的链接,因为你的方法将极大的影响应用程序的整体升级性。还有,必须仔细考虑在哪儿存放链接字符串。你需要一个可配置的、安全的位置。 

在管理数据库链接和链接字符串时,你应当努力: 

通过跨多个客户多路复用一池数据库链接来帮助实现应用程序的扩展性。 
采用可配置的、高性能的链接池战略。 
在访问SQL Server时使用微软Windows操作系统认证。 
避免中间层的冒充。 
安全地存储链接字符串。 
较晚地打开数据库链接,而较早地关闭它们。 
本节讨论链接池,并帮你选择合适的链接池战略。其它可选方法也是存在的。本节也将考虑如何管理、存储、控制数据库链接字符串。最后,本节还提供了两个编码方案,使用它们将有助于确保链接已可靠关闭,并返回到链接池中。 

链接池 

数据库链接池使应用程序能够重用池中的现有链接,而不是重复地建立对数据库的链接。这种技术将极大地增加应用程序的可扩展性,因为有限的数据库链接可以为很多的客户提供服务。此技术也将提高性能,因为能够避免用于建立新链接的巨大时间。 

数据访问技术,如ODBC和OLE DB,提供了多种形式的链接池,它们可配置到不同级别上。这两种方式对数据库客户端应用程序来说都是透明的。OLE DB链接池经常被称为会话或资源池。 

关于微软数据访问组件(MDAC)中池的一般讨论,见http://msdn.microsoft.com/library/en-us/dnmdac/html/pooling2.asp。 

ADO.NET数据供应器提供了透明的链接池,每种链接池的确切机制对每种供应器来说是不同的。本节讨论的链接池是关于: 

SQL Server .NET数据供应器 
OLE DB .NET数据供应器 
用SQL Server .NET 数据供应器池化 

如果正在使用SQL Server .NET数据供应器,那么就可使用该供应器提供的链接池化支持特性。它是由供应器在管理代码内内置实现的对事务敏感的高效机制。每个过程都将创建池,并且直到过程结束,池才被取消。 

你可以透明地使用此种链接池,但应当清楚池是如何被管理的,并要知道可以用哪些选项来调整链接池。 

如何配置SQL Server .NET数据供应器链接池 

可以使用一组名称-值对以链接字符串的形式配置链接池。例如,可以配置池是否有效(默认是有效的),池的最大、最小容量,用于打开链接的排队请示被阻断的时间。下面的示例字符串配置了池的最大和最小容量。 

  1. "Server=(local); Integrated Security=SSPI; Database=Northwind;   
  2. Max Pool Size=75; Min Pool Size=5" 



当链接打开,池被创建时,多个链接增加到池中以使链接数满足所配置的最小值。此后,链接就能增加到池中,直到配置的最大池计数。当达到最大计数时,打开新链接的请求将排队一段可配置的时间。 

选择池容量 

能建立最大极限对于管理几千用户同时发出请求的大型系统来说是非常重要的。你需要监视链接池及应用程序的性能,以确定系统的最优池容量。最优容量还要依赖于运行SQL Server的硬件。 

在开发期间,也许需要减小默认的最大池容量(目前是100)以帮助查找链接泄漏。 

如果设立了最小池容量,那么当池最初被填充以达到该值时,会导致一些性能损失,尽管最初链接的几个客户会从中受益。注意,创建新链接的过程被序列化了,这就意味着当池最初被填充时,服务器无法处理同时发生的请求。 

关于监视链接池的更多信息,见本文监视链接池一节。关于链接池链接字符串关键字的完整列表,见http://msdn.microsoft.com/library/en-us/cpguide/html/cpconconnectionpoolingforsqlservernetdataprovider.asp。 

更多信息 

在使用SQL Server .NET数据供应器链接池时,必须清楚: 

链接是通过对链接字符串精确匹配的法则被池化的。池化机制对名称-值对间的空格敏感。例如,下面的两个链接字符串将生成单独的池,因为第二个字符串包含了一个额外的空字符。 

  1. SqlConnection conn = new SqlConnection(  
  2.          "Integrated Security=SSPI;Database=Northwind");  
  3. conn.Open(); // Pool A is created  
  4.  
  5. SqlConmection conn = new SqlConnection(  
  6.          "Integrated Security=SSPI ; Database=Northwind");  
  7. conn.Open(); // Pool B is created (extra spaces in string) 



在.NET框架Beta版中,当在调试器中运行时,链接池化总是失效了。在调试器外,对调试版和发行版,链接池都能正常运作。.NET框架的最终发行版(RTM)取消了这种限制,链接池在所有情况下都能运行。 
链接池被划分为了多个特定于事务的池和一个用于目前没有列在事务中的多个链接的池。对于与特定事务上下文相关的线程,将从(包含了与事务建立的链接的)合适的池中返回链接。这使得使用已建立的链接成为透明过程。 
用OLE DB .NET数据供应器池化 

OLE DB .NET数据供应器利用OLE DB资源池化的底层服务将链接存储到池中。很多方法可用于配置资源池化: 

可以使用链接字符串来配置、使能资源池化或使其使失效。 
可以使用注册表。 
可以通过程序来配置资源池化。 
为了避开与注册表相关的部署问题,应避免使用注册表配置OLE DB资源池化。 

关于OLE DB 资源池化的更多细节,见MSDN中“OLE DB程序员参考”一书的第19章:OLE DB服务中的资源池化部分。 

用池化对象管理链接池化 

作为Windows DNA开发人员,建议你使OLE DB资源池化和/或ODBC链接池化失效,并把COM+对象池化用作将数据库链接存储到池中的技术。这样做主要出于两个原因: 

池容量和极限可以(在COM+目录)被明确配置。 
性能提高了。池化对象的方法可以成倍的胜过固有池化。 
然而,由于SQL Server .NET数据供应器内置地使用池化,所以(在使用此供应器时)你不再需要开发自己的对象池化机制。这样就可以避免手工事务征募带来的复杂性。 

如果正在使用OLE DB .NET数据供应器,那么考虑COM+对象池化以从高级配置和改进的性能中受益。如果你为此目的开发一个池化对象,那么必须使用OLE DB资源池化和自动事务征募失效(例如,通过将“OLE DB Services=-4”包含进链接字符串中)。必须在池化对象的实现中处理事务征募。 

监视链接池化 

要监视应用程序对链接池化的应用情况,可以使用随SQL Server发行的Profiler工具,或随微软Windows 2000发行的性能监视器。 

要利用SQL Server Profiler 监视链接池化,操作如下: 

单击开始,指向程序,指向Microsoft SQL Server,然后单击Profiler运行Profiler。 
在文件菜单中,指向新建,然后单击跟踪。 
提供链接内容,然后单击确定。 
在跟踪属性对话框中,单击事件标签。 
在已选事件类别列表中,确保审核登录和审核登出事件显示在安全审核下面。 
单击运行开始跟踪。在链接建立时,将会看到审核登录事件;在链接关闭时看到审核登出事件。 
要通过性能监视器监视链接池化,操作如下: 

单击开始,指向程序,指向管理工具,然后单击性能运行性能监视器。 
在图表背景中右击,然后单击增加计数器。 
在性能对象下拉列表框中,单击SQL Server:通用统计。 
在出现的列表中,单击用户链接。 
单击增加,然后单击关闭。 
注意 .NET框架的RTM版本将另外包含一组ADO .NET性能计数器(这些计数器能与性能监视器结合起来使用),这些计数器用于为SQL Server .NET数据供应器监视并积累链接池化状态。 

管理安全性 

尽管数据库链接池化提高了应用程序的整体扩展性,这也意味着你不再能够在数据库端管理安全性。这是因为为了支持链接池化,链接字符串必须是相同的。如果需要跟踪每个用户的数据库操作,那么考虑为每个操作增加一个参数,通过这个参数就可以传递用户身份,手工将用户活动记入数据库。 

使用Windows 认证 

在链接到SQL Server时,应当使用Windows认证,因为它提供了许多优点: 

安全性易于管理,因为使用了单一(Windows)安全模型而不是分散的SQL Server安全模型。 
避免了在链接字符串中嵌入用户名和密码。 
用户名和密码不是以明文方式在网络中传输的。 
通过密码过期期限,最小长度,多次无效登录请求后帐号锁定提高了登录的安全性。 
性能 

.NETBeta 2版的性能测试表明,使用Windows认证与使用SQL Server认证相比,要花费更多的时间才能打开池化的数据库链接。然而,尽管Windows认证的成本较高,但与执行一个命令或存储过程所花费的时间相比,其(引起的)性能损失相对来说并不重要。结果,上面所列出的Windows认证的优点通常会稍微超过性能损失。 

同样,当打开一个池化链接时,在.NET框架的RTM版本中,Windows认证与SQL Server认证的差别有望变得更不明显。 

避免在中间层中冒充 

Windows认证需要访问数据库的Windows帐号。虽然看上去在中间层中使用冒充更符合逻辑,但必须避免这样做,因为损害链接池化并对应用程序的扩展性产生严重影响。 

为了解决这个问题,考虑对有限的Windows帐号(而不是被认证的负责人)实施冒充,每个帐号代表一个特定的角色。 

例如,可以考虑下面的方法: 

创建两个Windows帐号,一个用于读操作,一个用于写操作(也可以用单独的帐号映射针对特定应用程序的角色。例如,可以为互联网用户使用一个帐号,而为内部操作员和/或管理员使用另外的帐号)。 
将每个帐号映射到一个SQL Server数据库角色,然后为每个角色设置所需的数据库权限。 
在数据访问层中使用应用程序逻辑确定执行数据库操作时,哪个Windows帐号需要冒充。 
注意 每个帐号必须是同一域或信任域中在Internet信息服务(IIS)和SQL Server中存在的域帐号;也可以是在每台计算机上创建(具有相同用户名和密码)的匹配帐号。 

为网络库使用TCP/IP 

SQL Server 7.0及其以后版本支持用于所有网络库的Windows认证。使用TCP/IP可以获得配置、性能及扩展性优点。关于使用TCP/IP的更多信息,见本文通过防火墙建立链接 一节。 

存储链接字符串 

有多种方法可存储链接字符串,每种方法具有不同程度的灵活性和安全性。尽管在源代码中对字符串进行硬编码提供了最优性能,但文件系统缓存确保了与在文凭系统外部存储字符串相关的性能损失可被忽略。实际上外部链接字符串(允许管理员进行配置)所提供的附加灵活性在任何情况下都是受欢迎的。 

选择存储链接字符串的方法时,首先要考虑的两个重要因素是配置的安全性与简易性,其次是性能。 

可以选择将数据库链接字符串存储在下列位置: 

应用程序配置文件 例如用于ASP.NET Web应用程序的Web.config文件。 
通用数据链接文件(UDL) (只被OLE DB .NET 数据供应器所支持) 
Windows 注册表 
定制文件 
COM+ 目录,通过过使用构造字符串(只用于服务组件) 
使用Windows认证访问SQL Server,就可以避免在链接字符串存储用户名和密码。如果 安全需求要求更严格的方式,那么就考虑以加密格式存储链接字符串。 

对于ASP.NET Web应用程序,以加密格式将链接字符串存储在Web.config文件中是一种安全而可配置的解决方案。 

注意,在链接字符串中将Persist Security Info命名值设置为假,就可以阻止利用SqlConnection 或OleDbConnection对象的ConnectionString属性返回对安全敏感的内容,如密码。 

下面几个小节讨论了如何用这些方法存储链接字符串,并说明了相对的优点和缺点。这使你能根据特定的应用程序环境作出相应的的选择。 

使用XML应用程序配置文件 

可以使用元素appSettings将数据库链接字符串存储在应用程序配置文件的定制设置部分。该元素支持任意关键字-值对,如下面的代码片段所示: 

  1. <configuration> 
  2.  <appSettings> 
  3.   <add key="DBConnStr" 
  4.      value="server=(local);Integrated Security=SSPI;database=northwind"/> 
  5.  </appSettings> 
  6. </configuration> 


注意:appSettings元素现在在configuration元素下面,并且不能直接出现在system.web下面。 

优点 

易于部署。通过常规.NET xcopy部署,链接字符串随配置文件一起被部署。 
通过程序易于访问。ConfigurationSettings类的AppSettings属性使得在运行时读取数据库链接字符串更为简单。 
支持动态更新(仅限于ASP.NET)。如果管理员更新了Web.config文件中的链接字符串,那么下次在字符串被访问时所作出的变化生效,这对一个无状态的组件来说,就象客户再次利用组件作出了数据访问请求一样。 
缺点 

安全性。尽管ASP.NET Internet 服务器应用程序编程接口(ISAPI)DLL阻止了客户直接访问带.config扩展名的文件,并且NTFS文件系统权限也用于进一步限制访问,但你可能仍希望避免以明文方式将这些内容存储在前端的Web服务器上。要增加安全性,需将链接字符串以加密格式存储在配置文件中。 

更多信息 

利用System.Configuration.ConfigurationSettings类的AppSettings静态属性,可以获取应用程序的定制设置。如下面的代码片段所示,此处假定先前示例的定置关键字为DBConnStr。 

  1. using System.Configuration;  
  2. private string GetDBaseConnectionString()  
  3. {  
  4.   return ConfigurationSettings.AppSettings["DBConnStr"];  

关于配置.NET 框架应用程序的更多信息,见http://msdn.microsoft.com/library/en-us/cpguide/html/cpconconfiguringnetframeworkapplications.asp. 

使用UDL文件 

OLE DB .NET数据供应器支持在它的链接字符串中使用统一数据链接(UDL)文件名。可以以构建参数的形式将链接字符串传给OleDbConnection对象,或利用对象的ConnectionString属性设置链接字符串。 

注意 SQL Server .NET数据供应器不支持在它的链接字符串中使用UDL文件。因此,只有使用OLE DB .NET数据供应器,此方法才有效。 

对于OLE DB 供应器,要利用链接字符串引用UDL文件,使用“File Name=name.udl.”。 

优点 

标准方法。你也许已经在用UDL文件进行链接字符串的管理了。 

缺点 

性能。每次打开链接时,包含UDLs的链接字符串都被读取并被解析。 
安全性。UDL文件以纯文本格式存储。利用NFTS文件权限可以确保这些文件的安全性,但这样做将引发与使用.config文件相同的问题。 
SqlClient对象不支持UDL文件。此方法不被 SQL Server .NET数据供应器所支持,而你要用此供应器访问 SQL Server 7.0及其以后版本。

 
更多信息 

必须确保管理员拥有该文件的读/写访问权限以便进行管理,并且还要确保运行应用程序的身份拥有读权限。对于ASP.NET Web应用程序,应用程序工作者进程默认是以SYSTEM帐号运行的,但利用机器范围的配置文件(Machine.config)中的元素可以将其覆盖掉。利用Web.config文件中的元素,及一个可选的指定帐号,可以进行冒充。 
对于Web应用程序,要确保没有将UDL文件放在虚目录中,因为那样会使该文件可通过网络下载。 
关于这些及其它与安全性相关的ASP.NET特性的更多信息,见http://msdn.microsoft.com/library/en-us/dnbda/html/authaspdotnet.asp.。 

使用Windows注册表 

可以利用定制关键字将链接字符串存储在Windows注册表中,但由于部署问题,建议不要使用。 

优点 

安全性。利用访问控制列表(ACLs),可以对所选的注册表关键字的访问进行管理。对更高级别的安全性,考虑对数据进行加密。 
通过程序易于访问。.NET类支持从注册表中读取字符串。 
缺点 

部署。相关的注册表设置必须同应用程序一起部署,从某种程度上抵消了xcopy部署的优点。 
使用定置文件 

可以使用定制文件来存储链接字符串,然而这种技术没有优点,因此并不推荐使用。 

优点 

没有 
缺点 

额外编码。这种方法需要额外编码,并迫使你明确处理同时发生的问题。 
部署。此文件必须同其它ASP.NET应用程序文件一起拷贝。避免将此文件放在ASP.NET应用程序的目录或子目录中,就可以阻止通过网络对其进行下载。 

使用构建参数和COM+目录 

可以将链接字符串存储在COM+目录中,并利用对象的构造字符串将它自动地传递给对象。COM+在初始化对象,提供配置构造字符串后,将立即调用对象的Construct方法。 

注意这个方法只用于服务组件。只有管理组件使用了其它服务,如分布式事务处理支持或对象池化时,才考虑使用此方法。 

优点 

管理性。利用组件服务MMC插件,管理员可以很方便地配置链接字符串。 
缺点 

安全性。COM+目录被认为是一个不安全的存储区(虽然利用COM+角色你可以限制对它的访问),并因此不能用于以明文维护链接字符串。 
部署。COM+目录中的条目必须随.NET应用程序一同部署。如果使用了其它企业服务,如分布式事务或对象池化,那么将数据库链接字符串存储在目录中不会增加部署的额外开销,因为要支持其它服务,必须部署COM+目录。 
必须为组件提供服务。可以只为所服务的组件使用构造字符串。要使能构造字符串,不能简单地从ServicedComponent类中派生所需组件类(这将为组件提供服务)。 

更多信息 

关于如何为对象构造配置.NET类的更多信息,见附录中的如何为.NET类使能对象构造 。 
关于开发服务组件的更多信息,见http://msdn.microsoft.com/library/en-us/cpguide/html/cpconwritingservicedcomponents.asp。 
链接使用方式 

不管何种.NET数据供应器,你必须总是: 

尽可能晚地打开数据库链接。 
以尽可能短的时间使用该链接。 
尽可能快地关闭该链接。链接直到通过Close或Dispose方法关闭后,它才返回到池中。即使发现它处于崩溃状态,也应当关闭它。这样做确保了它能返回池中,并被标记为无效。对象池周期性地扫描池,以查找已被标记为无效的对象。 
为确保在方法返回前链接已经关闭,考虑使用下面两个代码片段中演示的方法。第一个示例使用了finally块,第二个示例使用了C# using声明,此声明确保了对象的Dispose方法被调用。 

下面的代码确保finally块关闭了链接。注意,此方法只用于Visual Basic .NET及C#中,因为Visual Basic .NET支持结构化例外处理。 

  1. public void DoSomeWork()  
  2. {  
  3.   SqlConnection conn = new SqlConnection(connectionString);  
  4.   SqlCommand cmd = new SqlCommand("CommandProc", conn );  
  5.   cmd.CommandType = CommandType.StoredProcedure;  
  6.  
  7.   try 
  8.   {  
  9.     conn.Open();  
  10.     cmd.ExecuteNonQuery();  
  11.   }  
  12.   catch (Exception e)  
  13.   {  
  14.     // Handle and log error  
  15.   }  
  16.   finally 
  17.   {  
  18.     conn.Close();  
  19.   }  


现在的代码显示了另外一种方法,此方法使用了C# using声明。注意,Visual Basic .NET并不支持using声明,或任何功能相同的对应语句。 

  1. public void DoSomeWork()  
  2. {  
  3.   // using guarantees that Dispose is called on conn, which will  
  4.   // close the connection.  
  5.   using (SqlConnection conn = new SqlConnection(connectionString))  
  6.   {  
  7.     SqlCommand cmd = new SqlCommand("CommandProc", conn);  
  8.     fcmd.CommandType = CommandType.StoredProcedure;  
  9.     conn.Open();  
  10.     cmd.ExecuteQuery();  
  11.   }  
  12. }  

此方法也适用于其它对象,如SqlDataReader 或OleDbDataReader,在其它任何对象对当前链接进行处理前,这些对象必须被关闭。 

错误处理 

ADO.NET错误生成后,将由.NET框架内置的底层结构化异常处理支持所处理。结果,在数据访问代码中的错误处理方式与应用程序中其它地方的错误处理方式完全相同。通过标准的.NET异常处理语法和技术,异常被检测到并被处理。 

本节描述了如何开发强壮的数据访问代码,并解释了如何处理数据访问错误。本节还提供了与SQL Server .NET数据供应器相关的异常处理详尽指南。 

.NET 异常 

.NET数据供应器将特定的数据库的错误状态转化为标准的异常类型,应当在数据访问代码中对这些异常进行处理。通过相关的异常对象的属性,可以获得特定数据库的错误细节。 

所有.NET异常类型最终是从System名称空间的Exception基类中派生的。.NET数据供应器释放特定的供应器异常类型。例如,一旦SQL Server 返回一个错误状态时,SQL Server .NET数据供应器释放SqlException对象。类似的,OLE DB .NET数据供应器释放 OleDbException类型的异常,此对象包含了由底层OLE DB供应器暴露的细节。 

图3显示了.NET数据供应器异常的层次结构。注意,OleDbException类是从 ExternalException类派生的ExternalException类是所有COM例外的基类。对象的ErrorCode属性存储了OLE DB生成的COM HRESULT。 

按此在新窗口打开图片
图3 NET数据供应器层次结构 

缓存并处理.NET异常 

要处理数据访问例外状态,将数据访问代码放在try块中,并在catch块中利用合适的过滤器捕获生成的任何例外。例如,当利用SQL Server .NET数据供应器编写数据访问代码时,应当捕获SqlException类型的异常,如下面的代码所示: 

  1. try 
  2. {  
  3.   // Data access code  
  4. }  
  5. catch (SqlException sqlex) // more specific  
  6. {  
  7. }  
  8. catch (Exception ex) // less specific  
  9. {  


如果为不止一个catch声明提供了不同的过滤标准,记住,按最特殊类型到最不特殊类型的顺序排列它们。通过这种方式,catch块中最特殊类型将将为任何给定的类型所执行。 

SqlException 类所暴露的属性包含了例外状态的细节。其中包括: 

Message属性,它包含了用于描述错误的文本。 
Number属性,它包含唯一标识错误类型的错误号。 
State属性。它包含了关于错误启用状态的附加信息。它经常用于指示特殊错误状态的某个特定事件。例如,如果单一存储过程从不止一行中生成同样的错误,那么本属性将用于标识某个具体的事件。 
Errors集合。它包含了SQL Server生成的错误的详细信息。此集合部是包含至少一个SqlError类型的对象。 
下面的代码片段演示了如何利用SQL Server .NET数据供应器处理SQL Server 错误状态: 

  1. using System.Data;  
  2. using System.Data.SqlClient;  
  3. using System.Diagnostics;  
  4.  
  5. // Method exposed by a Data Access Layer (DAL) Component  
  6. public string GetProductName( int ProductID )  
  7. {  
  8.   SqlConnection conn = new SqlConnection(  
  9.         "server=(local);Integrated Security=SSPI;database=northwind");  
  10.   // Enclose all data access code within a try block  
  11.   try 
  12.   {  
  13.     conn.Open();  
  14.     SqlCommand cmd = new SqlCommand("LookupProductName", conn );  
  15.     cmd.CommandType = CommandType.StoredProcedure;  
  16.  
  17.     cmd.Parameters.Add("@ProductID", ProductID );  
  18.     SqlParameter paramPN =   
  19.          cmd.Parameters.Add("@ProductName", SqlDbType.VarChar, 40 );  
  20.     paramPN.Direction = ParameterDirection.Output;  
  21.  
  22.     cmd.ExecuteNonQuery();  
  23.     // The finally code is executed before the method returns  
  24.     return paramPN.Value.ToString();    
  25.   }  
  26.   catch (SqlException sqlex)  
  27.   {  
  28.     // Handle data access exception condition  
  29.     // Log specific exception details  
  30.     LogException(sqlex);  
  31.     // Wrap the current exception in a more relevant  
  32.     // outer exception and re-throw the new exception  
  33.     throw new DALException(  
  34.                   "Unknown ProductID: " + ProductID.ToString(), sqlex );  
  35.   }  
  36.   catch (Exception ex)  
  37.   {  
  38.     // Handle generic exception condition . . .  
  39.     throw ex;  
  40.   }  
  41.   finally 
  42.   {  
  43.     conn.Close(); // Ensures connection is closed  
  44.   }  
  45. }  
  46.  
  47. // Helper routine that logs SqlException details to the   
  48. // Application event log  
  49. private void LogException( SqlException sqlex )  
  50. {  
  51.   EventLog el = new EventLog();  
  52.   el.Source = "CustomAppLog";  
  53.   string strMessage;  
  54.   strMessage = "Exception Number : " + sqlex.Number +   
  55.                "(" + sqlex.Message + ") has occurred";  
  56.   el.WriteEntry( strMessage );  
  57.  
  58.   foreach (SqlError sqle in sqlex.Errors)  
  59.   {  
  60.     strMessage = "Message: " + sqle.Message +  
  61.                  " Number: " + sqle.Number +  
  62.                  " Procedure: " + sqle.Procedure +  
  63.                  " Server: " + sqle.Server +  
  64.                  " Source: " + sqle.Source +  
  65.                  " State: " + sqle.State +  
  66.                  " Severity: " + sqle.Class +  
  67.                  " LineNumber: " + sqle.LineNumber;  
  68.     el.WriteEntry( strMessage );  
  69.   }  


在SqlException catch块中,代码最初利用LogException帮助函数记录错误状态,此函数利用foreach声明枚举了Errors集合中特定于供应器的细节,并将错误细节记录到错误日志中。 Catch块中的代码然后将特定于SQL Server的例外封装在DALException类型的对象中,这样做对调用者的GetProductName方法更具有意义。例外处理程序使用关键字throw将例外传回调用者。 

更多信息 

关于SqlException类成员的完整列表,见http://msdn.microsoft.com/library/en-us/cpref/html/frlrfSystemDataSqlClientSqlExceptionMembersTopic.asp。 
关于定置例外的开发,.NET例外的记录与封装,返回例外的不同方法的使用的更多信息,见http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/exceptdotnet.asp。 

从存储过程中生成错误 

T-SQL提供了一个RAISERROR(注意拼写)函数。你可用此函数生成定置错误,并将错误返回客户。对于ADO.NET客户,SQL Server .NET数据供应器对这些数据错误进行解释,并把它们转化为SqlError对象。 

使用RAISERROR函数是简单地方法是将消息文本作为第一个参数包括进来,然后指定严重及状态参数,如下面的代码片段所示: 

  1. RAISERROR( 'Unknown Product ID: %s', 16, 1, @ProductID ) 


在这个例子中,替代参数用于将当前产品ID作为错误消息文本的一部分返回,参数2是消息的严重性,参数3是消息状态。 

更多信息 

为了避免对消息文本进行硬编码,你可以利用sp_addmessage系统存储过程或SQL Server 企业管理器将你自己的消息增加到sysmessages表中。然后你就可以使用传递到RAISERROR函数的ID引用消息了。你所定义的消息Ids必须大于50000,如下代码片段所示: 

  1. RAISERROR( 50001, 16, 1, @ProductID )  


关于RAISERROR函数的完整细节,请在SQL Server的在线书目中查询RAISERROR。 

正确使用严重性等级 

仔细选择错误严重性等级,并要清楚每个级别造成的冲击。错误严重性等级的范围是0-25,并且它用于指出SQL Server 2000所遇到的问题的类型。在客户端代码中,通过在SqlException类的Errors集合中检查SqlError对象的 Class属性,你可以获得错误的严重性。表1 指出了不同严重性等级的意义及所造成的冲击。 

表1.错误严重性等级--冲击及意义 

严重性等级 链接已关闭 生成SqlException对象 意义 
10及其以下  No No 通知型消息,并不表示犯错误状态。 
11-16 No Yes 可由用户修改的错误,例如,使用修改后的输入数据重试操作。 
17-19 No Yes 资源或系统错误。 
20-25 Yes Yes 致命的系统错误(包括硬件错误)。客户链接被终止。 

控制自动化事务 

SQL Server .NET数据供应器对它所遇到的任何严重性大于10的错误都抛出SqlException对象。当作为自动化(COM+)事务一部分的组件检测到SqlException对象后,该组件必须确保它能取消事务。这也许是,也许不是自动化过程,并要依赖该方法是否已经对AutoComplete属性作出了标记。 

关于在自动化事务上下文中处理对象的更多信息,见本文中的确定事务结果一节。 

得到通知型消息 

10及其以下严重性等级用于表示通知型消息,并且不会引发SqlException对象的抛出。 

要获得通知型消息: 

>创建事件处理程序,并提交给SqlConnection对象所暴露的InfoMessage事件。下面的代码片段显示了事件代理。 

  1. public delegate void SqlInfoMessageEventHandler( object sender,   
  2.                                                      SqlInfoMessageEventArgs e ); 


通过传递到你的事件处理处理程序中的SqlInfoMessageEventArgs对象,可以得到消息数据。此对象暴露了Errors属性,该属性包含一组SqlError对象--每个通知消息一个SqlError对象。下面的代码片段演示了如何注册用于记录通知型消息的事件处理程序。 

  1. public string GetProductName( int ProductID )  
  2. {  
  3.   SqlConnection conn = new SqlConnection(  
  4.         "server=(local);Integrated Security=SSPI;database=northwind");  
  5.   try 
  6.   {  
  7.     // Register a message event handler  
  8.     conn.InfoMessage += new SqlInfoMessageEventHandler( MessageEventHandler );  
  9.     conn.Open();  
  10.     // Setup command object and execute it  
  11.     . . .  
  12.   }  
  13.   catch (SqlException sqlex)  
  14.   {  
  15.     // log and handle exception  
  16.     . . .  
  17.   }  
  18.   finally 
  19.   {  
  20.     conn.Close();  
  21.   }  
  22. }  
  23. // message event handler  
  24. void MessageEventHandler( object sender, SqlInfoMessageEventArgs e )  
  25. {  
  26.   foreach( SqlError sqle in e.Errors )  
  27.   {  
  28.     // Log SqlError properties  
  29.     . . .  
  30.   }  



性能 

本节介绍了一些常见的数据访问方案,对每种方案,以ADO.NET 数据访问代码的形式描述了最优性能和扩展性解决方案。在合适的地方,还对性能,功能及开发最作出了比较。本节考虑了下面的功能方案。 

获取多行. 获取一个结果集,并在得到的行中重复。 
获取一行. 获取具有指定关键字的一行。 
获取一项. 从指定的行中得到一项。 
确定某项数据的存在性. 检查具有特定关键字的一行是否存在。这是单项查找方案的一种变体,这里返回一个简单的布尔值就足够了。 
获取多行 

在这个方案中,你要获取一组表格化数据,并在得到的行中重复执行某个操作。例如你得到了一组数据,并以非链接的方式处理,然后(可能通过Web服务)将它作为XML文档传递给客户应用程序。可选的,你也可以以HTML表的形式将这些数据显示出来。 

为了帮助确定最合适的数据访问方法,考虑你是否需要(非链接)DataSet 对象的附加灵活性,还是只需要SqlDataReader对象提供的原有性能,这些性能非常适合于B2C Web应用程序的数据表示。图4显示了这两种基本场景。 

注意用于填充DataSet的SqlDataAdapter利用SqlDataReader方法数据。 

按此在新窗口打开图片
图4 多行数据访问方案 

方法比较 

当从数据源中获取多行时,你可以使用下面的方法: 

使用SqlDataAdapter对象生成DataSet 或 DataTabl对象。 
利用SqlDataReader对象提供只读的只向前的数据流。 
利用XmlReader对象提供只读的只向前的XML数据流。 
SqlDataReader 与 DataSet/DataTable间的选择本质上是性能与功能间的选择。SqlDataReader 提供了最优性能,而DataSet提供了额外的功能与灵活性。 

数据绑定 

所有这三个对象都可以作为数据绑定控件的数据源。而DataSet 和 DataTable 可作为更广范围控件的数据源。这是因为DataSet 和 DataTable 实现了(生成Ilist接口)IlistSource接口,而SqlDataReader 实现了Ienumerable接口。许多能进行数据绑定的WinForm控件需要实现了Ilist接口的数据源。 

这种不同是因为为每种对象类型设计的场景类型不同。DataSet (它包含 DataTable)是一个丰富的、非链接结构,它适合于Web和桌面(WinForm)应用程序。另一方面,数据阅读器已经为Web应用程序进行了优化,这种应用程序需要优化的、只能向前的数据访问。 

检查将要绑定到的特定控件类型的数据源需求。 

在应用程序层间传递数据 

DataSet提供了可作为XML被任意操纵数据的关系图,并允许数据的非链接缓存拷贝在应用程序层与组件间传递。然而,SqlDataReader提供了更优化的性能,因为它避免了与创建DataSet相关的性能及内存开销。记住,DataSet对象的创建将导致多个子对象--包括DataTable, DataRow 和DataColumn--及作为这些子对象容器的集合对象的创建。 

使用DataSet 

使用SqlDataAdapter填充的DataSet对象,当: 

你需要非链接的驻留内存的缓存数据,以便你能将它传递到其它组件或应用程序中的其它层。 
你需要内存中的数据关系图以执行XML或非XML操作。 
你正在使用的数据来自多个数据源,如多个数据库、表或文件。 
你希望更新获得的一些或所有行,并希望利用SqlDataAdapter的批更新功能。 
你要对控件绑定数据,而此控件需要支持IList接口的数据源。 
更多信息 

如果使用SqlDataAdapter生成DataSet 或 DataTable,需注意: 

不必明确打开或关闭数据库链接。SqlDataAdapter Fill方法打开数据库链接,并在此方法返回前关闭该链接。如果链接原来已经打开,那么此方法仍使链接处于打开状态。 
如果出于其它目的需要链接,那么考虑在调用Fill方法前打开链接。这样你就可以避免不必要的打开/关闭操作,提高性能。 
尽管能重复使用同一SqlCommand对象多执行同样的命令,但不要重复使用此对象执行不同的命令。 
关于如何利用SqlDataAdapter对象填充DataSet 或 DataTable对象的代码示例,见附录中的如何利用SqlDataAdapter 对象获得多行。 
使用SqlDataReader 

些劣情况,可以使用通过调用 SqlCommand 对象的ExecuteReader方法得到的SqlDataReader对象: 

正在处理大量数据时--太多了而不能在单个缓冲区内维护。 
希望减少应用程序在内存中的印迹。 
希望避免与DataSet对象创建相关的开销。 
希望对某控件执行数据绑定操作,而此控件支持实现了IEnumerable接口的数据源。 
希望流水线化数据访问,并对其优化。 
正在读取包含二进制大对象(BLOB)列的行。你可以使用SqlDataReader对象以可管理的大块为单位从数据库中将BLOB数据拉出来,而不是一次性地将所有数据提取出来。关于处理BLOB数据的更多细节,见本文处理BLOBs 一节。 
更多信息 

如果使用SqlDataReader对象,请注意: 

在数据阅读器活动期间,底层的数据库链接保持打开,并不能用于其它任何目的。尽可能早地对SqlDataReader对象调用Close方法。 
每个链接只能有一个数据阅读器。 
通过向ExecuteReader方法传递CommandBehavior.CloseConnection枚举值,可以在使用完数据阅读器后,明确地关闭链接;或者,将链接生命周期绑定到SqlDataReader对象。这预示着当SqlDataReader对象关闭时,链接也将关闭。 
在利用阅读器访问数据时,如果你知道列的底层数据类型,那么就应使用类型化存取器方法(如GetInt32 和 GetString),这是因为在读取列数据时,这些方法减少了读取列数据所需的类型转换量。 
为避免将不必要的数据从服务器发送到客户端,如果你要关闭阅读器并抛弃所有保留的结果,那么在对阅读器调用Close方法前调用命令对象的Cancel方法。Cancel方法确保了服务器的结果被抛弃,而不会被发送到客户端。相反,对数据阅读器调用Close方法会使阅读器不必要地提取出保留的结果,以清空数据流。 
如果要得到从存储过程返回的输出值或返回值,并且你在利用SqlCommand对象的ExecuteReader方法,那么在得到输出或返回值前,必须对阅读器调用Close方法。 
关于演示如何利用SqlDataReader对象的代码示例,附录中的如何利用SqlDataReader对象获取多行数据。 
使用XmlReader 

下列情况下,使用通过调用SqlCommand对象的ExecuteXmlReader方法得到的XmlReader对象: 

希望将得到的数据作为XML 处理,但不希望引发因创建DataSet对象而造成的额外性能开销,并且不需要数据的非链接缓存。 
希望利用SQL Server FOR XML 语法的功能,这种语法允许以灵活的方式从数据库中得到XML片段(即,不带根元素的XML文档)。例如,这种方法使你能够精确指定元素名,是使用元素还是使用以属性为核心的图解,图解是否随XML数据一起被返回,等等。 
更多信息 

如果使用XmlReader,请注意: 

在从XmlReader对象中读取数据时,链接必须保持打开。SqlCommand对象的 ExecuteXmlReader方法目前不支持CommandBehavior.CloseConnection枚举值,因此在使用完阅读器后必须明确关闭链接。 
对于如何使用XmlReader对象的代码示例,见附录中的如何利用 XmlReader获取多行数据。 
获取单行数据 

在这种场景中,将从数据源中获取包含一组指定列的单行数据。例如,你得到一个客户ID,并希望查找与客户相关的细节;或得到一个产品ID,并希望得到产品信息。 

方法比较 

如果要对从数据源中得到的一行数据执行绑定操作,可以用SqlDataAdapter对象填充DataSet 或DataTable对象,其方式与在先前讨论过的获取多行数据及重复场景中描述的方式相同。然而,除非特别需要DataSet 或DataTable对象的功能,否则应当避免创建这些对象。 

如果需要获取单行数据,那么请使用下面的一种方法: 

使用存储过程输出参数. 
使用SqlDataReader对象. 
这两种方法都避免了在服务器端创建结果集,在客户端创建DataSet对象的不必要额外开销。每种方法的相对性能要依赖于强度等级及数据库链接池化是否被使能。当数据库链接池化使能时,性能测试表明存储过程方法在高强度环境下(同时存在200多链接)其性能比SqlDataReader方法高近30%。 

使用存储过程输出参数 

如下情况中使用存储过程输出参数: 

要从链接池化使能的多层Web应用程序中获得一行数据。 
更多信息 

关于演示如何使用存储过程输出参数的代码示例,见附录中的使用存储过程输出参数获取一行数据。 
使用SqlDataReader对象 

下列情况,需使用SqlDataReader对象: 

除了数据值,还需要元数据时。可以利用数据阅读器的GetSchemaTable方法获取列元数据。 
未使用链接池化时。在链接池化无效时,SqlDataReader对象在所有强度环境下都是好方式;性能测试表明,在200浏览器链接时,此方法比存储过程方法在性能上要高约20%。 
更多信息 

如果知道查询结果只需返回一行,那么在调用SqlCommand对象的ExecuteReader 方法时,使用CommandBehavior.SingleRow枚举值。一些供应器,如OLE DB .NET数据供应器,用此技巧来优化性能。例如,供应器使用IRow接口(如果此接口存在)而不是代价更高的IRowset接口。这个参数对SQL Server .NET数据供应器没有影响。 
在使用SqlDataReader对象时,总是应当通过SqlDataReader对象的类型化存取器方法,如GetString 和GetDecimal,获得输出参数。这样做就避免了不必要的类型转换。 
关于如何使用SqlDataReader对象获取单行数据的代码示例,见附录中的如何使用 SqlDataReader对象获取单行数据。 
获取单项数据 

在本场景中,要获取单项数据。例如,提供了产品ID后,希望查询单一的产品名;或,给出了客户名后,希望查询客户的信用等级。在这种场景中,为得到单项数据,通常不希望引发创建DataSet 对象或甚至是 DataTable对象的额外开销。 

也许只希望检查数据库中是否存在特定的行。例如,当新用户在网站注册时,需要检查所选用户名是否已经存在。这是单项数据查询中很特殊的例子,但在此例子中,返回一个简单的布尔返回值就足够了。 

方法比较 

当从数据源获取单项数据时,考虑下面的方法: 

同存储过程一起使用SqlCommand对象的ExecuteScalar方法。 
使用存储过程输出或返回参数。 
使用SqlDataReader对象。 
ExecuteScalar方法直接返回数据项,因为它是为只返回单个值的查询设计的,与存储过程输出参数和SqlDataReader方法相比,它需要更少的代码。 

从性能方面来说,应当使用存储过程输出或返回参数,因为测试结果表明,存储过程方法在从低强度到高强度环境中(从同时不到100浏览器链接到200浏览器链接)提供了一致的性能。 

更多信息 

如果通过ExecuteQuery方法所执行的查询返回多列和/或行,那么此方法只返回第一行的第一列。 
关于演示如何使用ExecuteScalar方法的代码片段,见附录中的如何使用 ExecuteScalar获取单项数据。 
关于演示如何利用存储过程输出或返回参数获取单项数据的代码示例,见附录中的如何利用存储过程输出或返回参数获取单项数据 
关于演示如何使用SqlDataReader对象获取单项数据的代码示例,见附录中的如何使用 SqlDataReader对象获取单项数据。 
通过防火墙建立链接 

需要经常配置互联网应用程序以使它能够通过防火墙链接到SQL Server。例如,许多Web应用程序及防火墙的主要结构组件是周边网络(也被称为DMZ或非军事化区),它们用于隔离高端Web服务器与内部网络。 

通过防火墙链接到SQL Server时,需要对防火墙,客户和服务器进行明确配置。SQL Server提供了客户网络应用程序和服务器网络应用程序以帮助进行配置。 

选择网络库 

当通过防火墙建立链接时,使用SQL Server TCP/IP网络库来简化配置,这是SQL Server2000安装的默认选项。如果使用先前版本的SQL Server,那么分别利用客户端网络应用程序和服务器端网络应用程序检查TCP/IP是否在客户和服务器端已经被配置为默认的网络库。 

除了配置优点,使用TCP/IP库还意味着: 

受益于大宗数据的改进性能和增加的扩展性。 
避免与指定管道相关的附加安全信息。 
必须在客户和服务器计算机上配置TCP/IP,因为大多数防火墙限制了流量通过的端口,所以必须仔细考虑SQL Server所使用的端口号。 

配置服务器 

SQL Server的默认实例监听1433端口。然而,SQL Server 2000的指定实例在它们首次开启时,动态地分配端口号。网络管理员有希望在防火墙打开一定范围的端口;因此,当随防火墙使用SQL Server的指定实例时,利用服务网络应用程序对实例进行配置,使它监听特定的端口。然后管理员对防火墙进行配置,以使防火墙允许流量到达特定的IP地址及服务器实例所监听的端口。 

注意,客户端网络库所使用的源端口号在1024-5000间动态分配。这是TCP/IP客户端应用程序的标准作法,但这意味着防火墙必须允许途经此范围的任何端口流量能够通过。关于SQL Server所使用的端口的更多信息,在微软产品支持服务网站上,参见INF: P 通过防火墙对SQL Server进行通讯所需的TCP端口 。。 

动态查找指定实例 

如果改变了SQL Server所监听的默认端口,那么就要对客户端进行配置,以使它链接到此端口。更多细节,见本文中的配置客户端 一节。 

如果改变了SQL Server 2000默认实例的端口号,那么不修改客户端将导致链接错误。如果存在多个SQL Server 实例,最新版本的MDAC数据访问堆栈(2.6)将进行动态查找,并利用用户数据报协议(UDP)协商(通过UDP端口1434)对指定实例进行定位。尽管这种方法在开发环境下也许有效,但在现在环境中却不大可能正常工作,因为典型发问下防火墙阻止UDP协商流量的通过。 

为了避开这种情况,总是将客户端配置为链接到已配置好的目的端口号。 

配置客户端 

应当对客户端进行配置以利用TCP/IP网络库链接到SQL Server,并且也应当确保客户端库使用了正确的目的端口号。 

使用TCP/IP 网络库 

利用SQL Server客户端网络库,可以对客户端进行配置。在某些安装版本中,可能没有将这个应用程序安装到客户端(如Web服务器)。在这种情况下,可以按如下方式之一解决: 

利用通过链接字符串提供的“Network Library=dbmssocn”名称-值对指定网络库。字符串dbmssocn用于标识TCP/IP(套接字)库。 
注意 在使用SQL Server .NET数据供应器时,网络库的默认设置是使用“dbmssocn”。 

在客户端机器上修改注册表,把TCP/IP设置为默认库。关于配置SQL Server网络库的更多信息,参见HOWTO: 不使用客户端网络应用程序而修改SQL Server默认网络库(Q250550)。 
指定端口 

如果SQL Server的实例被配置为监听默认的1433以外的其它端口,那么通过以下操作,就能指定链接到的端口号: 

使用客户端网络应用程序 
利用提供给链接字符串的“Server”或“Data Source”名称-值对来指定端口号。要按下面的格式使用字符串: 
"Data Source=ServerName,PortNumber" 
注意 ServerName可以是IP地址,或域名系统(DNS)名,为了优化性能,可以使用IP 地址以避免DNS 查询。 

分布式事务处理 

如果开发了使用COM+分布式事务处理和微软分布式事务处理协调器(DTC)服务的服务组件,那么就需要对防火墙进行配置,以允许DTC流在不同DTC实例间及DTC与资源管理器(例如SQL Server)间流动。 

有关为DTC开放端口的更多信息,见INFO:为通过防火墙工作,配置微软分布式事务处理协调器 (DTC)。 

处理BLOBs 

目前,很多应用程序除了处理许多传统的字符串和数字型数据外,还要处理象图形或声音--甚至复杂的数据格式,如视频格式的数据。图形、声音与视频的数据格式类型不一。然而从存储角度来说,它们都可被视为二进制数据块,通常将其称为BLOBs(二进制大对象)。 

SQL Server提供了binary, varbinary, 和image数据格式来存储BLOBs。不考虑名称,BLOB数据也可被称为基于文件的数据。例如,你可能要存储与特定行相关的二进制长注释字段。SQL Server为此目的提供了ntext 和text数据类型。 

通常,对于小于8KB的二进制数据,使用varbinary数据类型。对于超过此大小的二进制数据,使用image 。表2 汇集了每个数据类型的主要特性。 

表2 数据类型特性 

数据类型 大小 描述 
binary 范围从1-8KB。存储大小是指定大小加4字节。 固定长度的二进制数据 
varbinary 范围从1-8KB。存储大小是所提供数据的实际大小加4字节。 可变长度的二进制数据 
image 从0-2GB大小的可变长度二进制数据 大容量可变长度二进制数据 
text 从0-2GB大小的可变长度数据 字符型数据 
ntext 从0-2GB大小的可变长度数据 宽字节字符数据 

何处存储BLOB数据 

SQL Server 7.0及其以后版本已经提高了存储在数据库中的BLOB数据的使用性能。这种情况的一个原因是数据库页面大小已经增加到了8KB。结果,小于8KB的文本或图象数据不必再存储在页面单独的二进制树结构中,而是能被存储在单行中。这意味着读取和写入text, ntext, 或 image数据能象读取或写入字符或二进制字符串那样快。超出8KB后,将在行中建立一个指针,数据本身存储在独立数据页面的二进制树结构中,这不可避免会对性能产生冲击。 

关于迫使text, ntext, 和 image数据存储在单行中的更多信息,见SQL Server在线图书中的使用text和image数据主题。 

一个经常使用的处理BLOB数据的可选方法是,将BLOB数据存储在文件系统中,并在数据库列中存储一个指针(通常是一个统一资源定位器--URL链接)以引用正确的文件。对于SQL Server 7.0以前的版本,将BLOB数据存储在数据库外的文件系统中,可以提高性能。 

然而,SQL Server 2000改进了BLOB支持,以及ADO.NET对读取和写入BLOB数据的支持,使在数据库中存储BLOB数据成为一种可行的方法。 

在数据库中存储BLOB 数据的优点 

将BLOB数据存储在数据库中,带来了很多优点: 

易于保持BLOB数据与行中其它项数据的同步。 
BLOB数据由数据库所支持,拥有单一的存储流,易于管理。 
通过SQL Server 2000所支持的XML可以访问BLOB数据,这将在XML流中返回64位编码描述的数据。 
对包含了固定或可变长度的字符(包括宽字符)数据的列可以执行SQL Server全文本搜索(FTS)操作。也可以对包含在image字段中的已格式化的基于文本的数据--Word 或 Excel文档--执行FTS操作。 
将BLOB数据写入到数据库中 

下面的代码演示了如何利用ADO.NET将从某个文件获得的二进制数据写入SQL Server image字段中。 

  1. public void StorePicture( string filename )  
  2. {  
  3.   // Read the file into a byte array  
  4.   FileStream fs = new FileStream( filename, FileMode.Open, FileAccess.Read );  
  5.   byte[] imageData = new Byte[fs.Length];  
  6.   fs.Read( imageData, 0, (int)fs.Length );  
  7.   fs.Close();  
  8.  
  9.   SqlConnection conn = new SqlConnection("");  
  10.   SqlCommand cmd = new SqlCommand("StorePicture", conn);  
  11.   cmd.CommandType = CommandType.StoredProcedure;  
  12.   cmd.Parameters.Add("@filename", filename );  
  13.   cmd.Parameters["@filename"].Direction = ParameterDirection.Input;  
  14.   cmd.Parameters.Add("@blobdata", SqlDbType.Image);  
  15.   cmd.Parameters["@blobdata"].Direction = ParameterDirection.Input;  
  16.   // Store the byte array within the image field  
  17.   cmd.Parameters["@blobdata"].Value = imageData;  
  18.   try 
  19.   {  
  20.     conn.Open();  
  21.     cmd.ExecuteNonQuery();  
  22.   }  
  23.   catch 
  24.   {  
  25.     throw;  
  26.   }  
  27.   finally 
  28.   {  
  29.     conn.Close();  
  30.   }  



从数据库中读取BLOB数据 

在通过ExecuteReader方法创建SqlDataReader对象以读取包含BLOB数据的行时,需使用CommandBehavior.SequentialAccess枚举值。如果没有此枚举值,阅读器一次只从服务器中向客户端发送一行数据。如果行包含了BOLB数据,这预示着要占用大量内存。通过利用枚举值,就获得了更好的控制权,因为BLOB数据只在被引用时才被发出(例如,利用GetBytes方法,可以控制读取的字节数)。这在下面的代码片段中进行了演示。 

  1. // Assume previously established command and connection  
  2. // The command SELECTs the IMAGE column from the table  
  3. conn.Open();  
  4. SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess);  
  5. reader.Read();  
  6. // Get size of image data - pass null as the byte array parameter  
  7. long bytesize = reader.GetBytes(0, 0, null, 0, 0);  
  8. // Allocate byte array to hold image data  
  9. byte[] imageData = new byte[bytesize];  
  10. long bytesread = 0;  
  11. int curpos = 0;  
  12. while (bytesread < bytesize)  
  13. {  
  14.   // chunkSize is an arbitrary application defined value   
  15.   bytesread += reader.GetBytes(0, curpos, imageData, curpos, chunkSize);  
  16.   curpos += chunkSize;  
  17. }  
  18. // byte array 'imageData' now contains BLOB from database 



注意使用CommandBehavior.SequentialAccess需要以严格的顺序访问列数据。例如,如果BLOB数据存在于第3列,并且还需要从第1,2列中读取数据,那么在读取第3列前必须先读取第1,2列。 

事务处理 

实际上所有用于更新数据源的面向商业的应用程序都需要事务处理支持。通过提供四个基本担保,即众所周知的首字缩写ACID:可分性,一致性,分离性,和耐久性,事务处理将用于确保包含在一个或多个数据源中的系统的完整性。 

例如,考虑一个基于Web的零售应用程序,它用于处理购买订单。每个订单需要3个完全不同操作,这些操作涉及到3个数据库更新: 

库存水准必须减少所订购的数量。 
所购买的量必须记入客户的信用等级。 
新订单必须增加到数据库中。 
这三个不同的操作作为一个单元并自动执行是至关重要的。三个操作必须全部成功,或都不成功--任何一个操作出现误差都将破坏数据完整性。事务处理提供了这种完整性及其它保证。 

要进一步了解事务处理过程的基本原则,见http://msdn.microsoft.com/library/en-us/cpguide/html/cpcontransactionprocessingfundamentals.asp。 

可以采用很多方法将事务管理合并到数据访问代码中。每种方法适合下面两种基本编程模型之一。 

手工事务处理。可以直接在组件代码或存储过程中分别编写利用ADO.NET 或 Transact-SQL事务处理支持特性的代码。 
自动化(COM+)事务处理。可以向.NET类中增加声明在运行时指定对象事务处理需要的属性。这种模型使你能方便地配置多个组件以使它们在同一事务处理内运行。 
尽管自动化事务处理模型极大地简化了分布式事务处理过程,但两种模型都用于执行本地事务处理(即对单个资源管理器如SQL Server 2000执行的事务处理)或分布式事务处理(即,对位于远程计算机上的多个资源管理执行的事务处理)。 

你也许会试图利用自动化(COM+)事务处理来从易于编程的模型中获益。在有多个组件执行数据库更新的系统中,这种优点更明显。然而,在很多情况下,应当避免这种事务处理模型所带来的额外开销和性能损失。本节将指导你根据特定的应用程序环境选择最合适的模型。 

选择事务处理模型 

在选择事务处理模型前,首先应当考虑是否真正需要事务处理。事务处理是服务器应用程序使用的最昂贵的资源,在不必要使用的地方,它们降低了扩展性。考虑下面用于管理事务处理使用的准则: 

只在需要跨一组操作获取锁并需要加强ACID规则时才执行事务处理。 
尽可能短地保持事务处理,以最小化维持数据库锁的时间。 
永远不要将客户放到事务处理生命周期的控制之中。 
不要为单个SQL语句使用事务处理。SQL Server自动把每个语句作为单个事务处理执行。 
自动化事务处理与手工事务处理的对比 

尽管编程模型已经对自动化事务处理进行了简化,特别是在多个组件执行数据库更新时,但本地事务处理总是相当快,因为它们不需要与微软DTC交互。即使你对单个本地资源管理器(如SQL Server)使用自动化事务处理,也是这种情况(尽管性能损失减少了),因为手式本地事务处理避免了所有不必要的与DTC的进程间通信。 

对于下面的情况,需使用手工事务处理: 

对单个数据库执行事务处理。
对于下列情况,则宜使用自动事务处理: 

需要将单个事务处理扩展到多个远程数据库时。 
需要单个事务处理拥有多个资源管理器(如数据库和Windows 2000消息队列(被称为MSMQ)资源管理器)时。 
注意 避免混用事务处理模型。最好只使用其中一个。 

在性能足够好的应用程序环境中,(甚至对于单个数据库)选择自动化事务处理以简化编程模型,这种做法是合理的。自动化事务处理使多个组件能很容易地执行现一事务处理中的多个操作。 

使用手工事务处理 

对于手工事务处理,可以直接在组件代码或存储过程中分别编写使用ADO.NET 或 Transact-SQL事务处理支持特性的代码。多数情况下,应选择在存储过程中控制事务处理,因为这种方法提供了更高的封装性,并且在性能方面,此方法与利用ADO.NET 代码执行事务处理兼容。 

利用ADO.NET执行手工事务处理 

ADO.NET支持事务处理对象,利用此对象可以开始新事务处理过程,并明确控制事务处理是否执行还是回滚。事务处理对象与单个数据库链接相关,可以通过链接对象的BeginTransaction方法获得。调用此方法并不是暗示,接下来的命令是在事务处理上下文中发出的。必须通过设置命令的Transaction属性,明确地将每个命令与事务处理关联起来。可以将多个命令对象与事务处理对象关联,因此在单个事务处理中就针对单个数据库把多个操作进行分组。 

关于使用ADO.NET事务处理代码的示例,见附录中如何编码ADO.NET手工事务处理。 

更多信息 

ADO.NET手工事务处理的默认分离级别是读联锁,这意味着在读取数据时,数据库控制共享锁,但在事务处理结束前,数据可以被修改。这种情况潜在地会产生不可重复的读取或虚数据。通过将事务处理对象的IsolationLevel属性设置为IsolationLevel枚举类型所定义的一个枚举值,就可改变分离级别。 
必须仔细为事务处理选择合适的分离级别。其折衷是数据一致性与性能的比例。最高的分离等级(被序列化了)提供了绝对的数据一致性,但是以系统整体吞吐量为代价。较低的分离等级会使应用程序更易于扩展,但同时增加了因数据不一致而导致出错的可能性。对多数时间读取数据、极少写入数据的系统来说,较低的分离等级是合适的。 
关于选择恰当事务处理级别极有价值的信息,见微软出版社名为Inside SQL Server 2000的书,作者Kalen Delaney。 
利用存储过程执行手工事务处理 

也可以在存储过程中使用Transact-SQL语句直接控制手工事务处理。例如,可以利用包含了Transact-SQL事务处理语句(如BEGIN TRANSACTION、END TRANSACTION及ROLLBACK TRANSACTION)的存储过程执行事务处理。 

更多信息 

如果需要,可以在存储过程中使用SET TRANSACTION ISOLATION LEVEL语句控制事务处理的分离等级。读联锁是SQL Server的默认设置。关于SQL Server分离级别的更多信息,见SQL Server在线书目“访问和修改关系数据”一节中的分离级别部分。 
关于演示如何利用Transact-SQL事务处理语句执行事务更新的代码示例,见附录中的如何利用Transact-SQL执行事务处理。 
使用自动化事务 

自动化事务简化了编程模型,因为它们不需要明确地开始新事务处理过程,或明确执行或取消事务。然而,自动化事务的最大优点是它们能与DTC结合起来,这就使单个事务可以扩展到多个分布式数据源中。在大型分布式应用程序中,这个优点是很重要的。尽管通过手工对DTC直接编程来控制分布式事务是可能的,但自动化事务处理极大的简化了工作量,并且它是为基于组件的系统而设计的。例如,可以方便地以说明方式配置多个组件以执行包含了单个事务处理的任务。 

自动化事务依赖于COM+提供的分布式事务处理支持特性。结果,只有服务组件(即从ServicedComponent类中派生的组件)能够使用自动化事务。 

要为自动化事务处理配置类,操作如下: 

从位于EnterpriseServices名称空间的ServicedComponent类中派生新类。 
通过Transaction属性定义类的事务处理需求。来自TransactionOption的枚举值决定了如何在COM+类中配置类。可与此属性一同设置的其它属性包括事务处理分离等级和超时上限。 
为了避免必须明确选出事务处理结果,可以用AutoComplete属性对方法进行注释。如果这些方法释放异常,事务将自动取消。注意,如果需要,仍可以直接挑选事务处理结果。更多详情,见本文稍后确定事务处理结果的节。 
更多信息 

关于COM+自动化事务的更多信息,可在平台SDK文档中搜索“通过COM+的自动化事务”获取。 
关于.NE T事务处理类的示例,见附录中的如何编码.NET事务处理。 
配置事务处理分离级别 

用于COM+1.0版--即运行在Windows 2000中的COM+--的事务处理分离级别被序列化了。这样做提供了最高的分离等级,却是以性能为代价的。系统的整体吞吐量被降低了。因为所涉及到的资源管理器(典型地是数据库)在事务处理期间必须保持读和写锁。在此期间,其它所有事务处理都被阻断了,这种情况将对应用程序的扩展能力产生极大冲击。 

随微软Windows .NET发行的COM+ 1.5版允许有COM+目录中按组件配置事务处理分离等级。与事务中根组件相关的设置决定了事务处理的分离等级。另外,同一事务流中的内部子组件拥有的事务处理等级必须不能高于要组件所定义的等级。如果不是这样,当子组件实例化时,将导致错误。 

对.NET管理类,Transaction属性支持所有的公有Isolation属性。你可以用此属性陈述式地指定一特殊分离等级,如下面的代码所示: 

  1. [Transaction(TransactionOption.Supported, Isolation=TransactionIsolationLevel.ReadCommitted)]  
  2. public class Account : ServicedComponent  
  3. {  
  4.   . . .  


更多信息 

关于配置事务处理分离等级及其它Windows .NET COM+增强特性的更多信息,见MSDN杂志2001年8月期的“Windows XP:利用COM+ 1.5的增强特性使你的组件更强壮”一文。 

确定事务处理结果 

在单个事务流的所有事务处理组件上下文中,自动化事务处理结果由事务取消标志和一致性标志的状态决定。当事务流中的根组件成为非活动状态(并且控制权返回调用者)时,确定事务处理结果。这种情况在图5中得到了演示,此图显示的是一个典型的银行基金传送事务。 

按此在新窗口打开图片
图5 事务流上下文 

当根对象(在本例中是对象)变为非活动状态,并且客户的方法调用返回时,确定事务处理结果。在任何上下文中的任何一致性标志被设为假,或如果事务处理取消标志设为真,那么底层的物理DTC事务将被取消。 

可以以下面两种方式之一从.NET对象中控制事务处理结果: 

可以用AutoComplete属性对方法进行注释,并让.NET自动存放将决定事务处理结果投票。如果方法释放异常,利用此属性,一致性标志自动地被设为假(此值最终使事务取消)。如果方法返回而没有释放异常,那么一致性标志将设为真,此值指出组件乐于执行事务。这并没有得到保证,因为它依赖于同一事务流中其它对象的投票。 
可以调用ContextUtil类的静态方法SetComplete或 SetAbort,这些方法分别将一致性标志设为真或假。 
严重性大于10的SQL Server错误将导致管理数据供应器释放SqlException类型的异常。如果方法缓存并处理异常,就要确保或者通过手工取消了事务,或者方法被标记了[AutoComplete],以保证异常能传递回调用者。 

AutoComplete方法 

对于标记了属性的方法,执行下面操作: 

将SqlException传递加调用堆栈。 
将SqlException封装在外部例外中,并传递回调用者。也可以将异常封装在对调用者更有意义的异常类型中。 
异常如果不能传递,将导致对象不会提出取消事务,从而忽视数据库错误。这意味着共享同一事务流的其它对象的成功操作将被提交。 

下面的代码缓存了SqlException,然后将它直接传递回调用者。事务处理最终将被取消,因为对象的一致性标志在对象变为非活动状态时自动被设为假。 

  1. [AutoComplete]  
  2. void SomeMethod()  
  3. {  
  4.   try 
  5.   {  
  6.     // Open the connection, and perform database operation  
  7.     . . .  
  8.   }  
  9.   catch (SqlException sqlex )  
  10.   {  
  11.     LogException( sqlex ); // Log the exception details  
  12.     throw;                 // Rethrow the exception, causing the consistent   
  13.                            // flag to be set to false.  
  14.   }  
  15.   finally 
  16.   {  
  17.     // Close the database connection  
  18.     . . .  
  19.   }  



Non-AutoComlete方法 

对于没有AutoComplete的属性的方法,必须: 

在catch块内调用ContextUtil.SetAbort以终止事务处理。这就将相容标志设置为假。 
如果没有发生异常事件,调用ContextUtil.SetComplete,以提交事务,这就将相容标志设置为真(缺省状态)。 
代码说明了这种方法。 

  1. void SomeOtherMethod()  
  2. {  
  3.   try 
  4.   {  
  5.     // Open the connection, and perform database operation  
  6.     . . .  
  7.     ContextUtil.SetComplete(); // Manually vote to commit the transaction  
  8.   }  
  9.   catch (SqlException sqlex)  
  10.   {  
  11.     LogException( sqlex );   // Log the exception details  
  12.     ContextUtil.SetAbort();  // Manually vote to abort the transaction  
  13.     // Exception is handled at this point and is not propagated to the caller  
  14.   }  
  15.   finally 
  16.   {  
  17.     // Close the database connection  
  18.     . . .  
  19.   }  
  20. }  


注意 如果有多个catch块,在方法开始的时候调用ContextVtil.SetAbort,以及在try块的末尾调用ContextUtil.SetComplete都会变得容易。用这种方法,就不需要在每个catch块中重复调用ContextUtil.SetAbort。通过这种方法确定的相容标志的设置只在方法返回时有效。 

对于异常事件(或循环异常),必须把它传递到调用堆栈中,因为这使得调用代码认为事务处理失败。它允许调用代码做出优化选择。比如,在银行资金转账中,如果债务操作失败,则转帐分支可以决定不执行债务操作。 

如果把相容标志设置为假并且在返回时没有出现异常事件,则调用代码就没有办法知道事务处理是否一定失败。虽然可以返回Boolean值或设置Boolean输出参数,但还是应该前后一致,通过显示异常事件以表明有错误发生。这样代码就有一种标准的错误处理方法,因此更简明、更具有相容性。 

数据分页 

在分布式应用程序中利用数据进行分页是一项普遍的要求。比如,用户可能得到书的列表而该列表又不能够一次完全显示,用户就需要在数据上执行一些熟悉的操作,比如浏览下一页或上一页的数据,或者跳到列表的第一页或最后一页。 

这部分内容将讨论实现这种功能的选项,以及每种选项在性能和缩放性上的效果。 

选项比较 

数据分页的选项有: 

利用SqlDataAdapter的Fill方法,将来自查询处的结果填充到DataSet中。 
通过COM的可相互操作性使用ADO,并利用服务器光标。 
利用存储的过程手工实现数据分页。 
对数据进行分页的最优选项依赖于下列因素: 

扩展性要求 
性能要求 
网络带宽 
数据库服务器的存储器和功率 
中级服务器的存储器和功率 
由分页查询所返回的行数 
数据总页数的大小 
性能测试表明利用存储过程的手工方法在很大的应力水平范围上都提供了最佳性能。然而,由于手工方法在服务器上执行工作,如果大部分站点功能都依赖数据分页功能,那么服务器性能就会成一个关键要素。为确保这种方法能适合特殊环境,应该测试各种特殊要求的选项。 

下面将讨论各种不同的选项。 

使用SqlDataAdapter 

如前面所讨论的,SqlDataAdapter是用来把来自数据库的数据填充到DataSet中,过载的Fill方法中的任一个都需要两个整数索引值(如下列代码所示): 

  1. public int Fill(  
  2.    DataSet dataSet,  
  3.    int startRecord,  
  4.    int maxRecords,  
  5.    string srcTable  
  6. ); 


StartRecord值标示从零开始的记录起始索引值。MaxRecord值表示从startRecord开始的记录数,并将拷贝到新的DataSet中。 

SqlDataAdapter在内部利用SqlDataReader执行查询并返回结果。SqlDataAdapter读取结果并创建基于来自SalDataReader的数据的Dataset。SqlDataAdapter通过startRecord和maxRecords把所有结果都拷贝到新生成的DataSet中,并丢弃不需要的数据。这意味着许多不必要的数据将潜在的通过网络进入数据访问客户--这是这种方法的主要缺陷。 

比如,如果有1000个记录,而需要的是第900到950个记录,那么前面的899个记录将仍然穿越网络然后被丢弃。对于小数量的记录,这种开销可能是比较小的,但如果针对大量数据的分页,则这种开销就会非常巨大。 

使用ADO 

实现分页的另一个选项是利用基于COM的ADO进行分页。这种方法的目标是获得访问服务器光标。服务器光标通过ADO Recordset对象显示。可以把Recordset光标的位置设置到adUseServer中。如果你的OLE DB供应器支持这种设置(如SQLOLEDB那样),就可以使用服务器光标。这样就可以利用光标直接导航到起始记录,而不需要将所有数据传过网络进入访问数据的用户代码中。 

这种方法有下面两个缺点: 

在大多数情况下,可能需要将返回到Recordset对象中的记录翻译成DataSet中的内容,以便在客户管理的代码中使用。虽然OleDbDataAdapter确实在获取ADO Recordset对象并把它翻译成Dataset时过载了Fill方法,但是并没有利用特殊记录进行开始与结束操作的功能。唯一现实的选项是把开始记录移动到Recordset对象中,循环每个记录,然后手工拷贝数据到手工生成的新Dataset中。这种操作,尤其是利用COM Interop调用,其优点可能不仅仅是不需要在网络上传输多余的数据,尤其对于小的DataSet更明显。 
从服务器输出所需数据时,将保持连接和服务器光标开放。在数据库服务器上,光标的开放与维护需要昂贵的资源。虽然该选项提高了性能,但是由于为延长的时间两消耗服务器资源,从而也有可能降低可扩展性。 
提供手工实现 

在本部分中讨论的数据分页的最后一个选项是利用存储过程手工实现应用程序的分页功能。对于包含唯一关键字的表格,实现存储过程相对容易一些。而对于没有唯一关键字的表格(也不应该有许多关键字),该过程会相对复杂一些。 

带有唯一关键字的表格的分页 

如果表格包含一个唯一关键字,就可以利用WHERE条款中的关键字创建从某个特殊行起始的结果设置。这种方法,与用来限制结果设置大小的SET ROWCOUNT状态是相匹配的,提供了一种有效的分页原理。这一方法将在下面存储的代码中说明: 

  1. CREATE PROCEDURE GetProductsPaged  
  2. @lastProductID int,  
  3. @pageSize int 
  4. AS 
  5. SET ROWCOUNT @pageSize  
  6. SELECT *  
  7. FROM Products  
  8. WHERE [standard search criteria]  
  9. AND ProductID > @lastProductID  
  10. ORDER BY [Criteria that leaves ProductID monotonically increasing]  
  11. GO 


这个存储过程的调用程序仅仅维护LastProductID的值,并通过所选的连续调用之间的页的大小增加或减小该值。 

不带有唯一关键字的表格的分页 

如果需要分页的表格没有唯一关键字,可以考虑添加一个--比如利用标识栏。这样就可以实现上面讨论的分页方案了。 

只要能够通过结合结果记录中的两个或更多区域来产生唯一性,就仍然有可能实现无唯一关键字表格的有效分页方案。 

比如,考察下列表格: 

Col1 Col2 Col3 Other columns… 
A 1 W … 
A 1 X   . 
A 1 Y   . 
A 1 Z   . 
A 2 W   . 
A 2 X   . 
B 1 W … 
B 1 X   . 


对于该表,结合Col 、Col2 和Col3就可能产生一种唯一性。这样,就可以利用下面存储过程中的方法实现分布原理: 

  1. CREATE PROCEDURE RetrieveDataPaged  
  2. @lastKey char(40),  
  3. @pageSize int 
  4. AS  
  5. SET ROWCOUNT @pageSize  
  6. SELECT  
  7. Col1, Col2, Col3, Col4, Col1+Col2+Col3 As KeyField  
  8. FROM SampleTable  
  9. WHERE [Standard search criteria]  
  10. AND Col1+Col2+Col3 > @lastKey  
  11. ORDER BY Col1 ASC, Col2 ASC, Col3 ASC  
  12. GO  


客户保持存储过程返回的keyField栏的最后值,然后又插入回到存储过程中以控制表的分页。 

虽然手工实现增加了数据库服务器上的应变,但它避免了在网络上传输不必要的数据。性能测试表明在整个应变水平中这种方法都工作良好。然而,根据站点工作所涉及的数据分页功能的多少,在服务器上进行手工分页可能影响应用程序的可扩展性。应该在所在环境中运行性能测试,为应用程序找到最合适的方法。 

附录 

如何为一个.NET类启用对象结构 

要利用Enterprise (COM+)Services为对象结构启用.NET管理的类,需要执行下列步骤: 

从位于System. Enterprise Services名字空间中的Serviced Component中导出所需类。 

  1. using System.EnterpriseServices;  
  2. public class DataAccessComponent : ServicedComponent 

为该类添加Construction Enabled属性,并合理地指定缺省结构字符串,该缺省值保存在COM+目录中,管理员可以利用组件服务微软管理控制台(MNC)的snap-in来维护该缺省值。 

  1. [ConstructionEnabled(Default="default DSN")]  
  2. public class DataAccessComponent : ServicedComponent 


提供虚拟Construct方法的替换实现方案。该方法在对象语言构造程序之后调用。在COM目录中保存的结构字符串是该方法的唯一字符串。 

  1. public override void Construct( string constructString )  
  2. {  
  3.   // Construct method is called next after constructor.  
  4.   // The configured DSN is supplied as the single argument  

通过Assembly key文件或Assembly key Name属性为该汇编提供一个强名字。任何用COM+服务注册的汇编必须有一个强名字。关于带有强名字汇编的更多信息,参考:http://msdn.microsoft.com/library/en-us/cpguide/html/cpconworkingwithstrongly- namedassemblies.Asp。 
[assembly: AssemblyKeyFile("DataServices.snk")]
为支持动态注册,可以利用汇编层上的属性ApplicationName和Application Action分别指定用于保持汇编元素和应用程序动作类型的COM+应用程序的名字。关于汇编注册的更多信息,参考: http://msdn.microsoft.com/library/en-us/cpguide/html/cpconregisteringserviced components.asp。 

  1. // the ApplicationName attribute specifies the name of the  
  2. // COM+ Application which will hold assembly components  
  3. [assembly : ApplicationName("DataServices")]  
  4.      
  5. // the ApplicationActivation.ActivationOption attribute specifies   
  6. // where assembly components are loaded on activation  
  7. // Library : components run in the creator's process  
  8. // Server : components run in a system process, dllhost.exe  
  9. [assembly: ApplicationActivation(ActivationOption.Library)]  

下列代码段是一个叫做DataAccessComponent的服务组件,它利用COM+结构字符串来获得数据库连接字符串。 

  1. using System;  
  2. using System.EnterpriseServices;  
  3.  
  4. // the ApplicationName attribute specifies the name of the  
  5. // COM+ Application which will hold assembly components  
  6. [assembly : ApplicationName("DataServices")]  
  7.  
  8. // the ApplicationActivation.ActivationOption attribute specifies   
  9. // where assembly components are loaded on activation  
  10. // Library : components run in the creator's process  
  11. // Server : components run in a system process, dllhost.exe  
  12. [assembly: ApplicationActivation(ActivationOption.Library)]  
  13.  
  14. // Sign the assembly. The snk key file is created using the   
  15. // sn.exe utility  
  16. [assembly: AssemblyKeyFile("DataServices.snk")]  
  17.  
  18. [ConstructionEnabled(Default="Default DSN")]  
  19. public class DataAccessComponent : ServicedComponent  
  20. {  
  21.     private string connectionString;  
  22.     public DataAccessComponent()  
  23.     {  
  24.       // constructor is called on instance creation  
  25.     }  
  26.     public override void Construct( string constructString )  
  27.     {  
  28.       // Construct method is called next after constructor.  
  29.       // The configured DSN is supplied as the single argument  
  30.       this.connectionString = constructString;  
  31.     }  
  32. }  


如何利用SqlDataAdapter来检索多个行 

下面的代码说明如何利用SqlDataAdapter对象发出一个生成Data Set或Datatable的命令。它从SQL Server Northwind数据库中检索一系列产品目录。 

  1. using System.Data;  
  2. using System.Data.SqlClient;  
  3.  
  4. public DataTable RetrieveRowsWithDataTable()  
  5. {  
  6.   using ( SqlConnection conn = new SqlConnection(connectionString) )  
  7.   {  
  8.     SqlCommand cmd = new SqlCommand("DATRetrieveProducts", conn);  
  9.     cmd.CommandType = CommandType.StoredProcedure;  
  10.     SqlDataAdapter da = new SqlDataAdapter( cmd );  
  11.     DataTable dt = new DataTable("Products");  
  12.     da.Fill(dt);  
  13.     return dt;  
  14.   }  
  15. }  

按下列步骤利用SqlAdapter生成DataSet或DataTable: 

创建SqlCommand对象启用存储过程,并把它与SqlConnection对象(显示的)或连接字符串(未显示)相联系。 
创建一个新的SqlDataAdapter对象,并把它SqlCommand对象相联系。 
创建DataTable(或者DataSet)对象。利用构造程序自变量命名DataTable. 
调用SqlData Adapter对象的Fill方法,把检索的行转移到DataSet或Datatable中。 
如何利用SqlDataReader检索多个行 

下列代码说明了如何利用SqlDataReader方法检索多行: 

  1. using System.IO;  
  2. using System.Data;  
  3. using System.Data.SqlClient;  
  4.  
  5. public SqlDataReader RetrieveRowsWithDataReader()  
  6. {  
  7.   SqlConnection conn = new SqlConnection(  
  8.          "server=(local);Integrated Security=SSPI;database=northwind");  
  9.   SqlCommand cmd = new SqlCommand("DATRetrieveProducts", conn );  
  10.   cmd.CommandType = CommandType.StoredProcedure;  
  11.   try 
  12.   {  
  13.     conn.Open();  
  14.     // Generate the reader. CommandBehavior.CloseConnection causes the  
  15.     // the connection to be closed when the reader object is closed  
  16.     return( cmd.ExecuteReader( CommandBehavior.CloseConnection ) );  
  17.   }  
  18.   catch 
  19.   {  
  20.     conn.Close();  
  21.     throw;  
  22.   }  
  23. }  
  24.  
  25. // Display the product list using the console  
  26. private void DisplayProducts()  
  27. {  
  28.   SqlDataReader reader = RetrieveRowsWithDataReader();  
  29.   while (reader.Read())  
  30.   {  
  31.     Console.WriteLine("{0} {1} {2}",   
  32.                       reader.GetInt32(0).ToString(),   
  33.                       reader.GetString(1) );  
  34.   }  
  35.   reader.Close(); // Also closes the connection due to the  
  36.                   // CommandBehavior enum used when generating the reader  
  37. }  


按下列步骤利用SqlDataReader检索多行: 

创建用于执行存储的过程的SqlCommand对象,并把它与SqlConnection对象相联系。 
打开链接。 
通过调用SqlCommand对象的Excute Reader方法生成SqlDataReader对象。 
从流中读取数据,调用SqlDataReader对象的Read方法来检索行,并利用分类的存取程序方法(如GetIut 32和Get String方法)检索列的值。 
完成读取后,调用Close方法。 
如何利用XmlReader检索多个行 

可以利用SqlCommand对象生成XmlReader对象,它提供对XML数据的基于流的前向访问。该命令(通常是一个存储的过程)必须生成一个基于XML的结果设置,它对于SQL Server2000通常是由带有有效条款FOR XML的SELECT状态组成。下列代码段说明了这种方法: 

  1. public void RetrieveAndDisplayRowsWithXmlReader()  
  2. {  
  3.   SqlConnection conn = new SqlConnection(connectionString);  
  4.   SqlCommand cmd = new SqlCommand("DATRetrieveProductsXML", conn );  
  5.   cmd.CommandType = CommandType.StoredProcedure;  
  6.   try 
  7.   {  
  8.     conn.Open();  
  9.     XmlTextReader xreader = (XmlTextReader)cmd.ExecuteXmlReader();  
  10.     while ( xreader.Read() )  
  11.     {  
  12.       if ( xreader.Name == "PRODUCTS" )   
  13.       {  
  14.         string strOutput = xreader.GetAttribute("ProductID");  
  15.         strOutput += " ";  
  16.         strOutput += xreader.GetAttribute("ProductName");  
  17.         Console.WriteLine( strOutput );  
  18.       }  
  19.     }  
  20.     xreader.Close();    
  21.   }  
  22.   catch 
  23.   {  
  24.     throw;  
  25.   }  
  26.   finally 
  27.   {  
  28.     conn.Close();  
  29.   }  


上述代码使用了下列存储过程: 

  1. CREATE PROCEDURE DATRetrieveProductsXML  
  2. AS  
  3. SELECT * FROM PRODUCTS   
  4. FOR XML AUTO  
  5. GO 

按下列步骤检索XML数据: 

创建SqlCommand对象启用生成XML结果设置的过程。(比如,利用SELECT状态中的FOR XML条款)。把SqlCommand对象与一个链接相联系。 
调用SqlCommand对象的ExecuteXmlReader方法,并把结果分配给前向对象XmlTextReader。当不需要任何返回数据的基于XML的验证时,这是应该使用的最快类型的XmlReader对象。 
利用XmlTextReader对象的Read方法读取数据。 
如何利用存储过程输出参数检索单个行 

可以调用一个存储过程,它通过一种称做输出参数的方式可以在单个行中返回检索数据项。下列代码段利用存储的过程检索产品的名称和单价,该产品包含在Northwind数据库中。 

  1. void GetProductDetails( int ProductID,   
  2.                         out string ProductName, out decimal UnitPrice )  
  3. {  
  4.   SqlConnection conn = new SqlConnection(  
  5.         "server=(local);Integrated Security=SSPI;database=Northwind");  
  6.  
  7.   // Set up the command object used to execute the stored proc  
  8.   SqlCommand cmd = new SqlCommand( "DATGetProductDetailsSPOutput", conn );  
  9.   cmd.CommandType = CommandType.StoredProcedure;  
  10.   // Establish stored proc parameters.  
  11.   //  @ProductID int INPUT  
  12.   //  @ProductName nvarchar(40) OUTPUT  
  13.   //  @UnitPrice money OUTPUT  
  14.  
  15.   // Must explicitly set the direction of output parameters  
  16.   SqlParameter paramProdID =   
  17.          cmd.Parameters.Add( "@ProductID", ProductID );  
  18.   paramProdID.Direction = ParameterDirection.Input;  
  19.   SqlParameter paramProdName =   
  20.          cmd.Parameters.Add( "@ProductName", SqlDbType.VarChar, 40 );  
  21.   paramProdName.Direction = ParameterDirection.Output;  
  22.   SqlParameter paramUnitPrice =   
  23.          cmd.Parameters.Add( "@UnitPrice", SqlDbType.Money );  
  24.   paramUnitPrice.Direction = ParameterDirection.Output;  
  25.   try 
  26.   {  
  27.     conn.Open();  
  28.     // Use ExecuteNonQuery to run the command.   
  29.     // Although no rows are returned any mapped output parameters   
  30.     // (and potentially return values) are populated  
  31.     cmd.ExecuteNonQuery( );  
  32.     // Return output parameters from stored proc  
  33.     ProductName = paramProdName.Value.ToString();  
  34.     UnitPrice = (decimal)paramUnitPrice.Value;  
  35.   }  
  36.   catch 
  37.   {  
  38.     throw;  
  39.   }  
  40.   finally 
  41.   {  
  42.     conn.Close();  
  43.   }  
  44. }  

按下列步骤利用存储的过程输出参数检索单个行: 

创建一个SqlCommand对象,并把它与SqlConnection对象相联系。 
通过调用SqlCommand’s Parameters集合的Add方法设置存储过程参数。缺省情况下,参数假定为输出参数,所以必须明确设置任何输出参数的方向。 
注意 明确设置所有参数的方向是一次很好的练习,包括输入参数。 

打开连接。 
调用Sqlcommand对象的ExecuteNonQuery方法。它在输出参数(并潜在地带有一个返回值)中。 
利用Value属性从合适的SqlParameter对象中检索输出参数。 
关闭连接。 
上述代码段启用了下列存储过程。 

  1. CREATE PROCEDURE DATGetProductDetailsSPOutput  
  2. @ProductID int,  
  3. @ProductName nvarchar(40) OUTPUT,  
  4. @UnitPrice money OUTPUT 
  5. AS 
  6. SELECT @ProductName = ProductName,   
  7.        @UnitPrice = UnitPrice   
  8. FROM Products   
  9. WHERE ProductID = @ProductID  
  10. GO 

如何利用SqlDataReader检索单个行 

可以利用SqlDataReader对象检索单个行,以及来自返回数据流的所需栏的值。这由下列代码说明: 

  1. void GetProductDetailsUsingReader( int ProductID,   
  2.                         out string ProductName, out decimal UnitPrice )  
  3. {  
  4.   SqlConnection conn = new SqlConnection(  
  5.          "server=(local);Integrated Security=SSPI;database=Northwind");  
  6.  
  7.   // Set up the command object used to execute the stored proc  
  8.   SqlCommand cmd = new SqlCommand( "DATGetProductDetailsReader", conn );  
  9.   cmd.CommandType = CommandType.StoredProcedure;  
  10.   // Establish stored proc parameters.  
  11.   //  @ProductID int INPUT  
  12.  
  13.   SqlParameter paramProdID = cmd.Parameters.Add( "@ProductID", ProductID );  
  14.   paramProdID.Direction = ParameterDirection.Input;  
  15.   try 
  16.   {  
  17.     conn.Open();  
  18.     SqlDataReader reader = cmd.ExecuteReader();  
  19.     reader.Read(); // Advance to the one and only row  
  20.  
  21.     // Return output parameters from returned data stream  
  22.     ProductName = reader.GetString(0);  
  23.     UnitPrice = reader.GetDecimal(1);  
  24.     reader.Close();  
  25.   }  
  26.   catch 
  27.   {  
  28.     throw;  
  29.   }  
  30.   finally 
  31.   {  
  32.     conn.Close();  
  33.   }  


按下列步骤返回带有SqlDataReader对象: 

建立SqlCommand对象。 
打开连接。 
调用SqlDReader对象的ExecuteReader对象。 
利用SqlDataReader对象的分类的存取程序方法检索输出参数--在这里是GetString和GetDecimal. 
上述代码段启用了下列存储过程: 

  1. CREATE PROCEDURE DATGetProductDetailsReader  
  2. @ProductID int 
  3. AS 
  4. SELECT ProductName, UnitPrice FROM Products  
  5. WHERE ProductID = @ProductID  
  6. GO 


如何利用ExecuteScalar单个项 

ExecuteScalar方法是设计成用于返回单个值的访问。在返回多列或多行的访问事件中,ExecuteScalar只返回第一行的第一例。 

下列代码说明如何查询某个产品ID的产品名称: 

  1. void GetProductNameExecuteScalar( int ProductID, out string ProductName )  
  2. {  
  3.   SqlConnection conn = new SqlConnection(  
  4.          "server=(local);Integrated Security=SSPI;database=northwind");  
  5.   SqlCommand cmd = new SqlCommand("LookupProductNameScalar", conn );  
  6.   cmd.CommandType = CommandType.StoredProcedure;  
  7.  
  8.   cmd.Parameters.Add("@ProductID", ProductID );  
  9.   try 
  10.   {  
  11.     conn.Open();  
  12.     ProductName = (string)cmd.ExecuteScalar();  
  13.   }  
  14.   catch 
  15.   {  
  16.     throw;  
  17.   }  
  18.   finally 
  19.   {  
  20.     conn.Close();  
  21.   }  

按下列步骤利用Execute Scalar检索单个项: 

建立调用存储过程的SqlCommand对象。 
打开链接。 
调用ExecuteScalar方法,注意该方法返回对象类型。它包含检索的第一列的值,并且必须设计成合适的类型。 
关闭链接。
上述代码启用了下列存储过程: 

  1. CREATE PROCEDURE LookupProductNameScalar  
  2. @ProductID int 
  3. AS 
  4. SELECT TOP 1 ProductName  
  5. FROM Products  
  6. WHERE ProductID = @ProductID  
  7. GO 

如何利用存储过程输出或返回的参数检索单个项 

利用存储过程输出或返回的参数可以查询单个值,下列代码说明了输出参数的使用: 

  1. void GetProductNameUsingSPOutput( int ProductID, out string ProductName )  
  2. {  
  3.   SqlConnection conn = new SqlConnection(  
  4.         "server=(local);Integrated Security=SSPI;database=northwind");  
  5.   SqlCommand cmd = new SqlCommand("LookupProductNameSPOutput", conn );  
  6.   cmd.CommandType = CommandType.StoredProcedure;  
  7.  
  8.   SqlParameter paramProdID = cmd.Parameters.Add("@ProductID", ProductID );  
  9.   ParamProdID.Direction = ParameterDirection.Input;  
  10.   SqlParameter paramPN =   
  11.          cmd.Parameters.Add("@ProductName", SqlDbType.VarChar, 40 );  
  12.   paramPN.Direction = ParameterDirection.Output;  
  13.   try 
  14.   {  
  15.     conn.Open();  
  16.     cmd.ExecuteNonQuery();  
  17.     ProductName = paramPN.Value.ToString();    
  18.   }  
  19.   catch 
  20.   {  
  21.     throw;  
  22.   }  
  23.   finally 
  24.   {  
  25.     conn.Close();  
  26.   }  

按下列步骤利用存储过程的输出参数检索单个值: 

创建调用存储过程的SqlCommand对象。 
通过把SqlParmeters添加到SqlCommand’s Parameters集合中设置任何输入参数和单个输出参数。 
打开链接。 
调用SqlCommand对象的Execute NonQuery方法。 
关闭链接。 
利用输出SqlParameter的Value属性检索输出值。 
上述代码使用了下列存储过程: 

  1. CREATE PROCEDURE LookupProductNameSPOutput   
  2. @ProductID int,  
  3. @ProductName nvarchar(40) OUTPUT 
  4. AS 
  5. SELECT @ProductName = ProductName  
  6. FROM Products  
  7. WHERE ProductID = @ProductID  
  8. GO 

下列代码说明如何利用返回值确定是否存在特殊行。从编码的角度看,这与使用存储过程输出参数相类似,除了需要明确设置到ParameterDirection.ReturnValue的SqlParameter方向。 

  1. bool CheckProduct( int ProductID )  
  2. {  
  3.   SqlConnection conn = new SqlConnection(  
  4.        "server=(local);Integrated Security=SSPI;database=northwind");  
  5.   SqlCommand cmd = new SqlCommand("CheckProductSP", conn );  
  6.   cmd.CommandType = CommandType.StoredProcedure;  
  7.  
  8.   cmd.Parameters.Add("@ProductID", ProductID );  
  9.   SqlParameter paramRet =   
  10.          cmd.Parameters.Add("@ProductExists", SqlDbType.Int );  
  11.   paramRet.Direction = ParameterDirection.ReturnValue;  
  12.   try 
  13.   {  
  14.     conn.Open();  
  15.     cmd.ExecuteNonQuery();  
  16.   }  
  17.   catch 
  18.   {  
  19.     throw;  
  20.   }  
  21.   finally 
  22.   {  
  23.     conn.Close();  
  24.   }  
  25.   return (int)paramRet.Value == 1;  


按下列步骤,可以利用存储过程返回值检查是否存在特殊行: 

建立调用存储过程的SqlCommand对象。 
设置包含需要访问的行的主要关键字的输入参数。 
设置单个返回值参数。把SqlParameter对象添加到SqlCommand’s Parameter集合中,并设置它到ParameterDireetion.ReturnValue的方面。 
打开链接。 
调用SqlCommand对象的ExecuteNonQuery的方法. 
关闭链接。 
利用返回值SqlParameter的Value属性检索返回值。 
上述代码使用了下列存储过程: 

  1. CREATE PROCEDURE CheckProductSP   
  2. @ProductID int 
  3. AS 
  4. IF EXISTS( SELECT ProductID  
  5.            FROM Products  
  6.            WHERE ProductID = @ProductID )  
  7.   return 1  
  8. ELSE 
  9.   return 0  
  10. GO 

如何利用SqlDataReader检索单个项。 

通过调用命令对象的ExecuteReader方法,可以利用SqlDataReader对象获得单个输出值。这需要稍微多一些的代码,因为SqlDataReader Read方法必须调用,然后所需值通过读者存取程序方法得到检索。SqlDataReader对象的使用在下列代码中说明: 

  1. bool CheckProductWithReader( int ProductID )  
  2. {  
  3.   SqlConnection conn = new SqlConnection(  
  4.          "server=(local);Integrated Security=SSPI;database=northwind");  
  5.   SqlCommand cmd = new SqlCommand("CheckProductExistsWithCount", conn );  
  6.   cmd.CommandType = CommandType.StoredProcedure;  
  7.  
  8.   cmd.Parameters.Add("@ProductID", ProductID );  
  9.   cmd.Parameters["@ProductID"].Direction = ParameterDirection.Input;  
  10.   try 
  11.   {  
  12.     conn.Open();  
  13.     SqlDataReader reader = cmd.ExecuteReader(  
  14.                                 CommandBehavior.SingleResult );  
  15.     reader.Read();  
  16.  
  17.     bool bRecordExists = reader.GetInt32(0) > 0;  
  18.     reader.Close();  
  19.     return bRecordExists;  
  20.   }  
  21.   catch 
  22.   {  
  23.     throw;  
  24.   }  
  25.   finally 
  26.   {  
  27.     conn.Close();   
  28.   }  
  29.  

上述代码使用了下列存储过程: 

  1. CREATE PROCEDURE CheckProductExistsWithCount   
  2. @ProductID int 
  3. AS 
  4. SELECT COUNT(*) FROM Products  
  5. WHERE ProductID = @ProductID  
  6. GO 


如何编码ADO.NET手工事务 

下列代码说明如何利用SQL Server. NET数据供应器提供的事务支持来保护事务的支金转帐操作。该操作在位于同一数据库中的两个帐户之间转移支金。 

  1. public void TransferMoney( string toAccount, string fromAccount, decimal amount )  
  2. {  
  3.   using ( SqlConnection conn = new SqlConnection(  
  4.             "server=(local);Integrated Security=SSPI;database=SimpleBank" ) )  
  5.   {  
  6.     SqlCommand cmdCredit = new SqlCommand("Credit", conn );  
  7.     cmdCredit.CommandType = CommandType.StoredProcedure;  
  8.     cmdCredit.Parameters.Add( new SqlParameter("@AccountNo", toAccount) );  
  9.     cmdCredit.Parameters.Add( new SqlParameter("@Amount", amount ));  
  10.  
  11.     SqlCommand cmdDebit = new SqlCommand("Debit", conn );  
  12.     cmdDebit.CommandType = CommandType.StoredProcedure;  
  13.     cmdDebit.Parameters.Add( new SqlParameter("@AccountNo", fromAccount) );  
  14.     cmdDebit.Parameters.Add( new SqlParameter("@Amount", amount ));  
  15.  
  16.     conn.Open();  
  17.     // Start a new transaction  
  18.     using ( SqlTransaction trans = conn.BeginTransaction() )  
  19.     {  
  20.       // Associate the two command objects with the same transaction  
  21.       cmdCredit.Transaction = trans;  
  22.       cmdDebit.Transaction = trans;  
  23.       try 
  24.       {  
  25.         cmdCredit.ExecuteNonQuery();  
  26.         cmdDebit.ExecuteNonQuery();  
  27.         // Both commands (credit and debit) were successful  
  28.         trans.Commit();  
  29.       }  
  30.       catch( Exception ex )  
  31.       {  
  32.         // transaction failed  
  33.         trans.Rollback();  
  34.         // log exception details . . .  
  35.         throw ex;  
  36.       }  
  37.     }  
  38.   }  
  39. }  

如何利用Transact-SQL执行事务 

下列存储过程说明了如何在Transact-SQL过程内执行事务的支金转移操作。 

  1. CREATE PROCEDURE MoneyTransfer  
  2. @FromAccount char(20),  
  3. @ToAccount char(20),  
  4. @Amount money  
  5. AS 
  6. BEGIN TRANSACTION 
  7. -- PERFORM DEBIT OPERATION  
  8. UPDATE Accounts  
  9. SET Balance = Balance - @Amount  
  10. WHERE AccountNumber = @FromAccount  
  11. IF @@RowCount = 0  
  12. BEGIN 
  13.   RAISERROR('Invalid From Account Number', 11, 1)  
  14.   GOTO ABORT  
  15. END 
  16. DECLARE @Balance money  
  17. SELECT @Balance = Balance FROM ACCOUNTS  
  18. WHERE AccountNumber = @FromAccount  
  19. IF @BALANCE < 0  
  20. BEGIN 
  21.   RAISERROR('Insufficient funds', 11, 1)  
  22.   GOTO ABORT  
  23. END 
  24. -- PERFORM CREDIT OPERATION  
  25. UPDATE Accounts   
  26. SET Balance = Balance + @Amount   
  27. WHERE AccountNumber = @ToAccount  
  28. IF @@RowCount = 0  
  29. BEGIN 
  30.   RAISERROR('Invalid To Account Number', 11, 1)  
  31.   GOTO ABORT  
  32. END 
  33. COMMIT TRANSACTION 
  34. RETURN 0  
  35. ABORT:  
  36.   ROLLBACK TRANSACTION 
  37. GO 


该存储过程使用BEGIN TRANSACTION, COMMIT TRANSACTION,和ROLLBACK TRANSACTION状态手工控制事务。 

如何编码事务性的.NET类 

下述例子是三种服务性的NET类,它们配置或用于自动事务。每个类都带有Transaction属性,它的值将决定是否启动新事务流或者对象是否共享即时调用程序的数据流。这些元素一起工作来执行银行支金转移。Transfer类配置有RequiresNew事务属性,而Debit和Credit类配置有Required属性。这样,在运行的时候三个对象共享同一个事务。 

  1. using System;  
  2. using System.EnterpriseServices;  
  3.  
  4. [Transaction(TransactionOption.RequiresNew)]  
  5. public class Transfer : ServicedComponent  
  6. {  
  7.   [AutoComplete]  
  8.   public void Transfer( string toAccount,   
  9.                         string fromAccount, decimal amount )  
  10.   {  
  11.     try 
  12.     {  
  13.       // Perform the debit operation  
  14.       Debit debit = new Debit();  
  15.       debit.DebitAccount( fromAccount, amount );  
  16.       // Perform the credit operation  
  17.       Credit credit = new Credit();  
  18.       credit.CreditAccount( toAccount, amount );  
  19.     }  
  20.     catch( SqlException sqlex )  
  21.     {  
  22.       // Handle and log exception details  
  23.       // Wrap and propagate the exception  
  24.       throw new TransferException( "Transfer Failure", sqlex );      
  25.     }  
  26.   }  
  27. }  
  28. [Transaction(TransactionOption.Required)]  
  29. public class Credit : ServicedComponent  
  30. {  
  31.   [AutoComplete]  
  32.   public void CreditAccount( string account, decimal amount )  
  33.   {  
  34.     SqlConnection conn = new SqlConnection(  
  35.             "Server=(local); Integrated Security=SSPI"; database="SimpleBank");  
  36.     SqlCommand cmd = new SqlCommand("Credit", conn );  
  37.     cmd.CommandType = CommandType.StoredProcedure;  
  38.     cmd.Parameters.Add( new SqlParameter("@AccountNo", account) );  
  39.     cmd.Parameters.Add( new SqlParameter("@Amount", amount ));  
  40.     try 
  41.     {  
  42.       conn.Open();  
  43.       cmd.ExecuteNonQuery();  
  44.     }  
  45.     catch (SqlException sqlex)  
  46.     {  
  47.       // Log exception details here  
  48.       throw// Propagate exception  
  49.     }  
  50.   }  
  51. }  
  52. [Transaction(TransactionOption.Required)]  
  53. public class Debit : ServicedComponent  
  54. {  
  55.   public void DebitAccount( string account, decimal amount )  
  56.   {  
  57.     SqlConnection conn = new SqlConnection(  
  58.             "Server=(local); Integrated Security=SSPI"; database="SimpleBank");  
  59.     SqlCommand cmd = new SqlCommand("Debit", conn );  
  60.     cmd.CommandType = CommandType.StoredProcedure;  
  61.     cmd.Parameters.Add( new SqlParameter("@AccountNo", account) );  
  62.     cmd.Parameters.Add( new SqlParameter("@Amount", amount ));  
  63.     try 
  64.     {  
  65.       conn.Open();  
  66.       cmd.ExecuteNonQuery();  
  67.     }  
  68.     catch (SqlException sqlex)  
  69.     {  
  70.       // Log exception details here  
  71.       throw// Propagate exception back to caller  
  72.     }  
  73.   }  
  74. }  


合作者

非常感谢下列撰稿者和审校者: 

Bill Vaughn, Mike Pizzo, Doug Rothaus, Kevin White, Blaine Dokter, David Schleifer, Graeme Malcolm(内容专家), Bernard Chen(西班牙人), Matt Drucke(协调)和Steve kirk. 

读者有什么样的问题、评论和建议?关于本文的反馈信息,请发E-mail至devfdbck®microsoft.com。 

你希望学习并利用.NET的强大功能吗?与微软技术中心的技术专家一起工作,学习开发最佳方案。详细信息请访问: http://www.micrsoft.com/business/services/mtc.asp

本日志由 flyinweb 于 2009-06-19 23:55:36 发表到 DotNet专栏 中,目前已经被浏览 169 次,评论 0 次;

作者添加了以下标签: .NET 数据访问架构


    在主线程启动一个新线程来进行拷贝,然后主线程订阅新线程的事件,并定义相关的处理方法来处理事件。

下面来说说功能实现的部分代码。
1、将用来拷贝文件的主要类:

  1.   class DoWorks  
  2.   {  
  3. //定一个一个委托  
  4.    public delegate void CopyFileHandler(long lngPosition, long LngCount);  
  5. //定义一个事件(很重要),这个事件将被主线程捕捉,并对主线程的内容进行更改,比如:进度条。  
  6.    public event CopyFileHandler CopyFileEvent;  
  7. //定义两个字符串变量,sFile表示源文件,tFile表示目标文件,当然,这个可以使用属性的方式,这个为了简单明了,直接使用共有变量。  
  8.    public System.String sFile;  
  9.    public System.String tFile;  
  10.  
  11.    //这个就是工作线程使用到的方法了。  
  12.    public void CopyFile()  
  13.    {  
  14.    //定义一个字节数组,用来缓存从源文件读到的字节流。  
  15.     byte[] fb = new byte[2048];  
  16.    //定义当前已读字节数,用于主线程更新界面。  
  17.     long lngPosition = 0;  
  18.     //源文件流  
  19.     FileStream sfs = new FileStream(sFile,System.IO.FileMode.Open,System.IO.FileAccess.Read);  
  20.    //二进制文件读取器  
  21.     BinaryReader br = new BinaryReader(sfs);  
  22.     br.BaseStream.Seek(0,System.IO.SeekOrigin.Begin);  
  23.       
  24.     if(File.Exists(tFile))  
  25.      File.Delete(tFile);  
  26.      //目标文件流  
  27.      FileStream tfs = new FileStream(tFile,System.IO.FileMode.CreateNew,System.IO.FileAccess.Write);  
  28.  
  29.    //二进制文件写入器  
  30.     BinaryWriter bw = new BinaryWriter(tfs);  
  31.     //源文件的大小  
  32.     long positionLength = sfs.Length;  
  33.     int k = 10000;  
  34.    //当读到的字节数小于2048,表示已经读到文件流的末尾了。停止读取  
  35.     while(k>=2048)  
  36.     {  
  37.      k = br.Read(fb,0,fb.Length);  
  38.  
  39.      bw.Write(fb);  
  40.  
  41.      lngPosition += k;  
  42.     //触发事件(关键),参数:1、表示当前共读取了多少,2、表示文件的长度       
  43.      CopyFileEvent(lngPosition,positionLength);  
  44.     }  
  45.     tfs.Flush();  
  46.     bw.Close();  
  47.     br.Close();  
  48.     tfs.Close();  
  49.     sfs.Close();  
  50.    }  
  51.   } 


这个大家看看应该没有什么吧。下面我们就使用这个类来进行文件的拷贝
2、按钮的点击事件处理方法:

  1. private void button1_Click(object sender, System.EventArgs e)  
  2. {  
  3.  DoWorks dw = new DoWorks();  
  4.  //这两个公有变量一定要赋值,因为这次主要演示目的,一些判断及异常处理省略了。  
  5.  dw.sFile = sourceFile;  
  6.  dw.tFile = targetFile;  
  7.  //这个是关键,定义事件的处理方法。  
  8.  dw.CopyFileEvent += new AsyncCopyFile.Form1.DoWorks.CopyFileHandler(this.ChgProgress);  
  9.  //定义新线程。  
  10.  Thread t = new Thread(new ThreadStart(dw.CopyFile));  
  11.  //启动线程  
  12.  t.Start();  


这里用到一个this.ChgProgress方法,这个方法就是用来处理界面的显示。
3、看一下事件处理方法:

  1. private void ChgProgress(long k,long count)  
  2. {  
  3.  this.progressBar1.Maximum = (int)count;  
  4.  this.progressBar1.Minimum = 0;  
  5.  this.progressBar1.Value = (int)k;  
  6.  this.label4.Text = count.ToString();  
  7.  this.label2.Text = k.ToString();  
  8.    

本日志由 flyinweb 于 2009-06-19 23:51:29 发表到 DotNet专栏 中,目前已经被浏览 164 次,评论 0 次;

作者添加了以下标签: C#文件复制进度条

近日下载了火影忍者的漫画,结果目录中的图片文件命名方式是1,2,.....,10,....99,100,这样在acdsee中观看的顺序就是1,10,100....不是按照数字的顺序,看起来比较郁闷。故此就动手写一个批量文件改名的小程序,把文件名补齐为3位,按照001,002,...,009,010,...这样的顺序。

涉及到的知识:string的函数;File和Directory函数;Environment和一些界面类

核心代码如下:十分简单

  1.  // 清空log  
  2. this.listBoxLog.Items.Clear();  
  3.  // 获取当前路径下全部文件名  
  4. String[] files = Directory.GetFiles(Environment.CurrentDirectory);  
  5. foreach(String filename in files)  
  6. {  
  7.  // 最后一个"\"  
  8.  int lastpath = filename.LastIndexOf("\\");  
  9.  // 最后一个"."  
  10.  int lastdot = filename.LastIndexOf(".");  
  11.  //  纯文件名字长度  
  12.  int length = lastdot-lastpath-1;  
  13.  //  文件目录字符串 xx\xx\xx\  
  14.  String beginpart = filename.Substring(0, lastpath+1);  
  15.  //   纯文件名字  
  16.  String namenoext = filename.Substring(lastpath+1, length);  
  17.  //   扩展名  
  18.  String ext = filename.Substring(lastdot);  
  19.    
  20.  if(length < 3)  
  21.  {  
  22.   // 补齐为3位,组成新的文件名  
  23.   String namenew;  
  24.   if(length == 1)  
  25.    namenew = "00" + namenoext;  
  26.   else 
  27.    namenew = "0" + namenoext;  
  28.   String fullnewname = beginpart + namenew + ext;  
  29.  
  30.   // 改名  
  31.   File.Move(filename, fullnewname);  
  32.  
  33.   // log  
  34.   this.listBoxLog.Items.Add(namenoext + "--->" + namenew);  
  35.   this.listBoxLog.SelectedIndex = this.listBoxLog.Items.Count - 1;  
  36.  } 

本日志由 flyinweb 于 2009-06-19 23:50:04 发表到 DotNet专栏 中,目前已经被浏览 230 次,评论 0 次;

作者添加了以下标签: 文件改名C#

131/2