65.9K
CodeProject 正在变化。 阅读更多。
Home

一个按需加载数据、批量保存更改到 Oracle 数据库并在 ASP.NET 中进行排序和分页的 DataGrid

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (35投票s)

2005年8月23日

CPOL

22分钟阅读

viewsIcon

202029

downloadIcon

2816

本文演示了如何使用 Oracle 数据库实现分页 DataGrid。它还演示了如何通过仅检索必要的行和批量更新更改来限制 Web 服务器和浏览器之间交换的数据量。

Sample Image

引言

在开发企业应用程序时,总是需要以网格形式呈现数据,以便用户查看或修改。

微软通过 Visual Studio .NET 提供了一个非常强大的工具,即 `DataGrid` Web 组件。市面上有许多组件,它们具有不同的形状、颜色和功能。本示例使用的是微软提供的标准实现,并结合 Oracle 数据库。

本文的动机基于以下事实:

  • 我的一个客户希望使用 AIX 5.2 上的 Oracle 作为后端。
  • 在研究了不同的选项后,我们选择使用 ASP.NET,因为它提供了一个出色的工具 `DataGrid` 来呈现业务数据。
  • 作为技术经理,我有责任编写一个可工作的示例程序,以帮助开发人员启动项目。
  • MSDN 或 Code Project 等网站上的大多数(如果不是全部)文章都描述了 `DataGrid` 与 SQL Server 的实现,因此,本示例必须完全从零开始编写。
  • 结果集的分页通常通过将整个结果集加载到 ASP.NET 进程的内存中,或通过使用需要临时表的复杂技术来完成。由于系统需要支持 200 多个并发用户,因此这并非一个可行的选项。
  • 应用程序应尽可能避免与 IIS Web 服务器传输数据,因为生产系统中(我的客户的 200 个站点)将通过具有高延迟的卫星连接进行数据传输,这是系统的一个主要限制。

本示例展示了如何

  • 使用 SQL Loader 在 Oracle 中创建并填充示例表(脚本在 _Oracle_ 文件夹中提供)。
  • 编写查询以仅从 Oracle 检索所需数据(也称为检索第 _n_ 到第 _m_ 行)。
  • 使用带绑定变量的参数化查询,以提高性能并防止“SQL 注入”攻击。
  • 连接到 Oracle 并使用 ASP.NET 设置连接池。
  • 从 C# 代码向 Oracle 发送查询。
  • 对结果进行升序或降序排序。
  • 对结果集进行分页。
  • 更改每页的行数。
  • 在 PL/SQL 中“批量”更新数据以提高性能。
  • 调用 Oracle 中的 PL/SQL 过程。
  • 使用一些 CSS 来修改 `DataGrid` 的呈现。
  • 通过使用技巧来自定义 `DataGrid` 的输出。

本示例未展示如何

  • 设置 Oracle 数据库。
  • 安装 Oracle 提供的 ODP.NET 组件。

必备组件

在运行此示例之前,您的机器上需要准备好一些组件。

  • 您的电脑上安装了 Visual Studio .NET 2003。
  • 您需要访问 Oracle 数据库服务器(版本 9 或 10,此示例尚未在 8 上测试),运行在 Windows 或 Unix 上。此示例已在运行 Windows XP 和 IBM AIX 5.2 的 Oracle 9 和 10 上进行测试。
  • 为了将您的机器连接到 Oracle 数据库(本地或远程),您需要下载 Oracle 开发的 Oracle Data Provider。ODP.NET(如其名)可以在此处下载。如果您没有特殊要求,应下载版本 10。尽管 VS.NET 附带的默认 Oracle Data Provider 可能有效,但尚未经过测试。

在此示例中,还假设读者知道如何使用 VS.NET 2003 附带的标准 `DataGrid` Web 控件。CodeProject 上有许多文章演示了如何使用此组件。这里的目标是关注 Oracle 端以及如何利用其功能。

数据库部分的安装

为了查询示例表,必须首先创建表并填充它。此示例使用一个简单的表,用于存储美国邮政编码信息。脚本包含在名为 _Oracle_ 的文件夹中的存档中。

首先,我们需要创建表。您应该使用“普通”Oracle 用户,避免使用 `sys` 或 `system`,因为这些用于管理任务。为了执行脚本,您应该发出以下命令:

sqlplus orauser/orapasswd@INSTANCE

其中 `<orauser>` 和 `<orapasswd>` 是您的 DBA 给您的用户名和密码(不要忘记中间的“斜杠”),`INSTANCE` 是您要登录的实例名称,如您的 _tnsnames.ora_ 文件中定义的那样。

如果连接成功,您应该会看到类似以下消息(Windows 上的 Oracle 9i)

Connected to:
Oracle9i Enterprise Edition Release 9.2.0.1.0 - Production
With the Partitioning, OLAP and Oracle Data Mining options
JServer Release 9.2.0.1.0 - Production

SQL>

现在您可以执行脚本(假设您在 _table.sql_ 脚本所在的文件夹中运行了 _sqlplus_ 程序)。

@table.sql

此时,您应该能够在数据库中看到该表。尝试以下命令,查看它是否存在

desc ZIPCODE;

您应该会看到类似以下内容

