计算所得的列对数据库开发人员并不新鲜。实际上,据我所知,所有商业数据库管理系统 (DBMS) 都提供了此特性。尤其是,这意味着您总是可以改变查询的形式,以满足代码需要。例如,如果您的应用程序需要一些以某种方式表达和/或组合的数据,而这种方式是物理列本身所无法提供的,计算所得的列就可以执行此功能。计算所得的列可以用一种对用户更为友好的格式返回预处理的数据和预聚合的数据,从而便于进一步使用或处理。
表达式通过设计结合了现有的数据块,以便产生一个非标准结构,您在严格意义上不需要存储这个非标准结构,但希望得到它的最终形式。例如,假设您的数据库表有两列 — price(价格)和 tax percentage(税率)。不过,当开始显示数据时,您常常需要显示价格和正确的税额之和提供的信息。在数据库中存储最终价格是一个很差的设计,因为您仍然需要作为独立数据的价格和税额。另一方面,价格和税额之和是您的用户需要的信息。出现像这样的情况时,您可能需要使用虚拟列,它的值并非以物理方式存储在数据库中,而是根据需要计算得出的。
除了驻留在 DBMS 中的计算所得的列,您还需要直接在查询中动态创建的计算所得的列。例如,下面的查询返回一个表中不存在的、但临时创建的计算所得的列。
SELECT lastname + ', ' + firstname AS Name FROM employees
在 ADO.NET 中,基于表达式的列(表达式列)将此逻辑应用于内存中断开连接的数据。内存中的计算所得的列可以有许多用法。特别是,可以用它们克服 Web 窗体和 Windows 窗体应用程序的某些 .NET 数据绑定限制。此外,表达式列还结合了 GROUP BY SQL 语句的特性。ADO.NET 的计算所得列与 DataRelation 对象结合使用,可以对 “数据表” 对象中存储的记录组计算聚合值。
在本文中,我将讨论几种您可以充分利用 ADO.NET 的计算所得列的情况。不过,在进一步展开讨论之前,您需要了解一点背景信息。
在 ADO.NET 中,定义计算所得的列就像用字符串值在 DataColumn 对象上设置 “表达式” 属性一样容易。该字符串必须根据给定的语法表示一个有效的表达式。在内部,DataColumn 类构建表达式中涉及的一系列数据列,当更新一个或多个涉及的列时,会自动刷新计算所得的值。
注意,当其中的任何列的值发生更改时,父 “数据表” 对象触发一对特别事件(名为 ColumnChanging 和 ColumnChanged)。不过,对于由表达式驱动的列,根本不触发任何事件。
与大多数 DBMS 中的计算所得的列的情况不同,所有值始终存储在内存中,从这个意义上来说,计算所得的 ADO.NET DataColumn 对象表示真实的列。不过,计算所得的列将永久地标记为只读。也就是说,在删除绑定的表达式之前,您不能删除列的 “只读” 属性。当然,“只读” 属性还防止任何应用程序将直接的值分配给列的单元格。
要将计算所得的列与当前表达式分离,您可以将表达式设置为 “空”(如果您使用的是 Visual Basic,则设置为 Nothing)或设置为空字符串。在这种情况下,列中的所有值立即重置为该列的默认值。如果没有通过 DefaultValue 属性为列指定默认值,则分配 System.DBNull 值。如果您只是修改表达式,而不是取消表达式,该列的值同样重置和重新计算。
定义基于表达式的列时,您还需要指出列的类型。实际上,数据类型是允许您指定表达式字符串的 DataColumn 构造函数的唯一参数。
Dim col As DataColumn
col = dt.Columns.Add("ExpCol", GetType(Boolean), expr)
您还可以使用更简单的构造函数,并显式设置 “表达式” 属性:
Dim col As DataColumn = New DataColumn("ExpCol")
col.Expression = expr
如果您不显式设置列的数据类型,数据类型即默认为 String。当您创建了计算所得的列并且不指定类型时,您可能期望 .NET 框架为您推断类型。这是不可能的,而且有充分的理由。推断类型相当复杂,而且要耗费很大的计算成本。手动设置列的类型会产生更好更快的代码。
通常,表达式的结果被转换为列的类型。如果不能实现此目的,将引发 EvaluateException 异常。考虑到这一点,您应该记住,得到的数据的表示形式可能有很大的改变。例如,如果列类型为 String,布尔表达式会得出 True/False 字符串,如果是数值列,得到的值就是 1/0。
虽然您可以很轻松地即时更改计算所得的列的表达式,但不能对列类型也这样做。不过,这是一个一般限制,并不专门应用于计算所得的列。您得到的错误消息提示您,系统不能更改已经有数据的列的类型。为了解决这个问题,对于任何类型的列,先从表中删除该列,然后进行所有更改,再添加该列。
Dim col As DataColumn = dt.Columns(index)
' Remove (and automatically empty) the column object from the table
dt.Columns.Remove(col)
' Change the type and add the object back in the same position
col.DataType = GetType(Integer)
dt.Columns.AddAt(col, index)
从数据表删除列不会破坏 DataColumn 对象,只要它继续保持作用域。唯一的影响是,列已经为空 — 这是更改数据类型的理想条件。
计算所得的列由表达式确定。表达式使用运算符将同一个表的多个列结合起来,计算出一个值。您可以使用简单的算术运算符(如 + 和 *)以及逻辑运算符(如 = 和 )。另外,还允许使用布尔运算符(AND、OR、NOT)以及更复杂的运算符,如 IN、LIKE 和 %(取模)。运算符按照通常的先后次序进行计算,您可以根据需要使用括号来更改运算的先后次序。
下面的字符串表示计算所得的列的一些可行表达式。
Country = '
Year >= 1999 And LastName LIKE 'A%'
Price * 0.2 + Freight
列名可用作关键字,并且一定不要加引号。而名称则必须始终用单引号括起来。日期应该用 # 符号括起来。
HireDate > #2-1-2002#
表达式完全支持像 % 和 * 这样的通配符,但通配符只能出现在字符串的开头或结尾。表达式不识别枚举值,除非您将枚举值转换为实际的值和类型。可以使用十进制值,只要您根据当前的区域设置通过字符串来表示它们。
默认情况下,字符串是以不区分大小写的方式进行比较的。不过,具体的比较过程由 “数据集” 和 “数据表” 对象都具有的 CaseSensitive 属性来控制。当然,对于列,数据表上的属性优先。
表达式可以包括若干个用于计算聚合值的特别函数,它们是 Count、Max、Min、Sum 和 Avg。一个特殊的关键字 — child 关键字 — 还可以实现分组功能。(稍后将详细介绍。)
最后,还有六个函数使表达式语法更加丰富:Len、IsNull、Iif、Convert、Trim 和 Substring。这些函数为您可以定义的表达式增加了一定的灵活性。尤其是,Len 返回字符串的长度,如果您对非字符串列调用该函数,将引发异常。与相对应的 T-SQL 一样,IsNull 的参数是一个给定的表达式,如果对该表达式求值后得到 System.DbNull 值,则返回指定的值。
函数 Iif 使表达式的逻辑更加复杂,因为它允许您根据布尔表达式返回值。例如,
' Looks like a If..Then..Else statement
Iif(Len(description)>100, "[More]", description)
在这种情况下,如果 description 的长度超过 100,则返回标准文本。否则,您将得到字段的内容。与 Substring 结合使用的 Iif 可以产生非常好的效果。我们来看下面的代码片断。
DataTable dt = ds.Tables["MyTable"];
dt.Columns.Add("FullName", typeof(string),
"Iif(Len(LastName + ', ' + FirstName) >10, " +
"' <span title=\"' + LastName + ', ' + FirstName + '\">' +
"Substring(LastName + ', ' + FirstName, 1, 10) +
"' ...<span>', " +
"LastName + ', ' + FirstName)");
一个名为 FullName 的新的计算所得的列添加到目标 “数据表” 对象。该表达式在中间用逗号连接 LastName 和 FirstName。如果得出的表达式的长度超过 10 个字符,表达式将被剪裁,并附加省略号 (...);否则,返回正常文本。我们来看看更复杂的情况,假设您可以根据需要在表达式中插入任意多个 HTML 标记和属性。特别是,如果文本对应用程序来说过长,可使用标记和 “标题” 属性添加工具提示。结果如下图所示。毫无疑问,此技巧只有在 ASP.NET 页中才有效。
图 1. 添加的 标记和 title 属性的结果
从上面的代码可以看出,Substring 从给定的位置开始,提取指定数量的字符。而函数 Trim 则删除前导空白字符和尾随空白字符,如新行、制表符和软回车。最后,Convert 函数将列表达式转换为指定的 .NET 类型。如果不可能进行转换,将引发异常。
有关计算所得的列支持的语法的详细信息,请参阅 MSDN documentation。
Web 窗体和 Windows 窗体的数据绑定控件都可以与数据列相关联,并使用此类内容来填充它们自己的用户界面。诸如 ASP.NET BoundColumn、DropDownList、RadioButtonList 这样的类以及 Windows 窗体的 ComboBox、“文本框” 和 “列表框” 都可以使用主要的 “数据源” 属性绑定到数据。有时,诸如 DisplayMember、DataTextField 或 DataValueField 这样几个辅助属性也可以根据控件的性质和行为缩小绑定的行集和列集。
在 .NET 框架中,您只能将数据绑定的属性与一个列相关联。您不能将属性绑定到表达式或绑定到更简单的列组合。我们来看下图显示的 Windows 窗体组合框。
图 2. 多字段 Windows 窗体数据绑定组合框
该控件显示由下面的代码产生的全名。
cboEmployees.DataSource = data.Tables("Employees")
cboEmployees.DisplayMember = "EmployeeName"
您可以使用两种方法来突破可绑定到控件的单个字段的限制。您可以请求 SQL Server® 返回一个动态计算所得的列,或者,您可以使用 ADO.NET DataColumn 对象创建一个内存中的列。
使用 SQL Server 时,您通常会占用更大的带宽,因为要返回更多的数据 — 正常的列加上计算所得的列。在客户端,所有返回的数据均存储在内存中的 “数据表” 对象。如果您运行一个计算成本最小的查询(没有计算所得的列)并在内存中构建计算所得的列,那么,在客户端和数据库服务器之间传输的数据更少,同时客户端上的内存占用保持不变。
adapter.Fill(dataset, "Employees")
Dim dt As DataTable = data.Tables("Employees")
dt.Columns.Add("EmployeeName", GetType(String), _
"lastname + ', ' + firstname")
上面的代码片断显示了如何添加一个自定义的计算所得的列,以便组合更多的数据库字段进行显示。
通过在列表达式中使用 child 关键字,您可以实现一种客户端的分组功能。该关键字的工作方式是,对父行的子行计算聚合函数。为了使用 child 关键字,涉及的列之间必须存在某种关系。
Sum(child(RelationName).Price)
当上述表达式被分配给某个列时,将根据指定的关系针对(且只针对)当前行的所有子记录对 Price 列中的值求和。如果数据表只有一个关系,同一表达式可以重写为:
Sum(child.Price)
我们来分析一个示例,假设有一个包括 Customers 表和 Orders 表的 “数据集” 对象。您通过下面的代码设置这两个表的关系:
Dim rel As DataRelation
rel = New DataRelation("CustToOrders", _
data.Tables("Customers").Columns("customerid"), _
data.Tables("Orders").Columns("customerid"))
data.Relations.Add(rel)
“数据关系” 对象将每个客户行绑定到一个定单子集。从代码的角度来看,您使用 DataRow 的 GetChildRows 方法来访问这个子行子集。如果您只需要对这个行子集进行聚合函数求值,应使用 child 关键字定义表达式列。
data.Tables("Customers").Columns.Add("Total", _
GetType(Decimal), "Sum(child.price)")
上述列定义的实际效果是,添加到 Customers 表的 Total 列包含某个给定客户的所有定单的总额。
图 3. 使用 GetChildRows 方法时得到的表的示例
利用 ADO.NET 和利用数据库分组功能有什么区别呢?数据库返回可立即显示的数据,并在内部执行所有所需的操作,无论复杂程度如何。而利用 ADO.NET 对象,您只能提取需要的数据,并存储在不同的结果集中。一旦该数据下载到客户端,您可以根据需要使用关系和表达式来重排数据。数据库服务器和客户端之间工作负荷得到平衡,带宽的使用得以优化。
不过,如果客户端恰好是 ASP.NET 应用程序,您应该仔细考虑额外的操作可能对性能产生的整体可伸缩性方面的影响。如果您的客户端是 Windows 窗体,您一定要考虑使用内存中的 ADO.NET 对象,简化获得分层数据所必需的数据库查询。
您不能使用表达式列来定义 “数据表” 对象的主键。这并不奇怪,因为这正是某些 SQL Server 索引的情况。谈到 SQL Server 和 DataColumn 对象,您应该注意,您不可能将 SQL Server 计算所得的列作为 ADO.NET 基于表达式的列进行加载。也就是说,当提取列并将列加载到 “数据表” 对象时,不继承可计算性属性。不过,如果您希望检查 SQL Server 列是否基于表达式,可以用下面的方法来实现此目的。第一个步骤是,准备一个包括所有列的查询,然后执行该查询,获得 SqlDataReader 对象。接下来,调用得到的 GetSchemaTable 方法。该方法返回一个数据表,该数据表中的列包含有关基础表的架构的重要信息(IsExpression、IsIdentity、IsKey、IsUnique 和 NumericPrecision)。
当 “数据表” 对象使用 WriteXml 方法序列化到 XML 时,所有列的内容都保存到输出流。不过,如果您不保存架构信息,将不能还原任何计算所得的列。
dataTable.WriteXml(xmlFile, WriteMode.WriteSchema)
如果您不希望保存架构信息,则应该确保将序列化的无架构表还原到带有正确的架构信息的 DataTable 对象(也就是说,已在其中创建了列的 DataTable 对象)。上面讨论的 Total 列的架构的序列化如下所示:
<xs:element
name="Total"
msdata:ReadOnly="true"
msdata:Expression="sum(child(CustToOrders).price)"
type="xs:decimal"
minOccurs="0" />
最后,虽然您可以创建计算所得的列来对整个表计算聚合函数,但如果这就是您的目的,请考虑使用 DataTable.Compute 方法。Compute 方法计算出一个值,然后返回该值,它不创建列。