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

ADO.NET 中的并发模型,允许 Oracle 数据库协商断开模型限制的方法

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.24/5 (7投票s)

2006年12月5日

8分钟阅读

viewsIcon

48919

downloadIcon

316

在 Oracle 数据库中使用 ADO.NET 并发模型。

引言

.NET 应用程序访问数据库数据最流行的工具是 ADO.NET。这组组件包含三个主要类,用于操作和存储数据:DataReaderDataSetDataAdapterDataReader 只能读取数据,不能作为数据感知组件(如 DataGridView 等)的数据源;DataSet 提供了所有作为数据源的接口,但断开模型存在一些限制,对于某些类型的应用程序可能变得很重要,特别是对于在多用户并发环境中活动地更改数据库数据的桌面程序。下面我们将考虑主要的限制和流行的解决方案;还考虑了非标准解决方案。

必备组件

所有代码示例都适用于 Microsoft Visual Studio 2005;我们使用 Oracle 10i 作为数据库,并使用 Oracle Development Tools (ODT.NET) 来生成访问数据的示例代码,最新版本可在此处获取 此处,但展示的主要原理也适用于其他 RDBMS。

并发模型

ADO.NET 的一个严重问题是并发模型;众所周知,ADO.NET 使用乐观并发。DataSet 的断开性质使其功能强大。但是,乐观并发可能导致问题,尤其是在频繁更新数据的情况下。乐观并发意味着您假设在您进行编辑时,没有人会编辑同一条记录。因为您“乐观地”认为两个人不会同时编辑同一条记录,所以在用户开始编辑记录时不会立即对其应用锁定。而是,仅在实际尝试更新时才应用锁定。

为了检查乐观并发冲突,DataAdapter 配置向导会编写 SQL 语句来验证您即将更新或删除的记录自从您最初将其加载到 DataSet 中以来没有发生更改。向导通过向 SQL 语句添加一个相当大的 WHERE 子句来做到这一点,以验证它正在更新或删除与下载并放入 DataSet 中的内容完全匹配的内容。

在我们的示例中,我们使用了一个名为“COLORS”的简单表。

create table COLORS
(
  COLOR_ID   NUMBER not null,
  COLOR_NAME  varchar2(100) not null,
  RED      number not null,
  GREEN number not null,
  BLUE number not null,
  CONSTRAINT PK_COLORS PRIMARY KEY (COLOR_ID)
)

--fill table-------------
insert into colors(color_id, color_name, red, 
       green, blue) values(1, 'black', 0, 0, 0);
insert into colors(color_id, color_name, red, 
       green, blue) values(2, 'white', 254, 254, 254);
insert into colors(color_id, color_name, red, 
       green, blue) values(3, 'red', 254, 0, 0);
insert into colors(color_id, color_name, red, 
       green, blue) values(4, 'green', 0, 254, 0);
insert into colors(color_id, color_name, red, 
       green, blue) values(5, 'blue', 0, 0, 254);
insert into colors(color_id, color_name, red, 
       green, blue) values(6, 'yellow', 0, 254, 254);
commit;

要生成程序,只需将 OracleExplorer 面板中的“COLORS”表(这是 ODT.NET 工具的一部分)拖放到应用程序窗体上(图 1)。

concurmodel1

图 1。

向导会自动生成 OracleDataAdapterOracleConnection,因此我们应该手动添加一个非类型化的 DataSet 和一个 DataGridView,以及一个用于将更改发布到数据库的 Button,并添加几行代码使我们的程序能够工作。

public Form1()
{
   InitializeComponent();
   colorsOracleDataAdapter1.Fill(dataSet1, "COLORS");
   dataGridView1.DataSource = dataSet1.Tables["COLORS"];
}

private void button1_Click(object sender, EventArgs e)
{
        try
        {                
                colorsOracleDataAdapter1.Update(dataSet1, "Colors");
                //Display confirmation message
                MessageBox.Show("Changes saved successfully !");                
        }
        catch (Exception ex)
        {
             MessageBox.Show(ex.ToString(), "Exception Occured");
        }
}

现在我们可以看到向导生成的更新 SQL 是什么样的。更新命令文本

UPDATE "COLORS" SET "COLOR_ID"=:current_COLOR_ID_param0, 
       "COLOR_NAME"=:current_COLOR_NAME_param1, 
       "RED"=:current_RED_param2, "GREEN"=:current_GREEN_param3, 
       "BLUE"=:current_BLUE_param4 WHERE 
       "COLOR_ID"=:original_COLOR_ID_param5 AND 
       "COLOR_NAME"=:original_COLOR_NAME_param6 AND 
       "RED"=:original_RED_param7 AND 
       "GREEN"=:original_GREEN_param8 AND 
       "BLUE"=:original_BLUE_param9