Name                                  Null?    Type
------------------------------------- -------- --------------

ZIP                                   NOT NULL VARCHAR2(5)
STATEID                               NOT NULL CHAR(2)
NAME                                           VARCHAR2(40)
LONGITUDE                                      NUMBER
LATITUDE                                       NUMBER
POPULATION                                     NUMBER
ALLOCFACTOR                                    NUMBER

现在我们已经创建了表,我们需要填充它。由于我们正在创建一个包含数千条记录的表,我们将使用一个名为 SQL Loader 的工具,它可以将文件作为输入并直接在 Oracle 中填充表。该工具有很多参数化选项,所有这些都基于一个控制文件。在 _Oracle_ 文件夹中,提供了两个文件。这两个文件将用于将数据加载到表中。这两个文件是

  • 包含数据的 _zipcode.dat_。
  • 包含 SQL Loader 完成其工作所需信息的 _zipcode.ctl_。

现在,我们需要发出以下命令来实际触发加载。这假设您正在从 _dat_ 和 _ctl_ 文件所在的目录调用 _sqlldr_。如果不是这种情况,只需提供完整路径。

sqlldr orauser/orapass@INSTANCE control=zipcode.ctl data=zipcode.dat

其中 `<orauser>` 和 `<orapasswd>` 是您的 DBA 给您的用户名和密码(不要忘记中间的“斜杠”),`INSTANCE` 是您要登录的实例名称,如您的 _tnsnames.ora_ 文件中定义的那样。

完成此操作后,您应该能够查询表并查看它是否包含数据。再次使用 _sqlplus_ 登录

sqlplus orauser/orapasswd@INSTANCE

并发出以下命令

SQL> SELECT ZIP, NAME FROM ZIPCODE WHERE ROWNUM < 11;

ZIP   NAME
----- ----------------------------------------
01001 AGAWAM
01002 CUSHMAN
01005 BARRE
01007 BELCHERTOWN
01008 BLANDFORD
01010 BRIMFIELD
01011 CHESTER
01012 CHESTERFIELD
01013 CHICOPEE
01020 CHICOPEE

10 rows selected.

`where` 子句中的 `rownum < 11` 用于将输出限制为 10 行。

现在,如果您询问 Oracle 此表中有多少行,您应该得到与以下相同的确切结果

SQL> select count(1) from zipcode;

  COUNT(1)
----------
     29467

此时,您可以退出 _sqlplus_,表的设置已完成。

在进一步讨论之前,对我们刚刚创建的表进行简要评论。如果您查看 _table.sql_ 脚本,您可以看到表的结构声明包含以下内容

organization index

这告诉 Oracle,此表中的数据应在磁盘上以索引的形式物理组织,使用键字段作为组织索引的字段。在此示例中,主键(确保唯一性)是 ZIP 字段。这里的目标是减少 I/O 需求,因为这种表很可能是一个“查找”表,数据很少写入,大部分时间用于读取。这不是我们用于涉及大量 OLTP 活动的表的结构。对于查找表,这种物理组织非常有吸引力,因为我们可能需要通过提供 ZIP 来查找数据并检索与其关联的信息。如果您在不提供任何 `where` 子句或任何 `order by` 子句的情况下查询表,数据将按 ZIP 排序,因为这是数据在磁盘上的物理组织方式。

ASP.NET 部分的安装

此项目是 Visual Studio .NET 中的一个常规 Web 项目。将 Zip 存档中的 _datagrid_ 文件夹复制到您的硬盘驱动器,并在 IIS 中创建一个指向该文件夹的虚拟文件夹。

然后,修改 _Web.config_ 文件,使您的 IIS 实际指向您的 Oracle 实例。需要修改的三个参数是 `Source`、`User ID` 和 `Password`。

<appSettings>
 <add key="connectionString" value="Data Source=XXXX;Pooling=true; 
    User ID=XXXX;Password=XXXX; Min Pool Size=3;Max Pool Size=25;
    Connection Lifetime=7200;Connection 
    Timeout=15;Incr Pool Size=3;Decr Pool Size=1;" />
</appSettings>

检索第 n 到第 m 行

正如本文开头所承诺的,我们现在将介绍如何从一个集合中检索第 _n_ 行到第 _m_ 行。一个集合可以是一个表、视图生成的结果集,或任何 SQL 查询的结果。据我所知,Oracle 没有可理解的 SQL 命令,例如

Select X, Y, Z
From Table
Where ...
And ROWNUM BETWEEN 21 AND 40

能够只返回第 21 行到第 40 行。这里要小心,有一个陷阱。如果集合没有明确地完全排序,那么每次提取第 21 行到第 40 行并不保证意味着相同的事情。稍后会详细介绍。

在寻找关于 Oracle 的顶级信息时,我最喜欢的网站之一是Tom Kyte 的网站。关于检索第 _n_ 行到第 _m_ 行的问题是一个非常流行的问题,你可以在此处阅读更多内容。

整个想法是使用 Oracle 为每个结果集提供的“隐藏”列,称为 `ROWNUM`。这个伪列实际上是 Oracle 给我们的一个序号,如果我们想使用它的话。这是一个例子。假设我们想检索第 21 到 40 行,因为我们在第 2 页,每页显示 20 行。让我们看看这个查询

