在ASP.NET 2.0中操作数据之六十九:处理Computed Columns列

364 查看

导言:

  Microsoft SQL Server里有一种computed columns列.这种列的值是通过一个表达式来计算,而表达式引用的是同一张表的其它列的值.打个比方,有一张ServiceLog表,其包含了ServicePerformed, EmployeeID, Rate, Duration等列. 虽然我们可以在一个web页面或其它什么界面里计算每笔服务的费用(也就是 比率 rate乘以时间段duration),不过我们也可以手动向ServiceLog表添加一个 AmountDue列以反映该信息.我们可以将该列创建为一个普通列,只是任何时候当Rate 或 Duration列的值发生改变时需要更新AmountDue列的值.一个比较好的办法是将AmountDue创建成一个computed column 列,其使用的表达式为 Rate * Duration. 这样,当在一个查询里引用该列时SQL Server就可以自动的计算AmountDue列的值.

  由于computed column列的值是由表达式决定的,所以这种列是只读的,并且不能在INSERT 或 UPDATE statements里对其赋值.然而,对使用ad-hoc SQL statements的TableAdapter来说,如果主查询里引用了computed column列,那么自动生成的INSERT 和 UPDATE statements也会自动的引用computed column列.所以,我们必须更新TableAdapter的 INSERT 和 UPDATE 查询,以及InsertCommand 和 UpdateCommand属性,以删除对任何computed column列的引用.

  如果在使用 ad-hoc SQL statements的TableAdapter里使用computed columns的话,我们要面临的挑战之一便是,每当完成TableAdapte设置向导时,TableAdapter的 INSERT 和 UPDATE查询都会自动的生成,又再一次的自动引用computed column列.不过如果TableAdapters使用存储过程的话,就不会出现这个问题.

  在本文,我们将向Northwind数据库的Suppliers表添加一个computed column列,然后相应地创建一个TableAdapter来处理该表以及该computed column列.我们将在TableAdapter里使用存储过程而不是ad-hoc SQL statements.

第一步:向Suppliers表添加一个Computed Column

  在本文,我们将向Suppliers表添加一个名为FullContactName的computed column列,它以“ContactName (ContactTitle, CompanyName)”的格式返回contact的name, title,以及所在的公司.

  打开服务器资源管理器,在Suppliers表上单击右键,选“Open Table Definition”,这将会显示出表所包含的列以及列的属性,比如数据类型、是否允许为NULL值等等.要添加一个computed column列,只需在表定义里键入表的名称,接下来在Column属性窗口的Computed Column Specification部分的(Formula)文本框里输入表达式(如图1所示)。将该computed column列命名为FullContactName,并使用下面的表达式:

