列表达式、DataRelations 和计算


下载这篇文章的代码: DataPoints2007_01.exe (191KB)

ADO.NET 决不只是让您能够从数据库中检索数据或向数据库中存储数据。它还具有许多功能,可对数据进行操作,以用于业务逻辑分析和在实际的应用程序中显示。正如本专栏中的示例所示,ADO.NET 并不仅仅是一个数据存取工具,还可用于数据操作。

在本月的专栏中,我将专门回答一些有关使用 ADO.NET 进行数据操作的常见问题。我将讲述如何使用基于表达式的列和 DataRelations 来达到特定目的。此外,我还将讲述如何使用其他功能(例如 DataTable 的 Compute 方法和 SetOrdinal 方法)来满足一般业务需要。


问:我想使用 DataTable 逐个订单地显示订单信息并将其显示在汇总分组(按天、月和年)中。我希望能够编辑订单信息,而且父 DataTable 中的合计必须立刻反映出所做的更改。我希望每一组数据都显示在该窗体的单独的网格中。在 DataTable 中加载了数据之后,如何使用 ADO.NET DataColumn 表达式创建带有每组订单小计的分组信息?


答:您可以轻松创建计算字段,方法是使用 SQL 语句中的计算表达式或创建一个绑定了表达式的 DataColumn。这两种方法的原理和它们呈现的功能有所不同。

对于您的情况和要求,SQL 语句中的计算列不是最有效的方法。最好将基于表达式的 DataColumn 添加到一系列 DataTable 中。

在深入讲述此问题之前,我们先看一下我们要实现的最终结果(参见图 1)。此示例在一个窗体中显示了五个网格。最顶部的网络包含单独的订单行项。此网格中每个订单行的价格、数量和折扣都可以编辑。在编辑了这些值后,系统会为该行计算扩展价格,其他四个网格中的合计值也会自动重新计算并显示最新的合计以反映出所作的更改。在第二个网格中,每个订单占一行,包括显示该订单的扩展价格小计的计算列。第三、第四和第五个网格分别按天、月和年显示每个订单的小计。

图 1 按天、月和年的订单合计
图 1  按天、月和年的订单合计 (单击该图像获得较小视图)
图 1 按天、月和年的订单合计
图 1  按天、月和年的订单合计 (单击该图像获得较大视图))

每个网格都绑定到不同的 DataTable。第一个 DataTable 使用来自数据库的订单行进行加载,并追加了一个基于表达式的列。在使用 DataAdapter 的 Fill 方法将数据加载到 DataTable 中后,会向 DataTable 添加一个名为 ExtendedPrice 的 decimal 类型的列。该列定义为表达式列,它根据每个 DataRow 的“单价”、“数量”和“折扣”值计算该列的值。

ds.Tables["OrderLines"].Columns.Add("ExtendedPrice",
typeof(decimal), "(UnitPrice * Quantity) * (1 - Discount)");

 

OrderLines DataTable 包含此窗体的核心数据。一旦加载了它并创建了它的表达式列,就可以填充第一组 DataTable 并将其添加到 DataSet 中(参见图 2)。SQL 语句会提取每个订单的 OrderID 和 OrderDate,然后用结果填充 DataTable。在将 OrderTotal 列添加到此 DataTable 之前,必须先创建 DataRelation,以便此 Orders DataTable 中的表达式能够访问 OrderLines DataTable 中的列。

在两个 DataTable 的 OrderID 列之间建立 DataRelation。然后,可以创建 OrderTotal 表达式列并将其添加到 Orders DataTable:

Sum(Child.ExtendedPrice)

 

运算先从括号内进行,然后是括号外,Child 运算符会指示 OrderTotal DataColumn 向下找到子表 (OrderLines DataTable) 并获取 ExtendedPrice 列。然后,Sum 运算符再次使用 DataRelation 将 OrderLines DataTable 中 OrderID 值与父 DataTable 的 OrderID 相符的各行的 ExtendedPrice 列的值相加。最终的结果就是 Orders DataTable 的 OrderTotal 列中显示出每个订单的总金额。