SELECT P.*, ROWNUM RNUM
FROM (SELECT ZIP, STATEID, NAME, LONGITUDE, 
             LATITUDE, POPULATION, ALLOCFACTOR
      FROM   ZIPCODE
      ORDER BY ZIP) P
WHERE ROWNUM < 41;

此查询的结果是

ZIP   ST NAME          LONGITUDE   LATITUDE POPULATION ALLOCFACTOR     RNUM
----- -- ------------ ---------- ---------- ---------- ----------- --------

01001 MA AGAWAM         72.622739  42.070206     15338     .002549        1

01002 MA CUSHMAN        72.51565   42.377017     36963     .006144        2

01005 MA BARRE          72.108354  42.409698      4546     .000756        3

...

ZIP   ST NAME          LONGITUDE   LATITUDE POPULATION ALLOCFACTOR     RNUM
----- -- ------------ ---------- ---------- ---------- ----------- --------
01081 MA WALES         72.204592  42.062734      1732     .000288       40

40 rows selected.

这对于解决我们问题的一半来说已经足够了,但是我们得到了 40 行,而我们只想要 20 行。解决方案是使用这个结果集作为外部查询的源,外部查询将“修剪”它。

SELECT ZIP, STATEID, NAME, LONGITUDE, LATITUDE, POPULATION, ALLOCFACTOR
FROM (SELECT P.*,
             ROWNUM RNUM
      FROM (SELECT ZIP, STATEID, NAME, LONGITUDE, 
                   LATITUDE, POPULATION, ALLOCFACTOR
            FROM   ZIPCODE
            ORDER BY ZIP) P
      WHERE ROWNUM < 41)
WHERE RNUM > 20;

这一次,我们使用第二个 `ROWNUM` 伪列来过滤 `ROWNUM >= 20`。在使用 `ROWNUM` 时要小心,尽管它在查询中的所有表都具有相同的名称,但它并不具有相同的含义。`ROWNUM` 列是为每个集合动态生成的。

现在,查询的结果是

ZIP   ST NAME                  LONGITUDE   LATITUDE POPULATION ALLOCFACTOR
----- -- -------------------- ---------- ---------- ---------- -----------
01036 MA HAMPDEN               72.431823  42.064756      4709     .000783

01038 MA HATFIELD              72.616735   42.38439      3184     .000529

01039 MA HAYDENVILLE           72.703178  42.381799      1387     .000231

...

ZIP   ST NAME                  LONGITUDE   LATITUDE POPULATION ALLOCFACTOR
----- -- -------------------- ---------- ---------- ---------- -----------
01080 MA THREE RIVERS          72.362352  42.181894      2425     .000403

01081 MA WALES                 72.204592  42.062734      1732     .000288

20 rows selected.

这种查询的主要风险是在最内部查询中给出 `ORDER BY` 子句,该子句不能明确地排序我们正在“修剪”的集合。由于 ZIP 字段是表的主键,因此它足以满足要求。但是,在我们的网格中,我们希望允许用户按任何列进行升序和降序排序。我们需要确保所使用的排序足以明确。

参数化查询:使用绑定变量,提高性能并防止“SQL 注入”攻击

在上面的查询中,有三个我们想要在代码中定义的参数。显然,需要限制(20 和 41),也需要排序列。

为了向查询传递参数,通常采取的第一种方法是连接字符串以“动态”重新生成 SQL 文本。这可能看起来像这样

...
strSQL += "WHERE ROWNUM < ";
strSQL += (lowerLimit).ToString();
...

我们不会对此过多讨论。只是不要这样做!如果您这样做,从 ASCII 字符串的角度来看,每个参数化查询都是唯一的(或几乎是唯一的)。这将迫使 Oracle 每次都解析这些查询,以便选择执行计划。相反,请使用绑定变量。为此,我们只需以更通用的格式重写查询,并放置等效的占位符,以便让 Oracle 解析器知道我们需要在哪里使用变量。这是以更通用格式重写的查询

SELECT ZIP, STATEID, NAME, LONGITUDE, LATITUDE, POPULATION, ALLOCFACTOR
FROM (SELECT P.*,
             ROWNUM RNUM
      FROM (SELECT ZIP, STATEID, NAME, LONGITUDE, 
                   LATITUDE, POPULATION, ALLOCFACTOR
            FROM   ZIPCODE
            ORDER BY ZIP) P
      WHERE ROWNUM < :1)
WHERE RNUM > :2;

请注意粗体显示的占位符以及用于定义变量 1 和 2 的冒号符号。

通过使用这些 Oracle 称之为“绑定变量”的东西,发送到数据库的 ASCII 字符串始终相同,因此,引擎可以缓存其执行计划并反复重用。这大大提高了系统的速度和可伸缩性。

这些绑定变量的另一个好处是无法通过 SQL 注入攻击来欺骗系统,因为执行计划已经由引擎定义,并且它不会重新解析查询。如果尝试传递错误的参数(例如在 SQL 注入攻击之后),Oracle 将只会抛出一个异常,告诉调用代码参数类型错误,但它 **不会** 执行修改后的查询。

连接到 Oracle 并使用 ASP.NET 设置连接池

