本文英文原版以及代码下载:
http://www.asp.net/learn/dataaccess/tutorial48cs.aspx?tabid=63
第48章 使用SqlDataSource控件查询数据
导言:
在上一章里,我们探讨了怎样用SqlDataSource控件直接从数据库检索数据。通过设置数据源向导,我们可以选择要访问的数据库,然后要么从指定的表或视图,要么从自定义的SQL语句或存储过程来返回需要的记录。不管用那种方式,SqlDataSource控件的SelectCommand 属性都被赋值为一个ad-hoc SQL SELECT命令(SELECT statement)。当SqlDataSource控件的Select()方法被调用时(无论数据Web控件是自动还是通过编程设置来调用Select()方法),就将执行这个SELECT命令。
上一篇教程里使用的SQL SELECT命令里缺少WHERE字句,在SELECT命令里WHERE字句用来限制查询结果范围。比如,我们显示那些价格高于50的产品名称时,可以用如下所示的查询语句:
SELECT ProductName
FROM Products
WHERE UnitPrice > 50.00
特别的,WHERE字句中的参数值来源于外部(some external source),比如:查询字符串的值,一个session变量,或用户从Web控件输入的值。一般来说,通过使用参数的方式来传递这些值。在Microsoft SQL Server里,我们这样表示参数@parameterName,例如:
SELECT ProductName
FROM Products
WHERE UnitPrice > @Price
SqlDataSource控件支持带参数的查询,包括SELECT,INSERT, UPDATE和DELETE 命令而且,参数的值可以来源于一个查询字符串,session状态,页面上的控件等等,甚至可以通过编程来赋值。这一章我们探讨怎样通过显式地赋值和通过编程来赋值2种方式来构造带参数的查询。
注意:
在上一章我们将SqlDataSource控件和ObjectDataSource控件作了比较。除了在概念上类似外,它们在参数的使用上也很相像。不过SqlDataSource控件的参数与位于业务逻辑层的相关方法的对应参数相匹配,而ObjectDataSource控件的参数由SQL查询直接定义。此2种控件的Select(),Insert(), Update()和Delete()方法都有对应的参数集,此外这2种控件还可以包含通过编程传入的,或由查询字符串,session变量等预定义源(pre-defined sources)传入的参数值。
构造一个带参数的查询
SqlDataSource控件的数据源设置向导提供了三种检索数据的方式:
通过返回表或视图的相关列
通过使用自定义SQL查询
通过使用存储过程
当使用通过返回表或视图的相关列的方法的时候,需要在“添加WHERE字句”对话框里为WHERE字句设置参数。当使用通过使用自定义SQL查询方法时,需要直接在WHERE字句设置参数(每个参数的形式为:@parameterName)。对第三中方法,因为一个存储过程由一个或几个SQL语句组成,而且在SQL语句中可以设置参数,所以那些SQL语句中设置的参数对存储过程来说是输入参数。
一个带参数查询的构造取决于SqlDataSource控件的SelectCommand命令是如何设置的,我们接下来使用上面三种方法来构造带参数的查询。打开SqlDataSource文件夹里的ParameterizedQueries.aspx页面,进入设计模式,从工具箱拖一个SqlDataSource控件到页面上,设置其ID为Products25BucksAndUnderDataSource,然后在智能标签中点“配置数据源”链接,选择NORTHWINDConnectionString,点“下一步”。
第一步:在“指定来自表或视图的列”模式里添加WHERE字句
当选择使用SqlDataSource控件从数据库返回数据时,其数据源设置向导允许我们使用最简单的“指定来自表或视图的列”模式(如图1)。此模式会自动生成Select()方法调用时使用的SQL SELECT命名。象上一章探讨的一样,在Products表中选择ProductID,ProductName和UnitPrice三列。
图1:选择“指定来自表或视图的列”模式
为给SELECT命令添加WHERE字句,请点击“WHERE”按钮,进入“添加WHERE字句”界面(如图2),首选选择用来筛选记录的列,再选择操作符(比如=, <, <=, >等),最后选择参数值来源,比如来自查询字符串,视图状态等。完成设置后点“添加”按钮。
本例中,我们仅仅返回那些价格小于或等于25的记录。所以我们选“UnitPrice ”
列,在操作符中选“<=”。当使用“硬编码”参数值(hard-coded),或使用参数传递值时,在“源”下拉列表里选择“None”。本节我们使用“硬编码”方式,所以在“源”下拉列表里选择“None”,同时在右边“参数属性”里输入25.00,点“添加”完成设置。
图2:在“添加WHERE字句”对话框中添加WHERE字句
完成设置后,点“OK”,返回数据源设置向导,在底部的SELECT语句里应该包含了一个名为“@UnitPrice”的参数:
SELECT [ProductID], [ProductName], [UnitPrice]
FROM [Products]
WHERE ([UnitPrice] <= @UnitPrice)
注意:
如果你在“添加WHERE字句”对话框中指定了多条WHERE字句,那么向导会用字符AND把它们连接起来。当你需要在WHERE字句里使用字符OR时,(比如:WHERE UnitPrice <= @UnitPrice OR Discontinued = 1) ,就只能在“自定义SQL语句”模式里构建了。
完成设置(点下一步,再点完成)后,查看其声明代码,代码里现在包含了<SelectParameters> 项。如下:
<asp:SqlDataSource ID="Products25BucksAndUnderDataSource" runat="server"
ConnectionString="<%$ ConnectionStrings:NORTHWNDConnectionString %>"
SelectCommand=
"SELECT [ProductID], [ProductName], [UnitPrice]
FROM [Products] WHERE ([UnitPrice] <= @UnitPrice)">
<SelectParameters>
<asp:Parameter DefaultValue="25.00" Name="UnitPrice" Type="Decimal" />
</SelectParameters>
</asp:SqlDataSource>
当SqlDataSource控件的Select()方法被调用的时候,在访问数据库之前,UnitPrice参数值(25.00)就与SelectCommand里的@UnitPrice parameter挂钩。结果是表Products里只有那些UnitPrice等于或小于25.00的记录被返回。为验证起见,在页面上添加一个GridView控件,并绑定到这个数据源,在浏览器里浏览本页,你将看到那些价格小于等于25.00的产品显示出来了,如图3所示:
图3:只有那些价格小于等于25的产品显示出来了。
第2步 在自定义SQL语句里添加参数
当添加一个自定义SQL查询时,你可以直接在WHERE字句里输入参数,或者在查询生成器里指定一个值。我们来做个实例,返回那些价格低于某个值的产品。在ParameterizedQueries.aspx页面添加一个TextBox,设置其ID为MaxPrice,供用户输入某个值。再添加一个Button控件,设置其Text属性为“Display Matching Products”
下一步,在页面添加一个GridView控件,在其智能标签中配置一个名为ProductsFilteredByPriceDataSource的SqlDataSource数据源,在数据源配置向导中的“指定一个自定义SQL语句或存储过程”界面中(见图4),键入如下查询:
SELECT ProductName, UnitPrice
FROM Products
WHERE UnitPrice <= @MaximumPrice
完成键入后(不管是手工输入还是用查询生成器),点下一步。
图4:返回那些价格低于指定值的产品。
然后,向导提示我们指定参数值来源,在“参数来源”下拉列表里选“Control”,在“ControlID”下拉列表里选“MaxPrice”(即TextBox控件的ID)。你也可以在“默认值”里随意键入一个值,以防用户未在TextBox控件输入值的情况。
不过在本教程里,无需指定一个默认值。
图5:MaxPrice TextBox控件的Text属性指定为参数值来源。
点下一步,点完成,完成设置。现在这几个控件的声明代码看起来应跟下面差不多:
Maximum price:
$<asp:TextBox ID="MaxPrice" runat="server" Columns="5" />
<asp:Button ID="DisplayProductsLessThanButton" runat="server"
Text="Display Matching Products" />
<asp:GridView ID="GridView2" runat="server" AutoGenerateColumns="False"
DataSourceID="ProductsFilteredByPriceDataSource" EnableViewState="False">
<Columns>
<asp:BoundField DataField="ProductName" HeaderText="Product"
SortExpression="ProductName" />
<asp:BoundField DataField="UnitPrice" HeaderText="Price"
HtmlEncode="False" DataFormatString="{0:c}"
SortExpression="UnitPrice" />
</Columns>
</asp:GridView>
<asp:SqlDataSource ID="ProductsFilteredByPriceDataSource" runat="server"
ConnectionString="<%$ ConnectionStrings:NORTHWNDConnectionString %>"
SelectCommand=
"SELECT ProductName, UnitPrice
FROM Products
WHERE UnitPrice <= @MaximumPrice">
<SelectParameters>
<asp:ControlParameter ControlID="MaxPrice" Name="MaximumPrice"
PropertyName="Text" />
</SelectParameters>
</asp:SqlDataSource>
我们注意到SqlDataSource控件的<SelectParameters>项的参数现在变成了一个ControlParameter,并且还多出了诸如ControlID和PropertyName的属性。当SqlDataSource控件的Select()方法被调用时,ControlParameter将会从页面上指定的
Web控件属性获取值,并赋给SelectCommand里的相应参数。具体到本例, MaxPrice的Text属性的值被赋给了参数@MaxPrice。
花几分钟时间预览页面,当第一次访问该页面,或者MaxPrice TextBox里没输入值时,GridView里没有记录显示。
图6:当MaxPrice TextBox 未赋值时,没有记录显示。
没有产品显示的原因是,默认情况下,传递给参数的空字符串会被转换为NULL,
而[UnitPrice] <= NULL 的值总是为False,导致无法从数据库检索出记录。
在textbox里输入一个值,比如5.00,点击“Display Matching Products”按钮,页面回传, SqlDataSource控件提示GridView它的参数来源发生了改变,因此,GridView 重新绑定到SqlDataSource,显示那些价格低于5.00的产品。
图7:价格低于5的产品被显示出来
设置页面初次登录时显示所有产品
页面初次登录时,与其不显示数据不如设置成显示所有产品。当MaxPrice TextBox的输入值为空时,我们可以为参数设置一个高的离谱的默认价格,比如1000000,因为不大可以有价格超过1000000的库存产品。然而在其它情况下这个办法可能并不管用。
在以前的教程Declarative Parameters 和Master/Detail Filtering With a DropDownList中,我们都遇到了相类似的问题。我们的解决办法是在放在业务逻辑层处理,当业务逻辑层判断出输入值是NULL或其它的什么保留值(reserved value),则调用数据访问层的返回所有记录的方法,当输入值是正常的过滤值(filtering value)时,则执行数据访问层中WHERE字句包含相应参数的SQL语句。
不幸的是,当使用SqlDataSource时,这种方法并适用。因此当参数@MaximumPrice
是NULL或其它的什么保留值时,我们必须自定义SQL语句来返回所有的产品。比如,让我们把参数@MaximumPrice设置为-1,然后返回所有记录(-1就是一种保留值,因为没有产品的价格为负数),为达到目的,我们可以用下面的SQL语句:
SELECT ProductName, UnitPrice
FROM Products
WHERE UnitPrice <= @MaximumPrice OR @MaximumPrice = -1.0
在WHERE字句中,当@MaximumPrice刚好等于-1时,返回所有产品,如果不为-1,那么只返回那些价格等于或小于@MaximumPrice值的产品。当把参数@MaximumPrice的默认值设置为-1时,第一此显示页面时(或每当MaxPrice TextBox中没有键入值时)参数@MaximumPrice的值都默认为-1,因此将返回所有记录。
图8:当MaxPrice TextBox是空的时,显示所有的产品。
这种方法有2处容易出错。第一,因为参数的数据类型是在SQL查询中决定的,当你把WHERE字句由“@MaximumPrice = -1.0”改为“@MaximumPrice = -1”后,运行时参数就被当作整数处理。如果你试图在MaxPrice TextBox中输入一个小数(比如5.00),这时错误发生了,因为它不能将“5.00”转换为一个整数。要解决这个问题,一个办法是确保在WHERE字句使用“@MaximumPrice = -1.0”,而更好的办法是把 ControlParameter的Type属性设置为“Decimal”(即小数型)。
第二个容易错的是,当把“OR @MaximumPrice = -1.0”加入到WHERE字句后,将会使检索引擎遍历整个表,当表Products包含的记录数很庞大的时候将严重的削弱性能。比较好的解决办法是用一个存储过程中来处理,可以在存储过程中使用IF语句来处理“UnitPrice <= @MaximumPrice OR @MaximumPrice = -1.0”这个逻辑关系。
第3步:创建并使用参数化的存储过程
存储过程可以包含一个输入参数,用于存储过程里定义的SQL语句。当设置SqlDataSource使用带参数的存储过程时,其用法与ad-hoc SQL语句一样。
来看个实例,我们在Northwind数据库里创建一个名为GetProductsByCategory的存储过程,这个存储过程接受一个参数@CategoryID,并返回products表中所有CategoryID列的值与@CategoryID相匹配的记录。让我们开始创建吧,在服务器资源管理器中选定NORTHWND.MDF数据库。(如果你找不到服务器资源管理器,在视图菜单里选择服务器资源管理器项即可)
在NORTHWND.MDF数据库里,在存储过程项上点右键,选择“添加新存储过程”,输入如下语句:
CREATE PROCEDURE dbo.GetProductsByCategory
(
@CategoryID int
)
AS
SELECT *
FROM Products
WHERE CategoryID = @CategoryID
点保存图标(或Ctrl+S)保存新建的存储过程。你可以测试你创建的存储过程,方法是选中某个存储过程,在右键选“执行”。接下来将提示是输入参数(就本例而言,是@CategoryID),最后在输出窗口显示结果。
图9:当在GetProductsByCategory中给参数@CategoryID赋值1时的执行情况
让我们在GridView中显示那些种类为Beverages的产品。在页面新添加一个GridView ,并绑定到一个ID为BeverageProductsDataSource的SqlDataSource控件。启用SqlDataSource控件的数据源设置向导,在“指定自定义SQL语句或存储过程”界面中,在“存储过程”下拉列表中,选GetProductsByCategory。
图10:在下拉列表中,选GetProductsByCategory存储过程
因为这个存储过程接受一个输入参数(@CategoryID), 点下一步后,向导提示我们指定参数值来源。因为我们想返回所有类型为Beverages的产品,且Beverages的CategoryID为1,所以我们在“参数源”下拉列表里选“None”,在默认值文本框里输入1
图11:通过将默认值设为1这种硬编码方式返回所有种类为Beverages的产品
就像下面的编码所展示的那样,当使用存储过程的时候,SqlDataSource控件的
SelectCommand属性被赋值为存储过程的名称,相应的,SelectCommandType 属性被设置为StoredProcedure。这就指明了SelectCommand执行的是存储过程而不是一个ad-hoc SQL语句。
<asp:SqlDataSource ID="BeverageProductsDataSource" runat="server"
ConnectionString="<%$ ConnectionStrings:NORTHWNDConnectionString %>"
SelectCommand="GetProductsByCategory" SelectCommandType="StoredProcedure">
<SelectParameters>
<asp:Parameter DefaultValue="1" Name="CategoryID" Type="Int32" />
</SelectParameters>
</asp:SqlDataSource>
在浏览器里查看该页面,只有那些类型为Beverages的产品显示出来了。因为存储过程GetProductsByCategory从表Products里检索所有的列,所以在页面中显示了记录的所有列。当然,你可以在GridView控件的“编辑列”对话框中自定义要显示的列。
图12:所有Beverages类的产品被显示出来。
第四步:参数化的调用SqlDataSource控件的Select()命令
本教程到目前为止直接将SqlDataSource控件绑定到GridView控件。然而,可以通过在代码里使用参数的方法来访问SqlDataSource的数据。在我们需要访问但又用不着把这些数据显示出来的情况下,这种方法很有用。与其自己动手写诸如连接数据库、指定具体的访问命令、返回结果等典型的ADO.NET代码,不如让SqlDataSource控件代劳。
举一个参数化使用SqlDataSource控件数据的例子。想象一下,你的老板让你编写这样的一个页面,根据随机选择的类,在页面上显示这个类的名称,及其相关联的产品。那意味着,当我们访问这个页面时,随机从表Categories中抽取一个类,页面将该类的名字,以及属于该类的所有产品显示出来。
为了达到这样目的,我们需要2个SqlDataSource控件。一个从表Categories中随机的选择一个类,一个返回该类的所有产品。在第4步我们探讨如何构建一SqlDataSource控件,使其返回一个随机抽取的类记录;在第5步我们探讨如何构建一个返回所有该类型的产品的SqlDataSource控件。
在ParameterizedQueries.aspx页面上添加一个SqlDataSource控件,设置其ID为RandomCategoryDataSource,对其使用如下的SQL查询:
SELECT TOP 1 CategoryID, CategoryName
FROM Categories
ORDER BY NEWID()
其中“ORDER BY NEWID()”意味着对返回的记录随机(random order)排序(详细内容看see Using NEWID() to Randomly Sort Records),“SELECT TOP 1”则意味着从结果里返回第一条记录。合起来,此查询将对查询结果随机排序,并返回第一条记录的 CategoryID和CategoryName列。
在页面上添加一个Label控件用于显示CategoryName,设置其ID为CategoryNameLabel,并将Text属性清空。为了参数化地从SqlDataSource控件获取数据,我们需要调用它的Select()方法。Select()方法使用的是类型为DataSourceSelectArguments的输入参数。当某个数据Web控件对SqlDataSource控件传入的数据企用分页或排序功能时, 该类型的参数在Select()方法返回数据前,指出应该对数据进行筛选和排序。在本例中,我们并不需要对结果做排序等任何的改变,所以我们只需要参数的DataSourceSelectArguments.Empty 形式。
Select()方法返回什么类型的对象(object),取决于SqlDataSource控件的DataSourceMode属性。就像上篇教程所说的那样,DataSourceMode属性可以被设置为
DataSet或DataReader,DataSourceMode属性设置为哪种类型,Select()方法就返回那种类型的对象。因为SqlDataSource控件RandomCategoryDataSource的DataSourceMode属性被设置为DataSet(默认的),所有我们将与DataView对象打交道。
下面的代码揭示了SqlDataSource控件RandomCategoryDataSource怎样将返回的结果转换成DataView类型,以及怎样从DataView的第一行记录读取CategoryName列的值。
protected void Page_Load(object sender, EventArgs e)
{
// Get the data from the SqlDataSource as a DataView
DataView randomCategoryView =
(DataView)RandomCategoryDataSource.Select(DataSourceSelectArguments.Empty);
if (randomCategoryView.Count > 0)
{
// Assign the CategoryName value to the Label
CategoryNameLabel.Text =
string.Format("Here are Products in the {0} Category...",
randomCategoryView[0]["CategoryName"].ToString());
}
}
randomCategoryView(0)意味着返回DataView的第一个DataRowView,randomCategoryView(0)("CategoryName")返回这个第一行的CategoryName列的值。注意,DataView是一种“泛型”(loosely-typed)对象,当我们引用某个具体列时,需要使用列名的字符串形式(本例为“CategoryName”)。图13显示了当访问该页面时,CategoryNameLabel文本框显示的内容。当然,每次访问该页面(包括回传)时,文本框根据SqlDataSource控件CategoryDataSource里随机选择的种类,显示该种类的名称。
图13:显示随机选择的种类的名称
注意:
当SqlDataSource控件的DataSourceMode属性设置为DataReader时,从Select()方法返回的值需要转换为DataReader,我们用如下的代码来从第一行读取列CategoryName的值:
if (randomCategoryReader.Read())
{
string categoryName = randomCategoryReader["CategoryName"].ToString();
...
}
通过SqlDataSource,我们随机的选择了一种类型,然后我们将在页面上添加一个GridView控件,显示该类型的产品.
注意:
一般来讲,虽然用Label控件来展示类名,不如在页面使用一个 绑定到SqlDataSource控件的FormView或DetailsView来显示.但用Label控件的好处在于,它便于我们调用SqlDataSource控件的带参数Select()命令.
第5步:编程为参数赋值
到目前为止,本教程例子中的参数值要么通过“硬编码”(hard-coded )的方式设置,要么来自于查询字符串、Web控件等预定义参数源(pre-defined
然而,SqlDataSource的参数还可以通过编程的方式设置。为了完成这个实例,我们需要一个返回属于某个特定种类的产品的SqlDataSource控件,该控件包含一个参数“CategoryID”。在Page_Load事件中,ID为
RandomCategoryDataSource的SqlDataSource控件将返回一个CategoryID列的值,该值将传给我们新创建的那个SqlDataSource控件的参数“CategoryID”。
在页面上添加一个GridView,并将其绑定到一个ID为
ProductsByCategoryDataSource的SqlDataSource控件。象我们在第3步中做的那样,我们设置这个SqlDataSource控件调用存储过程GetProductsByCategory。如下图,我们在参数源下拉列表里选“None”,且不要设置默认值,因为我们将通过编程来设置。
图14不要设置参数源和默认值
完成设置后, SqlDataSource控件的代码看起来应该和下面的差不多:
<asp:SqlDataSource ID="ProductsByCategoryDataSource" runat="server"
ConnectionString="<%$ ConnectionStrings:NORTHWNDConnectionString %>"
SelectCommand="GetProductsByCategory" SelectCommandType="StoredProcedure">
<SelectParameters>
<asp:Parameter Name="CategoryID" Type="Int32" />
</SelectParameters>
</asp:SqlDataSource>
我们可以在Page_Load事件中设置参数“CategoryID”的默认值:
' Assign the ProductsByCategoryDataSource's
' CategoryID parameter's DefaultValue property
ProductsByCategoryDataSource.SelectParameters("CategoryID").DefaultValue = _randomCategoryView(0)("CategoryID").ToString()
现在,页面包含了一个GridView控件,它根据一个随机选择的类别,显示所有属于该类别的产品。
图15:GridView控件显示特定种类的产品
总结:
SqlDataSource控件允许页面开发者定义一个带参数的查询,该参数的值可以“硬编码”设置、从预定义参数源获取,或通过编程来设置。本篇教程我们探讨了怎样在数据源设置向导的ad-hoc SQL查询和存储过程中创建带参数的查询。以及如何通过“硬编码”、数据控件、编程等为参数赋值。
就ObjectDataSource控件而言,该控件也可以对其返回的数据优化。在下一节里我们将看看如何定义SqlDataSource控件的INSERT, UPDATE, 和DELETE命令。当添加这些命名后,我们就可以启用GridView,DetailsView, 和FormView控件的自身内置的插入,编辑,和删除功能。
祝编程快乐!
作者简介:
Scott Mitchell,著有六本ASP/ASP.NET方面的书,是4GuysFromRolla.com的创始人,自1998年以来一直应用 微软Web技术。Scott是个独立的技术咨询顾问,培训师,作家,最近完成了将由Sams出版社出版的新作,24小时内精通ASP.NET 2.0。他的联系电邮为[email protected],也可以通过他的博客http://ScottOnWriting.NET与他联系。