创建一个数据访问层
导言
作为web开发人员,我们的生活围绕着数据操作。我们建立数据库来存储数据,写编码来访问和修改数据,设计网页来采集和汇总数据。本文是研究在ASP.NET 2.0中实现这些常见的数据访问模式之技术的长篇系列教程的第一篇。我们将从创建一个软件框架开始,这个框架的组成部分包括一个使用强类型的DataSet的数据访问层(DAL),一个实施用户定义的业务规则的业务逻辑层(BLL),以及一个由共享页面布局的ASP.NET网页组成的表现层。在打下这个后端的基础工作之后,我们将开始转向报表,示范如何显示,汇总,采集,和验证web 应用的数据。这些教程旨在简明扼要,使用了许多屏幕截图,提供了按步就 班(step-by-step)的指导,带你经历这个开发过程。每个教程都有C# 版和VB版,并且附有涉及的完整的编码的下载。(这第一个教程比较长,但以后其他的教程将以更容易消化的篇幅推出。)
在这些教程中,我们将使用置于App_Data 目录内的微 软SQL Server 2005 Express版的Northwind数据库。除了数据库文件外,App_Data目录还带有用于创建数据库的SQL脚本,万一你想使用别的数据库版本的话。如果你愿意的话,你也可以直接从微软下载这些脚本。如果你使用别的SQL Server版本的Northwind数据库的话,你需要更新Web.config文件中的NORTHWNDConnectionString设置。本教程中的web应用是个基于文件系统的网站项目,是使用Visual Studio 2005 专业版建立起来的。但是,所有的教程都可以在Visual Studio 2005的免费版本Visual Web Developer中运行。
在这个教程里,我们将从头开始,先创建一个数据访问层(DAL),然后在第二个教程里创建一个业务逻辑层(BLL),在第三个教程里设计页面布局和导航。以后的教程将建立在这三个教程的基础之上。在第一个教程里,我们要讨论的内容多多,所以,请打开Visual Studio,让我们动起手来!
第一步:创建一个Web项目,配置数据库连接
在我们开始创建数据访问层(DAL)之前,我们首先需要创建一个网站,以及建立一个数据库。我们从创建一个基于文件系统的ASP.NET 网站开始。次序如下,打开文件(File)菜单,选择新的网站 (New Web Site),系统会显示一个新网站对话框,选择ASP.NET网站模板(Web Site template),设置定 位(Location)列表的选项为文件系统( File System),然后选这一个放置这个网站的文件夹,然后选择编程语 言为C#。
图 1: 创建一个基于文件系统的网站
Visual Studio会为你生成一个新的网站,同时生成一个名为Default.aspx
的网页,和一 个App_Data
文件夹。
网站生成之后,下一步是在Visual Studio的服务器资源管理器(Server Explorer)里为你的数据库添加一个引 用(reference)。把一个数据库添加到服务器资源管理器之后,你就能在Visual Studio环境里添加数据表,存 储过程,视图等等。你也能查看数据库里的数据,手工或用查询生成器(Query Builder)的图形界面建立你自己的查询语句。此外,当我们为DAL创建强类型的DataSet时,我们需要把Visual Studio指向作为DataSet数据源的目标数据库。虽然我们可以在适当时候提供所涉及的数据库连接信息,但假如我们预 先在服务器资源管理器里注册这些数据库的话,Visual Studio会自动把这些数据库填充到一个下拉列表中去 。
把Northwind数据库添加到服务器资源管理器中去的步骤取决于你想使用放置在App_Data
文件夹 里的SQL Server 2005 Express 版本数据库,还是你想使用已经建立好了的SQL Server 2000或2005 数据库服 务器。
使用置于App_Data
文件夹中的数据库
如果你没有可连接的SQL Server 2000 或2005服务器,或者你就是想避免给数据库服务器添加数据库,你可以使用SQL Server 2005 Express版的Northwind数据库,该数据库位于下载源码中的App_Data
文件夹里(NORTHWND.MDF
)。
置于App_Data
文件夹里的数据库会被自动添加到服务器资源管理器中。假设你已经在你的机器上安装了SQL Server 2005 Express版本,那么你应该在服务器资源管理器中看到一个名为NORTHWND.MDF的节点,你可以将这个节点扩展开来,浏览其中的数据表,视图,存储过程等等 (参考图2)。
App_Data
文件夹还可以放置微软的Access.mdb
数据库文件,跟SQL Server 的数 据库文件类似,这些Access文件会被自动地添加到服务器资源管理器中。如果你不想用任何SQL Server数据库,那么你总归可以下载微软Access版本的Northwind 数据库文件,然后将其放置于App_Data
文件夹中。但记住,Access数据库没有SQL Server那么多功能,而且它并不是设计来在网站情形下使用的。此外,在后面几个教程里将用到Access数据库不支持的数据库层次的功能。
连接到微软SQL Server 2000或2005数据库服务器中的数据库
或者,你也可以连接到安装在数据库服务器上的Northwind数据库。假如数据库服务器上尚未安装Northwind数据库的话,你首先必须运行本教程下载文件中的安装脚本来把数据库添加到数据库服务器上去,或者你也可以从微软网站上直接下载SQL Server 2000的Northwind数据库以及安装脚本。
安装数据库完毕之后,去Visual Studio中的服务器资源管理器,在数据连接(Data Connections)节点上按右鼠标,选择“添加连接(Add Connection)”。如果你看不到服务器资源管理器,去菜单“查看(View)”点击 “服务器资源管理器”,或者按组合键Ctrl+Alt+S来打开服务器资源管理器。这会打开添加连接的对话框,在这上面,你可以设置需要连接的服务器,认证信息,以及数据库名字。在你成功配置数据库连接信息,按OK按钮之后,数据库就会被添加成数据连接节点之下的一个节点。然后,你就可以扩展数据库节点来浏览数据表,视图,存储过程等等。
图 2: 添加一个到你的数据库服务器上的Northwind数据库的连接
第二步:创建一个数据访问层
与数据打交道时,一种做法是把跟数据相关的逻辑直接放在表现层中(在一个web应用里,ASP.NET网页构成了表现层)。其形式一般是在ASP.NET 网页的编码部分写ADO.NET 编码或者在标识符部 分使用SqlDataSource控件。在这两种形式里,这种做法都把数据访问逻辑与表现层紧密耦合起来了。但推荐 的做法是,把数据访问逻辑从表现层分离开来。这个分开的层被称作是数据访问层,简写为DAL,一般是通过 一个单独的类库项目来实现的。这种分层框架的好处在很多文献里都有阐述(详见本教程最后的“附加读物”里 的资源),在本系列中我们将采用这种方法。
跟底层数据源相关的所有编码,譬如建立到数据库的连接,发出SELECT
,INSERT
,UPDATE
,和DELETE
命令等的编码,都应该放置在DAL中。表现层不应该包含对 这些数据访问编码的任何引用,而应该调用DAL中的编码来作所有的数据访问请求。数据访问层包含访问底层数据库数据的方法。譬如,Northwind数据库 中,有Products
和Categories
两个表,它们记录了可供销售的产品以及这些产品 所属的分类。在我们的DAL中,我们将有下面这样的方法:
GetCategories(),
返回所有分类的信息GetProducts()
, 返回所有产品的信息GetProductsByCategoryID(categoryID)
, 返回属于指定分类的所有产品的信 息GetProductByProductID(productID)
, 返回指定产品的信息
这些方法,被调用后,将连接到数据库,发出合适的查询,然后返回结果。我们如何返回这些结果是很重要的 。这些方法可以直接返回数据库查询填充的DataSet 或者DataReader ,但理想的办法是把这些结果以强类 型对象的形式返回。一个强类型的对象,其schema是编译时严格定义好的,而相比之下,弱类型的对象, 其schema在运行时之前是未知的。
譬如,DataReader和普通的DataSet是弱类型对象,因为它们的schema是被用来填充它们的数据库查询返回的字段来定义的。要访问弱类型DataTable中的一个特定字段,我们需要用这样的句法:DataTable.Rows[index] ["columnName"]
。这个例子中的DataTable的弱类型性质表现在于,我们需要通过一个字符串或序号索引来访问字段名称。而在另一个方面,一个强类型的DataTable,它的所有的字段都是通过属性的形式来实现的 ,访问的编码就会象这样:DataTable.Rows[index].columnName
。
要返回强类型对象,开发人员可以创建自定义业务对象,或者使用强类型的DataSet。开发人员实现的业务对 象类,其属性往往是对相应的底层数据表的字段的映射。而一个强类型的DataSet,则是Visual Studio基于数 据库schema为你生成的一个类,其成员的类型都是由这个schema决定的。强类型的DataSet本身,是由继承 于ADO.NET中DataSet,DataTable,和DataRow类的子类组成的。除了强类型的DataTable外,强类型的DataSet现在还包括TableAdapter类,这些类包含了填充DataSet中的DataTable和把 DataTable的改动传回数据库的各种方法。
注意:想了解使用强类型DataSet比之业务对象的优缺点的更多信息,请参考设 计数据层组件以及在层间传输数据一文。
在这些教程的架构里,我们将使用强类型的DataSet。图3示范说明了使用强类型的DataSet之应用程序的不 同层间的流程(workflow)。
图 3: 把所有的数据访问编码委托给DAL
创建强类型的DataSet和Table Adapter
我们开始创建我们的DAL,先给我们的项目添加一个强类型的DataSet。做法如下,在解决方案管理器里的项目 节点上按右鼠标,选择“添加新项(Add a New Item)”。在模板列单里选择DataSet,将其命名 为Northwind.xsd
。
图 4: 给你的项目添加一个新的DataSet
在点击“添加(Add)”按钮后,Visual Studio会问我们是否将DataSet添加到App_Code文件夹中,选择“Yes” 。然后Visual Studio会显示强类型的DataSet的设计器,同时会启动TableAdapter配置向导,允许你给你的强 类型DataSet添加第一个TableAdapter。
强类型的DataSet 起了强类型对象的集合的作用,它由强类型DataTable实例组成,每个强类型DataTable又进 而由强类型的DataRow实例组成。我们将为这个教程系列要用到的每个数据表建立一个对应的强类型DataTable 。让我们开始吧,先为Products
表建立一个DataTable。
记住,强类型的DataTable并不包括如何访问对应底层的数据表的任何信息。要获取用来填充DataTable的数据 ,我们使用TableAdapter类,它提供了数据访问层的功能。对于我们的Products
DataTable, 相应的TableAdapter 类将包 括GetProducts()
和GetProductByCategoryID(categoryID)
等方法,而我 们将在表现层调用这些方法。DataTable的作用是在分层间传输数据。
TableAdapter配置向导首先要你选择使用哪个数据库。下拉框里列出了服务器资源管理器内的那些数据库。如 果你预先没有把Northwind数据库添加到服务器资源管理器里去的话,这时你可以点击新连接按钮来添加。
图 5: 在下拉框里选择Northwind数据库
选择好数据库后,按“下一步”按钮,向导会问你是否想在Web.config
文件里存放连接字符串。 将连接字符串存放在Web.config
文件里,你可以避免把连接字符串硬写在TableAdapter类的编 码中,如果将来连接字符串信息改动的话,这种做法会极大地简化要做的编码改动。如果你选择在配置文件存 放连接字符串,连接字符串将被置放于<connectionStrings>
段落中,这个段落可以被加密来提高安全,也可以通过IIS 图形界面管理工具中的新的ASP.NET 2.0属性页来修改。当然这个工具更适于管理员。
图6: 在Web.config
中存放连接字符串
接下来,我们需要定义第一个强类型的DataTable的schema,同时为用来填充强类型DataSet的TableAdapter类 提供第一个方法。这两步可以通过建立一个返回对应于DataTable的数据表的字段的查询同时完成。在向导的 最后,我们将为这个查询对应的方法命名。完成后,这个方法可以在表现层调用,它会执行设置好的查询,进 而填充一个强类型的DataTable。
开始定义SQL查询之前,我们必须首先选择我们想要TableAdapter执行查询的方式。我们可以直接用ad-hoc的SQL语句,或建立一个新的存储过程,或使用现存的存储过程。在这些教程里,我们将使用ad-hoc的SQL语句。请参考Brian Noyes的文章“使用Visual Studio 2005 DataSet 设计器创建数据访问层”中使用存储过程的例子。
图 7: 用SQL语句查询数据
至此,我们可以手工输入SQL查询。当生成TableAdapter的第一个方法时,你一般想要让你的查询返回那些需 要在对应的DataTable中存放的字段。我们可以建立一个从Products
表里返回所有字段,所有数 据行的查询来达到我们的目的:
图 8: 在文本框里输入SQL查询
或者,我们可以使用查询生成器(Query Builder),用图形界面来构造查询,如图9所示。
图 9: 通过查询编辑器生成查询
在生成查询之后,在移到下一屏之前,点击“高级选项(Advanced Options)”按钮。在网站项目里,在默认 情形下,“生成插入,更新,删除语句”是唯一已被选中的选项。如果你在类库项目或Windows项目里运行这个 向导的话,“采用优化的并发控制(optimistic concurrency)”选项也会被选中。现在先别选“采用优化的并发 控制”这个选项。在以后的教程里我们会详细讨论优化的并发控制。
图 10: 只选“生成插入,更新和删除语句”这个选项
在核实高级选项后,按“下一步(Next)”按钮转到最后一屏。在这里,配置向导会问我们要给TableAdapter选择添加什么方法。填充数据有两种模式:
- 填充DataTable – 这个做法会生成一个方法,该方法接受一个DataTable的参数,基于查询的结果 填充这个DataTable。譬如,ADO.NET的DataAdapter类就是在它的
Fill()
方法中实现这个模式的 。 - 返回DataTable – 这个做法会生成一个方法,该方法会创建并填充一个DataTable,然后将 其作为方法的返回值。
你可以让TableAdapter实现其中一个模式或者同时实现两个模式。你也可以重新命名这里提供的这些方法。让 我们对两个复选框的选项不做改动,虽然我们在这些教程里只需要使用后面这个模式。同时,让我们把那个很 一般性的GetData
方法名改成GetProducts
。
这最后一个复选框,“生成DB直接方法(GenerateDBDirectMethods)”,如果选了的话,会为TableAdapter自动生 成Insert()
,Update()
,和Delete()
方法。如果你不选这个选项 的话,所有的更新都需要通过TableAdapter唯一的Update()
方法来实现,该方法接受一个强类型的DataSet,或者一个DataTable,或者单个DataRow,或者一个DataRow数组。(假如你 在图9所示的高级属性里把“生成添加,更新和删除语句”的选项去掉的话,这个复选框是不起作用的)。让我们 保留这个复选框的选项。
图 11: 把方法名字从 GetData
改成 GetProducts
按“完成”按钮结束向导。在向导关闭后,我们回到DataSet设计器中,它会显示我们刚创建的DataTable。你可 以看到Products
DataTable的字段列单(ProductID
, ProductName
等),还有ProductsTableAdapter
的Fill()
和GetProducts()
方法 。
图 12: Products
DataTable和ProductsTableAdapter
被添加到强类 型DataSet中
至此,我们生成了含有单一DataTable类(Northwind.Products
)的强类型DataSet以及一个含 有GetProducts()
方法的强类 型DataAdapter类(NorthwindTableAdapters.ProductsTableAdapter
)。通过这些对象可以用下 列编码来获取所有产品的列单:
C# | |
1 2 3 4 5 6 7 |
NorthwindTableAdapters.ProductsTableAdapter productsAdapter = new NorthwindTableAdapters.ProductsTableAdapter(); Northwind.ProductsDataTable products; products = productsAdapter.GetProducts(); foreach (Northwind.ProductsRow productRow in products) Response.Write("Product: " + productRow.ProductName + "<br />"); |
这段编码不要求我们写一行的跟数据访问有关的编码。我们不需要生成任何ADO.NET类的实例,我们不需要 指明任何连接字符串,任何SQL查询语句,或者任何存储过程。TableAdapter为我们提供了底层的数据访问编 码!
这个例子里的每个对象都是强类型的,允许Visual Studio提供IntelliSense帮助以及编译时类型检查。最棒 的是,从TableAdapter 返回的DataTable可以直接绑定到ASP.NET数据Web 控件上去,这样的控件包 括GridView,DetailsView,DropDownList,CheckBoxList,以及另外几个控件。下面这个例子示范只要 在Page_Load
事件处理函数里添加短短的三行编码就能将从GetProducts()
方法返 回的DataTable绑定到一个GridView上去。
AllProducts.aspx
ASP.NET | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="AllProducts.aspx.cs" Inherits="AllProducts" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>View All Products in a GridView</title> <link href="Styles.css" rel="stylesheet" type="text/css" /> </head> <body> <form id="form1" runat="server"> <div> <h1> All Products</h1> <p> <asp:GridView ID="GridView1" runat="server" CssClass="DataWebControlStyle"> <HeaderStyle CssClass="HeaderStyle" /> <AlternatingRowStyle CssClass="AlternatingRowStyle" /> </asp:GridView> </p> </div> </form> </body> </html> |
AllProducts.aspx.cs
C# | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
using System; using System.Data; using System.Configuration; using System.Collections; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; using NorthwindTableAdapters; public partial class AllProducts : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { ProductsTableAdapter productsAdapter = new ProductsTableAdapter(); GridView1.DataSource = productsAdapter.GetProducts(); GridView1.DataBind(); } } |
图 13: 显示在GridView里的产品列单
这个例子要求我们在ASP.NET网页的Page_Load事件处理函数里,写三行编码。在以后的教程里,我们将讨 论使用ObjectDataSource,用声明的方式来从DAL中获取数据。用ObjectDataSource的话,我们一行编码都不 用写,而且还能得到分页和排序支持呢!
第三步:给数据访问层添加参数化的方法
至此,ProductsTableAdapter只有一个方法,GetProducts()
,它返回数据库里的所有产品。能够操作所有的产品当然有用,但很多时候我们想要获取关于一个指定产品的信息,或者属于某个特 定分类的所有产品。要想给我们的数据访问层添加这样的功能,我们可以给TableAdapter添加参数化的方法。
让我们来添加一个GetProductsByCategoryID(categoryID)
方法。为给DAL添加新的 方法,让我们回到DataSet设计器,在ProductsTableAdapter
上按右鼠标,然后选择“添加查 询(Add Query)”。
图 14: 在TableAdapter上按右鼠标,选择“添加查询”
向导首先会问我们是否要通过一个ad-hoc SQL语句还是生成一个新存储过程或者使用现有存储过程来访问 数据库。让我们还是选择使用SQL 语句。接着,向导会问我们使用什么类型的SQL查询。因为我们想返回属于 指定分类的所有产品,我们需要写一个返回数据行的SELECT
语句。
图 15: 选择生成一个返回数据行的SELECT
语句
下一步是定义用于访问数据的SQL查询语句。因为我们只想返回属于指定分类的那些产品,我重 用GetProducts()
里的SELECT
语句,但添加了一个WHERE
子 句:WHERE CategoryID = @CategoryID
。其中的@CategoryID
参数 向TableAdapter配置向导表示我们正在生成的方法将需要一个对应类(即,可为null-nullable的整数)的输入 参数。
图 16: 输入一个只返回指定分类的产品的查询
在最后一步,我们可以选择使用何种数据访问模式,还可以定制生成的方法的名字。对应于Fill 模式,让我们把名字改成FillByCategoryID
,对返回DataTable模式的方法(GetX
方法),让我们来用GetProductsByCategoryID
这个名字。
图 17: 为TableAdapter的方法选择名字
在结束向导后,DataSet设计器包含了这些新的TableAdapter的方法。
图18: 通过分类来查询产品
花点时间用同样的手法添加一个GetProductByProductID(productID)
方法。
这些参数化的查询可以在DataSet设计器里直接测试。在TableAdapter中的方法上按右鼠标,然后选择“预 览数据(Preview Data)”。接着,输入对应参数的值,然后按“预览(Preview)”。
图19: 属于饮料(Beverages)类的那些产品列单
通过我们的DAL中的GetProductsByCategoryID(categoryID)
方法,我们就能设计一 个ASP.NET网页来显示属于指定分类的那些产品。下面这个例子显示了属于Beverages(饮 料)类(CategoryID
=1)的所有产品。
Beverages.aspx
ASP.NET | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Beverages.aspx.cs" Inherits="Beverages" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Untitled Page</title> <link href="Styles.css" rel="stylesheet" type="text/css" /> </head> <body> <form id="form1" runat="server"> <div> <h1>Beverages</h1> <p> <asp:GridView ID="GridView1" runat="server" CssClass="DataWebControlStyle"> <HeaderStyle CssClass="HeaderStyle" /> <AlternatingRowStyle CssClass="AlternatingRowStyle" /> </asp:GridView> </p> </div> </form> </body> </html> |
Beverages.aspx.cs
C# | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
using System; using System.Data; using System.Configuration; using System.Collections; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; using NorthwindTableAdapters; public partial class Beverages : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { ProductsTableAdapter productsAdapter = new ProductsTableAdapter(); GridView1.DataSource = productsAdapter.GetProductsByCategoryID(1); GridView1.DataBind(); } } |
图 20: 属于Beverages(饮料)类的所有产品显示
第四步:插入,更新和删除数据
常用的插入,更新和删除数据的模式有两种。第一种模式,我称之为DB直接模式,涉及的方法被调用时,会向数据库里发出一个INSERT
, 或UPDATE
,或DELETE
命令,这个命令只对单个数据库记录做操作。象这样的方法一般接受一系列对应于插入,更新或删除的值的标量参数(譬如整数,字符串,布尔值,日期时间等)。譬如,用这个模式来操作Products
表的话,删除方法会接受一个整数参数,代表所需要删除的记录的ProductID,而插入方法则会接受一个对应于ProductName
的字符串,对应 于UnitPrice
的decimal值,对应于UnitsOnStock
的整数等等。
图 21: 每个插入,更新,和删除请求都被立刻发送到数据库
另外一个模式,我称之为批更新模式,可以在一个方法调用里更新整个DataSet,或者整个DataTable,或 者一个DataRow集合。在这个模式里,开发人员在一个DataTable中删除,插入,修改DataRow,然后把这 些DataRow或整个DataTable传给一个更新方法。然后这个方法会轮循传入的DataRow们,通过DataRow的RowState属 性属性来决定这些DataRow是否被改动过,或是新记录,或是被删除的记录,然后为每个记录发出合适的 数据库命令。
图 22: 在Update 方法调用之后,所有的变动都与数据库同步了
在默认情形下,TableAdapter采用批更新模式,但也支持DB直接模式。因为我们在创建我们的TableAdapter时的高级选项中选择了“生成插入,更新,和删除语句” 这个选项,ProductsTableAdapter
包含了一个 Update()
方法,该方法实现了批 更新模式。具体地说,TableAdapter包含了一个Update()
方法,可以传入一个强类型 的DataSet,或者一个强类型的DataTable,或者一个和多个DataRow。假如你在一开始创建TableAdapter时的选项中没有清除“生成DB直接方法(GenerateDBDirectMethods)”复选框的话,DB直接模 式也会通过Insert()
,Update()
和Delete()
方法来实现。
这两种数据修改模式都使用 了TableAdapter的InsertCommand
,UpdateCommand
, 和DeleteCommand
属性来向数据库发出对应 的INSERT
,UPDATE
和DELETE
命令。你可以在DataSet设计器里点击TableAdapter,然后在属性窗口查看和改 动InsertCommand
,UpdateCommand
, 和DeleteCommand
属性。(确 认你选择了TableAdapter,并且ProductsTableAdapter
对象是属性窗口中下拉框里被选中的项)
图23: TableAdapter包含InsertCommand
,UpdateCommand
, 和DeleteCommand
等属性
想查看或改动这些数据库命令的属性的话,点击CommandText
子属性,这会启动对应的查询 生成器。
图 24: 在查询生成器里配置插入,更新,删除语句
下面的编码例子示范了如何使用批更新模式来把没被终止的,且库存等于或少于25个单元的产品的价格加 倍:
C# | |
1 2 3 4 5 6 7 8 9 10 11 12 |
NorthwindTableAdapters.ProductsTableAdapter productsAdapter = new NorthwindTableAdapters.ProductsTableAdapter(); // For each product, double its price if it is not discontinued and // there are 25 items in stock or less Northwind.ProductsDataTable products = productsAdapter.GetProducts(); foreach (Northwind.ProductsRow product in products) if (!product.Discontinued && product.UnitsInStock <= 25) product.UnitPrice *= 2; // Update the products productsAdapter.Update(products); |
下面的编码示范如何使用DB直接模式删除一个产品,更新一个产品,然后添加一个新的产品:
C# | |
1 2 3 4 5 6 7 8 9 10 11 12 |
NorthwindTableAdapters.ProductsTableAdapter productsAdapter = new NorthwindTableAdapters.ProductsTableAdapter(); // Delete the product with ProductID 3 productsAdapter.Delete(3); // Update Chai (ProductID of 1), setting the UnitsOnOrder to 15 productsAdapter.Update("Chai", 1, 1, "10 boxes x 20 bags", 18.0m, 39, 15, 10, false, 1); // Add a new product productsAdapter.Insert("New Product", 1, 1, "12 tins per carton", 14.95m, 15, 0, 10, false); |
创建自定义的插入,更新,删除方法
用DB直接法生成的Insert()
, Update()
,和Delete()
方法有时 候会感觉有点不方便,特别是当数据表有许多字段的时候。看一下前面这个编码例子,没有IntelliSense的帮 助的话,不是很清楚Products
表的哪个字段对 应Update()
和Insert()
方法中的哪个输入参数。有时候我们只要更新一到二个字 段或者需要一个自定义的Insert()
方法,这个方法需要返回刚插入的记录 的IDENTITY
(自增)的字段值。
要创建这样的自定义方法,回到DataSet设计器。在TableAdapter上按右鼠标,选择“添加查询”,然后回 到TableAdapter配置向导。在第二屏上,我们可以指明要生成的查询的类型。让我们生成一个添加新 的product(产品)记录,然后返回新添加记录的ProductID
值的方法。因此,选择生成一个插 入(INSERT
)型查询。
图25: 创建一个给Products
表添加新记录的方法
下一个屏显示InsertCommand
的CommandText
属性。在查询语句后面,增添一 个SELECT SCOPE_IDENTITY()
的查询,这查询将返回当前同一个操作范围内插 入IDENTITY
字段的最后那个identity 值。(详见技术文档中关 于SCOPE_IDENTITY()
的内容以及为什么你应该使用SCOPE_IDENTITY()而不是 @@IDENTITY)。确认在添加SELECT
语句前,你在INSERT
语句后面添一个分号 。
图26: 增添查询返回SCOPE_IDENTITY()
值
最后,把这个新方法命名为InsertProduct
。
图 27:放方法名字设成InsertProduct
当你返回DataSet设计器时,你将看到ProductsTableAdapter
多了一个新的方 法,InsertProduct
。如果对应Products
表的每个字段,这个新的方法没有对应的参数的话,非常可能的原因是,你忘了给INSERT
语句的结尾添加一个分号(semi-colon)。重新配 置InsertProduct
方法,确认在INSERT
和SELECT
语句间有个分号。
在默认情形下,插入方法调用的是非查询(non-query)方法,意即,他们只返回受影响的记录数。但是,我们想要让InsertProduct
方法返回一个查询返回的值,而不是受影响的记录数。这可以把InsertProduct
方法的ExecuteMode
属性改 成Scalar
(标量)来实现。
图 28:把ExecuteMode
属性改成Scalar
下面的编码示范如何使用这个新的InsertProduct
方法:
C# | |
1 2 3 4 5 6 7 |
NorthwindTableAdapters.ProductsTableAdapter productsAdapter = new NorthwindTableAdapters.ProductsTableAdapter(); // Add a new product int new_productID = Convert.ToInt32(productsAdapter.InsertProduct("New Product", 1, 1, "12 tins per carton", 14.95m, 10, 0, 10, false)); // On second thought, delete the product productsAdapter.Delete(new_productID); |
第五步:完成数据访问层
注意,ProductsTableAdapters
类从Products
表中返回的 是CategoryID
和SupplierID
的值,但并不包括Categories
表 的CategoryName
字段和Suppliers
表的CompanyName
字段,尽管当 我们显示产品信息时,这些很可能是我们想要显示的字段。我们可以扩充TableAdapter的起始方 法GetProducts()
来包含CategoryName
和CompanyName
字段的值, 这方法进而会更新强类型的DataTable来包括这些新的字段。
但这会造成一个问题,因为TableAdapter的插入,更新,删除数据的方法是基于这个起始方法的,幸运的是, 自动生成的插入,更新,删除方法并不会受SELECT
子句中的子查询的影响。如果我们注意把 对Categories
和Suppliers
的查询添加成子查询,而不是用JOIN
语 句的话,我们可以避免重做这些修改数据的方法。在ProductsTableAdapter
中的GetProducts()
方法上按右鼠标,选择“配置”,然后,把SELECT
子句改成:
SQL | |
1 2 3 4 5 6 7 |
SELECT ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued, (SELECT CategoryName FROM Categories WHERE Categories.CategoryID = Products.CategoryID) as CategoryName, (SELECT CompanyName FROM Suppliers WHERE Suppliers.SupplierID = Products.SupplierID) as SupplierName FROM Products |
图29: 更新GetProducts()
方法的SELECT
语句
GetProducts()
方法使用这个新查询语句之后,对应的DataTable将包含2个新字段,CategoryName
和SupplierName
。
图30: Products
DataTable多了2个新字段
花点时间把GetProductsByCategoryID(categoryID)
方法中的SELECT
子句也更新一下。
如果你使用JOIN
句法更新GetProducts()
中的SELECT
语句的话 ,DataSet设计器不能使用DB直接模式自动生成插入,更新,以及删除数据库记录的方法。你必须手工生成这 些方法,就象本教程早先时候我们对InsertProduct
方法的做法一样。此外,你必须手工提供 InsertCommand
,UpdateCommand
和DeleteCommand
属性值,假如你 想使用批更新模式的话。
添加其他的TableAdapter
到目前为止,我们只讨论了针对单个数据表的单个TableAdapter。但是,Northwind数据库里含有我们需要在 我们的web应用中使用的几个相关的表。一个强类型的DataSet可以包含多个相关的DataTable。因此,为了完 成我们的DAL,我们需要为这些我们将来要用到的数据表添加相应的DataTable。步骤如下,打开 DataSet设计 器,在设计器上按右鼠标,选择“添加/TableAdapter”。这会生成一个新的DataTable和TableAdapter,然后我 们早先讨论过的配置向导会指引你完成配置。
花上几分钟,创建对应于下列查询的TableAdapter及其方法。注意,ProductsTableAdapter
的查询中包含了用以获取每个产品的分类和供应商名字的子查询。另外,如果你是随着教程在做的话,你已经添加过ProductsTableAdapter
类 的GetProducts()
和GetProductsByCategoryID(categoryID)
方法了。
- ProductsTableAdapter
- GetProducts:
SELECT ProductID, ProductName, SupplierID, CategoryID,
QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder,
ReorderLevel, Discontinued, (SELECT CategoryName FROM
Categories WHERE Categories.CategoryID =
Products.ProductID) as CategoryName, (SELECT CompanyName
FROM Suppliers WHERE Suppliers.SupplierID =
Products.SupplierID) as SupplierName
FROM Products
- GetProductsByCategoryID:
SELECT ProductID, ProductName, SupplierID, CategoryID,
QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder,
ReorderLevel, Discontinued, (SELECT CategoryName FROM
Categories WHERE Categories.CategoryID =
Products.ProductID) as CategoryName,
(SELECT CompanyName FROM Suppliers WHERE
Suppliers.SupplierID = Products.SupplierID) as SupplierName
FROM Products
WHERE CategoryID = @CategoryID
- GetProductsBySupplierID
SELECT ProductID, ProductName, SupplierID, CategoryID,
QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder,
ReorderLevel, Discontinued,
(SELECT CategoryName FROM Categories WHERE
Categories.CategoryID = Products.ProductID)
as CategoryName, (SELECT CompanyName FROM Suppliers
WHERE Suppliers.SupplierID = Products.SupplierID)
as SupplierName
FROM Products
WHERE SupplierID = @SupplierID - GetProductByProductID
SELECT ProductID, ProductName, SupplierID, CategoryID,
QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder,
ReorderLevel, Discontinued, (SELECT CategoryName
FROM Categories WHERE Categories.CategoryID =
Products.ProductID) as CategoryName,
(SELECT CompanyName FROM Suppliers
WHERE Suppliers.SupplierID = Products.SupplierID)
as SupplierName
FROM Products
WHERE ProductID = @ProductID
- GetProducts:
- CategoriesTableAdapter
- GetCategories
SELECT CategoryID, CategoryName, Description
FROM Categories
- GetCategoryByCategoryID
SELECT CategoryID, CategoryName, Description
FROM Categories
WHERE CategoryID = @CategoryID
- GetCategories
- SuppliersTableAdapter
- GetSuppliers
SELECT SupplierID, CompanyName, Address, City,
Country, Phone
FROM Suppliers
- GetSuppliersByCountry
SELECT SupplierID, CompanyName, Address,
City, Country, Phone
FROM Suppliers
WHERE Country = @Country
- GetSupplierBySupplierID
SELECT SupplierID, CompanyName, Address,
City, Country, Phone
FROM Suppliers
WHERE SupplierID = @SupplierID
- GetSuppliers
- EmployeesTableAdapter
- GetEmployees
SELECT EmployeeID, LastName, FirstName,
Title, HireDate, ReportsTo, Country
FROM Employees
- GetEmployeesByManager
SELECT EmployeeID, LastName, FirstName,
Title, HireDate, ReportsTo, Country
FROM Employees
WHERE ReportsTo = @ManagerID
- GetEmployeeByEmployeeID
SELECT EmployeeID, LastName, FirstName,
Title, HireDate, ReportsTo, Country
FROM Employees
WHERE EmployeeID = @EmployeeID
- GetEmployees
图31:添加了四个TableAdapter后的DataSet设计器
给DAL添加定制编码
添加到强类型DataSet中的TableAdapter和DataTable是在一个XML Schema定义文 件(Northwind.xsd
)中定义的。你可以在解决方案资源管理器里在Northwind.xsd
文件上按右鼠标,选择“查看编码(View Code)”,打开这个Schema文件来查看其中内容。
图32:Northwinds强类型DataSet的XML Schema定义文件
这个schema信息在设计时编译之后会被翻译成C#或Visual Basic 编码,或者如果有必要的话,会在运行时 翻译,然后你就能在调试器里单步遍历执行。想查看这些自动生成的编码的话,在类视图里,展 开TableAdapter 类或者强类型的DataSet 类。如果在屏幕上看不到类视图的话,在“查看”(View)菜单里选择“ 类视图”,或者按键组合Ctrl+Shift+C。在类视图里,你能看到强类型的DataSet类和TableAdapter类的属性,方法和事件。想看某个特定的方法的编码话,在类视图双击对应方法的名字或者在方法上按右鼠标,选 择“移至定义区(Go To Definition)”。
图33:在类视图里选择“移至定义区(Go To Definition)”,查看自动生成的编码
虽然自动生成的编码省时省力,但这样的编码往往是非常通用化的(generic),为满足一个应用程序特有的需 求需要做些定制。但扩展自动生成的编码的风险在于,如果生成这些编码的工具决定该是重新生成这些编码的 时候了,则会把你定制的编码冲掉。使用.NET 2.0中的一个新的部分(partial)类的概念,很容易将一个类的 定义分写在几个文件里。这允许我们给自动生成的类添加我们自己的方法,属性,和事件,而不用担心Visual Studio会冲掉我们的定制编码。
为示范如何定制DAL起见,让我们来给SuppliersRow 添加一个GetProducts()
方法。这 个SuppliersRow
类代表了Suppliers
表的个别记录,每个供应商(supplier)可以 提供0个到多个产品,所以GetProducts()
将返回指定的供应商的这些产品。做法如 下,在App_Code
文件夹里添加一个新的类文件,将其命名为SuppliersRow.cs
, 然后在其中添加下列编码:
C# | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
using System; using System.Data; using NorthwindTableAdapters; public partial class Northwind { public partial class SuppliersRow { public Northwind.ProductsDataTable GetProducts() { ProductsTableAdapter productsAdapter = new ProductsTableAdapter(); return productsAdapter.GetProductsBySupplierID(this.SupplierID); } } } |
这个部分(partial)类指示编译器在编译Northwind.SuppliersRow
类时,应该包含我们刚定义的这个GetProducts()
方法。如果你编译你的项目,然后返回类视图,你就会看到GetProducts()
已被列为Northwind.SuppliersRow
的一个方法。
图34: GetProducts()
方法成为Northwind.SuppliersRow
类的一部 分
GetProducts()
方法现在就能用来枚举一个指定供应商的产品列单,如下列编码所示:
C# | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
NorthwindTableAdapters.SuppliersTableAdapter suppliersAdapter = new NorthwindTableAdapters.SuppliersTableAdapter(); // Get all of the suppliers Northwind.SuppliersDataTable suppliers = suppliersAdapter.GetSuppliers(); // Enumerate the suppliers foreach (Northwind.SuppliersRow supplier in suppliers) { Response.Write("Supplier: " + supplier.CompanyName); Response.Write("<ul>"); // List the products for this supplier Northwind.ProductsDataTable products = supplier.GetProducts(); foreach (Northwind.ProductsRow product in products) Response.Write("<li>" + product.ProductName + "</li>"); Response.Write("</ul><p> </p>"); } |
This data can also be displayed in any of ASP.NET's data Web controls. The following page uses a GridView control with two fields:数据也可以在任何一种ASP.NET的Web控件中显示。下面这个网页 使用了含有2个字段的GridView 控件:
- 一个BoundField用以显示每个供应商的名字,
- 另一个TemplateField,包含了一个BulletedList控件,用来绑定针对每个供应商调用 的
GetProducts()
方法返回的结果
我们将在以后的教程里讨论怎样来显示这样的主/从(master-detail)报表。在这里,这个例子的目的是用 来示范如何使用添加到Northwind.SuppliersRow
类中的自定义的方法的。
SuppliersAndProducts.aspx
ASP.NET | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="SuppliersAndProducts.aspx.cs" Inherits="SuppliersAndProducts" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Untitled Page</title> <link href="Styles.css" rel="stylesheet" type="text/css" /> </head> <body> <form id="form1" runat="server"> <div> <h1> Suppliers and Their Products</h1> <p> <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" CssClass="DataWebControlStyle"> <HeaderStyle CssClass="HeaderStyle" /> <AlternatingRowStyle CssClass="AlternatingRowStyle" /> <Columns> <asp:BoundField DataField="CompanyName" HeaderText="Supplier" /> <asp:TemplateField HeaderText="Products"> <ItemTemplate> <asp:BulletedList ID="BulletedList1" runat="server" DataSource="<%# ((Northwind.SuppliersRow)((System.Data.DataRowView) Container.DataItem).Row).GetProducts() %>" DataTextField="ProductName"> </asp:BulletedList> </ItemTemplate> </asp:TemplateField> </Columns> </asp:GridView> </p> </div> </form> </body> </html> |
SuppliersAndProducts.aspx.cs
C# | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
using System; using System.Data; using System.Configuration; using System.Collections; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; using NorthwindTableAdapters; public partial class SuppliersAndProducts : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { SuppliersTableAdapter suppliersAdapter = new SuppliersTableAdapter(); GridView1.DataSource = suppliersAdapter.GetSuppliers(); GridView1.DataBind(); } } |
图 35: 供应商的公司名字列在左栏,他们的产品列在右栏
总结
构造web应用时,创建DAL应该是你最先做的步骤之一,应该在你开始创建表现层之前进行。使用Visual Studio的话,创建基于强类型DataSet的DAL是个可以不写一行编码,在10到15分钟内就可完成的任务。以后的 教程将建立在这个DAL基础之上。在下一个教程里,我们将定义一堆业务规则,然后看一下如何在一个分开的 业务逻辑层里实现这些规则。
祝编程快乐!
附加读物
想了解跟在本教程中讨论过的技术内容相关的更多的信息,请参考下列资源:
- Building a DAL using Strongly Typed TableAdapters and DataTables in VS 2005 and ASP.NET 2.0
- Designing Data Tier Components and Passing Data Through Tiers
- Build a Data Access Layer with the Visual Studio 2005 DataSet Designer
- Encrypting Configuration Information in ASP.NET 2.0 Applications
- TableAdapter Overview
- Working with a Typed DataSet
- Using Strongly-Typed Data Access in Visual Studio 2005 and ASP.NET 2.0
- How to Extend TableAdapter Methods
- Retrieving Scalar Data from a Stored Procedure
作者简介
Scott Mitchell,著有六本ASP/ASP.NET方面的书,是4GuysFromRolla.com的创始人,自1998年以来一直应用 微软Web技术。Scott是个独立的技术咨询顾问,培训师,作家,最近完成了将由Sams出版社出版的新作,24小时内精通ASP.NET 2.0。他的联系电邮为mitchell@4guysfromrolla.com,也可以通过他的博客http://ScottOnWriting.NET与他联系。