还记得我们在 _Web.config_ 文件中修改的连接字符串吗?此字符串由 ASP 工作进程用于根据逐字节匹配定义连接池。这就是为什么它直接在 _Web.config_ 中定义的原因。这样,我们就可以确保不会有拼写错误(从而确保所有线程都使用相同的连接池),并且当我们需要将整个应用程序指向另一个数据库服务器时,维护起来也更容易。当然,有一些参数需要定义

Pooling=true;
Min Pool Size=3;
Max Pool Size=25;
Connection Lifetime=7200;
Connection Timeout=15;
Incr Pool Size=3;
Decr Pool Size=1;

这些参数是不言自明的,除了“Connection Lifetime”和“Connection Timeout”有点令人困惑。以下是这两个参数的官方文档,摘自 ODP.NET 组件安装的文档

连接生命周期 (Connection Lifetime):连接的最长生命周期(以秒为单位)。此属性指定连接的生命周期(以秒为单位)。在连接放回池中之前,会检查连接的生命周期。如果连接的生命周期超过此属性值,则连接将被关闭并释放。如果此属性值为 0,则从不检查连接生命周期。如果超出生命周期的连接导致池中的连接数低于 Min Pool Size,则不会关闭和释放它们。

连接超时 (Connection Timeout):等待从连接池获取空闲连接的最长时间(以秒为单位)此属性指定 `Open()` 方法在终止请求之前可以获取池化连接的最长时间(以秒为单位)。此值仅在连接池中没有空闲连接且达到 Max Pool Size 时才生效。如果在指定时间内没有可用的空闲连接,则会抛出异常。连接超时不限制打开新连接所需的时间。

此属性值对池化连接请求生效,不对新连接请求生效。

对于已经安装 ODP.NET 的用户,以下是查找连接字符串中所有可能参数详细信息的链接:_ms-help://MS.VSCC.2003/Oracle.DataAccess.1033/DataProviderHelp/ HTML/ConnectionConnectionString.htm#i1000291_。

从现在开始,每次我们需要打开到 Oracle 的连接时,我们都会这样做

try
{
    // The "connectionString" is defined in the Web.config file
    connection = new 
      OracleConnection(ConfigurationSettings.AppSettings["connectionString"]);
    connection.Open();
    ...
}
catch (Exception ex)
{
    ...
}
finally
{
    // Clean up
    ...
    if (connection != null)
    {
        connection.Close();
        connection.Dispose();
    }
}

如您所见,我们在运行时从应用程序设置中检索 `connectionString` 字符串。

另一个值得注意的细节是 `finally` 部分中对 `Close()` 和 `Dispose()` 的调用,以便将连接返回到池中。如果我们不这样做,池就会“泄漏”,我们将不得不等待连接超时才能再次在池中可用。

从 C# 代码向 Oracle 发送查询

我们现在有一个在数据库中定义了数据的表,并且我们知道如何从池中获取连接并在使用完毕后将其归还。让我们看看如何通过向 Oracle 发送查询并检索结果集来实际完成一些有用的事情。

这是一个示例,展示了我们如何使用我们之前查看的查询。通用模式总是相同的。

