第8章 数据组件和DataSet
在前面的章节里,你对ADO.NET有了初步了解,并且研究了基于连接的数据访问。现在,将你的数据访问代码放入设计良好的应用中就摆上了桌面。
在组织合理的应用中,数据访问代码绝不会直接嵌入到网页的后置代码中。相反,它会被分离到专门的数据组件里。本章中,你将看到如何自己创建一个简单的数据访问类,并为每个要执行的数据任务添加一个单独的方法。最为重要的是,这个数据类并不限定于代码庞大的(code-intensive)场合。在下一章中,你将看到如何与ASP.NET的新数据绑定结构一起使用你的类。
本章也处理非连接数据(disconnected data)。它是以DataSet为核心的ADO.NET新特征,允许你在关闭到数据源的连接后仍能进行数据交换。在ASP.NET中并不强求使用DataSet,但是,使用DataSet可以为导航、过滤、索引数据提供更多灵活性。本章将围绕这个话题进行展开。
提示:你在本章中了解的信息与在asp.net1.1中并没有什么不同,你使用相同的技术来构建一个良好的数据库组件时,DataSet仍以同样方式工作(随之有一些重定义)。更多细节请看ADO.NET,关于如何在不同类型的.NET应用中使用DataSet,请参考ADO.NET 2.0(Apress, 2005)。
构建数据访问组件(Building Data Access Component)
在专业的应用中,数据库代码并不直接嵌入到客户端,而是封装在一个专门的类里面。为了执行数据库操作,客户端创建该类的实例,然后合理地调用方法。
创建数据类时,应当遵循下面这些基本要点。这些要点可以保证你创建封装良好的、优化的数据库组件来作为单独进程执行。如果需要,甚至可以在配置了负载均衡的多个服务器里应用。
l 快速打开和关闭连接(open and close connections quickly):在数据库连接方法调用里打开,并且在方法结束前关闭连接。不能在多个客户请求间保持连接打开,而且客户也不应该控制如何获得连接或释放连接。如果客户端有了这样的控制能力,就有可能导致连接不能尽快关闭甚至忘记关闭,这会影响扩展性。
l 实现错误处理(Impletment error handling):用户错误处理可以确保在SQL命令产生异常时仍能关闭连接。记住,连接是有限的资源,它直接影响了系统性能。
l 遵循无状态设计规则(Follow stateless design practices):方法需要的所有信息都通过参数来接收,并且通过返回值来返回所获得的数据。如果创建的类要维护状态,将该类作为web服务或者在负载均衡时执行就很困难。同时,如果数据库组件位于进程之外,且每次方法调用都会有较重的负载,使用多个调用来设置属性时,会花较长时间。而将所有信息作为参数来调用单个方法所花时间就相对较短。
l 不要让客户来指定连接串信息(don’t let the client specify connection string information):这会导致安全风险,增加过期客户失败的可能性,影响连接池的能力,连接池要求匹配连接串。
l 不要使用客户端的用户ID来建立连接(Don’t connect with the client’s user ID):在连接串中引入任何可变因素都将阻碍连接池的使用,这一点在前一章已经提到。相反,应当依靠基于角色的安全方法或者基于证书的系统用来鉴别用户,并且阻止他们试图执行限制的操作。这个模型比使用了无效的安全帐户来执行数据库查询并且等待错误信息要快得多。
l 不要让客户使用广开的查询(don’t let the client user wide-open queries):每个查询都应该明智地只选取需要的行。在可能的情况下,应该使用where子句来限制输出结果。例如,获取订单记录时,应该强制最小数据范围(或者诸如TOP 1000之类的SQL子句)。没有这些保护,你的应用可能开始时能工作很好,但随着数据库增长和客户执行大的查询时就变慢了,它加重了数据库和网络的负载。
良好且直观的数据库组件的设计为每个数据库表使用一个单独的类(或者逻辑的相关表的组合)。通用数据库访问方法(如插入、删除、修改记录等)全部封装进单独的无状态的方法中。最后,每个数据库调用都使用一个专门的存储过程。图8-1显示了这种谨慎的分层设计。
图8-1 使用数据库类的分层设计
下面的示例演示了一个简单的数据库组件。它遵循了一种更好设计规则,将代码分离到不同的类中,以便于在多个页面中使用,而不是在页面中替换数据库代码。这个类可以作为单独组件的一部分进行编译。另外,连接串是从web.config文件的<connectionStrings>节中获取的,而不是硬编码。
数据组件包含两个类:数据包类和数据库工具类。前者包装信息的单条记录,后者用ASP.NET代码执行数据库操作。
数据包装(The Data Package)
为了使信息输入数据库或从数据库输出更为容易,创建一个EmployeeDetails类就很有意义,它将所有字段作为公共属性。下面是这个类的全部代码:
public class EmployeeDetails
{
private int employeeID;
public int EmployeeID
{
get {return employeeID;}
set {employeeID = value;}
}
private string firstName;
public string FirstName
{
get {return firstName;}
set {firstName = value;}
}
private string lastName;
public string LastName
{
get {return lastName;}
set {lastName = value;}
}
private string titleOfCourtesy;
public string TitleOfCourtesy
{
get {return titleOfCourtesy;}
set {titleOfCourtesy = value;}
}
public EmployeeDetails(int employeeID, string firstName, string lastName,
string titleOfCourtesy)
{
this.employeeID = employeeID;
this.firstName = firstName;
this.lastName = lastName;
this.titleOfCourtesy = titleOfCourtesy;
}
}
注意:这个类并没有包含Employee表的所有信息,这是为了使示例能够简单明了。
存储过程
在编写数据访问的逻辑代码之前,需要编写一些存储过程来获取、插入和更新信息。下面的代码是所需的5个存储过程示例:
CREATE PROCEDURE InsertEmployee
@EmployeeID int OUTPUT
@FirstName varchar(10),
@LastName varchar(20),
@TitleOfCourtesy varchar(25),
AS
INSERT INTO Employees
(TitleOfCourtesy, LastName, FirstName, HireDate)
VALUES (@TitleOfCourtesy, @LastName, @FirstName, GETDATE());
SET @EmployeeID = @@IDENTITY
GO
CREATE PROCEDURE DeleteEmployee
@EmployeeID int
AS
DELETE FROM Employees WHERE EmployeeID = @EmployeeID
GO
CREATE PROCEDURE UpdateEmployee
@EmployeeID int,
@TitleOfCourtesy varchar(25),
@LastName varchar(20),
@FirstName varchar(10)
AS
UPDATE Employees
SET TitleOfCourtesy = @TitleOfCourtesy,
LastName = @LastName,
FirstName = @FirstName
WHERE EmployeeID = @EmployeeID
GO
CREATE PROCEDURE GetAllEmployees
AS
SELECT EmployeeID, FirstName, LastName, TitleOfCourtesy FROM Employees
GO
CREATE PROCEDURE CountEmployees
AS
SELECT COUNT(EmployeeID) FROM Employees
GO
CREATE PROCEDURE GetEmployee
@EmployeeID int
AS
SELECT FirstName, LastName, TitleOfCourtesy FROM Employees
WHERE EmployeeID = @EmployeeID
GO
数据工具类(the Data Utility Class)
最后,你需要工具来真正执行数据库操作。这个类使用了前一节创建的存储过程。
在这个例子中,数据工具类命名为EmployeeDB。它封装了所有数据访问代码和数据库特定的细节。下面是概略代码:
public class EmployeeDB
{
private string connectionString;
public EmployeeDB()
{
// Get default connection string.
connectionString = WebConfigurationManager.ConnectionStrings[
"Northwind"].ConnectionString;
}
public EmployeeDB(string connectionStringName)
{
// Get the specified connection string.
connectionString = WebConfigurationManager.ConnectionStrings[
"connectionStringName"].ConnectionString;
}
public int InsertEmployee(EmployeeDetails emp)
{ ... }
public void DeleteEmployee(int employeeID)
{ ... }
public void UpdateEmployee(EmployeeDetails emp)
{ ... }
public EmployeeDetails GetEmployee()
{ ... }
public EmployeeDetails[] GetEmployees()
{ ... }
public int CountEmployees()
{ ... }
}
注意,你可能已经注意到,EmployeeDB类使用了实例方法,而不是静态方法。因为即便EmployyDB类不存储任何来自于数据库的状态,但它存储了作为私有成员变量的连接串。因为是一个实例类,能够在每次创建类的时候获取连接串,而不是每次调用方法时获取,这种方法使代码更为清晰,而且运行更快(通过避免多次读取web.config文件)。当然,好处非常小,所以你尽可以在数据库组件里使用静态方法。
每个方法都使用相同的谨慎的方法,都仅仅依赖于存储过程来与数据库进行交互。下面是插入记录的代码:
public int InsertEmployee(EmployeeDetails emp)
{
SqlConnection con = new SqlConnection(connectionString);
SqlCommand cmd = new SqlCommand("InsertEmployee", con);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("@FirstName", SqlDbType.NVarChar, 10));
cmd.Parameters["@FirstName"].Value = emp.FirstName;
cmd.Parameters.Add(new SqlParameter("@LastName", SqlDbType.NVarChar, 20));
cmd.Parameters["@LastName"].Value = emp.LastName;
cmd.Parameters.Add(new SqlParameter("@TitleOfCourtesy",
SqlDbType.NVarChar, 25));
cmd.Parameters.Add(new SqlParameter("@EmployeeID", SqlDbType.Int, 4));
cmd.Parameters["@EmployeeID"].Direction = ParameterDirection.Output;
try
{
con.Open();
cmd.ExecuteNonQuery();
return (int)cmd.Parameters["@EmployeeID"].Value;
}
catch (SqlException err)
{
// Replace the error with something less specific.
// You could also log the error now.
throw new ApplicationException("Data error.");
}
finally
{
con.Close();
}
}
可以看到,InsertEmployee()方法使用了EmployeeDetails包来接收数据。任何错误都会被捕获,但敏感的内部细节并没有返回给Web页代码,这防止了网页提供可能被恶意利用的信息。这也是在日志组件中报告事件日志或其它数据库的全部信息时,调用其它方法的理想方式。
GetEmployee()和GetEmployee()方法使用EmployeeDetails包返回数据:
public EmployeeDetails GetEmployee(int employeeID)
{
SqlConnection con = new SqlConnection(connectionString);
SqlCommand cmd = new SqlCommand("GetEmployee", con);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("@EmployeeID", SqlDbType.Int, 4));
cmd.Parameters["@EmployeeID"].Value = employeeID;
try
{
con.Open();
SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleRow);
// Get the first row.
reader.Read();
EmployeeDetails emp = new EmployeeDetails(
(int)reader["EmployeeID"], (string)reader["FirstName"],
(string)reader["LastName"], (string)reader["TitleOfCourtesy"]);
reader.Close();
return emp;
}
catch (SqlException err)
{
throw new ApplicationException("Data error.");
}
finally
{
con.Close();
}
}
public List<EmployeeDetails> GetEmployees()
{
SqlConnection con = new SqlConnection(connectionString);
SqlCommand cmd = new SqlCommand("GetAllEmployees", con);
cmd.CommandType = CommandType.StoredProcedure;
// Create a collection for all the employee records.
List<EmployeeDetails> employees = new List<EmployeeDetails>();
try
{
con.Open();
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
EmployeeDetails emp = new EmployeeDetails(
(int)reader["EmployeeID"], (string)reader["FirstName"],
(string)reader["LastName"], (string)reader["TitleOfCourtesy"]);
employees.Add(emp);
}
reader.Close();
return employees;
}
catch (SqlException err)
{
throw new ApplicationException("Data error.");
}
finally
{
con.Close();
}
}
UpdateEmployee()方法担负了一个特别的角色,它确定了你的数据库组件的并发策略(见下一节,”并发策略”)。代码如下:
public void UpdateEmployee(int EmployeeID, string firstName, string lastName,
string titleOfCourtesy)
{
SqlConnection con = new SqlConnection(connectionString);
SqlCommand cmd = new SqlCommand("UpdateEmployee", con);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("@FirstName", SqlDbType.NVarChar, 10));
cmd.Parameters["@FirstName"].Value = firstName;
cmd.Parameters.Add(new SqlParameter("@LastName", SqlDbType.NVarChar, 20));
cmd.Parameters["@LastName"].Value = lastName;
cmd.Parameters.Add(new SqlParameter("@TitleOfCourtesy", SqlDbType.NVarChar,
25));
cmd.Parameters["@TitleOfCourtesy"].Value = titleOfCourtesy;
cmd.Parameters.Add(new SqlParameter("@EmployeeID", SqlDbType.Int, 4));
cmd.Parameters["@EmployeeID"].Value = EmployeeID;
try
{
con.Open();
cmd.ExecuteNonQuery();
}
catch (SqlException err)
{
throw new ApplicationException("Data error.");
}
finally
{
con.Close();
}
}
最后,DeleteEmployee()和CountEmployee()方法是必不可少的要素:
public void DeleteEmployee(int employeeID)
{
SqlConnection con = new SqlConnection(connectionString);
SqlCommand cmd = new SqlCommand("DeleteEmployee", con);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("@EmployeeID", SqlDbType.Int, 4));
cmd.Parameters["@EmployeeID"].Value = employeeID;
try
{
con.Open();
cmd.ExecuteNonQuery();
}
catch (SqlException err)
{
throw new ApplicationException("Data error.");
}
finally
{
con.Close();
}
}
public int CountEmployees()
{
SqlConnection con = new SqlConnection(connectionString);
SqlCommand cmd = new SqlCommand("CountEmployees", con);
cmd.CommandType = CommandType.StoredProcedure;
try
{
con.Open();
return (int)cmd.ExecuteScalar();
}
catch (SqlException err)
{
throw new ApplicationException("Data error.");
}
finally
{
con.Close();
}
}
并发策略(Concurrency Strategies)
在任何一个多用户应用中,包括Web应用,都存在多个用户执行重叠的查询和更新的可能性。当两个用户同时占有一行的当前状态,并且试图提交不同的更新时,会出现混乱情形。第一个用户的更新总是能够成功,而第二个用户更新的成功与失败则取决于并发策略。
并发管理有几个主要的方法。但最基本的理解是,并发策略是通过编写UPDATE命令(主要是WHERE字句)来确定的。
下面是最常用的策略:
l 后进胜更新(Last-in-wins updating):这种策略的并发控制有较少的限制。它总是提供更新(原有的行被删除除外),即每次提交更新时,所有值都会应用。在数据冲突较少的时候,后进胜更新是有意义的。例如,如果只有一人负责更新一组记录,使用这个方法就是安全的。执行这个策略时,你编写WHERE子句,根据它的主关键字来匹配要更新的记录。前面的示例中的UpdateEmployee()方法就使用了后进胜方法。
UPDATE Employees SET ... WHERE EmployeeID=@ID
l 匹配所有更新(Match-all updating):为了实现这个策略,需要添加WHERE子句到UPDATA语句中,WHERE子句尽力匹配记录中每个字段的当前值。这样,即便只有一个字段被修改了,记录都匹配不成功,导致更新失败。这个方法的缺点是,兼容的改变都是不允许的。例如,如果两个用户试图修改同一记录的不同部分,第二个用户的改变就会被拒绝,即便它与前面并不冲突。另外,也是更重要的问题,就是匹配所有更新策略会导致大块、低效的SQL语句。采用时间戮来实现相同的策略会高效得多。
UPDATE Employees SET ... WHERE EmployeeID=@ID AND FirstName=@FirstName AND LastName=@LastName ...
l 基于时间戮的更新(Timestamp-based updating):大多数数据库系统支持时间戮列。时间戮在数据源每次发生改变时自动更新,而不需要人工修改时间戮列。然而,你可以检查它的变化以确定是否另一用户刚应用了一次更新。如果使用了带WHERE子句(子句中合并了主关键字和时间戮)的UPDATE语句,就可以保证只更新没有被修改的记录,象匹配所有更新一样。
UPDATE Employees SET ... WHERE EmployeeID=@ID AND TimeStamp=@TimeStamp
l 变化值更新(Changed-value updating):这个方法试图在UPDATE命令中仅应用变动过的值,因此允许两个用户同时改变不同的字段。使用这个方法有点复杂,因为你需要跟踪哪些值被改变了(它们需要在WHERE子句中进行合并),哪些值没有改变。
为了更好地理解策略是如何工作的,考虑一下两个用户向一个employee记录提交不同更新的情况,它们使用UpdateEmployee()方法执行后进胜并发策略。第一个用户更新了邮件地址,第二个用户改变顾员名字,同时不经意地应用了旧的邮件地址。问题就出现了,UpdateEmployee()方法并不知道你提交的是哪个修改,这意味着它会将所有内存中的值应用到数据源(即便旧的值并没有被改变也是一样),然后完成对其它人更新的覆盖。
可能会出现有大量复杂的记录,并且需要支持不同类型的编辑的情况,解决这类问题的最简单方法是创建多目标(more-targeted)的方法,而不是创建普通的UpdateEmployee()方法,例如使用UpdateEmployeeAddress()或者ChangeEmployeeStatus()方法。这些方法执行更多限制的UPDATE语句,防止重复应用旧值的危险。
测试组件(Testing the Component)
现在,你已经创建了数据组件,接下就需要一个简单的页面来测试它。和使用其它组件一下,你必须向组件集合里添加引用,然后导入使用它的命名空间,简化EmployeeDetails和EmployeeDB类的实现。剩下的最后一步是编写代码来与这两个类交互。在这个例子中,代码在Page.Load事件句柄中运行。
首先,代码通过私有的WriteEmployeesList()方法获取Employee的数量和列表,并把输出结果转换成HTML。其次,代码添加一条记录并且再次列出表的内容,最后,代码删除添加的记录并且再一次显示Employee表的内容。
这是完整的页面代码:
public partial class ComponentTest : System.Web.UI.Page
{
// Create the database component so it's available anywhere on the page.
private EmployeeDB db = new EmployeeDB();
protected void Page_Load(object sender, System.EventArgs e)
{
WriteEmployeesList();
int empID = db.InsertEmployee(
new EmployeeDetails(0, "Mr.", "Bellinaso", "Marco"));
HtmlContent.Text += "<br />Inserted 1 employee.<br />";
WriteEmployeesList();
db.DeleteEmployee(empID);
HtmlContent.Text += "<br />Deleted 1 employee.<br />";
WriteEmployeesList();
}
private void WriteEmployeesList()
{
StringBuilder htmlStr = new StringBuilder("");
int numEmployees = db.CountEmployees();
htmlStr.Append("<br />Total employees: <b>");
htmlStr.Append(numEmployees.ToString());
htmlStr.Append("</b><br /><br />");
List<EmployeeDetails> employees = db.GetEmployees();
foreach (EmployeeDetails emp in employees)
{
htmlStr.Append("<li>");
htmlStr.Append(emp.EmployeeID);
htmlStr.Append(" ");
htmlStr.Append(emp.TitleOfCourtesy);
htmlStr.Append(" <b>");
htmlStr.Append(emp.FirstName);
htmlStr.Append("</b>, ");
htmlStr.Append(emp.LastName);
htmlStr.Append("</li>");
}
htmlStr.Append("<br />");
HtmlContent.Text += htmlStr.ToString();
}
}
图8-2显示了页的输出
图8-2 使用数据库组件
非连接的数据(Disconnected Data)
到目前为止,你所见到的所有例子都使用了ADO.NET基于连接的功能。数据通过这个方法获得后,它就与数据源没有任何关系。跟踪用户动作,存储信息及确定什么时间产生并执行一个新的命令,都完全依赖于代码。
ADO.NET强化了DataSet带来的全新观念。连接到数据库时,DataSet将由从数据库获取的信息拷贝填充。即便修改DataSet里的信息,数据库对应的表中的信息并不改变。这意味着你能够脱离了数据库连接后,仍能方便地处理和操作数据而不用担心发生任何问题。一旦有需要,你可以重新连接到原来的数据源并且在一个批命令里应用DataSet里的所有改变。
当然,这种方便并不是完全没有缺点,比如并发就是一个问题。产生的全部改变,根据你对系统的设计而可能一次性提交。任何一个错误(如一个用户试图更新一条记录的同时另一用户也在进行更新)都会终止整个更新进程。你可以小心谨慎地编写代码来防止这些问题,但这需要更多功夫。
另一方面,有时你可能希望使用ADO.NET的非连接访问模型和DataSet。使用DataSet比DataReader更简单的场合主要包含如下几方面:
l 当你需要一个方便的包来向另一个组件发送数据时(例如,你想与其它组件共享信息或者通过Web服务发布信息到客户端)。
l 当你需要将数据保存到磁盘上某种格式的文件中时(DataSet包含内建的功能来允许你将它保存为XML文件)。
l 当你想在大量的数据中进行向前或向后的导航时。例如,你可以使用DataSet来支持分页的列表控件,在列表控件中显示信息的子集。而使用DataReader就只能向前读取。
l 当你想在不同的表间进行导航时。DataSet能够存储所有的表,以及它们之间的关系信息,因此,你可以轻松创建高度细化的页面(master-detail page),而不需要多次查询数据库。
l 当你想绑定数据到用户接口控件时。尽管可以使用DataReader来进行数据绑定,但由于DataReader是前向游标,你不能绑定数据到多个控件,而且也不能应用自定义的索引和过滤标准,但在DataSet中就可以。
l 当你希望将数据作为XML进行处理时。
l 当你想通过web服务提供批更新时。例如,你可能创建了一个Web服务来允许客户端下载DataTable的所有行,进行多次修改,然后重新提交。此时,Web服务可以在一次操作里应用所有的改变(前提是不发生冲突)。
在本章的后面部分,你将学习怎样从数据源获取数据到DataSet,你也会学习到如何从多个表中获取数据,以及怎样在这些内存中的表间创建关系,怎样索引和过滤数据,怎样搜索指定的记录。然而,你不必考虑使用DataSet执行更新的问题,因为ASP.NET模型自动调用了相关命令进行处理,下一节将对此进行讨论。
Web应用和DataSet(Web Application and the DataSet)
对DataSet的一个常见错误观念是,认为DataSet用来保证Web应用的扩展性。现在理解了ASP.NET请求处理结构,你就明白并不是那么回事。Web应用仅仅运行大约几秒钟。这意味着即便Web应用使用了基于游标的直接访问,由于连接持续的时间很短,因此并不会显著地减少扩展能力。当然,高负载的Web应用除外。
事实上,在具有大量用户的分布式应用中,使用DataSet更有意义。在这种场合,客户端可以从服务器获取DataSet(有可能使用Web Service),并且长时间地使用DataSet对象,仅仅在需要将成批的改变更新到数据源时才重新连接到系统。这使系统处理大量的并发访问的用户数,比每个客户都维护一个直接的、长时间的连接的用户数要多。同时,使用DataSet, 可以通过缓存在服务器上的数据和在客户请求间使用连接池而高效地共享资源。
DataSet也充当了只需要偶尔连接到系统的多用户应用的灵巧信息包。例如,假设旅行售票员通过膝上型计算机输入订票信息或者查看售票信息。使用DataSet,用户膝上型计算机上的应用能够在本地存储未连接的数据,并将它们串行化到XML文档。这允许售票员在无法连接到Internet时,可以使用缓存的数据构建新的订单。新的数据能够在用户重新连接到售票系统之后提交。
那么,在ASP.NET Web应用中怎样使用DataSet呢?本质上,你有两个选择:你可以使用DataSet,或者绕过DataSet直接使用命令。通常来讲,在添加、插入或者更新记录时,应当避免使用DataSet。当然,也不必完全不用DataSet。事实上,当你获取记录时,你可能希望使用DataSet,因为它支持几个很有必要的功能。特别地,DataSet允许你很容易地将一块数据从数据库组件传递给网页。DataSet也支持数据绑定,这样就可以在高级数据控件如GriView中显示信息。正因为如此,大多数Web应用使用DataSet获取数据,但是使用直接的命令执行更新。
XML整合(XML Iintegration)
DataSet也提供本地XML串行化。你不需要感知这一点就能享受它的好处。如能够串行化DataSet到文件或者通过Web服务将DataSet传送给其它应用。最为重要的一点,是这个功能允许你与客户端分享数据,这些客户端是以不同的程序语言来编写的,并且运行在其它的操作系统上。
在DataSet中的XML整合允许你任何时候访问DataSet中的信息作为XML文档。你可以通过修改XML来修改值,移除行和添加新记录,这不会导致信息损失。当然,这种XML深度整合在典型的自包含的(self-contained)Web应用中并不要求。事实上,如果你通过一个XML模型来修改关系数据,可能会碰上几种错误,而这些错误是直接使用DataSet对象时不会遇到的。例如复制的数据或扰乱的关系的数据类型转换问题和错误。只有你需要将DataSet里的信息与其它的应用和商业进程交换时,DataSet对XML的支持才显得引人注目。
DataSet类
DataSet是非连接数据访问的核心。DataSet包含两个重要的因素:0或多个表的集合(通过表的属性展示)和0或者多个关系来将多个表连接在一起(通过关系属性展示)。图8-3显示了DataSet的基本结构。
图8-3 DataSet解剖
注意:偶尔,初级ADO.NET开发者会出现想当然的错误,认为DataSet应该包含从数据源里获得的表的所有信息。事实并非如此。基于性能的考虑,你可能只使用DataSet来对数据源里的一小部分信息进行操作。同时,DataSet里的表也不需要直接映射到数据源里的表。一个单独的表能够存放来自于某个表的查询结果。也可以存放JOIN查询(合并了多个连接的表)的结果。
在图8-3中可以看到,每个DataRow对象代表一条记录。为了管理非连接的变化,DataSet跟踪每个DataRow的版本信息。当你编辑行的值时,新值保留在内存中,并且行被标记为已改变。当你添加或删除一行,行被标记为已经添加或已删除。
不要忘记,当你在DataSet对象上工作时,并没有触及数据源中的数据。相反,所有的改变仅对本地内存中的DataSet有效。DataSet不再保留到数据源的任何连接。如果想从数据库中选取记录并且将它们填充到DataSet的表中,你需要使用另一个ADO.NET对象:DataAdapter。DataAdapter允许你根据DataSet中的改变来更新数据源(当然,推荐在ASP.NET中采用直接的命令来进行更新)。
DataSet也有读写XML数据和Schema的方法和用于快速清除和复制数据的方法。表8-1概略地描述了这些方法。第12章将对XML有更详细的介绍。
方法 | 描述 |
GetXml() GetXmlSchema() | 返回带数据(采用XML标记)的串或Schema信息给DataSet。Schema信息是结构化的信息,如表的数量、名字、行、数据类型和关系。 |
WriteXml() WriteXmlSchema() | 将DataSet描述的数据和Schema输出到文件或者XML格式的流中。 |
ReadXml() | 根据存在的XML文档或XML Schema文档在DataSet中创建表。XML源可以是文件或任何其它的的流。 |
Clear() | 清空表中的所有数据。但不会清除Schema或者关系信息。 |
Copy() | 返回一个DataSet的精确拷贝,包括相同的表、关系和数据。 |
Clone() | 返加一个DataSet的相同结构(包括表和关系),但没有数据。 |
Merge() | 将另一个DataSet作为输入,并且合并到当前的DataSet中。添加新的表,合并存在的表。 |
表8-1 DataSet XML相关的方法
DataTable类(the DataTable class)
如图8-3所见的那样,DataSet.Tables中的每一项都是一个DataTable。DataTable包含它自己的集合---DataColumn对象的列集合(它描述了每个字段的名字和数据类型)和DataRow对象的行集合(包含了一条记录中的真实数据)
提示:ASP.NET为DataSet和DataTable类添加了新的CreateDataReader()方法。可以通过调用这个方法来返回DataReader类型的对象,遍历所有非连接的数据。如果在既存的代码中要使用DataReader,这个功能就很有用。CreateDataReader()方法返回DataTableReader对象,它继承于DbDataReader和实现了IdataReader接口,就象所有的DataReader对象一样。
DataRow类(the DataRow Class)
每个DataRow对象代表从数据源获取的表中的一条记录。DataRow是真实的字段值的容器,可以通过字段名来访问它,如myRow[“FieldNameHere”]。
DataAdapter类(the DataAdapter Class)
DataAdapter提供了DataSet中的单个DataTbale与数据源之间的桥梁。它包含了所有查询和更新数据源的命令。
DataAdapter提供三个关键方法,列于表8-2中。
方法 | 描述 |
Fill() | 通过SelectCommand执行查询来添加一个DataTable到DataSet中。如果查询返回了多个结果集,就一次添加多个DataTable对象。也可以使用该方法向已存在的DataTable中添加数据。 |
FillSchema() | 通过SelectCommand执行查询来添加一个DataTable到DataSet中,并且只返回Schema信息。这个方法不向DataTable中添加任何数据。而是简单地使用列名、数据类型、主键和唯一约束的详细信息来预配置DataTable。 |
Update() | 检查单个的DataTable中的所有改变,并且通过执行正确的InsertCommand、UpdateCommand和DeleteCommand操作来所有改变成批地提交到数据源。 |
表8-2 DataAdapter方法
为了使DataAdapter能够编辑、删除和添加行,你需要为DataAdapter的UpdateCommand、DeleteCommand、InsertCommand属性指定Command对象。为了使用DataAdapter来填充DataSet,还必须设置SelectCommand。
图8-4显示了DataAdapter和相应的Command对象怎样与数据源和DataSet一起工作的。
图8-4 DataAdapter与数据源的交互
填充DataSet(Filling a DataSet)
在下面的示例中,你会看到怎样从SQL SERVER的表中获取数据,并且将其填充到DataSet中的DataTable对象里。也可以看到怎样使用Repeater控件来显示数据,或者通过程序来遍历记录,并且逐条地显示。所有的逻辑都存放在Page.Load事件句柄中。
首先,程序创建连接,并且定义SQL查询的text:
string connectionString =
WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
SqlConnection con = new SqlConnection(connectionString);
string sql = "SELECT * FROM Employees";
下一步是创建SqlDataAdapter类的新实例来获取顾员列表。尽管每个DataAdapter支持4个Command对象,只有SelectCommand可用于填充DataSet。为了更容易实现,你可以在一个步骤里创建Command对象并且将其指定给DataAdapter.SelectCommand属性。仅需要在DataAdapter构造器里提供Connection对象和查询串。如下所示:
SqlDataAdapter da = new SqlDataAdapter(sql, con);
现在,你需要创建一个新的、空的DataSet,使用DataAdapter.Fill()方法来执行查询,并将查询结果放入DataSet里的DataTable。此时,你也可以为表指定名字。否则,将会自动使用一个默认名字(如Table)。在下面的例子中,表的名字对应于数据库中的表的名字,当然这并不强求。
DataSet ds = new DataSet();
da.Fill(ds, "Employees");
注意,这段代码并没有通过调用Connection.Open()来打开连接。相反,在你调用 Fill()方法时,DataAdapter自动打开和关闭连接。因此,在异常处理块中,只需要考虑一行的代码,即DataAdater.Fill()。另外,你也可以手工打开和关闭连接。如果调用Fill()时连接是打开的,DataAdpater会使用那个连接,但不会自动关闭它。如果你想快速连续地执行与数据源相关的多个操作,而且不希望每次重复地打开和关闭连接带来的额外负载时,这个方法非常有用。
最后一步是显示DataSet中的内容。最快的方法是使用前一章介绍的相同的技术,即通过检查每一条记录来构建HTML字符串。下面的代码对DataTable中的所有DataRow对象进行了遍历,并且在动态的列表中显示了每条记录的字段值。
StringBuilder htmlStr = new StringBuilder("");
foreach (DataRow dr in ds.Tables["Employees"].Rows)
{
htmlStr.Append("<li>");
htmlStr.Append(dr["TitleOfCourtesy"].ToString());
htmlStr.Append(" <b>");
htmlStr.Append(dr["LastName"].ToString());
htmlStr.Append("</b>, ");
htmlStr.Append(dr["FirstName"].ToString());
htmlStr.Append("</li>");
}
HtmlContent.Text = htmlStr.ToString();
当然,ASP.NET模型使你从原因的HTML编码中解脱出来。一个更佳的方法是将DataSet中的数据绑定到一个数据绑定(data-bound)控件,它就会根据一个模板自动产生HTML。第9章详述了数据绑定控件。
注意:当你绑定DataSet到一个控件时,要视图状态中不会存储数据对象。数据控件存储了当前显示的数据的足够的信息。如果需要与DataSet交互时支持多个回滚,你需要在ViewState集合中(可能会显著地增大页面的大小),或者Session、Cache对象中手工存储视图状态。
使用多个表和关系(Working with multiple tables and relationships)
下一个示例显示了DataSet的高级应用,它除了提供非连接的数据,还使用表关系。这个示例演示了怎样从Northwind数据库中的Categoies表和Products表中获取一些记录,也演示了它们之间的关系,以便于从分类(category)记录导航到它的所有子产品,并且创建一个简单的报告。
第一步是初始化ADO.NET对象,并且声明两个SQL查询(用于获取分类和产品),如下所示:
string connectionString =
WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
SqlConnection con = new SqlConnection(connectionString);
string sqlCat = "SELECT CategoryID, CategoryName FROM Categories";
string sqlProd = "SELECT ProductName, CategoryID FROM Products";
SqlDataAdapter da = new SqlDataAdapter(sqlCat, con);
DataSet ds = new DataSet();
下一步,程序执行两个查询,添加两个表到DataSet。注意,连接在开始的时候显性地打开,在两个操作完成之后立即关闭,确保最好的性能。
try
{
con.Open();
// Fill the DataSet with the Categories table.
da.Fill(ds, "Categories");
// Change the command text and retrieve the Products table.
// You could also use another DataAdapter object for this task.
da.SelectCommand.CommandText = sqlProd;
da.Fill(ds, "Products");
}
finally
{
con.Close();
}
在这个示例中,使用了相同的DataAdapter填充两个表。这个技术非常合理,在这种场合也非常有意义,因为你不需要重用DataAdapter来更新数据源。当然,如果你正在使用DataAdapter来同时查询数据和提供改变,你可能不需要使用这个方法。相反,你应当每个表使用单独的DataAdapter,以保证每个DataAdapter为对应的表有正确的插入、更新和删除操作。
现在,你有了带两个表的DataSet。Northwind数据库中的这两个表通过DategoryID字段建立的关系连接在一起。这个字段是Categories表的主键和Product的外键。不幸的是,ADO.NET没有提供任何方法来从数据源中读取关系并自动将关系应用到DataSet中。因此,你需要手工创建DataRelation对象来描述关系。
关系是通过定义DataRelation对象并且添加到DataSet.Relations集中来创建的。创建DataRelation时,指定了3个构造器参数:关系的名字,父表主键的DataColumn和子表外键的DataCloumn。
这个例子的代码如下:
// Define the relationship between Categories and Products.
DataRelation relat = new DataRelation("CatProds",
ds.Tables["Categories"].Columns["CategoryID"],
ds.Tables["Products"].Columns["CategoryID"]);
// Add the relationship to the DataSet.
ds.Relations.Add(relat);
一旦获得了所有的数据,可以循环读取Categories表的记录,并且添加每个分类的名字到HTML串中:
StringBuilder htmlStr = new StringBuilder("");
// Loop through the category records and build the HTML string.
foreach (DataRow row in ds.Tables["Categories"].Rows)
{
htmlStr.Append("<b>");
htmlStr.Append(row["CategoryName"].ToString());
htmlStr.Append("</b><ul>");
...
这一部分相当有趣。在这一块中,你可以通过调用DataRow.GetChildRows()方法来访问当前分类的相关产品记录。一旦你有了产品记录数组,你就可以使用嵌入的foreach循环来读取它。相对于在一个独立的对象中查找信息,或者使用传统的基于连接的访问来执行查询来来说,这个方法要简单得多。
下面一段代码演示了这种方法,获取了子记录,并且完成了外部foreach循环:
...
// Get the children (products) for this parent (category).
DataRow[] childRows = row.GetChildRows(relat);
// Loop through all the products in this category.
foreach (DataRow childRow in childRows)
{
htmlStr.Append("<li>");
htmlStr.Append(childRow["ProductName"].ToString());
htmlStr.Append("</li>");
}
htmlStr.Append("</ul>");
}
最后一步是在页面上显示HTML串:
HtmlContent.Text = htmlStr.ToString();
本例的编码完毕,如果你运行网页,将会看到图8-5显示的输出。
图8-5 每个分类的产品列表
提示:ADO.NET程序员新手通常会问到的问题是,什么时候使用JOIN查询和什么时候使用DataRelation对象?考虑的重点是你是否要更新获得的数据。如果是,使用单独的表和DataRelation对象会有最大的灵活性。如果不是,你可以使用任何一种方法。当然,JOIN查询会更高效一些,因为它只包含了一次网络传递,而DataRelation方法一般需要两次来填充独立的表。
参照完整性(Referential Integrity)
如果向DataSet添加了关系,就要遵循参照完整性规则。例如,如果还有关联的子记录,就不能删除父记录,也不能创建没有父节点的子记录。如果DataSet只包含部分数据,会出现一些问题。例如,你有客户订单的全部列表,但只有部分客户,问题就出现了。因为订单指向了Dataset中不存在的客户。避免这个问题的方法就是创建一个DataRelation,而不创建相应的约束。为了做到这一点,使用DataRelation构造器接收布尔的createConstraints参数,并且将参数设置为false。如下所示:
DataRelation relat = new DataRelation("CatProds",
ds.Tables["Categories"].Columns["CategoryID"],
ds.Tables["Products"].Columns["CategoryID"], false);
另一个方法是禁用所有约束检查,即在添加关系前设置DataSet.EnbleContraints属性。
搜索特定的行(Searching for Specific Rows)
DataTable提供了有用的Select()方法,允许你基于SQL表达式获取DataRow对象数组。和Select()方法一起使用的表达式与SELECT语句中的WHERE子句的功能是一样的。
例如,下面的的代码获取了所有标记为discontinued的产品:
// Get the children (products) for this parent (category).
DataRow[] matchRows = DataSet.Tables["Products"].Select("Discontinued = 0")
// Loop through all the discontinued products and generate a bulleted list.
htmlStr.Append("</b><ul>");
foreach (DataRow row in childRows)
{
htmlStr.Append("<li>");
htmlStr.Append(row["ProductName"].ToString());
htmlStr.Append("</li>");
}
htmlStr.Append("</ul>");
在这个例子中,Select()语句使用了相当简单的过滤串。当然,你完全可以使用更复杂的操作符和不同标准的组合。更多信息,参照MSDN类库参考中关于DataColumn.Expression属性的内容,或者参考表8-3和关于过滤字符串的相关节“Filtering with a DataView”。
注意,Select()方法有一个潜在的警告:它不支持参数化的环境。因此,它对于SQL注入攻击是开放的。显然,在这种情形下能够进行的SQL注入攻击是相当有限的,因为没有办法访问真实的数据源或者执行附加的命令。然而,小心地设计的值仍能够欺骗应用,从表中返回额外的信息。如果你创建了带用户提供值的过滤表达式,你可以人工遍历DataTable来找到所需的行,而不是使用Select()方法。
在自定义数据类中使用DataSet(using the dataset in a custom data class)
在自定义的数据访问类中,可以使用DataSet或者DataTable作为一个方法的返回值。例如,你可以使用下面的DataSet代码重写前面的GetAllEmployees()方法:
public DataTable GetAllEmployees()
{
SqlConnection con = new SqlConnection(connectionString);
SqlCommand cmd = new SqlCommand("GetEmployee", con);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("@EmployeeID", SqlDbType.Int, 4));
cmd.Parameters["@EmployeeID"].Value = employeeID;
SqlDataAdapter da = new SqlDataAdapter(sql, con);
DataSet ds = new DataSet();
// Fill the DataSet.
try
{
da.Fill(ds, "Employees");
return ds.Tables["Employees"];
}
catch
{
throw new ApplicationException("Data error.");
}
}
有趣的是,当你使用这个方法时,唾手可得相同的功能。例如,在下一章中,你会了解使用ObjectDataSource来绑定自定义类。ObjectDataSource理解自定义类和理解DataSet一样好(而且它们的性能基本上也是一样的)。
DataSet方法有几个限制。尽管DataSet提供了非连接数据的理想容器,但你会发现,创建能返回DataTable对象和独特的DataRow对象(例如,作为GetEmployee()方法的返回值)的方法会更简单。然而,这些对象并没有象DataSet那样的数据绑定支持水平,因此,你需要在清晰代码模型(使用不同的非连接数据对象)和高度的灵活性(一般完全使用DataSet,即便只返回一个记录也是)之间进行选择。另一个限制是,DataSet是弱类型的,这意味着没有编译时语法检查或者智能感知来保证使用了正确的字段名(这与自定义数据访问类,如EmployeeDetails不同)。你可以通过构建强类型的DataSet来克服这个限制,但这需要更多的工作要做。有关创建强类型的DataSet的更多信息,请参考Pro ADO.NET 2.0(Apress,2005)。
数据绑定(Data Binding)
尽管你循环访问非连接数据并且手工产生HTML并没有什么不妥,但在大多数情形下,ASP.NET数据绑定可以大简化你的工作。第9章详细讨论了数据绑定,但在继续本章的DataView示例之前,你需要了解一些基础知识。
数据绑定的的本质是在数据对象和控件之间建立关联,然后ASP.NET数据绑定基础结构就能正确地创建输出。
其中一个最容易使用的数据绑定控件是GridView。GridView能够精妙地创建HTML表格,表格的每一行代表一条记录,一列代表一个字段。
为了绑定数据到GridView之类的绑定控件,你首先需要正确设置DataSource属性,这个属性指向了包含所要显示信息的对象。在这里,这对象是DataSet:
GridView1.DataSource = ds;
由于数据绑定控件不能绑定到一个单独的表(只能是整个DataSet),使用时需要显性地指定使用哪个表。即需要为DataMember属性设置正确的表名。如下所示:
GrieView1.DataMember = “Employees”;
最后,在你定义了数据的位置之后,你需要调用DataBind()方法来将DataSet中的信息拷贝到控件。如果忘掉了这一步,控件就是空的,信息就不会在页面上显示:
GridView1.DataBind();
作为一种快捷方式,你可以调用当前网页的DataBind()方法,它将遍询支持数据绑定的控件并且调用DataBind()方法。
注意:随后的示例使用数据绑定演示了GridView的过滤和索引功能。第9章和第10章有关于GridView的更多信息。
数据视图类(the DataView Class)
DataView定义了DataTable对象的视图,换句话说,它是DataTable中的数据的一种呈现,只是它包含了自定义的过滤和索引设置。为允许你进行这些设置,DataView有诸如Sort和RowFilter之类的属性。这些属性允许你选择通过视图查看哪些数据。然而,它们并不影响DataTable中的真实数据。例如,如果你通过隐藏一些行来过滤表,那些行仍然保留在DataTable里,只是通过DataView不能访问罢了。
DataView在数据绑定的场合是很有用的。它允许你不需要处理或修改那些数据的同时,显示一个表中的所有数据的子集。因为那些数据可能用于其它任务。
每个DataTable都有一个关联的默认的DataView。你可以创建多个DataView对象来代表同一表的不同视图,默认的DataView是通过DataTable.DefaultView属性来设置的。
在后面的示例中,你可以看到如何根据一个表达式来创建一些grid,以显示按不同字段索引和过滤的记录。
索引和数据视图(Sorting with a DataView)
下一个示例使用了有3个GridView控件的页面。页面加载时,为每个grid绑定了相同的DataTable,然而,它使用了3个不同的视图,每个都使用了不同的字段进行了索引。
代码最开始获取了雇员列员并放入DataSet:
// Create the Connection, DataAdapter, and DataSet.
string connectionString =
WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
SqlConnection con = new SqlConnection(connectionString);
string sql =
"SELECT TOP 5 EmployeeID, TitleOfCourtesy, LastName, FirstName FROM Employees";
SqlDataAdapter da = new SqlDataAdapter(sql, con);
DataSet ds = new DataSet();
// Fill the DataSet.
da.Fill(ds, "Employees");
第二步,通过数据绑定填充GridView控件。为了绑定第一个控件,可以简单直接地使用DataTable,它使用了默认DataView显示所有数据。对另两个控件,应当创建新的DataView对象,然后显性地设置它们的Sort属性。
// Bind the original data to #1.
grid1.DataSource = ds.Tables["Employees"];
// Sort by last name and bind it to #2.
DataView view2 = new DataView(ds.Tables["Employees"]);
view2.Sort = "LastName";
grid2.DataSource = view2;
// Sort by first name and bind it to #3.
DataView view3 = new DataView(ds.Tables["Employees"]);
view3.Sort = "FirstName";
grid3.DataSource = view3;
索引Grid只需要简单地设置DataView.Sort属性为有效的索引表达式。
这个示例每个视图的索引使用了一个字段,但你也可以使用多个字段,多个字段之间用逗号(,)分隔,如下所示:
View2.Sort = “LatstName, FirstName”;
注意:索引是根据列的数据类型进行的。数值和日期列的排序按从小到大。如果DataTable.CaseSensitive属性为false(默认值),字符串列将按字母排序。包含二进制数据的列不会被索引。你也可以使用ASC和DSC属性来指定升序或降序排序。第10章中将再次使用索引和学习DataView过滤。
绑定了grid之后,仍需要触发数据绑定进程来将数据从DataTable拷贝到控件中。可以分别对这些控件进行操作,也可以调用Page.DataBind()来对整个页面进行绑定操作。本例中使用了页面绑定:
Page.DataBind();
图8-6显示了结果页。
过滤和数据视图(Filtering with a DataView)
也可以使用DataView来应用自定义过滤,只显示需要的行。为了完成这一设想,需要设置RowFilter属性。RowFilter属性扮演了类似SQL语句中WHERE子句的角色。使用它,你就可以使用逻辑操作符(如<,>,=)和更广的标准来限制输出结果。表8-3显示了大多数过滤操作符。
操作符 | 描述 |
<, >, <=, and >= | Performs comparison of more than one value. These comparisons can be numeric (with number data types) or alphabetic dictionary comparisons (with string data types). |
<> and = | Performs equality testing. |
NOT | Reverses an expression. Can be used in conjunction with any other clause. |
BETWEEN | Specifies an inclusive range. For example, “Units BETWEEN 5 AND 15” selects rows that have a value in the Units column from 5 to 15. |
IS NULL | Tests the column for a null value. |
IN(a,b,c) | A short form for using an OR clause with the same field. Tests for equality between a column and the specified values (a, b, and c). |
LIKE | Performs pattern matching with string data types. |
+ | Adds two numeric values or concatenates a string. |
- | Subtracts one numeric value from another. |
* | Multiplies two numeric values. |
/ | Divides one numeric value by another. |
% | Finds the modulus (the remainder after one number is divided by another). |
AND | Combines more than one clause. Records must match all criteria to be |
| displayed. |
OR | Combines more than one clause. Records must match at least one of the |
表8-3 过滤操作符
下面的示例页包含了3个GridView控件,每个都绑定到相同的DataTable,但过滤设置不同。
string connectionString =
WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
SqlConnection con = new SqlConnection(connectionString);
string sql = "SELECT ProductID, ProductName, UnitsInStock, UnitsOnOrder, " +
Discontinued FROM Products";
SqlDataAdapter da = new SqlDataAdapter(sql, con);
DataSet ds = new DataSet();
da.Fill(ds, "Products");
// Filter for the Chocolade product.
DataView view1 = new DataView(ds.Tables["Products"]);
view1.RowFilter = "ProductName = 'Chocolade'";
GridView1.DataSource = view1;
// Filter for products that aren't on order or in stock.
DataView view2 = new DataView(ds.Tables["Products"]);
view2.RowFilter = "UnitsInStock = 0 AND UnitsOnOrder = 0";
GridView2.DataSource = view2;
// Filter for products starting with the letter P.
DataView view3 = new DataView(ds.Tables[“Products”]);
view3.RowFilter = "ProductName LIKE 'P%'";
GridView3.DataSource = view3;
this.DataBind();
运行网页会对3个grid进行填充,如图8-7所示:
图8-7 不同方法的Grid过滤
提示:DataView也包含了RowStateFilter属性,以便于过滤DataTable,使行显示特定的行状态(inserted, deleted, modified, unchanged)。默认情况下,这个属性也设置来显示没有标记为删除的所有行。
使用关系进行高级过滤(Advanced Filtering with Relationships)
DataView也支持非常复杂的过滤表达式。鲜为人知的一个功能是,它具有基于关系来过滤行的能力。例如,你能够显示包含了20种产品的分类,或者你可能显示已经进行了大量购买的客户。在这两种情况下,你需要根据关联表的信息进行过滤。
为了创建这种过滤字符串,需要合并两个要素:
l 连接两个表的关系。
l 一个合计函数,如AVG(),MAX(),MIN()或者COUNT()。这些函数对相关记录的数据起作用。
举个例,假定用Categories和Products表来填充DataSet,并且定义了关系:
// Define the relationship between Categories and Products.
DataRelation relat = new DataRelation("CatProds",
ds.Tables["Categories"].Columns["CategoryID"],
ds.Tables["Products"].Columns["CategoryID"]);
// Add the relationship to the DataSet.
ds.Relations.Add(relat);
你可以使用基于Products表的过滤表达式,过滤Categories表的显示。例如,假定你只想显示那些至少有一件产品价格高于50美元的分类记录。为了实现这个目标,使用COUNT()函数,同时使用表的关系名(CatProds)。过滤字符串应写成这样:
MAX(Child(CatProds).UnitPrice)>50
对DataView应用了这个过滤字符串的代码如下:
DataView view1 = new DataView(ds.Tables["Categories"]);
view1.RowFilter = "MAX(Child(CatProds).UnitPrice) > 50";
GridView1.DataSource = view1;
最终的结果就是GirdView显示了那些至少有一件产品价格高于50美元的分类。
计算列(Calculated Columns)
除了从数据源获取的字段外,你还可以添加计算列。计算列描述了既存值的合并计算值。计算列在获取和更新数据时被忽略。要创建计算列,只需要简单地创建一个新的DataColumn对象(指定它的名字和类型),并且设置其Expression属性,然后使用Add()方法,将DataColumn添加到DataTable里的Column集里面。
为了方便演示,下面一列使用了字符串连接将姓(first name)和名(last name)合并到了一个字段:
DataColumn fullName = new DataColumn(
"FullName", typeof(string),
"TitleOfCourtesy + ' ' + LastName + ', ' + FirstName");
ds.Tables["Employees"].Columns.Add(fullName);
提示:当然,你也可以执行一个查询来创建计算列。然而,这种方法会使随后对数据源的更新变得更加复杂,也增加了数据源的工作量。因此,在DataSet中创建计算列是更好的办法。
你也可以创建计算列来合并相关行的信息。例如,你可能向Categories表中添加一列来表明相关的产品数。在这种情况下,你必须首先使用DataRelation对象创建关系,同时还需要使用SQL合并函数,如AVG(), MAX(), MIN(), COUNT()。
下面的例子创建了3个计算列,它们都使用了合并函数和表关系:
string connectionString =
WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
SqlConnection con = new SqlConnection(connectionString);
string sqlCat = "SELECT CategoryID, CategoryName FROM Categories";
string sqlProd = "SELECT ProductName, CategoryID, UnitPrice FROM Products";
SqlDataAdapter da = new SqlDataAdapter(sqlCat, con);
DataSet ds = new DataSet();
try
{
con.Open();
da.Fill(ds, "Categories");
da.SelectCommand.CommandText = sqlProd;
da.Fill(ds, "Products");
}
finally
{
con.Close();
}
// Define the relationship between Categories and Products.
DataRelation relat = new DataRelation("CatProds",
ds.Tables["Categories"].Columns["CategoryID"],
ds.Tables["Products"].Columns["CategoryID"]);
// Add the relationship to the DataSet.
ds.Relations.Add(relat);
// Create the calculated columns.
DataColumn count = new DataColumn(
"Products (#)", typeof(int), "COUNT(Child(CatProds).CategoryID)");
DataColumn max = new DataColumn(
"Most Expensive Product", typeof(decimal), "MAX(Child(CatProds).UnitPrice)");
DataColumn min = new DataColumn(
"Least Expensive Product", typeof(decimal), "MIN(Child(CatProds).UnitPrice)");
// Add the columns.
ds.Tables["Categories"].Columns.Add(count);
ds.Tables["Categories"].Columns.Add(max);
ds.Tables["Categories"].Columns.Add(min);
// Show the data.
GridView1.DataSource = ds.Tables["Categories"];
GridView1.DataBind();
图8-8显示了输出页面:
图8-8 显示计算列
注意:不要忘记,这些示例仅演示了过滤和合并数据的简便方法。这些操作仅是正确呈现数据的一部分。还有一个同等重要的问题是正确的格式。在第9章和第10章,你会了解到GridView的更多信息,那时你可以以正确的格式来显示货币,并且自定义其它一些细节,如颜色、尺寸、列的顺序和字体。例如,通过设置格式,你可以将4.5000显示为$4.50,使显示含义更明确。
总结
在本章中,你学习了怎样创建基本的数据库组件,并且深入研讨了DataSet和DataView。下一章,你将继续使用相同的数据库组件和DataSet,只是使用了一个新的层。你还会了解到数据源控件怎样通过更高层的抽象来包装ASP.NET的,这将使你使用最少的代码构建丰富的数据绑定页面。
如果想了解DataSet的所有功能,包括那些应用于分布的和丰富的客户端应用的功能,你可以查阅Programming Microsoft ADO.NET 2.0: Core Reference (Microsoft Press,2005)或者 Pro ADO.NET2.0(Apress, 2005)。