ContactName + ' (' + CASE WHEN ContactTitle IS NOT NULL THEN
 ContactTitle + ', ' ELSE '' END + CompanyName + ')'

  请注意,在SQL里可以用操作符“+” 来连接字符串。而CASE声明类似于传统编程语言里的条件语句。上面代码里的CASE 声明可以这样来理解:如果ContactTitle 不为NULL,那么输出ContactTitle值,再紧接一个逗号;如果为NULL,则无操作。关于CASE 声明的更多信息请参阅文章《The Power of SQL CASE Statements》(http://www.4guysfromrolla.com/webtech/102704-1.shtml

  注意:除了CASE声明外,我们还可以使用ISNULL(ContactTitle, '')。语法ISNULL(checkExpression, replacementValue) returns是这样工作的,如果checkExpression 不为NULL,则对其进行返回;如果为NULL则返回replacementValue.虽然本文这2种语法都可以使用,但是在一些稍微复杂点的情况下,使用ISNULL的情况要多一些.添加完computed column列后,你的屏幕看起来应该和图1差不多:

http://files.jb51.net/file_images/article/201605/2016051910262344.png
图1:向Suppliers表添加一个名为FullContactName的Computed Column列

  添加完后点工具栏上的Save图标,或按Ctrl+S键,又或者在File菜单里选“保存Suppliers”.“保存”操作会自动地刷新服务器资源管理器,将刚刚添加的的列展现在Suppliers表里.此外,键入到(Formula)文本框的表达式会自动的进行调整,剔除不必要的空白,将列名用[]括起来,并使用圆括号()来显示操作的先后顺序:

(((([ContactName]+' (')+case when [ContactTitle] IS NOT NULL
 then [ContactTitle]+', ' else '' end)+[CompanyName])+')')

  关于Microsoft SQL Server里computed columns列的更多信息请参考文章《technical documentation》(http://msdn2.microsoft.com/en-us/library/ms191250.aspx);同时你也可以参考文章《How to: Specify Computed Columns》(http://msdn2.microsoft.com/en-us/library/ms188300.aspx),看如何一步步地创建computed columns列.

  注意:默认情况下,数据库表并没有“实际”(physically)的包含computed columns列,而是每次在一个查询里引用它时重新计算其值.不过,我们可以选择“Is Persisted”选项来让SQL Server实实在在的在数据库表里创建computed columns列.这样的话我们可以为computed column列创建一个索引,当在一个查询的WHERE字句里使用computed column列的值时就可以提高执行效率.更多的信息请参阅文章《Creating Indexes on Computed Columns》(http://msdn2.microsoft.com/en-us/library/ms189292.aspx)

第二步:查看Computed Column列的值

  在处理数据访问层前,让我们花点时间查看FullContactName列的值.在服务器资源管理器里,在Suppliers表上右键单击,选择“New Query”,这将启动一个查询窗口提示我们在查询里包含哪个表.添加Suppliers表,再点“Close”.接下来从Suppliers表里选择CompanyName, ContactName, ContactTitle,以及FullContactName列.最后,点击工具栏上的红色感叹号图标执行查询,查看结果.如图2所示,结果里包含了FullContactName列,它以ContactName (ContactTitle, CompanyName)”的格式使用了CompanyName, ContactName,ContactTitle这3列.

http://files.jb51.net/file_images/article/201605/2016051910262345.png
图2:FullContactName列的格式为“ContactName (ContactTitle, CompanyName)”

第三步:在数据访问层添加一个SuppliersTableAdapter

  为了在我们的应用程序里处理supplier信息,我们首先需要在DAL层创建一个TableAdapter 和 DataTable.我们可以用前面的教程探讨的方法来进行创建,稍微不同的是我们将要与computed columns列打交道.

  如果你用ad-hoc SQL statements来构造一个TableAdapter的话,你可以很简单的通过TableAdapter设置向导在TableAdapter的主查询里引用computed column列,这样,在自动生成的INSERT 和 UPDATE statements就会引用computed column列。如果你执行这2个方法的话,将会抛出这样的一个SqlException:“The column ‘ColumnName' cannot be modified because it is either a computed column or is the result of a UNION operator”.虽然我们可以在InsertCommand和UpdateCommand属性里手工改动INSERT 和 UPDATE statement,但是一旦重新运行TableAdapter设置向导后,我们所做的用户定制就会丢失掉.

  由于使用ad-hoc SQL statements的TableAdapters的这种不稳定性,我们倾向于使用存储过程来处理computed columns列.如果你使用的是现有的存储过程的话,你可以参阅第66章《在TableAdapters中使用现有的存储过程》那样来配置TableAdapter.如果你使用TableAdapter设置向导来创建存储过程的话,很重要的一点是最开始,你不要在主查询里引用computed columns列,如果你在主查询里引用了computed columns列的话,你刚完成设置,向导就会提示你不能创建相应的存储过程.简而言之,在设置TableAdapter时,最开始不要在主查询里引用computed column列,接下来再对相应的存储过程和TableAdapter的SelectCommand属性进行更改以引用computed column列.这种方法我们在第67章《在TableAdapters中使用JOINs》里探讨过.

  本文我们将新添加一个TableAdapter并自动创建存储过程.当然我们要在主查询里忽略这个名为FullContactName的computed column列.打开~/App_Code/DAL文件夹里的NorthwindWithSprocs DataSet数据集,在设计器里右键单击,选“add a new TableAdapter”,这将开启TableAdapter设置向导,指定数据库连接信息(也就Web.config文件里的NORTHWNDConnectionString),点Next。选“Create new stored procedures”项,再点Next.

http://files.jb51.net/file_images/article/201605/2016051910262346.png
图3:选择“Create new stored procedures”项

  接下来我们要指定主查询,键入如下的查询,其返回每个supplier的SupplierID, CompanyName, ContactName, ContactTitle列。注意,我们有意忽略了computed column列(即FullContactName列)。不过我们将在第四步更新该存储过程以引用该列:

SELECT SupplierID, CompanyName, ContactName, ContactTitle
FROM Suppliers

  输入完主查询后点Next,向导要我们为将要创建的4个存储过程命名,分别命名为Suppliers_Select, Suppliers_Insert, Suppliers_Update,以及 Suppliers_Delete。如图4所示:

http://files.jb51.net/file_images/article/201605/2016051910262447.png
图4:对自动生成的存储过程命名

  接下来要我们为TableAdapter的方法命名并指定用于访问和更新数据的模式.我们全部选中这3项,不过将GetData方法重命名为GetSuppliers.点击Finish完成配置.

http://files.jb51.net/file_images/article/201605/2016051910262448.png
图5:将GetData方法重命名为GetSuppliers

  完成后向导将创建这4个存储过程,并向类型化的DataSet添加ableAdapter以及对应的DataTable.

第四步:在TableAdapter的主查询里引用Computed Column列

接下来我们将对第三步创建的TableAdapter 和 DataTable进行更新以引用FullContactName列,这要经过2个步骤:

1.更新名为Suppliers_Select的存储过程以返回FullContactName列

2.更新DataTable以包含相应的FullContactName列

  首先在服务器资源管理器里打开存储过程文件夹,打开Suppliers_Select存储过程,更新其SELECT查询以引用FullContactName列:

SELECT SupplierID, CompanyName, ContactName, ContactTitle, FullContactName
FROM Suppliers

  保存所做的修改.接下来返回到DataSet Designer,在SuppliersTableAdapter上右键单击,选“Configure”.我们可以注意到Suppliers_Select里的Data Columns集里已经包含了FullContactName列.

http://files.jb51.net/file_images/article/201605/2016051910262449.png
图6:返回到TableAdapter的设置向导更新DataTable的列

  点击Finish完成设置,这将自动地为SuppliersDataTable添加相应的列.TableAdapter发觉FullContactName列是一个computed column列,且是只读的.因此将设置该列的ReadOnly属性为true.我们可以进行验证:在SuppliersDataTable里选择该列,打开其属性窗口(如图7),我们注意到FullContactName列的DataType 和 MaxLength属性都作了相应的设置.

http://files.jb51.net/file_images/article/201605/2016051910262450.png
图7:FullContactName列标记为Read-Only

第五步:向TableAdapter添加一个GetSupplierBySupplierID方法

  在本文我们将在一个具有更新功能的ASP.NET页面里展示suppliers信息.在前面的文章里,我们从DAL获取指定的记录并将其作为一个强类型的DataTable返回给BLL以做更新,然后将更新后的DataTable再传递给DAL,对数据库做相应的改动.为此,第一步——从DAL返回要更新的记录——我们需要向DAL层添加一个名为GetSupplierBySupplierID(supplierID)的方法.

  在DataSet Design设计器里右键单击SuppliersTableAdapter,选“Add Query” ,再选“Create new stored procedure”(可参考3图)。再选“SELECT which returns rows”再点Next.

http://files.jb51.net/file_images/article/201605/2016051910262451.png
图8:选“SELECT which returns rows”项

  接下来为该方法指定查询,键入如下的代码,它将检索某个具体的supplier返回的列与主查询一样.

SELECT SupplierID, CompanyName, ContactName, ContactTitle, FullContactName
FROM Suppliers
WHERE SupplierID = @SupplierID

  接下来我们将该存储过程命名为Suppliers_SelectBySupplierID,点Next.

http://files.jb51.net/file_images/article/201605/2016051910262452.png
图9:将存储过程命名为Suppliers_SelectBySupplierID

  在接下来的界面,全部选中图里的2项,并将FillBy 和 GetDataBy方法分别命名为FillBySupplierID 和 GetSupplierBySupplierID.

http://files.jb51.net/file_images/article/201605/2016051910262553.png
图10:将TableAdapter的方法命名为FillBySupplierID 和 GetSupplierBySupplierID

  点Finish完成向导

第六步:创建业务逻辑层Business Logic Layer

  在创建ASP.NET页面前,我们首先要在BLL添加相应的方法.我们将在第7步创建页面,其允许我们查看并编辑suppliers.因此我们在BLL至少要包含2个方法,一个获取所有的suppliers,一个用于更新某个具体的supplier.

在~/App_Code/BLL文件夹里创建一个名为SuppliersBLLWithSprocs的新类,添加代码如下:

using System;
using System.Data;
using System.Configuration;
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 NorthwindWithSprocsTableAdapters;

[System.ComponentModel.DataObject]
public class SuppliersBLLWithSprocs
{
 private SuppliersTableAdapter _suppliersAdapter = null;
 protected SuppliersTableAdapter Adapter
 {
 get
 {
  if (_suppliersAdapter == null)
  _suppliersAdapter = new SuppliersTableAdapter();

  return _suppliersAdapter;
 }
 }

 [System.ComponentModel.DataObjectMethodAttribute
 (System.ComponentModel.DataObjectMethodType.Select, true)]
 public NorthwindWithSprocs.SuppliersDataTable GetSuppliers()
 {
 return Adapter.GetSuppliers();
 }

 [System.ComponentModel.DataObjectMethodAttribute
 (System.ComponentModel.DataObjectMethodType.Update, true)]
 public bool UpdateSupplier(string companyName, string contactName,
 string contactTitle, int supplierID)
 {
 NorthwindWithSprocs.SuppliersDataTable suppliers =
  Adapter.GetSupplierBySupplierID(supplierID);
 if (suppliers.Count == 0)
  // no matching record found, return false
  return false;

 NorthwindWithSprocs.SuppliersRow supplier = suppliers[0];

 supplier.CompanyName = companyName;
 if (contactName == null)
  supplier.SetContactNameNull();
 else
  supplier.ContactName = contactName;
 if (contactTitle == null)
  supplier.SetContactTitleNull();
 else
  supplier.ContactTitle = contactTitle;

 // Update the product record
 int rowsAffected = Adapter.Update(supplier);

 // Return true if precisely one row was updated, otherwise false
 return rowsAffected == 1;
 }
}

  和其它的BLL class类一样,SuppliersBLLWithSprocs有一个protected Adapter属性,2个public方法:GetSuppliers 和 UpdateSupplier.其中,GetSuppliers方法调用Data Access Layer层对应的GetSupplier方法,该方法将SuppliersDataTable返回给BLL层;而UpdateSupplier方法通过调用DAL层的GetSupplierBySupplierID(supplierID)方法来获取某人具体supplier的信息,然后更新其CategoryName, ContactName,ContactTitle属性,再将修改后的SuppliersRow对象传递给Data Access Layer层的 Update方法,以对数据库做相应的更新.

  注意:除了SupplierID 和 CompanyName外,Suppliers表的所有列都允许为NULL值,所以如果传递的contactName 或 contactTitle参数为null的话,我们将分别调用SetContactNameNull 和 SetContactTitleNull方法来将ContactName 和 ContactTitle 属性设置为NULL.

第七步: 在表现层处理Computed Column列

  做完了所有的必要工作后, 我们将创建一个ASP.NET页面来处理FullContactName列, 打开AdvancedDAL文件夹里的ComputedColumns.aspx 页面,拖一个GridView控件到页面,设其ID为Suppliers,在其智能标签里绑定到一个名为SuppliersDataSource的ObjectDataSource控件,设置其调用SuppliersBLLWithSprocs类,点Next.

http://files.jb51.net/file_images/article/201605/2016051910262554.png
图11:设置ObjectDataSource调用SuppliersBLLWithSprocs Class类

  在SuppliersBLLWithSprocs类里只有2个方法GetSuppliers 和 UpdateSupplier.确保在SELECT 和 UPDATE标签里分别选中这2个方法,点Finish完成设置.完成设置后,Visual Studio将添加相应的BoundField,移除SupplierID列,并将CompanyName, ContactName, ContactTitle,和FullContactName列的HeaderText属性分别设置为“Company”, “Contact Name”, “Title”,“Full Contact Name”,再启用GridView的编辑功能.

  Visual Studio将ObjectDataSource控件的OldValuesParameterFormatString属性设置为“original_{0}”. 我们要将其改为默认值“{0}”.如此这般,GridView 和 ObjectDataSource控件的声明代码看起来和下面的差不多:

<asp:GridView ID="Suppliers" runat="server" AutoGenerateColumns="False"
 DataKeyNames="SupplierID" DataSourceID="SuppliersDataSource">
 <Columns>
 <asp:CommandField ShowEditButton="True" />
 <asp:BoundField DataField="CompanyName"
  HeaderText="Company"
  SortExpression="CompanyName" />
 <asp:BoundField DataField="ContactName"
  HeaderText="Contact Name"
  SortExpression="ContactName" />
 <asp:BoundField DataField="ContactTitle"
  HeaderText="Title"
  SortExpression="ContactTitle" />
 <asp:BoundField DataField="FullContactName"
  HeaderText="Full Contact Name"
  SortExpression="FullContactName"
  ReadOnly="True" />
 </Columns>
</asp:GridView>

<asp:ObjectDataSource ID="SuppliersDataSource" runat="server"
 SelectMethod="GetSuppliers" TypeName="SuppliersBLLWithSprocs"
 UpdateMethod="UpdateSupplier">
 <UpdateParameters>
 <asp:Parameter Name="companyName" Type="String" />
 <asp:Parameter Name="contactName" Type="String" />
 <asp:Parameter Name="contactTitle" Type="String" />
 <asp:Parameter Name="supplierID" Type="Int32" />
 </UpdateParameters>
</asp:ObjectDataSource>

  接下来我们在浏览器里登录该页面,如图12所示。每行都有一个FullContactName列,格式为“ContactName (ContactTitle, CompanyName)”.

http://files.jb51.net/file_images/article/201605/2016051910262555.png
图12:每行展示一个Supplier

  点击某行的Edit按钮将导致页面回传,且该行显示为一个编辑界面(如图13),头3行呈现为默认的编辑界面——一个TextBox控件,且其Text属性为该数据域(data field)的值.不过FullContactName列仍然呈现为一个文本框.在Data Source设置向导完成并向GridView控件添加完相应的BoundFields时,FullContactName BoundField的ReadOnly属性为true。我们在第四步注意到,FullContactName列的ReadOnly属性为true,因为TableAdapter意识到该列为一个computed column列.

http://files.jb51.net/file_images/article/201605/2016051910262556.png
图13:FullContactName列为只读

  我们改动这3个列中至少一个列的值,点Update按钮.我们发现FullContactName列的值跟着发生改变.

  注意:由于GridView当前用的是BoundFields,导致编辑时用的是默认的界面.又由于CompanyName列是必需的,我们应将其转化成一个TemplateField以包含一个RequiredFieldValidator控件.我将此作为一个练习留给读者,你可以参考第19章《给编辑和新增界面增加验证控件》,看如何一步步的将BoundField转换成 TemplateField,再添加一个确认控件.

结语:

  当创建一个表时,Microsoft SQL Server允许我们创建一个computed columns列.这些computed columns列引用该条记录的其它列,再通过一个表达式对其赋值.由于其值来源于一个表达式,因此这种列是只读的,且不能通过INSERT 或 UPDATE statement对其赋值.正是如此,当在一个TableAdapter的主查询里引用computed column的话,要想自动的生成对应的INSERT, UPDATE,和DELETE statements有点麻烦.

  在本文,我们探讨了使用computed columns列面临的挑战.具体来说,由于使用ad-hoc SQL statements的TableAdapters自身固有的不稳定性,我们探讨了使用存储过程的情况.当使用TableAdapter向导创建一个新的存储过程的时候,很重要的一点是,最开始不要在主查询里引用任何的computed columns,不然就不能自动的生成对应的存储过程.完成向导后,我们要手动修改SelectCommand属性以引用computed columns列.

  祝编程快乐!

作者简介

  本系列教程作者 Scott Mitchell,著有六本ASP/ASP.NET方面的书,是4GuysFromRolla.com的创始人,自1998年以来一直应用 微软Web技术。大家可以点击查看全部教程《[翻译]Scott Mitchell 的ASP.NET 2.0数据教程》,希望对大家的学习ASP.NET有所帮助。