private void DataBind(string orderby, string direction, 
                        int pageindex, int rowsperpage)
{
    // An important thing to note here:
    // When we select data one page at a time,
    // we need to fully qualify the inner most "ORDER BY"
    // clause, so that the data is explicitly
    // fully sorted. If not, then there is no guarantee
    // as to the order which implies that we may see
    // strange things like having the same entry duplicated
    // and showed on different pages.

    // It is the responsibility of this method
    // to do the "magic" and organize the "ORDER BY" clause
    // sent to Oracle to prevent this problem from occurring.

    // This needs to be modified for
    // all queries that would be processed like this.
    // The objective is to generate
    // a modified "ORDER BY" clause that FULLY sorts
    // UNAMBIGUOUSLY the result set before Oracle trims it.

    // Use a default sorting.
    string modifiedOrderby = "ZIP ASC";

    if (orderby.Equals("ZIP"))
        // Note the space after the column name.
        // ZIP being the key, the result set is guaranteed
        // to be fully sorted by using it alone.
        modifiedOrderby = "ZIP " + direction;
    else
        // Note the space and the comma after and before the column names.
        // Note also that we choose to sort on
        // the user's choice and then on the ZIP ascending.
        // The reason for this is to guarantee
        // that the result set is unambiguously sorted.
        // The ZIP column being the key of the table,
        // the sorting is guaranteed to be unique.
        modifiedOrderby = orderby + " " + direction + ", ZIP ASC";

    try
    {
        // Same as above. Get the connection string from the Web.config file.
        // IMPORTANT:
        // The way the ODP.NET code can
        // identify the connection pool is by matching
        // Connection strings. Thus, one should
        // only retrieve such a string from the
        // Web.config file to avoid typos and simplify everything when we need
        // to redirect to another DB server.
        connection = new 
          OracleConnection(ConfigurationSettings.AppSettings["connectionString"]);
        connection.Open();

        // Note in the following query that we are
        // using place holders where parameters will be used.
        // These place holders will be bound to variables, thus ensuring:
        // - Security: SQL-Injection is impossible with bind variables.
        // - Performance: We greatly improve the performance
        //           and scalability of the application,
        // by simplifying Oracle's job. The text
        // of the query will be recognized by the Oracle parser
        // and fetched from its library cache, thus avoiding "hard parses".
        string SQLString =    "SELECT ZIP, STATEID, NAME," + 
                    " LONGITUDE, LATITUDE, POPULATION, ALLOCFACTOR " +
                    "FROM (SELECT P.*, " +
                    "             ROWNUM RNUM " +
                    "      FROM (SELECT ZIP, STATEID, NAME," + 
                    " LONGITUDE, LATITUDE, POPULATION, ALLOCFACTOR " +
                    "            FROM   ZIPCODE " +
                    "            ORDER BY " + modifiedOrderby + ") P " +
                    "      WHERE ROWNUM < :1) " +
                    "WHERE RNUM >= :2";

        command = new OracleCommand( SQLString, connection);
        command.CommandType = CommandType.Text;
        // This is used to tell the ODP.NET code that the
        // binding should be done using the sequence the parameters
        // are passed in.
        command.BindByName = false;

        // The first parameter to use is defined
        // as a "Decimal" (Oracle datatype) and as "input" only (from
        // Oracle's perspective)
        command.Parameters.Add(new OracleParameter("ROWNUM", 
             OracleDbType.Decimal, ParameterDirection.Input));
        // Set the actual value of the parameter, based on the value passed
        command.Parameters["ROWNUM"].Value = (1 + pageindex 
                              * rowsperpage + rowsperpage);

        // The second parameter to use is defined
        // as a "Decimal" (Oracle datatype) and as "input" only (from
        // Oracle's perspective)
        command.Parameters.Add(new OracleParameter("RNUM", 
                OracleDbType.Decimal, ParameterDirection.Input));
        // Set the actual value of the parameter, based on the value passed
        command.Parameters["RNUM"].Value = (1 + pageindex * rowsperpage);

        // Execute the query and bind the grid to the result set.
        DATAGRID.DataSource = command.ExecuteReader();
        DATAGRID.DataBind();

    }
    catch(Exception ex)
    {
        ERROR.Visible = true;
        ERROR.Text += ex.Message;
    }
    finally
    {
        if (dataReader != null)
        {
            dataReader.Close();
            dataReader.Dispose();
        }

        if (command != null)
            command.Dispose();

        if (connection != null)
        {
            connection.Close();
            connection.Dispose();
        }
    }
}

这里唯一的“技巧”是我们使用字符串连接来传递 `"ASC"` 或 `"DESC"` 以进行升序或降序排序。没有办法通过绑定变量传递此信息。但是,这在这里不是问题,因为这些查询实际上是不同的,因为排序子句强制 Oracle 生成不同的执行计划。这种方法与使用绑定变量的建议不矛盾。

关于这段代码片段的最后一件事:传递给 Oracle 用于分页的值是动态计算的,基于用户想要查看的每页行数和指示网格当前查看的页码的页索引。

最后,实际调用是通过调用 `ExecuteReader()` 完成的,它返回一个 `OracleDataReader` 对象,该对象提供了解析结果集的方法。在此示例中,我们选择将返回的 `OracleDataReader` 对象直接传递给 `DataGrid` 对象进行绑定。

对结果进行升序或降序排序

一旦构建了检索数据页面的方法,对数据进行排序就轻而易举了。我们需要为用户单击列标题时触发的 `DATAGRID_SortCommand` 事件添加一个处理程序。这里的实现是第一次单击时升序排序,然后降序,然后升序... 为了做到这一点,我们需要保存当前数据排序的列和方向。这是通过将这些参数保存在 ViewState 中完成的。

private void DATAGRID_SortCommand(object source, 
         System.Web.UI.WebControls.DataGridSortCommandEventArgs e)
{

    // This event is triggered when the user
    // clicks on a column header, thus requesting
    // that the data be sorted, according to the selected column.

    string direction;

    // Check if this is the second time the same column
    // name is selected (this means we need to sort descending)
    // Otherwise, the sort should be done ascending since
    // this is a new column (compared to the one saved in the
    // view state) that was selected.
    if (CurrentSortDirection == "ASC" && CurrentSortColumn == e.SortExpression)
        direction = "DESC";
    else
        direction = "ASC";

    // Remember what column the data is sorted by.
    CurrentSortColumn = e.SortExpression;

    // Make sure the page remembers the sorting direction.
    CurrentSortDirection = direction;

    // Reselect the set of data to show. Note that the page index is reset to 0.
    DATAGRID.CurrentPageIndex = 0;
    DataBind(e.SortExpression, direction, 0, DATAGRID.PageSize);
}

逻辑很简单。当事件触发时,处理程序将用户单击的列与数据排序的列进行比较。如果它们相同,则切换排序方向;如果不同,则将方向重置为 `ASC`。然后,我们将新参数保存在 ViewState 中,重置 DataGrid 当前查看的页码(否则,用户会感到困惑),并调用方法获取新页面以重新绑定网格。

分页结果集

分页采用相同的方法。我们使用一个处理程序来捕获事件,识别网格将显示的页面索引,获取新页面并绑定。这在 `DATAGRID_PageIndexChanged()` 方法中完成,如下所示