请记住,如果有多个 DataRelation,则需要更改语法,以指明具体的 DataRelation。图 2 中的示例可更改为:

Sum(Child(Orders2OrderLines).ExtendedPrice)

 

您可以使用同样的方法来填充 OrdersByDay DataTable,并使用来自数据库中的不同的订单日期列表来加载它。然后在此 DataTable 和它的 OrderDate 列与 Orders DataTable 的 OrderDate 列之间建立 DataRelation。这样,OrdersByDay DataTable 就可以将订单合计汇总到 OrderTotalByDay 列,如以下所示:

ds.Relations.Add("OrdersByDay2Orders", ds.Tables["OrdersByDay"].Columns["OrderDate"],
ds.Tables["Orders"].Columns["OrderDate"]);
dayOrders.Columns.Add("OrderTotalByDay", typeof(decimal),
"Sum(Child.OrderTotal)");

 

最后两个窗格被绑定到以相同方式加载的 DataTable。您可以拓展此方法以显示其他计算值。例如,您可以轻松地显示包含特定客户的订单合计的网格,只要该客户的信息可以在基表中检索到,就能显示。使用 DataView,还可以针对某个表达式列筛选网格。


问:我有一个父 DataTable (Orders) 和一个子 DataTable (Order Lines)。父 DataTable 有一个针对 OrderTotal 的表达式列。我需要将父表中每一行的某几列显示在子表中。如何才能从父表中提取值?


基于表达式的列可以使用 Child 运算符访问子 DataTable 中的列。不过,它们也可以使用 Parent 运算符来访问父 DataTable 中的列。例如,若要访问父表中某列的值,您可以对先前的示例中的代码进行修改,增加以下代码片段:

ds.Tables["OrderLines"].Columns.Add("OrderTotalForOrderLinesTable",
typeof(decimal), "Parent.OrderTotal");

 

此代码将一个表达式列添加到名为 OrderLines 的子表,该子表从 Orders DataTable 中提取 OrderTotal 值。在创建此新列之前,必须先创建子 DataTable 和父 DataTable,建立这两表之间的 DataRelation 并创建 OrderTotal 列。这是基于子表创建将绑定到某网格的 DataView 的常用方法。

当您将 Parent 运算符用于表达式列时,等于将父 DataTable 中的数据复制到子 DataTable。由于这些表是同步的,所以,如果有人对父 DataTable 中的值进行了更改,则子表中的相应值也会显示这一更改。


问:我有一个 DataGridView 绑定到了 DataTable 的 DefaultView。该 DataTable 的数据源是一个存储过程,该存储过程从我的应用程序的数据库中检索数据。所有这些字段都以特定的顺序返回,但我需要在我的 DataGridView 中以另一种顺序显示这些字段。如何才能轻松地在 DataGridView 中对这些列进行重新排序?


答:有时候您会发现,当更改 SQL 语句中的字段的顺序不太现实时,便有必要更改 DataTable 中的列顺序。DataColumn 公开了一个 SetOrdinal 方法,可用于更改 DataTable 中的各列的顺序。

序号位置从 0 开始计算,因此,要将第一列挪为第四列,需要使用以下代码:

myDataTable.Columns[0].SetOrdinal(3);

 

如果您愿意,可以通过列的名称而不是其序号位置来引用列。

myDataTable.Columns["OrderID"].SetOrdinal(3);

 

第三个序号位置之前的各列都递减一个位置,以便为位置更改腾出空间。


问:我的屏幕上显示了一个客户列表。用户可以从此列表中选择特定的客户,从而显示该客户的所有数据。然后,用户可以从此列表中选择另一个客户,查看该客户的信息。我可以一次将所有客户数据加载到某个 DataTable 中,从而只访问该数据库一次;也可以只提取包含客户及其 ID 的列表,然后在用户选择某客户时从数据库中获取该客户的相关信息。请问这两个方法哪个更好?


