在ASP.NET 2.0中操作数据:在SqlDataSource中使用参数化查询
英文原版 | 本教程的代码(C#) | 翻译目录 | 原文目录
导言
在前一节教程中,我们看到了如何使用SqlDataSource控件直接从数据库中获取数据。通过“配置数据源”向导,我们选择一个特定的数据库,然后就可以:从一个表或视图中选择一些列;输入一个自定义SQL语句;使用一个存储过程。不管你是手工输入SQL语句还是在向导页中选择一堆列,反正最终都是给SqlDataSource控件的SelectCommand属性赋上一个SELECT语句,在SqlDataSource的Select()方法被调用的时候,执行的也就是这个语句(不管是通过编程方式调用还是数据Web控件自动调用,都是这样)。
前一节教程中的示例所使用的SELECT语句缺少WHERE子句。在SELECT语句中,WHERE子句可以用来限制返回的记录。比如说,我们想显示成本大于50美刀的产品的名字,相应的查询语句应该是这样的:
2FROM Products
3WHERE UnitPrice > 50.00
在实际应用中,WHERE子句所使用的值都是取决于某个外部因素的,比如QueryString、Session或者用户在页面上某个控件中的输入等等。通常,我们通过使用参数来对这些输入进行指定。在Microsoft SQL Server中,参数都是@parameterName这样的形式,比如:
2FROM Products
3WHERE UnitPrice > @Price
对于SELECT、INSERT、UPDATE以及DELETE语句,SqlDataSource都支持其参数化查询。此外,这些参数不仅可以自动地从各种各样的源中获取(比如QueryString、Session、页面上的控件等等),还可以通过编程的方式进行赋值。本教程中,我们将看到如何定义参数化查询,以及如何通过声明或编程的方式指定参数。
注意:在上一节教程中我们对ObjectDataSource(前面46节中都是将其作为我们的工具的)与SqlDataSource进行了比较,并注意到他们在概念上还是蛮相似的。这些相似点也扩展到了参数方面。ObjectDataSource的参数映射到了业务逻辑层中的方法的输入参数上,而SqlDataSource的参数则是直接在SQL语句中定义的。它们的Select()、Insert()、Update()以及Delete()方法都拥有一堆参数,而且这些参数都可以从预定义源(比如QueryString、Session等等)中获取或通过编程的方式赋值。
创建一个参数化查询
SqlDataSource控件的“配置数据源”向导提供了三种用以定义获取数据库记录的Command的方式:
· 从一个已经存在的表或视图中选择一些列
· 输入一个自定义SQL语句
· 选择一个存储过程
如果是从一个已经存在的表或视图中选择一些列,那么WHERE子句的参数就必须通过“添加WHERE子句”对话框来进行指定。如果是创建一个自定义SQL语句,你可以直接向WHERE子句中添加参数(记得要使用这样的格式,@parameterName)。存储过程是由一个或多个SQL语句组成的,而且这些语句都是可以参数化的。SQL语句的参数必须像存储过程的输入参数一样被传进去。
由于创建一个参数化的查询取决于SqlDataSource的SelectCommand是如何被指定的,所以让我们先来看看所有的这三种方式。首先,打开SqlDataSource文件夹中的ParameterizedQueries.aspx,从工具箱中拖一个SqlDataSource控件到设计器中,并将其ID设置为Products25BucksAndUnderDataSource。然后,在它的智能标签中点击“配置数据源”链接。选择数据库(使用NORTHWINDConnectionString)并点击“下一步”。
第一步:在选择表或视图的列时添加一个WHERE子句
在通过SqlDataSource选择数据库中的哪些列需要返回时,“配置数据源”向导允许我们简单的从表或视图中选取希望返回的列(见图一)。这样将自动生成一个SELECT语句,当SqlDataSource的Select()方法被调用时,发送到数据库的就是它了。就像我们在上一节教程中做的那样,从下拉列表中选择Products表,并勾上ProductID、ProductName以及UnitPrice这三列。
图一:从表或视图中选取需要返回的列
要在一个SELECT语句中加上一个WHERE子句,点击一下“WHERE”按钮,这会弹出一个“添加WHETE子句”对话框(见图二)。要添加一个用于限制SELECT查询所返回的结果的参数,首先需要选择用来筛选数据的列。然后选择一个用于筛选的操作符(=、<、<=、>、……)。最后,选择这个参数的值的来源,比如说它来自QueryString或Session。配置好参数之后,点击“添加”按钮就可以将它加入到SELECT语句中了。
比如说我们现在只想要结果中那些UnitPrice小于等于25美刀的东东。那么我们需要在“列”下拉列表中选中UnitPrice,并在“操作符” 下拉列表中选中“<=”。如果需要使用一个硬编码的参数值(比如25美刀),或者如果参数值需要通过编程的方式进行指定,就在“来源” 下拉列表中选中“无”。然后在“值”输入框中输入硬编码参数值(比如25美刀),并单击“添加”按钮以结束这个过程。
图二:在“添加WHERE子句”对话框中限制返回的结果
添加了参数之后,点击“确定”以返回“配置数据源”向导。现在,显示在向导底部的SELECT语句将会跟上一个带有名为@UnitPrice的参数的WHERE子句:
2FROM [Products]
3WHERE ([UnitPrice] <= @UnitPrice)
注意:如果你在“添加WHERE子句”对话框中为WHERE子句指定了多个条件,向导将使用AND操作符将它们连接起来。如果你需要在WHERE子句中包含一个OR操作符(比如WHERE UnitPrice <= @UnitPrice OR Discontinued = 1),那么就只能通过自定义SQL语句页来生成这个SELECT语句。
结束SqlDataSource的配置(点击“下一步”,然后点击“结束”),并检查其声明标记代码。现在的标记代码包含了一个<SelectParameters>集合,它详细的说明了SelectCommand中的参数的来源。
2 ConnectionString="<%$ ConnectionStrings:NORTHWNDConnectionString %>"
3 SelectCommand=
4 "SELECT [ProductID], [ProductName], [UnitPrice]
5 FROM [Products] WHERE ([UnitPrice] <= @UnitPrice)">
6 <SelectParameters>
7 <asp:Parameter DefaultValue="25.00" Name="UnitPrice" Type="Decimal" />
8 </SelectParameters>
9</asp:SqlDataSource>
在SqlDataSource的Select()方法被调用时,UnitPrice参数值(25美刀)将在SelectCommand被发送到服务器之前赋值给参数@UnitPrice。从Products表所返回的结果将仅仅包含单价小于等于25美刀的那些产品。来做一个确认,向页面添加一个GridView,绑定这个数据源,然后在浏览器中看看这个页面。如图三所示,你将只会看到单价小于等于25美刀的产品列在那里。
图三:只有单价小于等于25美刀的产品才被显示出来
第二步:给自定义SQL语句添加参数
添加一个自定义SQL语句时,你可以直接输入一个WHERE子句,也可以在“查询生成器”的“筛选器”那里指定一个值。为了说明这个操作,我们来试试在GridView中显示单价小于某一个起点的那些产品。首先,向ParameterizedQueries.aspx的页面上添加一个TextBox以从用户那里获取这个起点的值,并将其ID设置为MaxPrice。再添加一个Button服务器端控件,将其Text属性设置为“Display Matching Products”。
然后,拖一个GridView到页面上,并通过其智能标签选择创建一个新的名为ProductsFilteredByPriceDataSource的SqlDataSource。在“配置数据源”向导中,选择“指定一个自定义SQL语句或存储过程”(见图四),并输入如下代码:
2FROM Products
3WHERE UnitPrice <= @MaximumPrice
输入完了之后(可以手工添加,也可以通过“查询生成器”),点击“下一步”。
图四:只返回小于等于某个参数值的那些产品
由于查询含有参数,向导的下一页要求我们指定这些参数的来源。在“参数来源”下拉列表中选择“控件”,并在“ControlID” 下拉列表中选择“MaxPrice”(就是那个TextBox的ID)。你还可以输入一个默认值(可选的),在用户没有输入任何东西到MaxPrice TextBox中时就用得上了。暂时先不要输入这个默认值。
图五:名为MaxPrice的TextBox的Text属性被用作参数的来源
点击“下一步”,再点击“完成”,以结束“配置数据源”向导。GridView、TextBox、Button以及SqlDataSource的声明标记代码如下所示:
2$<asp:TextBox ID="MaxPrice" runat="server" Columns="5" />
3
4<asp:Button ID="DisplayProductsLessThanButton" runat="server"
5 Text="Display Matching Products" />
6
7<asp:GridView ID="GridView2" runat="server" AutoGenerateColumns="False"
8 DataSourceID="ProductsFilteredByPriceDataSource" EnableViewState="False">
9 <Columns>
10 <asp:BoundField DataField="ProductName" HeaderText="Product"
11 SortExpression="ProductName" />
12 <asp:BoundField DataField="UnitPrice" HeaderText="Price"
13 HtmlEncode="False" DataFormatString="{0:c}"
14 SortExpression="UnitPrice" />
15 </Columns>
16</asp:GridView>
17
18<asp:SqlDataSource ID="ProductsFilteredByPriceDataSource" runat="server"
19 ConnectionString="<%$ ConnectionStrings:NORTHWNDConnectionString %>"
20 SelectCommand=
21 "SELECT ProductName, UnitPrice
22
23 FROM Products
24WHERE UnitPrice <= @MaximumPrice">
25 <SelectParameters>
26 <asp:ControlParameter ControlID="MaxPrice" Name="MaximumPrice"
27 PropertyName="Text" />
28 </SelectParameters>
29</asp:SqlDataSource>
注意在SqlDataSource的<SelectParameters>节中的参数是一个ControlParameter,它拥有一些附加的属性,如ControlID和PropertyName。在SqlDataSource的Select()方法被调用时,ControlParameter从指定控件的指定属性中获取值,并将其赋给SelectCommand中相应的参数。在本例中,MaxPrice的Text属性将被用作@MaxPrice参数值。
花一点时间在浏览器中看看这个页面。当第一次访问这个页面,或MaxPrice TextBox缺少一个值的时候,GridView中将不会显示任何内容。
图六:当MaxPrice TextBox 是空的时,没有任何记录被显示出来
没有显示任何产品记录的原因是,默认情况下,参数值的空字符串将被转换为数据库NULL值。由于[UnitPrice] <= NULL永远为False,所以就不会返回任何记录了。
在TextBox中输入一个值,比如5.00,然后点击“Display Matching Products”按钮。通过回发,SqlDataSource告诉GridView它有一个参数来源发生了变化。因此,GridView重新绑定SqlDataSource,并把那些单价小于等于5美刀的产品显示出来。
图七:单价小于等于5美刀的产品被显示出来了
初始时显示所有产品
当页面第一次加载时,我们可能希望显示所有产品而不是啥也不显示。当MaxPrice TextBox是空的的时候,要显示所有的产品,一个办法就是将这个参数的值设得超级无敌大,比如1000000,因为Northwind不太可能有哪个产品的单价是超过1000000美刀的。不过,这个办法显然是有些目光短浅的,在某些情况下可能根本就不奏效。
在前面的教程中(声明参数以及使用DropDownList过滤的主/从报表),我们也面对了这样一个相似的问题。那时我们的办法就是把这样的逻辑放到业务逻辑层中。实际上,BLL检查了输入的值,如果是NULL或别的什么预先定义好的保留值,调用将被路由到返回所有记录的DAL方法。如果输入的值是一个普通的过滤值,则使用这个值去调用可执行带有参数化WHERE子句的SQL语句的DAL方法。
不幸的是,在使用SqlDataSource的时候我们绕过了这个架构。所以,我们需要自定义SQL语句,以便在@MaximumPrice参数值为NULL或某个保留值的时候可以获取到所有记录。为了做这么一个练习,我们就让@MaximumPrice参数值为-1.0的时候返回所有记录(这里,-1.0就是一个保留值,因为没有哪个产品的单价会是负数)。我们可以使用下面的SQL语句来达到这个目的:
2FROM Products
3WHERE UnitPrice <= @MaximumPrice OR @MaximumPrice = -1.0
这个WHERE子句在@MaximumPrice参数值为-1.0时将返回所有记录。如果参数值不是-1.0,则只有那些单价小于等于@MaximumPrice参数值的产品才会被返回。通过将@MaximumPrice参数的默认值设置为-1.0,在页面第一次加载时(或当MaxPrice TextBox没有内容时),@MaximumPrice将使用-1.0,这样所有的记录就会被返回了。
图八:现在,当MaxPrice TextBox里面什么也没有的时候将会显示所有产品
使用这个方法我们需要注意两个问题。第一,我们要知道,参数的数据类型是通过它在SQL语句中的作用来进行推断的。如果你把“@MaximumPrice = -1.0”改成“@MaximumPrice = -1”,运行时将把参数当作整型来对待。这时如果你试图在MaxPrice TextBox中输入一个小数(比如“5.00”),这时将会引发一个错误,因为它不能将“5.00”转换成整型。要修正这个问题,你可以确认在WHERE子句中的确使用的是“@MaximumPrice = -1.0”,也可以将ControlParameter对象的Type属性设置为“Decimal”(后者也许更好一些)。
第二,由于在WHERE子句中加入了“OR @MaximumPrice = -1.0”,所以查询引擎将无法使用UnitPrice上的索引(我们就假装那里有一个索引吧^_^),这样将引发一个全表扫描(译者注:就是Table Scan)。如果Products表中有很多记录的话,这会明显的影响到性能。更好的解决办法是,把这个逻辑放到存储过程中,使用一个IF语句,在需要返回所有记录的时候,就使用一个不带WHERE子句的SELECT语句,需要过滤的时候,就使用一个仅带有UnitPrice约束的WHERE子句,这样就可以用上索引了。
第三步:创建并使用参数化存储过程
存储过程可以拥有一组输入参数,这些参数可以用在存储过程内部的SQL语句中。要让SqlDataSource使用带输入参数的存储过程,指定参数值的方法可参照使用SQL语句时的方法。
为了演示在SqlDataSource中使用存储过程,我们先在Northwind数据库中创建一个新的名为GetProductsByCategory的存储过程,它接受一个名为@CategoryID的参数,返回CategoryID值为@CategoryID的产品的所有列。要创建一个存储过程,先去到服务器浏览器中,并找到NORTHWND.MDF数据库。(如果你看不到“服务器浏览器”,就到“视图”菜单中选择“服务器浏览器”选项即可。)
在NORTHWND.MDF数据库中,右键单击“存储过程”文件夹,选择“添加新存储过程”,并输入以下代码:
2(
3 @CategoryID int
4)
5AS
6
7SELECT *
8FROM Products
9WHERE CategoryID = @CategoryID
点击“保存”按钮(或直接按Ctrl+S)以保存此存储过程。你可以在“存储过程”文件夹中右键单击这个存储过程,并选择“执行”以进行测试。它会提示要你提供这个存储过程的参数(这里是@CategoryID),然后相应的记录就会显示在“输入”窗口中了。(译者注:这里还不错,虽然类似Oracle的那个plus,不过VS在排版方面好看多了。)
图九:存储过程GetProductsByCategory以@CategoryID值为1执行后的结果
我们使用这个存储过程来在GridView中显示所有分类为饮料(译者注:Beverages)的产品。先往页面上添加一个新的GridView,并绑定一个新的名为BeverageProductsDataSource的SqlDataSource。接着,我们来到“指定一个自定义SQL语句或存储过程”页,选择“存储过程”单选框,并在下拉列表中选中GetProductsByCategory存储过程。
图十:在下拉列表中选中GetProductsByCategory存储过程
由于这个存储过程接受一个输入参数(@CategoryID),点击“下一步”将提示我们需要指定这个参数的值的来源。饮料(译者注:Beverages)的CategoryID为1,所以我们在“默认值”输入框中填入1,并确保“参数来源”下拉列表选中了“无”。
图十一:使用1作为硬编码的值来返回饮料类的产品
如下面的声明标记代码所展示的那样,当使用一个存储过程时,SqlDataSource的SelectCommand属性被设置成该存储过程的名字,且SelectCommandType属性被设置为StoredProcedure,这样就表明SelectCommand是一个存储过程名字而不是一个SQL语句。
2 ConnectionString="<%$ ConnectionStrings:NORTHWNDConnectionString %>"
3 SelectCommand="GetProductsByCategory" SelectCommandType="StoredProcedure">
4 <SelectParameters>
5 <asp:Parameter DefaultValue="1" Name="CategoryID" Type="Int32" />
6 </SelectParameters>
7</asp:SqlDataSource>
在浏览器中试试这个页面。只有那些属于饮料类的产品显示出来了,由于存储过程GetProductsByCategory返回了Products表的所有列,所以这里也列出了各产品的所有字段。当然,我们也可以在GridView中,通过其“编辑列”(译者注:Edit Columns)来限制或自定义需要显示的字段。
图十二:显示了所有的饮料
第四步:通过编程的方式调用SqlDataSource的Select()方法
在上一节教程以及本节教程前面的所有例子中,我们都是直接将SqlDataSource控件绑定到GridView上的。其实,SqlDataSource中的数据可以通过编程的方式在代码中进行访问和枚举。在你需要将数据查出来做某些处理而不是仅仅只是把它们显示出来的时候,这将特别有用。你可以将诸如连接数据库、指定Commond以及获取数据等这些郁闷乏味的事情交给SqlDataSource去处理,而不用自己编写所有的这些刻板的ADO.NET代码。
为了说明如何通过编程的方式使用SqlDataSource,想象一下,你的BOSS叫你做一个可随机显示一个分类的名称及其相关的产品的网页(译者注:为了不引起各位读者的误解,说明一下,这个例子跟boss一点关系都没有,估计作者也是为了好玩吧^_^)。也就是说,当一个用户访问到了这个页面,我们需要随机的从Categories表中选取一个分类,显示其名称,并将所有属于这个分类的产品都列出来。
要达到这个目的,我们需要两个SqlDataSource控件,一个用来从Categories表中随机选取一个分类,另一个用来获取这个分类的产品。我们将在这个步骤中创建第一个SqlDataSource,第五步中将详细讲解如何创建第二个SqlDataSource。
首先,我们需要向ParameterizedQueries.aspx中添加一个SqlDataSource,将其ID设置为RandomCategoryDataSource,并配置它以使其使用下面这个SQL语句:
2FROM Categories
3ORDER BY NEWID()
“ORDER BY NEWID()”将返回随机排序的记录(参见使用NEWID()进行随机排序)。“SELECT TOP 1”将返回记录集中的第一项。把它们放到一起,即返回一个随机分类的CategoryID和CategoryName。
要显示分类的CategoryName,需要往页面上添加一个Label控件,将其ID属性设置为CategoryNameLabel,并清除其Text属性。要通过编程的方式从SqlDataSource控件中获取数据,我们需要调用其Select()方法。Select()方法需要一个DataSourceSelectArguments类型的输入参数,它将指定数据在返回之前应该被如何预处理(译者注:原文为messaged,真的不知道该怎么翻译,DataSourceSelectArguments其实主要还是给诸如GridView之类的控件使用的,具体的使用方法请参看MSDN)。它可以包含一些排序或分页处理的说明,当数据Web控件在对SqlDataSource控件的数据进行排序或分页时就可以使用了。不过在我们的例子中,我们不需要数据在返回之前被进行任何的处理,所以我们传一个DataSourceSelectArguments.Empty过去就可以了。
Select()方法将返回一个实现了IEnumerable的对象。具体的返回类型取决于SqlDataSource控件的DataSourceMode属性。上一节中已经讨论过了,它可以设置为DataSet或DataReader。如果设置为DataSet,Select()将返回一个DataView对象;如果设置为DataReader,则返回一个实现了IDataReader的对象。由于RandomCategoryDataSource的DataSourceMode设置成了DataSet(这是默认值),我们将会得到一个DataView对象。
下面的代码演示了如何从RandomCategoryDataSource中以DataView的形式获取记录,还说明了如何从DataView的第一行获取CategoryName列的值:
2{
3 // 从SqlDataSource中以DataView的形式获取数据
4 DataView randomCategoryView =
5 (DataView)RandomCategoryDataSource.Select(DataSourceSelectArguments.Empty);
6
7 if (randomCategoryView.Count > 0)
8 {
9 // 将CategoryName的值赋给Label
10 CategoryNameLabel.Text =
11 string.Format("Here are Products in the {0} Category",
12 randomCategoryView[0]["CategoryName"].ToString());
13 }
14}
randomCategoryView[0]返回DataView中的第一个DataRowView,而randomCategoryView[0]["CategoryName"]返回第一行中的CategoryName列的值。注意DataView是弱类型的。要引用一个特定的列的值,我们需要把这个列的列名以字符串的形式传过去(这里就是“CategoryName”)。图十三向我们展示了当访问这个页面时,这个消息将会显示在CategoryNameLabel中。当然了,每次访问这个页面时,具体显示出来的分类名都是通过RandomCategoryDataSource随机选出来的(包括postback)。
图十三:随机选择的分类的名称显示出来了
注意:如果SqlDataSource控件的DataSourceMode属性被设置为DataReader的话,从Select()方法返回的值需要转换成一个IDataReader。要从第一行中读取CategoryName列的值的话,我们需要使用这样的代码:
2{
3 string categoryName = randomCategoryReader["CategoryName"].ToString();
4
5}
由于SqlDataSource已经随机的选取了一个分类,所以我们现在可以准备添加一个用以显示其产品的GridView的了。
注意:其实我们可以通过向页面添加一个FormView或是一个DetailsView,并绑定这个SqlDataSource以显示这个分类的名称,而不是通过Label。使用Label的原因是,我们可以以此来说明如何通过编程的方式来调用SqlDataSource的Select()方法并在代码中使用其返回的结果。
第五步:通过编程的方式赋予参数值
本节教程前面的所有例子都是使用一个硬编码的参数值或一个来自预定义参数源的值(比如QueryString、页面上的Web控件等等)。不过,SqlDataSource控件的参数还可以通过编程的方式来设定。为了完成现在的这个例子,我们需要一个可以返回指定分类的所有产品的SqlDataSource。这个SqlDataSource将含有一个CategoryID参数,其值需要在Page_Load事件处理方法中根据RandomCategoryDataSource返回的CategoryID值来进行设定。
首先,我们向页面添加一个GridView,并绑定一个新的名为ProductsByCategoryDataSource的SqlDataSource。就像我们在第三步中做的那样,配置这个SqlDataSource以使其可以调用存储过程GetProductsByCategory。确保“参数来源”下拉列表设置为“无”,也不要输入默认值,因为我们要通过编程来设置这个默认值。
图十四:不要指定“参数来源”和“默认值”
结束了SqlDataSource向导之后,声明标记代码应该像下面这个样子:
2 ConnectionString="<%$ ConnectionStrings:NORTHWNDConnectionString %>"
3 SelectCommand="GetProductsByCategory" SelectCommandType="StoredProcedure">
4 <SelectParameters>
5 <asp:Parameter Name="CategoryID" Type="Int32" />
6 </SelectParameters>
7</asp:SqlDataSource>
我们可以在Page_Load事件处理方法中通过编程的方式给CategoryID参数设定一个默认值:
2ProductsByCategoryDataSource.SelectParameters["CategoryID"].DefaultValue =
3 randomCategoryView[0]["CategoryID"].ToString();
做完了这些工作之后,页面上就包含了一个可以显示某随机分类的所有产品的GridView了。
图十五:最终的完整页面(译者注:原文错了,原文这里是“Do Not Specify a Parameter Source or Default Value”)
总结
SqlDataSource使页面开发人员可以定义一个参数化查询,其参数值可以是硬编码的,也可以是来自某个预定义参数源的,还可以是通过编程的方式赋值的。在本节教程中,我们看到了如何通过“配置数据源”向导创建一个参数化查询(包括SQL语句以及存储过程)。我们还看到了通过硬编码、作为参数源的Web控件以及编程的方式来指定参数值。
跟ObjectDataSource一样,SqlDataSource也提供了修改其数据的能力。在下一节教程中,我们将看到如何在SqlDataSource中定义INSERT、UPDATE以及DELETE语句。一旦添加了这些语句,我们就可以使用GridView、DetailsView以及FormView控件内建的插入、编辑以及删除功能了。
编程愉快!
关于作者
Scott Mitchell,著有六本ASP/ASP.NET方面的书,是4GuysFromRolla.com的创始人,自1998年以来一直应用微软Web技术。Scott是个独立的技术咨询顾问,培训师,作家,最近完成了将由Sams出版社出版的新作,24小时内精通ASP.NET 2.0。他的联系电邮为mitchell@4guysfromrolla.com,也可以通过他的博客http://ScottOnWriting.NET与他联系。