private void DATAGRID_PageIndexChanged(object source, 
        System.Web.UI.WebControls.DataGridPageChangedEventArgs e)
{
    // Let the grid know that it needs to update the pager at the bottom
    DATAGRID.CurrentPageIndex = e.NewPageIndex;
    // Retrieve a new subset to refresh the grid.
    DataBind(CurrentSortColumn, CurrentSortDirection, 
                  e.NewPageIndex, DATAGRID.PageSize);
}

更改每页行数

同样的技术也用于更改网格每页显示的行数。这在 `CHANGEROWSPERPAGE_Click` 方法中完成,如下所示

protected void CHANGEROWSPERPAGE_Click(object sender, System.EventArgs e)
{
    // Assume the number entered is valid
    ROWSPERPAGE.BackColor = Color.FromArgb(255, 255, 255);

    try
    {
        DATAGRID.PageSize = int.Parse((ROWSPERPAGE).Text);
        DATAGRID.CurrentPageIndex = 0;
        DataBind(CurrentSortColumn, 
                 CurrentSortDirection, 0, DATAGRID.PageSize);
    }
    catch (Exception ex)
    {
        // There was a problem when trying to parse the number passed.
        // Let the user know about it.
        ROWSPERPAGE.BackColor = Color.FromArgb(235, 0, 235);

        // Let the user know about the problem
        ERROR.Visible = true;
        ERROR.Text = "Please enter a valid integer" + 
                     " to change the number of rows per page.";
    }
}

在 PL/SQL 中“批量”更新数据以提高性能

我们还没有查看 _GridPager.aspx_ 文件,因为假设读者熟悉 ASP.NET。然而,其中有一个特别有趣的部分

...
<asp:TemplateColumn SortExpression="NAME" HeaderText="Name">
    <ItemTemplate>
        <asp:TextBox id="NAME" CssClass="textbox" 
                runat="server" 
                Text='<%# DataBinder.Eval(Container, "DataItem.NAME") %>'>
        </asp:TextBox>
    </ItemTemplate>
</asp:TemplateColumn>
...

重要的部分是使用“模板列”来提供一个文本框,用户可以在其中修改内容。当网格绑定数据时,此文本框会通过从基于查询返回的 `OracleDataReader` 对象构建的数据源中检索 `NAME` 字段来填充。

用户完成更改后,这些更改将“批量”保存到数据库中。为了实现这一点,我们使用一个处理程序来响应单击“保存更改”时触发的事件。方法如下:

  • 构建一个包含邮政编码值的数组,因为它们代表表的主键。我们需要它们来更新数据库。
  • 构建一个包含 `NAME` 值的数组,通过检索用户可能进行的更改。我们在这里不尝试弄清楚值是否已更改。批量更新整个子集要快得多。
  • 确保这两个数组之间的索引匹配。
  • 调用 PL/SQL 过程,通过传递这两个数组,让 PL/SQL 完成繁重的工作。
  • 如果一切正常则提交更改,否则回滚更改。

让我们看看 `SAVE_Click` 处理程序以响应按钮单击

protected void SAVE_Click(object sender, System.EventArgs e)
{

    // The objective here is to build arrays that contain:
    // - The ZIP code (key of the table)
    // - The names as entered on the page
    // Since the number of rows is dynamic,
    //    these arrays should be dynamic as well
    string[] ar_zip     = new string[DATAGRID.PageSize];
    string[] ar_name    = new string[DATAGRID.PageSize];

    int index = 0;

    foreach ( DataGridItem row in DATAGRID.Items)
    {
        //Need to get the ZIP code and the new name out of the grid
        ar_zip[index]  = DATAGRID.Items[row.ItemIndex].Cells[0].Text;
        ar_name[index] = ((TextBox)row.FindControl("NAME")).Text;
        ++index;
    }

    // These two arrays will be passed
    // to the parameters objects to let the ODP know
    // about the size of the varchar2 variables
    // defining the fields in the table.

    // In the ZIPCODE table, the ZIP field is defined as VARCHAR2(5)
    int[] p_zip_bind = new int[index];
    for (int i=0; i<index; ++i)
        p_zip_bind[i] = 5;

    // In the ZIPCODE table, the NAME field is defined as VARCHAR2(40)
    int[] p_name_bind = new int[index];
    for (int i=0; i<index; ++i)
        p_name_bind[i] = 40;

    // Oracle parameters declarations.
    OracleParameter p_zip     = new OracleParameter("p_zip", 
          OracleDbType.Varchar2, ParameterDirection.Input);
    p_zip.CollectionType      = OracleCollectionType.PLSQLAssociativeArray;
    p_zip.Value               = ar_zip;
    p_zip.ArrayBindSize       = p_zip_bind;
    p_zip.Size                = index;

    OracleParameter p_name    = new OracleParameter("p_name", 
          OracleDbType.Varchar2, ParameterDirection.Input);
    p_name.CollectionType     = OracleCollectionType.PLSQLAssociativeArray;
    p_name.Value              = ar_name;
    p_name.ArrayBindSize      = p_name_bind;
    p_name.Size               = index;

    // Save the new values.
    // Note: We don't attempt to determine what row has been changed.
    // We just update the whole subset, it is way easier.
    try
    {
        connection = new 
          OracleConnection(ConfigurationSettings.AppSettings["connectionString"]);
        connection.Open();

        command = new OracleCommand("GRIDPAGER.SaveChanges", connection);
        command.CommandType = CommandType.StoredProcedure;
        command.BindByName = false;

        // add the parameters to the command object
        command.Parameters.Add(p_zip);
        command.Parameters.Add(p_name);

        // Start a transaction
        transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted);

        // execute the procedure
        command.ExecuteNonQuery();

        // Commit the changes
        transaction.Commit();
    }
    catch(Exception ex)
    {
        // There could be a problem in the transaction itself
        // Check if we have a valid transaction object first.
        if(transaction != null)
            transaction.Rollback();

        // Since this is a sample program, we need
        // to get some feedback in case there
        // was an exception raised when calling Oracle
        ERROR.Visible = true;
        ERROR.Text = ex.Message;
    }
    finally
    {

        if (transaction != null)
            transaction.Dispose();

        if (command != null)
            command.Dispose();

        if (connection != null)
        {
            connection.Close();
            connection.Dispose();
        }
    }

}