DataAdapter 配置向导的默认行为是将所有列都包含在 WHERE 子句中。这可以防止您的代码覆盖其他用户在您的代码检索行到您的代码尝试提交行中待定更改之间所做的更改。由于另一位用户更新的行的值在数据库中已更改,因此表中没有行满足查询 WHERE 子句中所有条件。因此,数据库不会修改该行。

此更新查询可以工作,但显然不是最优的,因为 RDBMS 必须选择非索引参数。因此,它适用于像我们的“COLORS”表这样的小表,但对于包含大量行的实际表,此查询可能会减慢数据库速度。

我们应该修改此查询并进行一些优化。作为一种选择,我们可以只将主键包含在 SQL UPDATE 查询中

UPDATE "COLORS" SET "COLOR_ID"=:current_COLOR_ID_param0, 
   "COLOR_NAME"=:current_COLOR_NAME_param1,
   "RED"=:current_RED_param2, 
   "GREEN"=:current_GREEN_param3, 
   "BLUE"=:current_BLUE_param4 
   WHERE "COLOR_ID"=:original_COLOR_ID_param5

这会创建一个“后发先至”的更新场景。两个更新尝试都将成功。显然,数据库不会维护两组更改。最后一次更新所做的更改将覆盖之前的更改。“后发先至”场景不适用于您希望防止用户无意中覆盖其他用户更改的情况。

无论如何,我们可以改进这种情况,并在“COLORS”表中添加时间戳列,并将主键和时间戳列包含在 WHERE 子句中

alter table COLORS add TIMESTAMP date;

此外,我们应该创建一个触发器,在插入或更新后生成该列的新值

create or replace trigger TBIU_COLORS
  before insert or update on COLORS
  for each row
declare
begin  
  select sysdate into :new.timestamp from dual;
end TBIU_COLORS;

并创建索引以优化查询

create index INDX_COLORS_ID_STAMP on COLORS (color_id, timestamp);

每次行的内容更改时,SQL Server 都会修改该行的时间戳列的值。我们将查询修改为如下所示

UPDATE "COLORS" SET "COLOR_ID"=:current_COLOR_ID_param0, 
       "COLOR_NAME"=:current_COLOR_NAME_param1, 
       "RED"=:current_RED_param2, 
       "GREEN"=:current_GREEN_param3, 
       "BLUE"=:current_BLUE_param4 
       WHERE "COLOR_ID"=:original_COLOR_ID_param5 and 
       "TIMESTAMP"=:original_TIMESTAMP_param6

由于服务器每次更新行时都会为时间戳列生成一个新值,因此您可以在查询更新的 WHERE 子句中使用主键和时间戳列的组合来确保您不会覆盖其他用户的更改。此方法的缺点是必须进行额外的工作,特别是如果您必须处理现有数据库,并且所有表都是在没有时间戳列的情况下创建的,并且在数据库中保存了额外的数据。

正如我们所见,我们可以选择这些方法中的一种在我们的 ADO.NET 应用程序中使用。这些方法的最大优点是通用性,但通用性并不意味着最优性和便利性,当然也不是万能的。除了每种方法各自的缺点外,还有共同的缺点

  1. 如果您在“COLORS”表中添加列,则必须手动修改 UPDATE 语句并重新编译应用程序;
  2. 每次,即使您只更改了一个列值,应用程序也必须更新整行。

使用 RDBMS 特定的机制来处理乐观并发冲突

每个数据库都有一个维护不同并发模型的机制。此机制的重要部分是记录锁定。一旦您锁定数据库记录,请考虑并发的类型:乐观或悲观。如果您在用户开始编辑记录后立即对其应用锁定,则为悲观并发,其他用户在第一个用户完成编辑并将更改发布到数据库之前无法更改该记录。如果您仅在实际尝试更新时才对记录应用锁定,则为乐观并发。使用 ADO.NET DataSetDataAdatper,您别无选择,只能使用乐观变体,但其实现远非理想。

好的,那么“正确”的实现是什么样的?乐观并发的主要场景

  1. 用户更改了记录并尝试将其发布到数据库。
  2. 应用程序尝试对当前记录执行“select for update”。如果记录被另一位用户锁定,则第一个用户会遇到并发冲突。
  3. 如果记录未锁定,应用程序会检查记录是否被另一位用户更改。如果记录已被更改,应用程序会告知用户并提供覆盖或取消已发布记录的选项。
  4. 如果记录未更改,或者用户决定覆盖它,应用程序将仅更新当前用户更改的列。
  5. 如果更改成功,应用程序将执行“commit”;如果操作失败,则执行“rollback”以解锁当前记录。

“Select for update”非常重要,因为在 Oracle 数据库中,用户在“commit”之前看不到另一位用户所做的更改,但由于我们已经为某些记录执行了“select for update”,如果另一位用户尝试更改它,Oracle 将抛出相应的异常。

谢天谢地,Oracle 为每个表都有一个唯一的标识符,名为 ROWID,因此我们可以将其用作任何程序中的通用行标识符。要为 Oracle 数据库实现乐观模型,首先我们应该像下面这样更改 UpdateCommandCommandText

