您已经了解到如何构建DataTable来表示内存中单个数据表。虽然DataTable可用作独立实体,但更多情况下都把它们包含在DataSet中。实际上,ADO.NET提供的大部分数据访问类型都只返回一个已填充的DataSet,而不是单个DataTable。
简单点说, DataSet就是任意数目的表(也可以是一个DataTable)在内存中的表示形式,以及这些表和任何(可选)约束之间的关系(可选)。为了让您更好地明白这些核心类型之间的关系,可以参考一下图A-18所示的逻辑结构。
图A-18 DataSet的集合
DataSet的Tables属性可以访问那些包含单独DataTable的DataTableCollection。DataSet用到的另一个重要集合就是DataRelationCollection。由于DataSet是一个数据库模式的断开连接的版本,我们就可以以编程方式表示它的表之间父/子关系。
例如,可以用DataRelation类型创建两个表之间的关系来模拟一个外键约束。这个对象可以通过Relations属性添加到DataRelationCollection中。这样您可以在被连接的表之间查找所需的数据。在本章稍后的部分您将看到这些内容。
ExtendedProperties属性能够访问PropertyCollection类型,这样就可以把外部的信息以名/值对的形式关联到DataSet上。这个信息可以是任何信息,即便它和数据本身一点关系也没有也可以。例如,可以把您的公司名关联到DataSet上,这样就可以把它作为一个内存元数据使用,如下所示:
// Make a DataSet and add some metadata.
DataSet ds = new DataSet("MyDataSet");
ds.ExtendedProperties.Add("CompanyName", "Intertech, Inc");
// Print out the metadata.
Console.WriteLine(ds.ExtendedProperties["CompanyName"].ToString());
还有其他一些扩展属性的例子,例如一个必须提供来访问DataSet内容的内部密码、一个代表数据刷新率的数值等。注意,DataTable本身也支持ExtendedProperties属性。
A.9.1 DataSet的成员
在探究更多的编程细节之前,先来看一下DataSet的公共接口。定义在DataSet中的属性都集中于对内部集合的访问、生成XML数据表示以及提供详细的错误信息。表A-9列出了一些比较重要的核心属性。
表A-9 强大的DataSet的属性
DataSet属性
|
意 义
|
CaseSensitive
|
表示DataTable对象的字符串比较是否区分大小写
|
DataSetName
|
获得或设置DataSet的名称。一般把这个参数当作构造函数的参数
|
DefaultViewManager
|
建立DataSet中数据的自定义视图
|
EnforceConstraints
|
获得或设置在试图执行任何更新操作时是否遵循约束规则的值
|
HasErrors
|
获得一个表示DataSet中任何表上的行是否有错误的值
|
Relations
|
获得连接表的关系集合,并可以从父表导航到子表
|
Tables
|
可以访问DataSet中表的集合
|
DataSet的方法模拟了一些由上述属性提供的功能。除了能和XML流交互之外,其他方法还可以复制DataSet的内容,当然也可以为一个批处理更新操作建立开始点和结束点。表A-10列出了一些核心的方法。
表A-10 功能强大的DataSet的方法
DataSet方法
|
意 义
|
AcceptChanges()
|
在该DataSet加载后或者前一次AcceptChanges()方法调用时提交对它所做的修改
|
Clear()
|
完全清除DataSet的数据,删除每个表上的行
|
Clone()
|
克隆DataSet的结构,包括所有的DataTable以及所有的关系和约束
|
Copy()
|
复制DataSet的结构和数据
|
GetChanges()
|
返回DataSet的副本,包括它被加载后或者前一次AcceptChanges()方法调用时对DataSet所做的修改
|
GetChildRelations()
|
返回属于指定表的子关系集合
|
GetParentRelations()
|
获得属于指定表上的父关系集合
|
HasChanges()
|
已重载。获得一个值表示这个DataSet是否被修改,包括添加、删除或修改行
|
Merge()
|
已重载。把一个指定的DataSet和这个DataSet进行合并
|
ReadXml()
ReadXmlSchema()
|
可从一个有效流中读取XML数据到DataSet中(这个流可以基于文件、基于内存或网络)
|
RejectChanges()
|
回滚它被创建后或者前一次AcceptChanges()方法调用时对DataSet所做的修改
|
WriteXml()
WriteXmlSchema()
|
可把一个DataSet的内容写入到有效流中
|
现在您应该已经能很好地明白DataSet所扮演的角色了(当然您也可以有些其他想法),让我们通过一些特例来加深了解。在讨论完ADO.NET的DataSet之后,本章后面的部分将会着重讨论如何使用由System.Data.SqlClient和System.Data.OleDb命名空间定义的类型,从外部源获取DataSet类型。
A.9.2 构建内存中的DataSet
下面新建一个保存单个DataSet的Windows Forms应用程序来说明其用法,这个DataSet包含有3个分别叫作Inventory、Customers和Orders的DataTable对象。每个表中的列都非常少,但都很完整,而且每个表上都有一列被标记为主键。更重要的是,您可以用DataRelation类型定义表之间的父/子关系。下面的任务就是要在内存中创建一个如图A-19所示的数据库。
图A-19 内存中的Automobile数据库
这儿的Inventory表是Orders表的父表,Orders表中有一个外键列(CarID)。同时Customers表也是Orders表的父表(还是将CarID作为外键)。稍后将看到,当往DataSet中添加DataRelation类型后,可以用这些类型在表之间导航来获得并操作相关数据。
首先假定您已经往主Form中添加了一些成员变量,这些变量表示DataTable和DataSet,如下所示:
public class mainForm : System.Windows.Forms.Form
{
// Inventory DataTable.
private DataTable inventoryTable = new DataTable("Inventory");
// Customers DataTable.
private DataTable customersTable = new DataTable("Customers");
// Orders DataTable.
private DataTable ordersTable = new DataTable("Orders");
// Our DataSet!
private DataSet carsDataSet = new DataSet("CarDataSet");
. . .
}
现在尽可能把任务面向对象化,创建一些非常简单的包装类来表示系统中的Car和Customer类。注意,Customer类有一个表示客户感兴趣购买车的字段,如下所示:
public class Car
{
// Make public for easy access.
public string petName, make, color;
public Car(string petName, string make, string color)
{
this.petName = petName;
this.color = color;
this.make = make;
}
}
public class Customer
{
public Customer(string fName, string lName, int currentOrder)
{
this.firstName= fName;
this.lastName = lName;
this.currCarOrder = currentOrder;
}
public string firstName, lastName;
public int currCarOrder;
}
主Form中用两个ArrayList类型来保存一组Car和Customer,它们用Form构造函数中的一些示例数据填充。接下来构造函数会调用许多私有的帮助函数来构建表以及表之间的关系。最后,这个方法把Inventory和Customer的DataTable分别绑定到相应的DataGrid上。注意,下面的代码使用SetDataBinding()方法绑定到DataSet中的特定DataTable上:
// Your list of Cars and Customers.
private ArrayList arTheCars, arTheCustomers;
public mainForm()
{
// Fill the car array list with some cars.
arTheCars = new ArrayList();
arTheCars.Add(new Car("Chucky", "BMW", "Green"));
. .
// Fill the other array list with some customers.
arTheCustomers = new ArrayList();
arTheCustomers.Add(new Customer("Dave", "Brenner", 1020));
. . .
// Make data tables (using the same techniques seen previously).
MakeInventoryTable();
MakeCustomerTable();
MakeOrderTable();
// Add relation (seen in just a moment).
BuildTableRelationship();
// Bind to grids (Param1 = DataSet, Param2 = name of table in DataSet).
CarDataGrid.SetDataBinding(carsDataSet, "Inventory");
CustomerDataGrid.SetDataBinding(carsDataSet, "Customers");
}
可以使用本章前面讲到的技术来构造出每个DataTable。为了继续关注DataSet逻辑,我不再重复关于表构建的逻辑。然而,要知道每个表都指定了自动增加的主键。下面列出了一部分表构建的逻辑(通过相同的代码可了解详细的内容):
private void MakeOrderTable()
{
. . .
// Add table to the DataSet.
carsDataSet.Tables.Add(customersTable);
// Create OrderID, CustID, CarID columns and add to table. . .
// Make the ID column the primary key column. . .
// Add some orders.
for(int i = 0; i < arTheCustomers.Count; i++)
{
DataRow newRow;
newRow = ordersTable.NewRow();
Customer c = (Customer)arTheCustomers[i];
newRow["CustID"] = i;
newRow["CarID"] = c.currCarOrder;
carsDataSet.Tables["Orders"].Rows.Add(newRow);
}
}
MakeInventoryTable()和MakeCustomerTable()帮助函数的行为几乎完全一样。
真正有趣的工作都在BuildTableRelationship()帮助函数中。在用一些表填充DataSet后,您可以选择用编程的方式来模拟表之间的父/子关系。要知道这并不是强制的。您可以用一个DataSet将DataTable集合(甚至是DataTable)保存在内存中。然而,如果在DataTable之间建立了内部的相互关系,您就可以快速在表之间导航,并收集任何您感兴趣的信息,所有信息这时候都已经和数据源断开连接。
System.Data.DataRelation类型是一个包含表对表关系的OO包装器。创建新的DataRelation类型时必须指定一个名称,然后是父表(例如,Inventory)和关联的子表(Orders)。要建立关系的话,那么每个表都必须有一个相同数据类型(本例中为Int32)的同名列(CarID)。这样,DataRelation
必须根据关系数据库的相同规则来约束。下面是完整的
BuildTableRelationship()
帮助函数的实现:
private void BuildTableRelationship()
{
// Create a DR obj.
DataRelation dr = new DataRelation("CustomerOrder",
carsDataSet.Tables["Customers"].Columns["CustID"], // Parent.
carsDataSet.Tables["Orders"].Columns["CustID"]); // Child.
// Add to the DataSet.
carsDataSet.Relations.Add(dr);
// Create another DR obj.
dr = new DataRelation("InventoryOrder",
carsDataSet.Tables["Inventory"].Columns["CarID"], // Parent.
carsDataSet.Tables["Orders"].Columns["CarID"]); // Child.
// Add to the DataSet.
carsDataSet.Relations.Add(dr);
}
可以看到,DataSet所维护的DataRelationCollection中保存了一个DataRelation。这个DataRelation类型提供了很多属性,根据这些属性您可以获得对参与该关系中的父/子表的引用、指定关系的名称等(见表A-11)。
表A-11 DataRelation类型的属性
DataRelation属性
|
意 义
|
ChildColumns
ChildKeyConstraint
ChildTable
|
获得这个关系中子表以及这个表的相关信息
|
DataSet
|
获得关系集合所属的DataSet
|
ParentColumns
ParentKeyConstraint
ParentTable
|
获得这个关系中父表以及这个表的相关信息
|
RelationName
|
获得或设置在父表数据集的DataRelationCollection中查找这个关系所用的名称
|
在关联表之间导航
我们可以把GUI扩展为包含一个新的按钮类型和相关的文本框,这样可以说明DataRelation如何允许在关联表进行移动。终端用户可以输入客户的ID并获得这个客户的所有订单信息,这些信息都放在一个简单的消息框中(见图A-20)。
图A-20 导航数据关系
下面显示了按钮的单击事件处理程序(为了简洁起见,已经删除了错误检查部分):
protected void btnGetInfo_Click (object sender, System.EventArgs e)
{
string strInfo = "";
DataRow drCust = null;
DataRow[] drsOrder = null;
// Get the specified CustID from the TextBox.
int theCust = int.Parse(this.txtCustID.Text);
// Now based on CustID, get the correct row in Customers table.
drCust = carsDataSet.Tables["Customers"].Rows[theCust];
strInfo += "Cust #" + drCust["CustID"].ToString() + "/n";
// Navigate from customer table to order table.
drsOrder = drCust.GetChildRows(carsDataSet.Relations["CustomerOrder"]);
// Get customer name.
foreach(DataRow r in drsOrder)
strInfo += "Order Number: " + r["OrderID"] + "/n";
// Now navigate from order table to inventory table.
DataRow[] drsInv =
drsOrder[0].GetParentRows(carsDataSet.Relations["InventoryOrder"]);
// Get Car info.
foreach(DataRow r in drsInv)
{
strInfo += "Make: " + r["Make"] + "/n";
strInfo += "Color: " + r["Color"] + "/n";
strInfo += "Pet Name: " + r["PetName"] + "/n";
}
MessageBox.Show(strInfo, "Info based on cust ID");
}
可以看到,在数据表之间移动最关键就是使用一些DataRow类型定义的方法。让我们一步步地看这段代码。首先从文本框中获得正确的客户ID,然后用它来找到Customer表中正确的行(当然是使用Rows属性),如下所示:
// Get the specified CustID from the TextBox.
int theCust = int.Parse(this.txtCustID.Text);
// Now based on CustID, get the correct row in Customers table.
DataRow drCust = null;
drCust = carsDataSet.Tables["Customers"].Rows[theCust];
strInfo += "Cust #" + drCust["CustID"].ToString() + "/n";
接下来通过CustomerOrder数据关系从Customers表导航到Orders表。注意,DataRow.GetChildRows()方法可以获得子表中的行,这样就可以读取表中信息,如下所示:
// Navigate from customer table to order table.
DataRow[] drsOrder = null;
drsOrder = drCust.GetChildRows(carsDataSet.Relations["CustomerOrder"]);
// Get customer name.
foreach(DataRow r in drsOrder)
strInfo += "Order Number: " + r["OrderID"] + "/n";
最后一步就是使用GetParentRows()方法从Orders表导航到父表(Inventory)。这时您可以用Make、PetName和Color列来读取Inventory表的信息,如下所示:
// Now navigate from order table to inventory table.
DataRow[] drsInv =
drsOrder[0].GetParentRows(carsDataSet.Relations["InventoryOrder"]);
foreach(DataRow r in drsInv)
{
strInfo += "Make: " + r["Make"] + "/n";
strInfo += "Color: " + r["Color"] + "/n";
strInfo += "Pet Name: " + r["PetName"] + "/n";
}
作为以编程方式导航关系的最后一个例子,下面的代码打印出间接通过InventoryOrders关系获得的Orders表中的值:
protected void btnGetChildRels_Click (object sender, System.EventArgs e)
{
// Ask the CarsDataSet for the child relations of the inv. table.
DataRelationCollection relCol;
DataRow[] arrRows;
string info = "";
relCol = carsDataSet.Tables["inventory"].ChildRelations;
info += "/tRelation is called: " + relCol[0].RelationName + "/n/n";
// Now loop over each relation and print out info.
foreach(DataRelation dr in relCol)
{
foreach(DataRow r in inventoryTable.Rows)
{
arrRows = r.GetChildRows(dr);
// Print out the value of each column in the row.
for (int i = 0; i < arrRows.Length; i++)
{
foreach(DataColumn dc in arrRows[i].Table.Columns )
{
info += "/t" + arrRows[i][dc];
}
info += "/n";
}
}
MessageBox.Show(info,
"Data in Orders Table obtained by child relations");
}
}
图A-21显示了输出结果。
图A-21 导航父/子关系
希望最后这个例子能让您彻底了解DataSet类型的用法。由于DataSet完全断开了与底层数据源的连接,您就可以对数据的内存副本进行操作,并在每个表之间导航来进行所需的更新、删除或添加。当完成操作后,您可以把修改提交到数据存储中去处理。当然,您还不知道如何进行连接!在讨论这个问题之前,还有一个与DataSet有关的有趣主题。
读取和写入基于XML的DataSet
ADO.NET最主要的设计目的就是广泛使用XML结构。使用DataSet类型,可以把表内容、关系和其他表细节的XML表示形式写入某个流中(比如一个文件)。只要简单地调用WriteXml()方法就可以了,如下所示:
protected void btnToXML_Click (object sender, System.EventArgs e)
{
// Write your entire DataSet to a file in the app directory.
carsDataSet.WriteXml("cars.xml");
MessageBox.Show("Wrote CarDataSet to XML file in app directory");
btnReadXML.Enabled = true;
}
如果在Visual Studio.NET的IDE中打开这个新建的文件(图A-22),您就会看到整个DataSet已经被转换成XML(如果您不太适应这个XML语法,不要担心。DataSet能很好地了解XML)。
图A-22 XML格式的DataSet
可以用一个小的试验来测试一下DataSet的ReadXml()方法。CarDataSet应用程序有一个按钮会完全清除掉当前的DataSet(包括所有的表和关系)。在取出所有内存中的内容后,指示DataSet读取文件Cars.xml,这个文件可恢复整个DataSet,如下所示:
protected void btnReadXML_Click (object sender, System.EventArgs e)
{
// Kill current DataSet.
carsDataSet.Clear();
carsDataSet.Dispose();
MessageBox.Show("Just cleared data set. . .");
carsDataSet = new DataSet("CarDataSet");
carsDataSet.ReadXml( "cars.xml" );
MessageBox.Show("Reconstructed data set from XML file. . .");
btnReadXML.Enabled = false;
// Bind to grids.
CarDataGrid.SetDataBinding(carsDataSet, "Inventory");
CustomerDataGrid.SetDataBinding(carsDataSet, "Customers");
}
注意, XML的这些核心方法都使用了System.Xml.dll程序集(特别是XmlReader和XmlWriter类)中定义的类型。因此,除了设置一个对这个程序集的引用之外,您还必须显式地引用它的类型,如下所示:
// Need this namespace to call ReadXml() or WriteXml()!
using System.Xml;
图A-23显示了最终的结果。
图A-23 最终的内存中的DataSet应用程序
源代码:
CarDataSet应用程序位于第13章子目录中。
A.12 构建一个简单的测试数据库
现在您已经知道如何创建和操作内存中的DataSet,下面您将会看到怎样建立数据连接以及如何填充DataSet。为使全书连贯,我使用了两个版本的示例Cars数据库(可以从www.apress.com下载),这两个数据库模拟了本章用到的Inventory、Orders和Customers表。
第一个版本是一个可以构建表(包括表之间的关系)的SQL脚本,SQL Server 7.0(或更高版本)的用户可以使用它。打开SQL Server中附带的Query Analyzer实用程序,可以创建Cars数据库。然后连接到您的主机,打开cars.sql文件。在运行这个脚本之前,确保这个SQL文件中
所列的路径指向的就是您的
MS SQL Server
安装路径。然后根据需要编辑如下的
DDL(
粗体显示
)
:
CREATE DATABASE [Cars] ON (NAME = N'Cars_Data', FILENAME
=N'
D:/MSSQL7/Data /Cars_Data.MDF' ,
SIZE = 2, FILEGROWTH = 10%)
LOG ON (NAME = N'Cars_Log', FILENAME
= N'
D:/MSSQL7/Data/Cars_Log.LDF' ,
SIZE = 1, FILEGROWTH = 10%)
GO
现在运行这个脚本。运行之后打开SQL Server Enterprise Manager(图A-24)。您会看到有3个相关表的Cars数据库(有一些示例数据)。
图A-24 SQL Server版本的Cars数据库
第二个Cars数据库版本是针对MS Access用户的。在Access DB文件夹下,您可以找到cars.mdb文件,这个文件包含了与SQL Server版本相同的信息和底层结构。在后面的部分,都假定您连接的是SQL Server版本的Cars数据库而不是Access版本的数据库。其实您也可以看到如何配置一个ADO.NET连接字符串来连接到*.mdb文件。
A.13 ADO.NET托管提供者
如果您准备从典型ADO背景步入ADO.NET,可假定.NET中的托管提供者就等同于OLE DB提供者。换句话说,托管提供者就是原始数据存储和已填充的DataSet之间的通道。
本章前面已经提到,ADO.NET提供了两种托管提供者。第一个就是OleDb托管提供者,这个是由System.Data.OleDb命名空间定义的类型组成。OleDb提供者可以访问所有支持OLE DB协议的数据存储中的数据。因此,和典型ADO一样,也可以使用ADO.NET托管提供者来访问SQL Server、Oracle或MS Access数据库。由于System.Data.OleDb命名空间中的类型必须和非托管代码进行通信(比如OLE DB提供者),您就必须意识到在这背后有大量.NET和COM之间的转换,这当然也会影响执行性能。
另外一个托管提供者(SQL提供者)能直接访问MS SQL Server数据存储,而且只能是SQL Server数据存储(7.0版本和更高版本)。System.Data.SqlClient命名空间包含了SQL提供者所用的类型,并提供了与OleDb提供者相同的功能。实际上,这两个命名空间大部分的命名项都相似。关键的不同之处就是SQL提供者不能使用OLE DB或典型ADO协议,但它却提供更加强大的性能。
您应该记得System.Data.Common命名空间定义了很多抽象类,这些类为每个托管提供者提供一个通用接口。首先,每个类型都定义了一个IDbConnection接口的实现,可以用这个接口配置和打开与数据存储的会话。实现IDbCommand接口的对象可用于对数据库进行查询。下一个就是IDataReader,这个接口可以使用一个只前向只读的游标来读取数据。最后但很重要的就是实现IDbDataAdapter类型,这个接口负责根据客户端需要填充DataSet。
大多数时候您不用直接与System.Data.Common命名空间交互。然而,如果使用这些提供者,会要求您指定正确的using指令,如下所示:
// Going to access an OLE DB compliant data source.
using System.Data;
using System.Data.OleDb;
// Going to access SQL Server (7.0 or greater).
using System.Data;
using System.Data.SqlClient;
A.14 使用OleDb托管提供者
如果您熟悉了某个托管提供者,那么就可以很容易地操作其他提供者。首先来看一下如何使用OleDb托管提供者进行连接。当您需要连接到除MS SQL Server之外的数据源时,就得使用定义在System.Data.OleDb中的类型。表A-12列出了一些核心的类型。
表A-12 System.Data.OleDb命名空间的类型
System.Data.Oledb类型
|
意 义
|
OleDbCommand
|
表示一个可用于数据源的SQL查询命令
|
OleDbConnection
|
表示对数据源的一个开放连接
|
OleDbDataAdapter
|
表示一些数据命令和用来填充DataSet、更新数据源的数据库连接
|
OleDbDataReader
|
能够从一个数据源中读取一个前向型的数据记录流
|
OleDbErrorCollection
OleDbError
OleDbException
|
OleDbErrorCollection拥有一些从数据源返回的警告和错误集合,每个OleDbException都表示为OleDbError类型。如果遇到错误,就会抛出OleDbException类型的异常
|
OleDbParameterCollection
OleDbParameter
|
与典型ADO非常类似,OleDbParameterCollection 集合保存了要传递给数据库中存储过程的参数。每个参数的类型都是OleDbParameter
|
A.14.1 使用OleDbConnection类型建立连接
使用OleDb托管提供者的第一步就是使用OleDbConnection类型建立一个与数据源的会话。类似于典型ADO Connection对象,OleDbConnection类型也提供了一个格式化的连接字符串,包含了一些名/值对。您可以用这个信息来表示标识要连接的机器名称、所需的安全设置、机器上数据库的名称,以及最重要的OLE DB提供者的名称(可以从在线帮助中找到每个名/值对的完整说明)。
可以使用OleDbConnection来设置连接字符串。ConnectionString属性可以作为构造函数的参数。假设您想用SQL OLE DB提供者连接到一个叫做BIGMANU的机器上的Cars数据库。可以用下面的逻辑来完成这一步:
// Build a connection string.
OleDbConnection cn = new OleDbConnection();
cn.ConnectionString = "Provider=SQLOLEDB.1;" + // Which provider?
"Integrated Security=SSPI;" +
"Persist Security Info=False;" + // Persist security?
"Initial Catalog=Cars;" + // Name of database.
"Data Source=BIGMANU;"; // Name of machine.
从前面代码的注释可以知道,Initial Catalog名称指的就是您要建立与之会话的数据库(Pubs,Northwind,Cars等)。Data Source名称表示维护这个数据库的机器名称。最后一个就是
Provider
部分,它指定了用来访问数据存储的
OLE DB
提供者的名称。表
A-13
列出了一些可能值。
表A-13 核心的OLE DB提供者
提供者部分值
|
意 义
|
Microsoft.JET.OLEDB.4.0
|
可以用Jet OLE DB提供者连接Access数据库
|
MSDAORA
|
可以用OLE DB提供者连接Oracle
|
SQLOLEDB
|
可以用OLE DB提供者连接MS SQL Server
|
当配置好连接字符串后,接下来就是打开与数据源的会话,执行一些操作,然后释放与这个数据源的连接,如下所示:
// Build a connection string (can specify User ID and Password if needed).
OleDbConnection cn = new OleDbConnection();
cn.ConnectionString = "Provider=SQLOLEDB.1;" + // Which provider?
"Integrated Security=SSPI;" +
"Persist Security Info=False;" + // Persist security?
"Initial Catalog=Cars;" + // Name of database.
"Data Source=BIGMANU;"; // Name of machine.
cn.Open();
// Do some interesting work here.
cn.Close();
除了ConnectionString、Open()和Close()成员之外,OleDbConnection类还提供了很多可以配置与连接相关的设置的成员,比如超时设置和事务信息。表A-14显示了一部分内容。
表A-14 OleDbConnection类型的成员
OleDbConnection成员
|
意 义
|
BeginTransaction()
CommitTransaction()
RollbackTransaction()
|
用来以编程方式提交、取消或回滚当前事务
|
Close()
|
关闭与数据源的连接。这是首选的方法
|
ConnectionString
|
获得或设置用于打开一个与数据存储的会话的字符串
|
ConnectionTimeout
|
获得或设置在终止和生成错误之前建立一个连接需要等待的事件。默认值为15秒
|
Database
|
获得或设置当前数据库或者是连接打开后所用到的数据库名称
|
DataSource
|
获得或设置要连接的数据库名称
|
Open()
|
用当前的属性设置打开数据库连接
|
Provider
|
获得或设置提供者的名称
|
State
|
获得当前连接的状态
|
A.14.2 构建SQL命令
OleDbCommand类是SQL查询的OO表示形式,并且可以用CommandText属性操作查询。ADO.NET命名空间中的很多类型都需要OleDbCommand作为一个方法参数来发送请求到数据源。在保留了原先的SQL查询之外,OleDbCommand类型还定义了其他一些成员,您可以使用它们来配置不同类型的查询(表A-15)。
表A-15 OleDbCommand的成员
OleDbCommand成员
|
意 义
|
Cancel()
|
取消命令的执行
|
CommandText
|
获得或设置对数据源执行的SQL命令文本或提供者特定的语法
|
CommandTimeout
|
获得或设置在终止企图和生成错误之前执行一条命令所等待的事件。默认值为30秒
|
CommandType
|
获得或设置CommandText属性如何被解析
|
Connection
|
获得或设置OleDbCommand 的实例所用到的OleDbConnection
|
ExecuteReader()
|
返回一个OleDbDataReader实例
|
Parameters
|
获得的OleDbParameterCollection集合
|
Prepare()
|
在数据源上创建一个预制(或已编译)版本的命令
|
OleDbCommand类型使用起来非常简单,而且跟OleDbConnection对象一样,它也有很多的方式可以获得相同的最终结果。比如,注意,下面一个用活动的OleDbConnection对象配置SQL查询的方式(语义一样)。每个例子都假设已经有一个名为cn的OleDbConnection:
// Specify a SQL command (take one).
string strSQL1 = "Select Make from Inventory where Color='Red'";
OleDbCommand myCommand1 = new OleDbCommand(strSQL1, cn);
// Specify SQL command (take two).
string strSQL2 = "Select Make from Inventory where Color='Red'";
OleDbCommand myCommand2 = new OleDbCommand();
myCommand.Connection = cn;
myCommand.CommandText = strSQL2;
A.14.3 使用OleDbDataReader
在建立好活动连接和SQL命令后,下一步就是向数据源提交查询。有很多方式可以完成这一步。OleDbDataReader是最简单、最快但或许是最不灵活的从数据存储中获取信息的方式。这个类表示了一个只读只前向的数据流,一次返回一条记录作为SQL命令的结果。
如果想非常快速地迭代大量数据时,这个OleDbDataReader就非常有用了,因为无需再处理内存中的DataSet表示了。例如,如果从一个表中查询了2000行记录并存储到一个文本文件中,用DataSet来保存这些信息就可能造成内存紧张。更好的方式就是创建一个DataReader,它能以最快速的方式遍历每条记录。然而要注意DataReader(和DataSet不同)保持了一个到数据源的连接,该连接会等到显式地关闭掉这个会话后才被关闭。
为了进行说明,下面的类对Cars数据库执行了一个简单的SQL查询,它使用了OleDbCommand类型的ExecuteReader()方法。使用这个返回的OleDbDataReader的Read()方法就可以把每个成员转储到标准的输入输出流中:
public class OleDbDR
{
static void Main(string[] args)
{
// Step 1: Make a connection.
OleDbConnection cn = new OleDbConnection();
cn.ConnectionString = "Provider=SQLOLEDB.1;" +
"Integrated Security=SSPI;" +
"Persist Security Info=False;" +
"Initial Catalog=Cars;" +
"Data Source=BIGMANU;";
cn.Open();
// Step 2: Create a SQL command.
string strSQL = "SELECT Make FROM Inventory WHERE Color='Red'";
OleDbCommand myCommand = new OleDbCommand(strSQL, cn);
// Step 3: Obtain a data reader ala ExecuteReader().
OleDbDataReader myDataReader;
myDataReader = myCommand.ExecuteReader();
// Step 4: Loop over the results.
while (myDataReader.Read())
{
Console.WriteLine("Red car: " +
myDataReader["Make"].ToString());
}
myDataReader.Close();
cn.Close();
}
}
结果就是Cars数据库中所有红色车的清单(图A-25)。
图A-25 运行的OleDbDataReader
应该记得DataReader是只前向只读的数据流。因此,不可能在OleDbDataReader的内容中进行导航。您要做的就是读取每条记录并在应用程序中使用它:
// Get the value in the 'Make' column.
Console.WriteLine("Red car: {0}", myDataReader["Make"].ToString());
在使用好DataReader后,确保用Close()方法终止会话。除了Read()和Close()方法之外,还有很多方法可以根据给定的格式从指定列中获取值(比如GetBoolean()、GetByte()等)。另外,FieldCount属性返回了当前记录的列数等。
源代码:
OleDbDataReader应用程序代码位于第13章子目录中。
A.14.4 连接到Access数据库
现在您已经知道如何从SQL Server读取数据了,下面让我们花点时间来看看如何从Access数据库中得到数据。为了进行说明,把前面的OleDbDataReader应用程序修改为读取cars.mdb文件。
与典型ADO非常类似,使用ADO.NET连接到Access数据库的这个过程只要求更新您的构造字符串即可。首先,设定Provider部分为JET引擎,而不是SQLOLEDB。除了这个改动之外,还要把数据源部分指向*.mdb文件的路径,如下所示:
// Be sure to update the data source segment if necessary!
OleDbConnection cn = new OleDbConnection();
cn.ConnectionString = "Provider=Microsoft.JET.OLEDB.4.0;" +
@"data source = D:/Chapter 13/Access DB/cars.mdb";
cn.Open();
当连接完成后,您就可以读取和操作数据表的内容。另外一个要注意的就是,由于使用JET引擎需要OLEDB,因此必须使用定义在System.Data.OleDb命名空间中的类型(比如OleDb托管提供者)。记住,SQL提供者只能访问MS SQL Server数据存储!
A.14.5 执行存储过程
当您在构建分布式应用程序时,要面临的一个设计选择就是存放业务逻辑的位置。一个方法就是建立可复用的二进制代码库,这个库可以由代理进程比如Windows 2000 Component Services管理器来管理。另外就是用存储过程来表示数据层上的系统业务逻辑。当然还有另外的方法,那就是把上面两种技术混合使用。
存储过程就是一个存储在数据库中的已命名的SQL代码块。可以通过构建存储过程来返回一些行(或者原始数据类型)给调用组件,另外存储过程还可以采用一些可选的参数。最终就是一个行为类似于典型函数的工作单元,明显的不同就是存储过程存放在数据存储中而不是二进制业务对象中。
下面来看已有的Cars数据库上的GetPetName存储过程,它接受一个整型类型的输入参数(如果运行我提供的SQL脚本,会发现已经定义好这个存储过程了)。这是车的数字ID,通过它可获得pet名称,pet名称作为一个字符类型的输出参数返回。语法如下:
CREATE PROCEDURE GetPetName
@carID int,
@petName char(20) output
AS
SELECT @petName = PetName from Inventory where CarID = @carID
现在已经有了一个存储过程,下面来看一下执行这个过程所需的代码。当然总是先从创建一个新的OleDbConnection开始,然后配置连接字符串,最后打开会话。接着创建一个新的OleDbCommand类型,确保指定了这个存储过程的名称,并相应设定好CommandType属性,如下所示:
// Open connection to data store.
OleDbConnection cn = new OleDbConnection();
cn.ConnectionString = "Provider=SQLOLEDB.1;" + "Integrated Security=SSPI;" +
"Persist Security Info=False;" + "Initial Catalog=Cars;" +
"Data Source=BIGMANU;";
cn.Open();
// Make a command object for the stored proc.
OleDbCommand myCommand = new OleDbCommand("GetPetName", cn);
myCommand.CommandType = CommandType.StoredProcedure;
这个OleDbCommand类的CommandType属性可以通过相关的CommandType枚举值来设定(表A-16)。
表A-16 CommandType枚举的值
CommandType枚举的值
|
意 义
|
StoredProcedure
|
用来配置一个可以触发存储过程的OleDbCommand
|
TableDirect
|
这个OleDbCommand 表示返回其所有列的表名称
|
Text
|
OleDbCommand 类型包含了一个标准的SQL命令。这是默认的值
|
当对数据源进行一些基本的SQL查询(比如,“SELECT * FROM Inventory”)时,默认的CommandType.Text设置很合适。然而如果要用命令调用存储过程,则需要指定CommandType.StoredProcedure。
1. 使用OleDbParameter类型指定参数
下面的任务就是为这个调用建立参数。OleDbParameter类型是传递给(或从中接收的)存储过程中特定参数的一个OO包装器。这个类有很多属性,可以配置参数的名称、大小、数据类型以及它的传递方向。表A-17列出了OleDbParameter类型的一些关键属性。
表A-17 OleDbParameter类型的成员
OleDbParameter属性
|
意 义
|
DataType
|
在.NET中,建立参数的类型
|
DbType
|
使用OleDbType枚举获得或设置数据源的原始数据类型
|
Direction
|
获得或设置参数是否只输入、只输出、双向或者是一个返回值参数
|
IsNullable
|
获得或设置参数是否能接收null值
|
ParameterName
|
获得或设置OleDbParameter 的名称
|
Precision
|
获得或设置用来表示这个值的最大位数
|
Scale
|
获得或设置数据的最大小数位数
|
Size
|
获得或设置数据的最大参数大小
|
Value
|
获得或设置参数的值
|
由于上面的存储过程有一个输入和一个输出参数,因此也就需要按照下面的方式配置您的类型。注意,您得把这些项添加到OleDbCommand类型的ParametersCollection中去(这个当然也可以通过Parameters属性访问):
// Create the parameters for the call.
OleDbParameter theParam = new OleDbParameter();
// Input.
theParam.ParameterName = "@carID";
theParam.DbType = OleDbType.Integer;
theParam.Direction = ParameterDirection.Input;
theParam.Value = 1; // Car ID = 1.
myCommand.Parameters.Add(theParam);
// Output.
theParam = new OleDbParameter();
theParam.ParameterName = "@petName";
theParam.DbType = OleDbType.Char;
theParam.Size = 20;
theParam.Direction = ParameterDirection.Output;
myCommand.Parameters.Add(theParam);
最后一步就是用OleDbCommand.ExecuteNonQuery()执行这个命令。注意,可以通过访问OleDbParameter类型的Value属性来取得返回的pet名称,如下所示:
// Execute the stored procedure!
myCommand.ExecuteNonQuery();
// Display the result.
Console.WriteLine("Stored Proc Info:");
Console.WriteLine("Car ID: " + myCommand.Parameters["@carID"].Value);
Console.WriteLine("PetName: " + myCommand.Parameters["@petName"].Value);
图A-26显示了输出结果。
图A-26 触发存储过程
源代码:
OleDbStoredProc项目的源代码位于第13章子目录中。
A.15 OleDbDataAdapter类型的角色
这时您应该了解到如何通过OleDbConnection类型连接到数据源、发送命令(使用OleDbCommand和OleDbParameter类型)并处理OleDbDataReader了。当您想非常快速地迭代大量数据或者触发存储过程时这非常有用。然而,如果想从数据存储中获得一个完整的DataSet,最灵活的方式就是使用OleDbDataAdapter。
简而言之,这个类型从数据存储中获取信息,并用OleDbDataAdapter.Fill()方法在DataSet中填充一个DataTable,Fill()方法已经重载很多次。下面列出了几种可能(FYI,这个整型返回类型保存了返回的记录数)。
// Fills the data set with records from a given source table.
public int Fill(DataSet yourDS, string tableName);
// Fills the data set with the records located between
// the given bounds from a given source table.
public int Fill(DataSet yourDS, string tableName,
int startRecord, int maxRecord);
在调用这个方法之前,您必须有一个有效的OleDbDataAdapter对象引用。构造函数同样也被多次重载,但多数情况下必须提供用来填充DataTable的连接信息和SQL SELECT语句。
这个OleDbDataAdapter类型不仅仅是帮助您填充DataSet中表的实体,而且也负责维护一些核心的SQL语句,这些语句可用来更新数据存储。表A-18列出了OleDbDataAdapter类型的一些核心成员。
表A-18 OleDbDataAdapter的核心成员
OleDbDataadapter成员
|
意 义
|
DeleteCommand
InsertCommand
SelectCommand
UpdateCommand
|
用来建立SQL命令,当调用Update() 方法时可以把它发到数据存储。这些属性都可以通过OleDbCommand类型设定
|
Fill()
|
用一些记录填充DataSet的指定表
|
GetFillParameters()
|
当执行select命令时返回所有用到的参数
|
Update()
|
在DataSet中为指定的表进行添加、更新和删除行操作时调用各自的INSERT、UPDATE或DELETE语句
|
OleDbDataAdapter(与SqlDataAdapter一样)关键的几个属性就是DeleteCommand、InsertCommand、SelectCommand以及UpdateCommand。数据适配器知道如何根据给定的命令提交所作的改变。例如在调用Update()时,数据适配器会自动使用存储在每个属性中的SQL命令。可以看到,配置这些属性所需的代码比较冗长。在直接检查这些属性之前,让我们先来了解一下如何通过编程的方式使用数据适配器填充DataSet。
使用OleDbDataAdapter类型填充DataSet
下面的代码使用了OleDbDataAdapter填充DataSet(包含一个表):
public class MyOleDbDataAdapter
{
// Step 1: Open a connection to Cars db.
OleDbConnection cn = new OleDbConnection();
cn.ConnectionString = "Provider=SQLOLEDB.1;" +
"Integrated Security=SSPI;" +
"Persist Security Info=False;" +
"Initial Catalog=Cars;" +
"Data Source=BIGMANU;";
cn.Open();
// Step 2: Create data adapter using the following SELECT.
string sqlSELECT = "SELECT * FROM Inventory";
OleDbDataAdapter dAdapt = new OleDbDataAdapter(sqlSELECT, cn);
// Step 3: Create and fill the DataSet, close connection.
DataSet myDS = new DataSet("CarsDataSet");
try
{
dAdapt.Fill(myDS, "Inventory");
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
cn.Close();
}
// Private helper function.
PrintTable(myDS);
return 0;
}
注意,和本章前面一部分所做的不同,您不用手动处理DataTable类型并把它加入到DataSet中去。这儿您只要把Inventory表作为Fill()方法的第二个参数即可。Fill()在内部使用SELECT命令创建一个DataTable,采用数据存储中的表名。在这个迭代中,SQL SELECT语句和OleDbDataAdapter之间的连接共同组成了构造函数的参数:
// Create a SELECT command as string type.
string sqlSELECT = "SELECT * FROM Inventory";
OleDbDataAdapter dAdapt = new OleDbDataAdapter(sqlSELECT, cn);
还有一个更加面向对象的方式,那就是使用OleDbCommand类型来控制SELECT语句。可以使用SelectCommand属性把OleDbCommand和OleDbDataAdapter关联起来,如下所示:
// Create a SELECT command object.
OleDbCommand selectCmd = new OleDbCommand("SELECT * FROM Inventory", cn);
// Make a data adapter and associate commands.
OleDbDataAdapter dAdapt = new OleDbDataAdapter();
dAdapt.SelectCommand = selectCmd;
注意,本例把
OleDbConnection
作为参数附加到
OleDbCommand
中。图
A-27
显示了最终的结果。
图A-27 活动的OleDbDataAdapter
PrintTable()方法的格式看起来有点混乱:
public static void PrintTable(DataSet ds)
{
// Get Inventory table from DataSet.
Console.WriteLine("Here is what we have right now:/n");
DataTable invTable = ds.Tables["Inventory"];
// Print the Column names.
for(int curCol= 0; curCol< invTable.Columns.Count; curCol++)
{
Console.Write(invTable.Columns[curCol].ColumnName.Trim() + "/t");
}
Console.WriteLine();
// Print each cell.
for(int curRow = 0; curRow < invTable.Rows.Count; curRow++)
{
for(int curCol= 0; curCol< invTable.Columns.Count; curCol++)
{
Console.Write(invTable.Rows[curRow][curCol].ToString().Trim()
+ "/t");
}
Console.WriteLine();
}
}
源代码:
FillSingleDSWithAdapter项目的源代码位于第13章子目录。
A.16 使用SQL托管提供者
在了解如何使用数据适配器添加、更新以及删除记录的细节之前,这里先介绍一下SQL托管提供者。应该记得,OleDb提供者可以访问任意一个支持OLE DB的数据存储,但不能达到SQL提供者的优化水平。
当您知道要操作的数据源是MS SQL Server时,如果直接使用System.Data.SqlClient会得到很好的性能。总的来说,下面这些类构成了SQL托管提供者所有的功能,这个提供者看起来非常类似于前面用到的OleDb提供者(表A-19)。
表A-19 System.Data.SqlClient命名空间的核心类型
System.Data.Sqlclient类型
|
意 义
|
SqlCommand
|
表示在SQL Server数据源上执行的Transact-SQL查询
|
SqlConnection
|
表示对SQL Server数据源的开放连接
|
SqlDataAdapter
|
表示用来填充DataSet和更新SQL Server数据源的数据命令和数据库连接
|
SqlDataReader
|
能从SQL Server数据源读取一个只前向的数据记录流
|
SqlErrors
SqlError
SqlException
|
SqlErrors维护SQL Server返回的警告和错误的集合,每个都用SQLError类型表示。如果遇到一个错误,就会抛出SQLException类型的异常
|
SqlParameterCollection
SqlParameter
|
SqlParametersCollection保存发送到数据库中存储过程的参数。每个参数的类型都为SQLParameter
|
由于使用这些类型几乎和使用OleDb托管提供者完全一样,您就应该已经知道如何处理这些类型,因为它们都有相同的公共接口。为了帮助您适应这些新的类型,下面的例子都会使用SQL托管提供者。
A.16.1 System.Data.SqlTypes命名空间
作为一个快速提示,在使用SQL托管提供者时,您还需要额外使用大量表示原始SQL Server数据类型的托管类型。表A-20给出了一个快速参考。
表A-20 System.Data.SqlTypes命名空间的类型
System.Data.Sqltypes包装
|
原始的SQL Server
|
SqlBinary
|
binary, varbinary, timestamp, image
|
SqlInt64
|
Bigint
|
SqlBit
|
Bit
|
SqlDateTime
|
datetime, smalldatetime
|
SqlNumeric
|
Decimal
|
SqlDouble
|
Float
|
SqlInt32
|
Int
|
SqlMoney
|
money, smallmoney
|
SqlString
|
nchar, ntext, nvarchar, sysname, text, varchar, char
|
SqlNumeric
|
Numeric
|
SqlSingle
|
Real
|
(续表)
System.Data.Sqltypes包装
|
原始的SQL Server
|
SqlInt16
|
Smallint
|
System.Object
|
sql_variant
|
SqlByte
|
Tinyint
|
SqlGuid
|
Uniqueidentifier
|
A.16.2 使用SqlDataAdapter插入新记录
现在您已经从OleDb提供者转移到SQL提供者了,接下来的任务还是来了解数据适配器的角色。可以使用SqlDataAdapter来看一下如何往给定表中插入新的记录(这和使用OleDbDataAdapter几乎相同)。通常都是先从创建一个活动连接开始,如下所示:
public class MySqlDataAdapter
{
public static void Main()
{
// Step 1: Create a connection and adapter (with select command).
SqlConnection cn = new
SqlConnection("server=(local);uid=sa;pwd=;database=Cars");
SqlDataAdapter dAdapt = new
SqlDataAdapter("Select * from Inventory", cn);
// Step 2: Kill record you inserted.
cn.Open();
SqlCommand killCmd = new
SqlCommand("Delete from Inventory where CarID = '1111'", cn);
killCmd.ExecuteNonQuery();
cn.Close();
}
}
可以看到,连接字符串变得非常简单。特别是您不需要定义提供者部分了(因为SQL类型只能和SQL server进行对话)。然后创建一个新的SqlDataAdapter并把SelectCommand属性的值指定为构造函数的一个参数(非常类似于OleDbDataAdapter)。
第二步纯粹是些“小事”。这儿创建了一个新的SqlCommand类型来销毁将要键入的记录(为了防止主键冲突)。下一步有些复杂,目的是创建一个新的SQL语句来作为SqlDataAdapter的InsertCommand。首先,创建SqlCommand并指定一个标准的SQL插入,接下来是描述Inventory表中每一列的SqlParameter类型,如下所示:
public static void Main()
{
. . .
// Step 3: Build the insert Command!
dAdapt.InsertCommand = new SqlCommand("INSERT INTO Inventory" +
"(CarID, Make, Color, PetName) VALUES" +
"(@CarID, @Make, @Color, @PetName)", cn)";
// Step 4: Build parameters for each column in Inventory table.
SqlParameter workParam = null;
// CarID.
workParam = dAdapt.InsertCommand.Parameters.Add(new
SqlParameter("@CarID", SqlDbType.Int));
workParam.SourceColumn = "CarID";
workParam.SourceVersion = DataRowVersion.Current;
// Make.
workParam = dAdapt.InsertCommand.Parameters.Add(new
SqlParameter("@Make", SqlDbType.VarChar));
workParam.SourceColumn = "Make";
workParam.SourceVersion = DataRowVersion.Current;
// Color.
workParam = dAdapt.InsertCommand.Parameters.Add(new
SqlParameter("@Color", SqlDbType.VarChar));
workParam.SourceColumn = "Color";
workParam.SourceVersion = DataRowVersion.Current;
// PetName.
workParam = dAdapt.InsertCommand.Parameters.Add(new
SqlParameter("@PetName", SqlDbType.VarChar));
workParam.SourceColumn = "PetName";
workParam.SourceVersion = DataRowVersion.Current;
}
现在已经格式化好每个参数,最后就是填充DataSet,然后添加新行(注意,在本例中还用到了帮助函数PrintTable()):
public static void Main()
{
. . .
// Step 5: Fill data set.
DataSet myDS = new DataSet();
dAdapt.Fill(myDS, "Inventory");
PrintTable(myDS);
// Step 6: Add new row.
DataRow newRow = myDS.Tables["Inventory"].NewRow();
newRow["CarID"] = 1111;
newRow["Make"] = "SlugBug";
newRow["Color"] = "Pink";
newRow["PetName"] = "Cranky";
myDS.Tables["Inventory"].Rows.Add(newRow);
// Step 7: Send back to database and reprint.
try
{
dAdapt.Update(myDS, "Inventory");
myDS.Dispose();
myDS = new DataSet();
dAdapt.Fill(myDS, "Inventory");
PrintTable(myDS);
}
catch(Exception e){ Console.Write(e.ToString()); }
}
运行这个应用程序后,您可以看到图A-28所示的输出结果。
图A-28 运行的InsertCommand属性
源代码:
InsertRowsWithSqlAdapter项目位于第13章子目录中。
A.16.3 使用SqlDataAdapter更新已有记录
现在您已经能插入新行了,来看一下如何更新已有的行。同样,行获得一个连接(使用SqlConnection类型),然后创建一个新的SqlDataAdapter。接下来使用和设定InsertCommand属性的值一样的方法设定UpdateCommand属性的值。下面是Main()的相关代码:
public static void Main()
{
// Step 1: Create a connection and adapter (same as previous code)
. . .
// Step 2: Establish the UpdateCommand.
dAdapt.UpdateCommand = new SqlCommand
("UPDATE Inventory SET Make = @Make, Color = " +
"@Color, PetName = @PetName " +
"WHERE CarID = @CarID" , cn);
// Step 3: Build parameters for each column in Inventory table.
// Same as before, but now you are populating the ParameterCollection
// of the UpdateCommand. For example:
SqlParameter workParam = null;
workParam = dAdapt.UpdateCommand.Parameters.Add(new
SqlParameter("@CarID", SqlDbType.Int));
workParam.SourceColumn = "CarID";
workParam.SourceVersion = DataRowVersion.Current;
// Do the same for PetName, Make, and Color params.
// Step 4: Fill data set.
DataSet myDS = new DataSet();
dAdapt.Fill(myDS, "Inventory");
PrintTable(myDS);
// Step 5: Change columns in second row to 'FooFoo'.
DataRow changeRow = myDS.Tables["Inventory"].Rows[1];
changeRow["Make"] = "FooFoo";
changeRow["Color"] = "FooFoo";
changeRow["PetName"] = "FooFoo";
// Step 6: Send back to database and reprint.
try
{
dAdapt.Update(myDS, "Inventory");
myDS.Dispose();
myDS = new DataSet();
dAdapt.Fill(myDS, "Inventory");
PrintTable(myDS);
}
catch(Exception e)
{ Console.Write(e.ToString()); }
}
图A-29显示了输出结果。
图A-29 更新已有的行
源代码:
UpdateRowsWithSqlAdapter项目的源代码位于第13章子目录。
A.17 自动生成的SQL命令
现在您可以使用数据适配器类型(OleDbDataAdapter和SqlDataAdapter)来选择、删除、插入并更新给定数据源的记录。虽然这个过程一般不会像火箭科学一样繁琐,但在建立所有的参数类型和手动配置InsertCommand、UpdateCommand和DeleteCommand属性时还是会有点麻烦的。您可能会想到,应该有一些帮助。
我们可以使用SqlCommandBuilder类型。如果有一个来自于单个表的DataTable(不是多个连接的表),SqlCommandBuilder会自动根据原先的SelectCommand设置InsertCommand、UpdateCommand和DeleteCommand属性。除了非连接约束之外,这个表必须有一个主键列,而且必须在原先的SELECT语句中指定这个列。这样做的好处就是您可以不用手动创建那些SqlParameter类型。
为了说明这一点,先假设您已经有一个新的Windows Forms示例,它可以让用户在DataGrid中编辑这些值。完成编辑后,用户可以用一个Button类型把修改提交回数据库。首先,假设有如下的构造函数逻辑:
public class mainForm : System.Windows.Forms.Form
{
private SqlConnection cn = new
SqlConnection("server=(local);uid=sa;pwd=;database=Cars");
private SqlDataAdapter dAdapt;
private SqlCommandBuilder invBuilder;
private DataSet myDS = new DataSet();
private System.Windows.Forms.DataGrid dataGrid1;
private System.Windows.Forms.Button btnUpdateData;
private System.ComponentModel.Container components;
public mainForm()
{
InitializeComponent();
// Create the initial SELECT SQL statement.
dAdapt = new SqlDataAdapter("Select * from Inventory", cn);
// Autogenerate the INSERT, UPDATE,
// and DELETE statements.
invBuilder = new SqlCommandBuilder(dAdapt);
// Fill and bind.
dAdapt.Fill(myDS, "Inventory");
dataGrid1.DataSource = myDS.Tables["Inventory"].DefaultView;
}
. . .
}
在退出时关闭连接!现在这个SqlDataAdapter已经有了所有将修改提交给数据存储所需的信息。假设您已经有了如下的按钮单击事件的逻辑:
private void btnUpdateData_Click(object sender, System.EventArgs e)
{
try
{
dataGrid1.Refresh();
dAdapt.Update(myDS, "Inventory");
}
catch(Exception ex)
{
MessageBox.Show(ex.ToString());
}
}
通常可以调用Update()并指定需要更新的DataSet和表。如果把这个调用从测试运行中取走,可以看到如图A-30所示的信息(确保在提交结果之前您已经不再编辑这个DataTable了)。
很好!我确信您已经认为自动生成的命令要比使用原始的参数简单得多。当然,和所有事情一样,这也有代价。特别是当您有通过连接操作组成的DataTable时,就不能再使用这个技术。同时可以看到,使用这些原始的参数可以进行更好的粒度控制。
图A-30 在DataSet中扩展新的DataRow
源代码:
WinFormSqlAdapter项目位于第13章子目录中。
A.18 填充多表的DataSet(添加DataRelation)
最后,让我们重新建立一个Windows Forms例子来模拟本章前面部分创建的应用程序。GUI非常简单。在图A-31中,您可以看到有3个保存了从Cars数据库的Inventory、Orders和Customers表检索到的数据的DataGrid类型。另外,还有一个按钮会把所有修改送回到数据存储中。
图A-31 显示了多表的DataSet
为了使事情更加简单,可以对每个SqlDataAdapter(一个SqlDataAdapters对应着一个表)使用自动生成命令。首先看一下Form的状态数据:
public class mainForm : System.Windows.Forms.Form
{
private System.Windows.Forms.DataGrid custGrid;
private System.Windows.Forms.DataGrid inventoryGrid;
private System.Windows.Forms.Button btnUpdate;
private System.Windows.Forms.DataGrid OrdersGrid;
private System.ComponentModel.Container components;
// Here is the connection.
private SqlConnection cn = new
SqlConnection("server=(local);uid=sa;pwd=;database=Cars");
// Our data adapters (for each table).
private SqlDataAdapter invTableAdapter;
private SqlDataAdapter custTableAdapter;
private SqlDataAdapter ordersTableAdapter;
// Command builders (for each table).
private SqlCommandBuilder invBuilder = new SqlCommandBuilder();
private SqlCommandBuilder orderBuilder = new SqlCommandBuilder();
private SqlCommandBuilder custBuilder = new SqlCommandBuilder();
// The dataset.
DataSet carsDS = new DataSet();
. . .
}
这个Form的构造函数完成了一些乏味的工作,比如创建数据成员变量、填充DataSet。还要注意到有一个对私有帮助函数BuildTableRelationship()的调用,如下所示:
public mainForm()
{
InitializeComponent();
// Create adapters.
invTableAdapter = new SqlDataAdapter("Select * from Inventory", cn);
custTableAdapter = new SqlDataAdapter("Select * from Customers", cn);
ordersTableAdapter = new SqlDataAdapter("Select * from Orders", cn);
// Autogenerate commands.
invBuilder = new SqlCommandBuilder(invTableAdapter);
orderBuilder = new SqlCommandBuilder(ordersTableAdapter);
custBuilder = new SqlCommandBuilder(custTableAdapter);
// Add tables to DS.
invTableAdapter.Fill(carsDS, "Inventory");
custTableAdapter.Fill(carsDS, "Customers");
ordersTableAdapter.Fill(carsDS, "Orders");
// Build relations between tables.
BuildTableRelationship();
}
这个BuildTableRelationship()帮助函数实现了需要完成的工作。应该记得,Cars数据库表示了很多父/子关系。这个代码看起来和本章前面的逻辑相同,如下所示:
private void BuildTableRelationship()
{
// Create a DR obj.
DataRelation dr = new DataRelation("CustomerOrder",
carsDS.Tables["Customers"].Columns["CustID"],
carsDS.Tables["Orders"].Columns["CustID"]);
// Add relation to the DataSet.
carsDS.Relations.Add(dr);
// Create another DR obj.
dr = new DataRelation("InventoryOrder",
carsDS.Tables["Inventory"].Columns["CarID"],
carsDS.Tables["Orders"].Columns["CarID"]);
// Add relation to the DataSet.
carsDS.Relations.Add(dr);
// Fill the grids!
inventoryGrid.SetDataBinding(carsDS, "Inventory");
custGrid.SetDataBinding(carsDS, "Customers");
OrdersGrid.SetDataBinding(carsDS, "Orders");
}
现在已经填充好DataSet,并断开了与数据源之间的连接,这样您就可以本地化操作每个表。只要对3个DataGrid中的任意一个简单地插入、更新或删除值即可。如果您准备提交数据进行处理的话,单击Form的Update按钮。Click事件后的代码非常清晰,如下所示:
private void btnUpdate_Click(object sender, System.EventArgs e)
{
try
{
invTableAdapter.Update(carsDS, "Inventory");
custTableAdapter.Update(carsDS, "Customers");
ordersTableAdapter.Update(carsDS, "Orders");
}
catch(Exception ex)
{
MessageBox.Show(ex.Message);
}
}
更新之后您可以发现Cars数据库中的每个表都已经被正确地修改了。
这时您应该感觉到使用OleDb和SQL这两个托管提供者的方便之处,并且也能明白如何操作和更新输出的DataSet。显然ADO.NET还有很多其他方面的问题,比如事务编程、安全问题等。我想您可能会自己再进一步摸索它们吧。
ADO.NET中您还没有了解的另一个方面就是大量的VS.NET数据向导。例如,把一个Data组件(从Toolbox窗口)拖到设计模板中时,可以运行一些向导,包括为SqlConnection和OleDbConnection创建连接字符串;自动为一个数据适配器建立SELECT、INSERT、DELETE和UPDATE命令等。如果您仔细阅读完本章后,就会知道和这些工具打交道应该非常轻松。
A.19 小结
ADO.NET
是随人们所熟知的断开连接的
N
层应用程序发展起来的一个新的数据访问技术。
System.Data
命名空间包含了很多需要用编程的方式与行、列、表以及视图进行交互的核心类。可以看到,
System.Data.SqlClient
和
System.Data.OleDb
命名空间定义了建立活动连接所需的类型。
ADO.NET的中心就是DataSet。这个类型提供了任意数目的表和任意数目的可选内部关系、约束和表达式在内存中的表示。在本地表之间创建关系的好处就是在断开与远程数据存储的连接时,可用编程的方式对它们进行导航。
最后,本章讨论了数据适配器的角色(OleDbDataAdapter和SqlDataAdapter)。通过这个类型(相关的SelectCommand、InsertCommand、UpdateCommand和DeleteCommand属性),适配器可以将对DataSet的修改更新到原始的数据存储中。当然在ADO.NET命名空间中有太多的内容,而我也不能在一章中全部讲到,但您现在应该有一个很扎实的基础了。