前面代码中值得注意的三个主要点

  • 对我们的参数使用 `OracleParameter` 对象的 `ArrayBindSize` 属性。这是必需的,因为参数在数据库中定义为 `VARCHAR2`,我们需要让 ODP.NET 知道实际的最大长度。这是通过传递一个数组的引用来完成的,该数组包含每个邮政编码字段和每个 `NAME` 字段的长度。我们在这里不试图耍花招,我们只是传递数据库中定义的最大长度。
  • 使用事务来定义提交或回滚点。
  • 调用 PL/SQL 过程是通过传递包含它的包的名称和过程本身的实际名称来完成的,如下所示:`command = new OracleCommand("GRIDPAGER.SaveChanges", connection);`。显然,用于连接到 Oracle 的 Oracle 用户(在 _Web.config_ 中定义的)必须被允许执行该过程。

这里的重点是将所有行一次性传递给数据库。现在让我们看看 PL/SQL 过程如何“批量”处理这些数据。

Oracle 中的 PL/SQL 过程

对于那些刚接触 Oracle PL/SQL 开发的人,我强烈推荐使用 PL/SQL Developer,因为它是事实上的标准工具。您可以在此处找到更多信息。我们正在调用的过程定义在名为“_GRIDPAGER_”的包中,其名称为“`SaveChanges`”。这是其内容。首先,让我们看看包定义

create or replace package common.GRIDPAGER is

  -- Author  : Patrice Borne (patriceborne@yahoo.com)
  -- Created : 06/19/2005 15:50:27
  -- Purpose : This package contains all the PL/SQL
  --           procedures used by the GridPager.aspx sample.

  -- Public type declarations
    type t_zip  is table of ZIPCODE.ZIP%TYPE  index by pls_integer;
    type t_name is table of ZIPCODE.NAME%TYPE index by pls_integer;

    PROCEDURE SaveChanges ( p_zip   in  t_zip,
                            p_name  in  t_name);

end GRIDPAGER;

然后,让我们看看实际的实现

create or replace package body common.GRIDPAGER is


   ---------------------------------------------------------------------
   -- This procedure updates the changes sent by the C# code
   -- Note that there is no point in checking if something was changed
   -- or not since it is way faster to just update "in bulk" using the
   -- FORALL construction.
   ---------------------------------------------------------------------
   PROCEDURE SaveChanges ( p_zip   in  t_zip,
                           p_name  in  t_name ) IS

   BEGIN

     -------------------------------------------------------------------------
     -- The parameters passed are PL/SQL associative arrays and are declared
     -- as such in the calling code.
     -- The loop here is implicit with the FORALL statement.
     -- Note 1: There is no commit nor exception handling code in here
     -- since we want the calling code to be fully in charge.
     -- Note 2: In order to update the entries
     -- into the table, we need to receive
     -- the ZIP code as this is the key of the table.
     -------------------------------------------------------------------------
     FORALL i IN p_zip.FIRST..p_zip.LAST
      UPDATE ZIPCODE
      SET    NAME  = p_name(i)
      WHERE  ZIP   = p_zip(i);

   END;


end GRIDPAGER;