答:这个问题很普遍。是应该进行一次较大的访问,还是进行多次较小的访问呢?如果从数据库中提取一些列客户及其全部信息,并将结果以 DataSet 形式保存在内存中,则需要检索大量可能从不会用到的信息。不过,也潜在地减少了对该数据库的访问次数。例如,如果客户表中有 1000 位客户,您提取了每个客户的 40 列数据,则会有 40000 个值检索和存储到 DataTable 中。相比之下,客户列表只需要显示值(例如 CompanyName)和 ID(例如 CustomerID)即可。其余各列仅当用户从客户列表中选择特定客户时才显示。

您描述的另一种方法是只加载包含 CompanyName 和 CustomerID 的客户列表。然后,当用户选择某客户时,应用程序再次调用数据库,从中检索该特定客户的详细信息。此方法预先加载的数据明显要少,但访问数据库的次数更多。

哪个方法更好取决于其他因素。例如,如果客户数据很少,很快就能收集到且不会使性能低于负载测试,则预先收集所有数据可能更好。不过,如果数据更改非常频繁,则此方法的主要缺陷是加载的数据很快就会失效。因此,在选择预先将所有数据加载到内存之前,首先应该考虑加载数据所需的时间、此数据需要耗费多少内存、实际加载多少数据、加载时数据库中的数据是否可能会更改(这会导致您屏幕上显示的数据过时)。通常,当数据集较小且后台不会更新时,我推荐采用此方法。

第二个方法是仅当用户请求时才提取每个用户的详细信息。使用这种方法,列表的加载速度通常更快,因为加载的数据较少。而且,使用这种方法得到的数据是最新的,因为唯一不变的数据是客户列表。这种方法的不足之处是:每次用户选择一个客户时,应用程序都要访问数据库。因此,应该对获取客户详细信息的调用进行优化,因为您是使用键字段 (CustomerID) 来获取客户数据。如果这些检索客户数据的调用导致了明显的延迟,速度下降的原因可能是网络带宽、查询本身或通过应用程序的体系结构传送数据出现了问题。如果您决定使用此方法,但之后却遇到了速度下降的问题,则必须确定速度变慢的原因以及该问题是否能解决。

最后要说的是,如何做出此类决定绝不是三言两语就能回答的。大部分情况下,我们只能视具体情况而定。不过,一般情况下,我发现较好的方法是,先加载包含最少数据的列表,然后在用户选择特定的项时再回去检索其他数据。 要知道,性能不是要考虑的唯一问题,您还需要考虑数据是否过时的问题。


问:我需要呈现已绑定到 DataTable 并加载了销售信息的 DataGridView。我的应用程序必须允许用户对该网格中的销售数据进行计算,并允许他们选择将哪些行和列包含在计算中。如何才能做到在不用再次访问数据库的情况下对 DataTable 进行计算?


答:这种情况下,Compute 方法可以很好地解决问题。基于表达式的列一次只能对一行执行计算,而使用 DataTable 的 Compute 方法却可以在给定筛选器和表达式的条件下对一组行执行计算。筛选器用于限定 Compute 方法应对哪些行执行计算,而表达式则给出了要执行的聚合函数表达式。例如,您可以向 DataTable 添加一列,用于计算美国客户的平均订单合计。这个工具非常好,不需要再次访问数据库即可快速对一组行执行计算。

图 3 显示的示例窗体包含一个订单列表以及每个订单的合计。此示例包含了表达式和筛选器所需的所有基本要素。在左侧的“筛选器”组合框中,用户可以选择要筛选的字段、要应用于该字段的运算符以及筛选条件。通过绑定到 DataColumn 的列表填充字段列表:

