级别: 初级 Jeremy McGee
| XML error: Please enter a value for the author element's jobtitle attribute, or the company-name element, or both. |
2003 年 10 月 01 日 Borland Data Provider 经过改进的特性为那些需要访问 IBM DB2 的 .NET 开发人员提供了一种高效的、可伸缩的创建 Windows 客户机和 Web 应用程序的方法。在本文中,Jeremy McGee 通过使用 C#Builder Enterprise、DB2 UDB 和用于 Microsoft .NET Framework 的 IBM Managed Data Provider 查看 ADO.NET 类和接口。
简介
如果您使用过 Borland® Delphi、C++Builder
或 Kylix,您就会知道为了连接到像 IBM® DB2® Universal Database
TM (UDB)
这样的数据库,最方便的是使用来自 Borland Visual Component Library (VCL)
的一些组件。有了 Borland C#Builder
TM,一切都变了:现在不需要使用
Borland VCL,而是使用 Microsoft® .NET
Framework,特别是 ADO.NET,而且也不需要使用
Borland Database Engine 或 dbExpress。
Bob Swart 关于连接到 DB2 UDB
数据的两篇文章说明了通过 C#Builder
可以使得创建简单的使用 Borland Data Provider
(BDP) 和 ADO.NET
的数据库应用程序变得很容易。(
第一篇文章
展示了如何可视化地连接 BDP
组件以便创建简单的用户界面。
第二篇文章
则更详细地介绍了 DataSet 类。)
在本文中,我们将看看所有那些 ADO.NET
类和接口可以为我们做些什么。我们将使用
C#Builder Enterprise、DB2 UDB 和用于 Microsoft .NET
Framework 的 IBM Managed Data Provider。
注意,我们将使用 DB2
管理的代码提供者作为一些示例。要安装该代码管理者,您需要 DB2 UDB 8.1 FixPak 3
或更佳版本的 Windows
客户机,该客户机要安装 Microsoft .NET Framework DB2 UDB
驱动程序。如果您现在使用的是 C#Builder,您将需要添加
IBM.Data.DB2 程序集(assembly)——使用
Project | Add Reference,然后选择
Browse 并导航到 IBM DB2 .NET Framework 1.1
程序集。默认情况下这个程序集安装在 C:\\Program Files\\IBM\\SQLLIB\\BIN\\netf11
中。必要时,我将使用常规的
DB2 UDB 样本数据库。
使用 ADO.NET:可视化还是非可视化?
VCL
用户马上会碰到的一个主要挑战就是在 .NET Framework
中没有相应的数据模型。这意味着在使用用户界面的可视情况下,要从其他格式的对象引用某个对象时没有容易的方法。虽然也有许多可以替代的选择,但是它们都要依赖于代码。在本文中,我包含了一些代码示例,以启示您该做些什么。幸好,与
VCL 一样,ADO.NET
给人印象深刻的一个方面是非常容易通过程序代码创建数据库访问逻辑。这些代码示例都将使用
C#。一旦您理解了程序代码(尤其是接口和类的精妙之处),则编写用于数据库访问逻辑的“包装器”类就非常简单了。如果以适当的方式编写了包装器类,则最终得到的将是“业务逻辑”类,这些业务逻辑类既可以处理所有的数据库访问,也可以在整个应用程序中使用。关于这一点的一个好例子就是我们在
前一篇文章 中讨论过的 IBuySpy
应用程序。
简短的概述
下面是经过简化的 ADO.NET
的架构视图,图中将 ADO.NET 与 BDE
作了比较。
图 1. ADO.NET 与 BDE
比较的简单视图
如果您熟悉 VCL 组件,那么您将发现 ADO.NET
背后的原理相当简单。ADO.NET
看上去虽然很不同,但是大部分的特性都还在——只不过是放在不同的地方而已。
与 DBConnection 连接
第一个类返回一个数据库连接对象。所有其他组件都要用这个对象作为句柄,以便引用数据库连接。
例如,在 C# 语言中,对于使用 DB2
管理的提供者的 IBM DB2:
DB2Connection myConn = new
IBM.Data.DB2.DB2Connection("DATABASE = SAMPLE;
UID=<username>; PWD=<password>;");
|
一旦该句柄被赋值,便可以打开该句柄,使用它作为查询或存储过程的
Connection 属性,然后再关闭它。例如:
DB2Connection myConn = new
IBM.Data.DB2.DB2Connection("DATABASE = SAMPLE;
UID=<username>; PWD=<password>;");
string myInsertQuery = "INSERT INTO STAFF (ID,
NAME) Values(1002,"Jane Doe")";
DB2Command myComm = new DB2Command(myInsertQuery);
myComm.Connection = myConn;
myConn.Open();
myComm.ExecuteNonQuery();
myConn.Close();
|
我们将在后面讲述 DB2Command 对象。
很直观地,我们可以看到 ADO.NET 和 VCL
之间的一个重要区别。假如要使用 BDE
连接到一个数据库,我们最好使用一个 TDatabase
组件,而不管底层数据库发生了什么。至于 ADO.NET,我们为每种数据库服务器使用数据访问组件的一个专门化的变种。
不同的数据提供者是如何实现的
为每种类型的数据库服务器专门定制数据库访问类这种方法是
ADO.NET
的设计的主要特性之一。尽管驱动程序类各不相同,但是所有这些类都有相同的基本功能,通常可以交换使用。诀窍是通过定义了特定于数据库的类的
接口来实现。
使用 IBM.Data.DB2 管理的提供者访问 DB2
服务器上数据的应用程序将使用一个 DB2Connection
组件,上一个例子中便是如此。对于 SQL
Server,有一个类似的组件,即 SqlConnection:
SqlConnection myConn
= new
System.Data.SqlClient.SqlConnection("Persist
Security Info=False;Integrated
Security=SSPI;database=northwind;server=mySQLServer");
|
对于 Oracle,有一个 OracleConnection 组件:
OracleConnection myConn
= new
System.Data.OracleClient.OracleConnection("Data
Source=Oracle8i;Integrated Security=yes");
|
所有这些组件都有相同的一套基本属性、方法和事件:换句话说,这些组件共享相同的接口。用于数据库连接的接口是
IDbConnection,在 Microsoft .NET Framework
帮助中对此有文档说明。这个接口定义了最小级别的功能;实际上,每个组件都很可能有附加的属性、方法和事件,用以支持特定数据库或驱动程序的一些特殊的特性。
例如,DB2Connection
组件包括一个附加的方法,该方法可用于强制释放连接池资源。默认情况下,ADO.NET
提供者可以共享和重用连接,这样可以节省资源,但同时也意味着连接要一直开放,以备可能的重用。这个方法为您提供了更多的控制,这是通过像
SQL Server
管理的提供者这样的驱动程序所不能提供的。
虽然特定于数据库的组件提供了很大的灵活性,但是它们并不能带来可移植的代码。或许正是这个讨厌的特点使得您想从一种数据库换到另一种数据库。考虑到这一点,Borland
创建了 Borland Data Provider (BDP)。
BDP 是一个常规的受管 .NET
数据提供者,但是又有一点不同。它不是只使用一种数据库,而是可以使用多种不同的服务器。实际的驱动程序由创建
BdpConnection
对象时传递给这个对象的连接字符串(或者
ConnectionString 属性)决定。
所以为了使用 BDP 连接到 SQL Server,您可以使用:
BdpConnection myConn = new
Borland.Data.Provider.BdpConnection("assembly=Borl
and.Data.Mssql,Version=1.1.0.0,
Culture=neutral,PublicKeyToken=91d62ebb5b0d1b1b;ve
ndorclient=sqloledb.dll;
osauthentication=False;database=<database>;usernam
e=<user>;hostname=<host>;
password=<password>;provider=MSSQL");
|
注意,对于这种特定的驱动程序,主机名嵌入在连接字符串中。对于
DB2
驱动程序来说这不是必需的,您可以使用:
BdpConnection myConn = new
Borland.Data.Provider.BdpConnection("assembly=Borl
and.Data.Db2,Version=1.1.0.0,
Culture=neutral,PublicKeyToken=91d62ebb5b0d1b1b;ve
ndorclient=db2cli.dll;
database=<database>;username=<user>;
password=<password>;provider=DB2");
|
虽然连接语法稍微有点不同,但是返回的对象是一样的。也就是说,
所有其他的代码并没有变。您甚至可以将这个字符串存放在一个资源文件中,并在运行时引用它。
Borland 提供了一个 Connections Editor,窗体设计者可以用它来创建连接字符串。我可以在
C#Builder
的另一个实例中另外创建一个项目,将一个
bdpConnection 对象放到 Windows Form
上,然后使用剪切板将连接字符串复制到我正在编写的应用程序的一个资源文件中。
在大多数情况下,BDP
在内部调用与特定于服务器的组件一样的本地客户机库,因此速度是一样快的。
DbConnection 类与 BDE TDatabase 或 dbExpress TSQLConnection
组件大致相当。与这些组件一样,DbConnection
是管理事务的地方。不过事务管理的方法却颇有不同:DbConnection
类的 BeginTransaction
方法返回一个事务对象,这个对象可用作后面的
SQL 命令的一个属性。
使用 DbCommand 执行查询
一旦打开了连接,ADO.NET
将通过组件直接运行对数据库的查询。与 DbConnection
类一样,DbCommand
是一个抽象接口,有其特定于服务器的实现。我们已经见过这个例子了:
DB2Connection myConn = new
IBM.Data.DB2.DB2Connection("DATABASE = SAMPLE");
string myInsertQuery = "INSERT INTO STAFF (ID,
NAME) Values(1002,"Jane Doe")";
DB2Command myComm = new DB2Command(myInsertQuery);
myComm.Connection = myConn;
myConn.Open();
myComm.ExecuteNonQuery();
myConn.Close();
|
我们可以看到,DB2Command——这个特定于
DB2 风格的 DbCommand——可用于直接运行对
DB2 服务器的查询。对于 BDP
也有相应的接口,因此
BdpConnection myConn = new
Borland.Data.Provider.BdpConnection(connectionstring);
string myInsertQuery = "INSERT INTO STAFF (ID,
NAME) Values(1002,"Jane Doe")";
BdpCommand myComm = new BdpCommand(myInsertQuery);
myComm.Connection = myConn;
myConn.Open();
myComm.ExecuteNonQuery();
myConn.Close();
|
可以运行在任何有 BDP
驱动程序的服务器上。
SQL INSERT
命令不能返回结果,但是可以带参数。ADO.NET
可以使用 DbDataParameter 类通过 Parameter
属性来包含参数。同样,参数类也是在特定于服务器的基础上实现的。
下面是一个关于参数的例子,它调用一个
DB2 存储过程以便从数据库返回完整的名称,假设是一个电子邮件地址。首先是存储过程:
CREATE PROCEDURE Job(
IN v_Name VARCHAR(50),
OUT v_Job VARCHAR(50))
DYNAMIC RESULT SETS 1
LANGUAGE SQL
BEGIN
SELECT Job INTO v_Job FROM Staff WHERE Name = v_Name
FETCH FIRST 1 ROWS ONLY;
END
|
然后是 C#
代码片断。我们创建连接,创建一个命令,然后创建两个
BdpParameter 对象并将它们添加到命令对象。
public String GetJob(String Name)
{
BdpConnection myConn = new BdpConnection(<connection string>);
BdpCommand myCommand = new BdpCommand("Job", myConn);
// This is a stored procedure - mark the command accordingly
myCommand.CommandType = CommandType.StoredProcedure;
// Add the input parameter
BdpParameter paramName = new BdpParameter("V_NAME", BdpType.String, 50);
paramName.Direction = ParameterDirection.Input;
paramName.Precision = 50;
myCommand.Parameters.Add(paramName);
// The output parameter
BdpParameter paramJob = new BdpParameter("V_JOB", BdpType.String, 50);
paramJob.Direction = ParameterDirection.Output;
paramJob.Precision = 50;
myCommand.Parameters.Add(paramJob);
paramName.Value = Name;
myConn.Open();
myCommand.ExecuteNonQuery();
myConn.Close();
// The return value is passed as an Object. We need to cast as a string
return (string)paramJob.Value;
} |
这里有两个重要的地方需要注意。首先,参数声明的顺序应该与存储过程中声明的顺序一致。其次,您需要显式地将参数类型覆盖为服务器所期望的特定的底层数据类型。下面是
DB2 管理的提供者和 BDP
所支持的一些不同的数据类型的例子:
|
DB2 数据类型
|
DB2 管理的提供者
|
Borland Data Provider
| | VARCHAR | DB2Type.VarChar | BdpType.String | | INT | DB2Type.Integer | BdpType.Int16, BdpType.Int32, 或 BdpType.Int64, 根据需要而定 | | IMAL (x, y) | DB2Type.Decimal | BdpType.Decimal | | TIMESTAMP | DB2Type.TimeStamp | BdpType.DateTime |
您还将需要为这些数据类型设置 Size 和 Precision
属性。
这种方法与 BDE and dbExpress
有很大的不同,后者使用 TQuery 和 TSQLQuery
组件来调用存储过程或者其他数据定义查询,这种调用不会返回列表结果集。这些应该用
DbCommand
对象代替。然而,我们可以看到,如果数据需要更新的话,通常 SELECT 查询要么需要使用一个 DataReader,要么需要使用一个
DataSet。
使用 DataReader 直接访问 DbCommand 数据
我刚才为 DbCommand
类列出的这些例子没有返回一个数据集。DbCommand
可以处理这一点,但是您将需要使用 DataReader
组件来获取数据。
乍一看来,DataReader
好像受到一些限制。您只能向前移动,每次读取一条记录,没有数据缓存。然而,这些限制比起它的主要特性,即速度来就不足道了——DataReader 速度极快。
奇怪的是一个程序频繁地使用适合于 DataReader
的一条 SELECT 查询。其中一个例子就是从一个准备显示在
Web 页面上的表返回一个记录块:
public String GetStaff(string Job)
{
BdpConnection myConn = new
BdpConnection(<connection string>);
BdpCommand myCommand = new BdpCommand("SELECT
NAME, YEARS FROM STAFF WHERE JOB=? FETCH FIRST 10
ROWS ONLY", myConn);
BdpParameter paramJob = new BdpParameter("JOB",
BdpType.String, 50);
paramJob.Direction = ParameterDirection.Input;
paramJob.Precision = 50;
myCommand.Parameters.Add(paramJob);
paramJob.Value = Job;
myConn.Open();
BdpDataReader readerStaff =
myCommand.ExecuteReader();
string staffList="";
while (readerStaff.Read()) {
staffList += readerStaff.GetString(0)+"
("+readerStaff.GetInt16(1)+") <br />";
}
readerStaff.Close();
myConn.Close();
return staffList;
} |
这个例子使用了 BDP。DB2
管理的提供者的语法非常的相似:只是把 "Bdp? components to be "Db2?
组件和 BdpType.String 改为 DB2Type.VarChar。
这个例子显示了如何使用 SELECT
查询中的参数——这个过程与存储过程中使用的过程很相似。您需要添加
Parameter 对象到 Command.Parameters
属性,添加的顺序与查询中定义 "?"
占位符的顺序一致。注意,我们将显式地告诉系统需要什么样的数据,这样可以使代码运行得更快。
对于 BDE 没有 IDataReader 的同等物:尽管
TQuery 有一个 UniDirectional 属性,但这只能影响
BDE,而对于对数据库的底层查询没有作用,因此这里总是要准备一定程度的缓冲。
在 dbExpress 中最接近的同等物是 TSQLQuery
组件,该组件同样返回一个前向结果集。然而,我们已经看到,使用
ADO.NET 检索结果集更加容易。
DbDataAdapter 和 DataSet
如果您想缓存数据并交互作用地浏览数据时该怎么办呢?这就是
DataSet
对象可以做的。这种强大的对象可以管理一个本地数据缓存,缓存的数据是通过使用一个
DbDataAdapter
对象从一个数据库中检索到的。然后可以使用这个缓存作为其他可视化
.NET Framework 组件(例如 DataGrid 或 TextBox)的数据源。
在缓存数据的时候,DataSet 对象与 VCL
TClientDataSet
相似。然而,它要强大得多:例如,它可以存储来自多个表中的数据。也就是说,当我们建立 DataSet
对象时,我们需要告诉这个对象要针对什么表以及要将哪些列存入该对象中。例如:
// Create a new dataset
DataSet StaffDataSet = new DataSet("DSStaff");
// Set up the columns for the table for the dataset
DataColumn IDCol = new DataColumn("ID", typeof(short));
DataColumn NameCol = new DataColumn("NAME");
DataColumn YearsCol = new DataColumn("YEARS", typeof(short));
// Set up the table for the dataset and add the columns
DataTable StaffTable = new DataTable("TabStaff");
StaffTable.Columns.AddRange(new DataColumn[] {IDCol, NameCol, YearsCol});
// Add the table to the dataset
StaffDataSet.Tables.AddRange(new DataTable[] {StaffTable});
|
您可以使用很多参数来微调这些对象中每一个对象的行为——参见联机帮助以获得一个概述。我将使用可以将所需代码减至最少的构造函数。注意,DataSet
组件只有一种风格。不管您选择哪个受管的提供者,DataSet
组件都是一样的。
我们的代码只配置 DataSet,但是不检索数据。要检索数据,我们需要一个
DbDataAdapter。这个组件不仅可以从数据库检索数据,还可以从
DataSet 将改过的数据放回数据库。
因为还必须将不同的数据类型映射到 .NET Framework
数据类型,DbDataAdapter
对象因所使用的数据库服务器的不同而采用不同的风格。为了使用 DbDataAdapter
组件,通常需要设置一些属性,以便定义用于检索、更新、插入和删除记录的查询。如果您想更新数据,则可以调用这些查询。
所有这些查询都存储在一个 DbCommand
对象中,因此,如果您想处理更新,需要创建
4 个查询,每一个查询都要参数化——这很难掌握。
不过,如果您只是想读取数据,那么事情就比较简单了。下面的代码片断展示了如何使用
BDP 来做这种工作:
BdpConnection myConn = new
BdpConnection(ConnString);
// Create a command object to retrieve data
BdpCommand SelComm = new BdpCommand("SELECT ID,
NAME, YEARS FROM STAFF", myConn);
// Create the data adapter
BdpDataAdapter StaffDA = new BdpDataAdapter();
// Point the data adapter to the command object to
get data
StaffDA.SelectCommand = SelComm;
|
首先,我建立连接,然后建立一个 BdpCommand
对象,用以检索数据。DataAdapter 用于将 BdpCommand
链接到 DataSet 中的 DataTable。至此您可能会预想建立
DB2DataAdapter 的过程是类似的:用 DB2
中的同等物替代 BDP 组件。
创建好 DbDataAdapter 之后,您需要填充
DataSet。这里,在 BdpDataAdapter
与其他风格之间会有所不同。对于 DB2DataAdapter
和其他特定于服务器的驱动程序,您需要自己在代码中告诉
DbDataAdapter 填充 DataSet。
例如,下面的代码可以添加到使用 DB2
管理的提供者的一个窗体的 Open 方法中:
StaffDA.Fill(StaffDataSet);
this.dataGrid1.DataMember = "TabStaff";
this.dataGrid1.DataSource = StaffDataSet;
|
BDP 使得事情更容易一点。一个额外的
active 属性可用于告诉 DataAdapter
自动地根据请求填充一个 DataSet 中的一个 DataTable。因此这里不是在
Open
方法中设置属性,而是将以下几行添加到初次建立连接时运行的代码当中:
// Point the data adapter to the DataSet and the table within it
StaffDA.DataSet = StaffDataSet;
StaffDA.DataTable = StaffTable;
// Tell the DataAdapter to automatically populate the table
StaffDA.Active = true;
|
这样做的一个很大的副作用是 DataSet
可以在设计的时候填充,从而暴露了活动数据。尽管这种情况只有当
BdpDataAdapter 被与 DataSet
和相关的控件放在同一个窗体上的时候才会发生,但这的确是个陷阱。实际上您可能会发现,对于更大的系统,您将使用一个中央对象来封装
DataSet,以便您可以在多个窗体之间共享它。
如果您以前使用的是 BDE,您可以认为 DataSet
组件与 TClientDataSet
有点相像。它们之间在表面上有些类似之处:与
TClientDataSet 一样,DataSet
将数据缓存在工作站本地,这是一种使用数据的有效方法,因为数据是在一个大块中读取和更新的。与
BDE 一样,当它返回一个只读结果集时,DataAdapter
为开发者提供了 SQL
查询以更新源数据库。
然而,在 TClientDataSet
的背后却颇有不同。与 BDE TClientDataSet
不一样,DataSet 可以缓存多个表。DataSet
在内部管理数据,把数据当作 XML
模式的关系视图,因此它没有“游标”或当前记录的概念。相反,当前记录指针是由数据绑定层管理的。
另一个不同之处就是处理更新时所用的技术。TClientDataSet
保留了对表的更新日志,这种方法使得处理多层的“undo”非常方便。相反,当使用数据绑定控件进行更新时,DataSet
将更新一行接一行地保存,只保留被更新和删除的行的最初版本。
从 BDE 转移到 ADO.NET
和 BDP
如果您习惯使用 BDE,您会希望看看一些组件,下面就是对这些组件的一个简短的总结。这里并没有列出所有的组件,而是很自然地经过了简化,但应该可以帮助您理解从哪开始。
| BDE 组件 | 功能 | ADO.NET 组件 | 功能 | | TSession 和TDataBase | 建立到数据库的连接和管理事务 | DB2Connection, BdpConnection | 建立到数据库的连接和管理事务 | | Ttable | 管理一个表的结果集的本地缓存 | 没有直接的同等物。要么同时使用 DataSet 和 DbDataAdapter,要么同时使用
DbCommand 和 DataReader | DataSet 根据查询创建一个或多个表的本地缓存 | | Tquery | 管理一个查询的本地缓存 | 没有直接的同等物。要么使用 DataSet / DbDataAdapter,要么使用
DbCommand / DataReader | DbCommand / DataReader 管理查询结果的前向游标 | | TstoredProc | 执行一个存储过程 | DbCommand | 不能执行存储过程,可以可选地返回一个结果集 | | TdataSource | 充当 dataset 与数据感知控件(data aware control)之间的通道 | 没有直接的同等物 | 数据感知控件链接到 DataSet 对象中的列 | | TDBText, TDBEdit, etc. | Windows 控件的数据感知版本 | 使用常规的 Windows Forms | 所有适当的 Windows Forms 都是数据感知的 |
结束语
ADO.NET
是一个丰富的、先进的数据库访问库。虽然
ADO.NET
与 Borland Database Engine 有很大的不同,但是
Borland Data Provider
经过改进的特性使得它更易于使用。BDP
为需要访问 DB2 的 .NET
开发人员提供了一种高效的、可伸缩的创建
Windows 客户机和 Web 应用程序的方法。
免责声明
本文包含样本代码。IBM 授予您(“被许可方”)使用这个样本代码的非专有的、版权免费的许可证。然而,该样本代码是以“按现状”的基础提供的,没有任何形式的(不论是明示的,还是默示的)保证,包括对适销性、适用于特定用途或非侵权性的默示保证。IBM 及其许可方不对被许可方由于使用该软件所导致的任何损失负责。任何情况下,无论损失是如何发生的,也不管责任条款怎样,IBM 或其许可方都不对由使用该软件或不能使用该软件所引起的收入的减少、利润的损失或数据的丢失,或者直接的、间接的、特殊的、由此产生的、附带的损失或惩罚性的损失赔偿负责,即使 IBM 已经被明确告知此类损害的可能性,也是如此。
关于作者  | |  |
Jeremy McGee
一开始是在 Commodore PET 上使用 BASIC
编写应用程序的。他还能亲切地想起在一台带有冷藏柜那么大的激光打印机的早期苹果机上为一本书那么长的出版物排版的情形。从那以后,他有了各种不同的工作经验:DEC VAX
系统管理员,Borland Paradox
的技术支持工程师,以及在欧洲的 Borland Delphi
发布小组的成员。Jeremy
现在在美国南安普敦运营着一家咨询公司。
|
对本文的评价
|