前面代码中需要注意的几点是

  • 所有过程都必须在包声明中定义,并且必须与实际实现的签名匹配。
  • 定义类型 `t_zip` 和 `t_name` 告诉 PL/SQL 引擎,它应该根据表中字段的类型从 Oracle 数据字典中检索实际定义。
  • `FORALL` 结构是 PL/SQL 特有的,允许我们一次性调用 SQL 引擎进行更新,一次性传递所有参数(性能更好)。
  • 没有错误处理或事务管理(没有 `commit` 或 `rollback` 命令),因为我们希望调用者(C# 代码)完全负责这两个方面(向用户发送反馈并管理此代码可能属于的“大局”)。

如果您无法访问 PL/SQL developer,仍然可以从命令行安装该包

sqlplus <orauser>/<orapassword>@INSTANCE @GRIDPAGER.pck

使用 CSS 修改数据网格的渲染

正如您在本文开头截图中看到的,网格在 IE 中的渲染是使用层叠样式表(CSS)定制的。首先要做的是在 _GridPager.aspx_ 源文件的 `` 部分添加一行

<head>
    ...
    <style type="text/css" 
      media="all">@import url( css/grid.css ); </style>
</head>

这将指示 IE 从服务器下载 _grid.css_ 文件并将其用于渲染。

这个 CSS 文件没什么特别的,它只是纯粹的普通 CSS。不过,更有趣的是,看看我们如何在框架渲染 `DataGrid` 时修改生成的 HTML,以及如何使用 CSS 文件中定义的 CSS 类。

使用 DataGrid 的有趣技巧

在网格渲染过程中一个重要的方法是 `DATAGRID_Created` 方法,它处理 `OnItemCreated` 事件。这个方法在渲染过程的每个主要步骤都会被框架调用。为了知道网格在渲染过程中的位置,框架会传递一个 `DataGridItemEventArgs` 对象。这个对象的一个关键成员是 `DataGridItemEventArgs.Item.ItemType`,它告诉我们当前正在渲染什么。我们只需要将其值与 `ListItemType` 的枚举值进行比较。

在此示例中,我们检查了两个值

  • `ListItemType.Header` 在标题即将渲染时发生。然后我们将一个委托设置为我们自己的 `RenderTitle` 方法。
  • `ListItemType.Pager` 在寻呼机(网格底部)即将渲染时发生。然后我们将一个委托设置为我们自己的 `RenderBottom` 方法。

RenderTitle 和 RenderBottom 都基于相同的模式构建。它们都接收一个 HtmlTextWriter 和一个 Control 引用。HtmlTextWriter 让我们直接访问 HTML 输出。Control 对象的引用让我们访问属于网格并在我们自己的处理过程中需要渲染的控件集合。

这两个方法只是生成直接发送到输出的 HTML,以便创建额外的行(用于标题)来为网格渲染标题,或者设置 CSS 类。这是 `RenderTitle` 方法的代码

protected virtual void RenderTitle(HtmlTextWriter writer, Control ctl)
{
    // TR is on the stack writer's stack at this point...
    writer.AddAttribute("colspan", "7");
    writer.AddAttribute("class", "header");
    writer.RenderBeginTag("td");
    writer.Write("US ZIP Codes");
    writer.RenderEndTag(); // Writes </td>
    writer.RenderEndTag(); // Writes </tr>

    // New Row
    writer.AddAttribute("class", "subheader");
    writer.RenderBeginTag("tr");

    writer.AddAttribute("colspan", "3");
    writer.RenderBeginTag("td");
    writer.Write("Location");
    writer.RenderEndTag(); // Writes </td>

    writer.AddAttribute("colspan", "2");
    writer.RenderBeginTag("td");
    writer.Write("Coordinates");
    writer.RenderEndTag(); // Writes </td>

    writer.AddAttribute("colspan", "2");
    writer.RenderBeginTag("td");
    writer.Write("Population");
    writer.RenderEndTag(); // Writes </td>

    writer.RenderEndTag(); // Writes </tr>

    writer.RenderBeginTag("tr");
    //Render the cells for the header row.
    foreach(Control control in ctl.Controls)
        control.RenderControl(writer);

    // We don't need to write the </tr>.
    // The grid will do that for us.
}

我们希望 IE 使用的 CSS 类通过调用 `AddAttibute()` 方法传递。另请注意我们如何使用 `RenderBeginTag()` 和 `RenderEndTag()` 方法“打开”和“关闭” `<tr>` 标签。

最后,我们需要使用 `foreach` 循环渲染网格在正常过程中需要渲染的控件(意味着如果我们不劫持渲染过程会发生什么)。

结论:局限性和改进空间

这篇文章确实有点长。然而,它是为那些需要尽快使用 Oracle 作为数据库开始 ASP.NET 的人而写的。

ODP.NET 组件还有许多内容有待演示,例如使用 Oracle 的 REF Cursors(它们工作得非常好),或者使用 PL/SQL 过程返回的多个 REF Cursors,这些 REF Cursors 需要在解析结果集时保持同步。填充 `DataGrid` 的另一个绝佳方法是动态构建 `DataView` 对象并将其用作 `DataGrid` 的源。最后,此代码没有展示如何通过使用 Oracle 最伟大的功能之一(在我看来):其锁定机制,实现更安全的数据库更新。由于我们正在构建一个 Web 应用程序并使用连接池,因此我们不能简单地在用户查看网格中的数据时锁定行。我们需要实现所谓的“乐观锁定”机制。这些都是未来文章的好主题!

参考文献

对于那些有兴趣获取更多 Oracle 信息的人,这里有一些链接

  • AskTom:这是 Oracle 的 **权威** 参考。
  • Oracle 文档库:包含所有文档。不过,信息量**非常大**。
  • 在此处下载 Oracle 10g Release 2 here
  • 在此处下载 ODP.NET 安装程序 here
  • ODP.NET 论坛:提问并帮助他人使用 ODP.NET。
  • 在此处获取有关 PL/SQL developer 的更多信息 here

历史

  • 2005年8月:第一版。
© . All rights reserved.