UPDATE "COLORS" SET "COLOR_ID"=:current_COLOR_ID_param0, 
       "COLOR_NAME"=:current_COLOR_NAME_param1, 
       "RED"=:current_RED_param2, 
       "GREEN"=:current_GREEN_param3, 
       "BLUE"=:current_BLUE_param4 
       WHERE "ROWID"=:original_ROWID_param5.

此外,我们应该删除大于四个的参数,并添加一个类型为 Varchar2 的新参数(见下图 2)。

concurmodel2

图 2。

然后,我们应该为 OracleDataAdapter1 编写一个 RowUpdating 事件处理程序。

private void colorsOracleDataAdapter1_RowUpdating(object sender, 
        Oracle.DataAccess.Client.OracleRowUpdatingEventArgs e)
{        
            OracleCommand oracleDMLCommand = null;
            OracleDataReader oracleDataReader = null;
            if (e.StatementType != StatementType.Update)
                return;
            try
            {
                //clear row erros
                e.Row.ClearErrors();
                //create command
                oracleDMLCommand = colorsOracleConnection1.CreateCommand();
                oracleDMLCommand.CommandType = CommandType.Text;
                //select for update nowait
                oracleDMLCommand.CommandText = "select * from colors c where
                        c.rowid= :cur_rowid for update nowait";
                        oracleDMLCommand.Parameters.Add("cur_rowid", 
                        OracleDbType.Varchar2,
                        e.Row["ROWID"].ToString().Length, 
                e.Row["ROWID"].ToString(), ParameterDirection.Input);
                //execute command
                oracleDataReader = oracleDMLCommand.ExecuteReader();
                //read data from database reader
                while (oracleDataReader.Read())
                {
                    //iterate all fields from datareader
                    for (int i = 0; i < oracleDataReader.FieldCount; i++)
                    {
                        //compare original data in column with data from
                        // database
                        if (e.Row[oracleDataReader.GetName(i), 
                            DataRowVersion.Original].ToString() !=
                            oracleDataReader.GetValue(i).ToString())
                        {
                            //We found a difference, inform user about it
                            //and offer to overwrite record
                            if (DialogResult.Cancel == MessageBox.Show("Row 
                                 was changed by another user, rewrite anyway?",
                                 "Warning", MessageBoxButtons.OKCancel, 
                                 MessageBoxIcon.Question))
                                 throw new Exception("Row was changed 
                                 by another user");
                             else
                                break;
                        }
                    }
                }

            }
            catch (OracleException ee)
            {
               //set row error
               e.Row.SetColumnError("COLOR_ID", ee.ToString());
               throw ee;
            }
            catch (Exception ex)
            {   
               //set row error
               e.Row.SetColumnError("COLOR_ID", ex.ToString());             
               throw ex;
            }
            finally
            {
               if(oracleDataReader !=  null)
                  oracleDataReader.Close();
               if (oracleDMLCommand != null)
                  oracleDMLCommand.Dispose();
            }
}

OracleDataAdapter 会在更新命令之后自动执行“commit”命令,因此我们不必担心它。这不是理想的解决方案,但至少它可以检查记录是否被锁定,检查是否被另一位用户更改,并允许用户在必要时覆盖新记录。

测试

要测试我们的应用程序,我们可以使用 SQLPlus 应用程序。当应用程序启动时,会出现如图 3 所示的窗口。

concurmodel3

图 3。

然后使用 SQLPlus 以 scott/tiger 身份连接,并执行命令

select * from colors where color_id=1 for update nowait;

然后我可以用我们的应用程序尝试更改一些数据:例如,我将红色列从 0 更改为 5。当我尝试发布更改时,我收到:ORA-00054: resource busy and acquire with NOWAIT specified。好的,在 SQLPlus 中,我执行以下命令

update colors set red=1 where color_id=1;
commit;

这样我就可以更改数据行并提交更改。现在这条记录应该可以进行修改了。我尝试使用我们的应用程序发布更改

concurmodel4

图 4。

现在我看到该记录已准备好进行修改,并且它已被另一位用户更改(图 4)。我决定重写它并按 OK,但我当然可以取消。如果您从此表中选择数据,您可以看到该行已被我们的应用程序更改,并且并发冲突已得到正确处理。

正如这里所示,优雅地处理乐观并发冲突需要一些编码工作。

ADO.NET 中有关并发模型的问题也可以通过其他方式解决。显然,可以编写可视组件来设置合适的并发模型,这些模型不仅是乐观的,而且像 DataSet 一样,可以作为数据感知组件的数据源。

关注点

我从 Delphi 转到 .NET,对断开的 ADO.NET 组件提供的可能性感到有些沮丧,所以我决定编写自己的解决方案,该解决方案允许我使用所需的并发模型和其他一些附加功能,如单条记录刷新、部分数据选择等。

© . All rights reserved.