DataColumn[] colList = new DataColumn[orders.Columns.Count];
orders.Columns.CopyTo(colList, 0);
ddlFilterColumn.DataSource = colList;
ddlFilterColumn.DisplayMember = "ColumnName";

 

图 3 使用 Compute 方法计算
图 3  使用 Compute 方法计算 (单击该图像获得较小视图)
图 3 使用 Compute 方法计算
图 3  使用 Compute 方法计算 (单击该图像获得较大视图))

此代码创建了一个 DataColumn 数组,使用 DataTable 中的列填充它并将其绑定到组合框。通过将 DataColumn 数组绑定到组合框,可以访问能够使用的所选 DataColumn 的其他属性。例如,如果所选列是一个字符串,则该值必须加上引号。由于 DataColumn 已绑定到该组合框,所以此代码能判断出是否应为该值加引号。

Calculation 组合框包含一个聚合函数列表和一个所选聚合函数要运算的列的列表。列列表绑定到 ddlCalculationColumn 组合框的方式与绑定到 ddlFilterColumn 组合框的方式相同。聚合函数列表加载有以下七个可供 Compute 方法使用的函数:Sum、Count、Avg、Min、Max、StDev 和 Var。

单击该按钮,便会执行图 4 中显示的代码。它首先确定为筛选器所选的列的类型。如果列的类型为字符串或日期,则会为条件值加引号。形成了筛选器和表达式之后,系统会将它们传递给 Compute 方法,结果将发送到文本框。


将您想向 John 询问的问题和提出的意见发送至 mmdata@microsoft.com.

John Papa是 ASPSOFT (aspsoft.com) 的高级 .NET 顾问,也是一名棒球迷,在夏季的夜晚,他的大多数时光都是和家人及其忠实的狗 Kadi 一起为洋基队加油助威度过的。John 是一位 C# MVP,他撰写了多本有关 ADO、XML 和 SQL Server 的书籍,并经常在一些行业大会(例如 VSLive)上发表演讲,还撰写博客,博客地址为 codebetter.com/blogs/john.papa

Subscribe  摘自  January 2007 期刊  MSDN Magazine.
Figure 2 求和的表达式列
using(SqlCommand cmd = new SqlCommand("SELECT o.OrderID, o.OrderDate " +
"FROM Orders o WHERE YEAR(o.OrderDate) = @year " +
"ORDER BY o.OrderDate DESC", cn))
{
cmd.CommandType = CommandType.Text;
cmd.Parameters.AddWithValue("@year", year);
SqlDataAdapter adpt = new SqlDataAdapter(cmd);
DataTable orders = new DataTable("Orders");
adpt.Fill(orders);
}
ds.Tables.Add(orders);
ds.Relations.Add("Orders2OrderLines", ds.Tables["Orders"].Columns["OrderID"], ds.Tables["OrderLines"].Columns["OrderID"]);
orders.Columns.Add("OrderTotal", typeof(decimal),
"Sum(Child.ExtendedPrice)");

Figure 4 执行计算
if (!ValidateControls()) return;
Type colType = ((DataColumn) (ddlFilterColumn.SelectedItem)).DataType;
string filterCriteria = string.Empty;
if (ddlFilterOperator.Text == "Like")
{
filterCriteria = string.Format("'{0}%'", txtFilterCriteria.Text);
}
else
{
filterCriteria = (colType.Name == "String" ||
colType.Name == "DateTime") ?
string.Format("'{0}'", txtFilterCriteria.Text) :
txtFilterCriteria.Text;
}
string filter = string.Format("{0} {1} {2}", ddlFilterColumn.Text,
ddlFilterOperator.Text, filterCriteria);
string expression = string.Format("{0}({1})", ddlAggregate.Text,
ddlCalculationColumn.Text);
object value = this.orders.Compute(expression, filter);
txtResult.Text = value.ToString();

转载于:https://www.cnblogs.com/baiwei1977/archive/2007/02/02/637675.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值