11.5
分离和重新集中改变
11.5 . 1 用GetChanges方法节省带宽
DataSet和DataTable对象都公开了一个重载的GetChanges方法。在调用DataSet对象的GetChanges方法时,收到一个新的DataSet对象,它具有与原来的对象一样的结构,但是只包含原来DataSet中已修改的行。
注意:严格来说上面的说法并不准确,除了包含请求的已修改行外,它可能还包含其他维持引用完整性所需要的行。
在前面我们提到了获取时间戳的开放式并发。我们获取新的值并存储在web服务的DataSet中。但是DataSet与客户端应用程序是分离的。怎样将新的时间戳加入到客户端应用程序的DataSet中呢?
当然,我们可以返回一个包含与客户端应用程序所有数据相同的新DataSet,但这显然不是利用带宽的最好方式。
最经济的办法是让web服务返回它所收到的DataSet,并包括新的时间戳值。
但这只是解决了问题的一部分。客户端现在有了新的时间戳值,但是如何将web服务返回的DataSet集成到客户端应用程序的DataSet中去呢?
11.5 . 1.1 DataSet对象的Merge方法。
简单的答案是使用DataSet对象的Merge方法。DataSet对象的Merge方法让您可以将DataSet,DataTable,DataRow对象数组的内容合并到一个现有的DataSet中。
不过,这个例子并不有用。一些开发人员会将包含DataTable对象的、具有相同名称但是有完全不同结构的两个DataSet对象结合在一起。
注意两个例子不同之处在于主键。如果ADO.NET在合并数据时遇到有同样主键的行,会把内容合并到一行中。注意图中有错误,第二个DataTable最后一个ID应该是4。
注意在Merge方法的结果中,来自被合并的DataSet的数据具有优先权。
所以我们的解决方案是:
代码:
WebServiceClass objWebService = new WebServiceClass();
DataSet dsMain = objWebService.GetDataSet();
ModifyDataSetContents(dsMain);
DataSet dsChanges = dsMain.GetChanges();
dsChanges = objWebService.SubmitChanges(dsChanges);
dsMain.Merge(dsChanges);
11.5 . 1.2 Merge方法和RowState属性
我们几乎已经完成了,但是还差一点。如果我们检查原来在主DataSet中修改的内容,那么您就会发现他们确实有了新的时间戳值。不过这些行的RowState属性仍然是Modified。
如果用户再次提交更改,那么GetChanges方法返回的DataSet仍然包括用户以前修改的行。当web服务收到这个DataSet并试图提交更改时,更新尝试会引发一个异常,因为数据库已经包含了这些更改。
因为ado.net并不知道我们已经向数据库提交了更改。当web服务提交改变时,ado.net将已修改的行的RowState从Modified改为Unmodified。但这个改变发生在web服务的DataSet上。ADO.NET不改变客户端应用程序的主DataSet的已修改行的RowState,因为这两个DataSet间并没有链接。
合并DataSet不会改变主DataSet的修改行的RowState属性。我们可以在调用Merge后,对主DataSet调用AcceptChanges方法将已修改行的RowState改回为Unmodified,如下:
WebServiceClass objWebService = new WebServiceClass();
DataSet dsMain = objWebService.GetDataSet();
ModifyDataSetContents(dsMain);
DataSet dsChanges = dsMain.GetChanges();
dsChanges = objWebService.SubmitChanges(dsChanges);
dsMain.Merge(dsChanges);
dsMain.AcceptChanges();
11.5 . 1.3 Merge方法和自动递增值
在提交新增的定单后,新产生的OrderID包含在web服务返回的DataSet中,不过这些值无法与原值合并,因为主键不一样。
我们有下面几种解决方案:
ü 合并前清理
在合并前从主DataSet中删除新定单达到所需要的结果:
WebServiceClass objWebService = new WebServiceClass();
DataSet dsMain = objWebService.GetDataSet();
ModifyDataSetContents(dsMain);
DataSet dsChanges = dsMain.GetChanges();
dsChanges = objWebService.SubmitChanges(dsChanges);
// Remove the pending new orders from the main DataSet
// before merging the orders returned by the Web service.
DataTable tbl = dsMain.Tables[ " Orders " ];
foreach (DataRow row in tbl.Select( "" , "" , DataRowViewState.Added))
tbl.Rows.Remove(row);
dsMain.Merge(dsChanges);
dsMain.AcceptChanges();
ü 改变DataSet对象中的主键
在原来的DataTable中增加一个新列,起名为PseudoKey。
如何在合并前改变主键并在其后将主键重新设置回去呢?代码:
WebServiceClass objWebService = new WebServiceClass();
DataSet dsMain = objWebService.GetDataSet();
ModifyDataSetContents(dsMain);
DataSet dsChanges = dsMain.GetChanges();
dsChanges = objWebService.SubmitChanges(dsChanges);
// Change the primary key in each table to be the pseudokey.
DataTable tblMain = ds.Tables[ " Orders " ];
DataColumn[] pkOriginal = tblMain.PrimaryKey;
tblMain.PrimaryKey = new DataColumn[] {tblMain.Columns["PseudoKey"]} ;
DataTable tblChanges = dsChanges.Tables[ " Orders " ];
tblChanges.PrimaryKey = new DataColumn[] {tblChanges.Columns["PseudoKey"]} ;
dsMain.Merge(dsChanges);
// Set the primary key in the main table back to its original value.
tblMain.PrimaryKey = pkOriginal;
dsMain.AcceptChanges();
但是PseudoKey列的值应该怎样取呢?如果它不对应于数据库中的一列,如何产生一个唯一值呢?我们可以使用另一个递增列。
11.5 . 1.3 可用的选项小结
略。
11.6 得体地处理失败的更新尝试
11.6 . 1 事先为冲突做出计划
DataAdapter对象的ContinueUpdateOnError属性
11.6 . 2 通知用户失败
代码1:
try
{
MyDataAdapter.ContinueUpdateOnError = true;
MyDataAdapter.Update(MyDataTable);
if (MyDataTable.HasErrors)
{
string strMessage;
strMessage = "The following row(s) were not updated " +
"successfully:";
foreach (DataRow row in MyDataTable.Rows)
if (row.HasErrors)
strMessage += "\n\r" + (string) row["ID"] +
row.RowError;
MessageBox.Show(strMessage);
}
else
MessageBox.Show("All updates succeeded");
}
catch (Exception ex)
{
MessageBox.Show("The following exception occurred: \n\r" +
ex.Message);
}
我们可以为用户提交这样的信息:(图略)
代码:
DataTable tbl = CreateFillAndModifyTable();
DataRow row = tbl.Rows[ 0 ];
Console.WriteLine( " Current Balance Due: " + row[ " BalanceDue " ]);
Console.WriteLine( " Original Balance Due: " +
row[ " BalanceDue " , DataRowVersion.Original]);
我们已经知道如何用DataRow访问一行的当前和原来内容,但我们如何从数据库中提取当前内容呢?
11.6 . 3 提取冲突行的当前内容
要提取所需行的当前内容,我们可以使用DataAdapter对象的RowUpdated事件。下面代码确定在更新尝试中DataAdapter是否遇到错误。如果错误是一个并发异常,那么代码会用带参数的查询提取数据库中相应行的内容。
代码:
private void HandleRowUpdated( object sender, OleDbRowUpdatedEventArgs e)
{
if ((e.Status == UpdateStatus.ErrorsOccurred) &&
(e.Errors.GetType == typeof(DBConcurrencyException))
{
ConflictAdapter.SelectCommand.Parameters[0].Value = e.Row["ID"];
int intRowsReturned = ConflictAdapter.Fill(ConflictDataSet);
if (intRowsReturned == 1)
e.Row.RowError = "The row has been modified by another user.";
else
e.Row.RowError = "The row no longer exists in the database.";
e.Status = UpdateStatus.Continue;
}
}
注意:如果没有将Status改变为Continue或SkipCurrentRow,那么在更新尝试失败时DataAdapter会自动将文字附加到RowError属性上。
这个代码段提取数据库中相应行的当前内容并加入到单独的DataSet中,这样您就可以在更新尝试之后对数据进行检查。现在您有了构建上图信息的所有数据。
11.6 . 4 如果第一次没有成功
用户可能不希望重新查询数据库并重新对新数据进行同样的改变才能重新提交这些改变。我们如何使用ado.net模型简化这一过程?
失败的原因在于DataAdapter在并发检查中使用的数据与数据库中这行的当前内容不匹配。在刷新DataRow的内容前,欧文们不能向数据库存储在DataRow中的改变。
只要我们可以改变DataRow的original 值,就可以顺利提交改变。
使用DataSet对象的Merge方法导入“New Original”值
在前面代码片段中,我们捕获了DataAdapter对象的RowUpdated事件。如果当前行没有成功更新,那么代码就捕获数据库中相应行的内容加入到ConflictDataSet的新DataSet中。假设主DataSet称为MainDataSet,我们可以用下面一行代码将ConflictDataSet的内容合并到MainDataSet中:
MainDataSet.Merge(ConflictDataSet, true );
这段代码不改变主DataSet中的DataRow对象的当前内容。它只是用冲突的DataSet的数据覆盖DataRow对象的原来值。
于是便可以更新了。
注意:不能得到有关在数据库中不再存在的行的“New Original”信息。如果一个更新尝试因为行在数据库中不再存在而失败,那么就不能用这种方法刷新行中的原来值。如果希望向数据库重新添加DataRow对象的当前内容,那么可以用从DataTable对象的Rows集合中删除这个DataRow再重新添加它。这样会使DataRow对象的RowState改变为Added。
11.5 . 1 用GetChanges方法节省带宽
DataSet和DataTable对象都公开了一个重载的GetChanges方法。在调用DataSet对象的GetChanges方法时,收到一个新的DataSet对象,它具有与原来的对象一样的结构,但是只包含原来DataSet中已修改的行。
注意:严格来说上面的说法并不准确,除了包含请求的已修改行外,它可能还包含其他维持引用完整性所需要的行。
在前面我们提到了获取时间戳的开放式并发。我们获取新的值并存储在web服务的DataSet中。但是DataSet与客户端应用程序是分离的。怎样将新的时间戳加入到客户端应用程序的DataSet中呢?
当然,我们可以返回一个包含与客户端应用程序所有数据相同的新DataSet,但这显然不是利用带宽的最好方式。
最经济的办法是让web服务返回它所收到的DataSet,并包括新的时间戳值。
但这只是解决了问题的一部分。客户端现在有了新的时间戳值,但是如何将web服务返回的DataSet集成到客户端应用程序的DataSet中去呢?
11.5 . 1.1 DataSet对象的Merge方法。
简单的答案是使用DataSet对象的Merge方法。DataSet对象的Merge方法让您可以将DataSet,DataTable,DataRow对象数组的内容合并到一个现有的DataSet中。
不过,这个例子并不有用。一些开发人员会将包含DataTable对象的、具有相同名称但是有完全不同结构的两个DataSet对象结合在一起。
注意两个例子不同之处在于主键。如果ADO.NET在合并数据时遇到有同样主键的行,会把内容合并到一行中。注意图中有错误,第二个DataTable最后一个ID应该是4。
注意在Merge方法的结果中,来自被合并的DataSet的数据具有优先权。
所以我们的解决方案是:
代码:
WebServiceClass objWebService = new WebServiceClass();
DataSet dsMain = objWebService.GetDataSet();
ModifyDataSetContents(dsMain);
DataSet dsChanges = dsMain.GetChanges();
dsChanges = objWebService.SubmitChanges(dsChanges);
dsMain.Merge(dsChanges);
11.5 . 1.2 Merge方法和RowState属性
我们几乎已经完成了,但是还差一点。如果我们检查原来在主DataSet中修改的内容,那么您就会发现他们确实有了新的时间戳值。不过这些行的RowState属性仍然是Modified。
如果用户再次提交更改,那么GetChanges方法返回的DataSet仍然包括用户以前修改的行。当web服务收到这个DataSet并试图提交更改时,更新尝试会引发一个异常,因为数据库已经包含了这些更改。
因为ado.net并不知道我们已经向数据库提交了更改。当web服务提交改变时,ado.net将已修改的行的RowState从Modified改为Unmodified。但这个改变发生在web服务的DataSet上。ADO.NET不改变客户端应用程序的主DataSet的已修改行的RowState,因为这两个DataSet间并没有链接。
合并DataSet不会改变主DataSet的修改行的RowState属性。我们可以在调用Merge后,对主DataSet调用AcceptChanges方法将已修改行的RowState改回为Unmodified,如下:
WebServiceClass objWebService = new WebServiceClass();
DataSet dsMain = objWebService.GetDataSet();
ModifyDataSetContents(dsMain);
DataSet dsChanges = dsMain.GetChanges();
dsChanges = objWebService.SubmitChanges(dsChanges);
dsMain.Merge(dsChanges);
dsMain.AcceptChanges();
11.5 . 1.3 Merge方法和自动递增值
在提交新增的定单后,新产生的OrderID包含在web服务返回的DataSet中,不过这些值无法与原值合并,因为主键不一样。
我们有下面几种解决方案:
ü 合并前清理
在合并前从主DataSet中删除新定单达到所需要的结果:
WebServiceClass objWebService = new WebServiceClass();
DataSet dsMain = objWebService.GetDataSet();
ModifyDataSetContents(dsMain);
DataSet dsChanges = dsMain.GetChanges();
dsChanges = objWebService.SubmitChanges(dsChanges);
// Remove the pending new orders from the main DataSet
// before merging the orders returned by the Web service.
DataTable tbl = dsMain.Tables[ " Orders " ];
foreach (DataRow row in tbl.Select( "" , "" , DataRowViewState.Added))
tbl.Rows.Remove(row);
dsMain.Merge(dsChanges);
dsMain.AcceptChanges();
ü 改变DataSet对象中的主键
在原来的DataTable中增加一个新列,起名为PseudoKey。
如何在合并前改变主键并在其后将主键重新设置回去呢?代码:
WebServiceClass objWebService = new WebServiceClass();
DataSet dsMain = objWebService.GetDataSet();
ModifyDataSetContents(dsMain);
DataSet dsChanges = dsMain.GetChanges();
dsChanges = objWebService.SubmitChanges(dsChanges);
// Change the primary key in each table to be the pseudokey.
DataTable tblMain = ds.Tables[ " Orders " ];
DataColumn[] pkOriginal = tblMain.PrimaryKey;
tblMain.PrimaryKey = new DataColumn[] {tblMain.Columns["PseudoKey"]} ;
DataTable tblChanges = dsChanges.Tables[ " Orders " ];
tblChanges.PrimaryKey = new DataColumn[] {tblChanges.Columns["PseudoKey"]} ;
dsMain.Merge(dsChanges);
// Set the primary key in the main table back to its original value.
tblMain.PrimaryKey = pkOriginal;
dsMain.AcceptChanges();
但是PseudoKey列的值应该怎样取呢?如果它不对应于数据库中的一列,如何产生一个唯一值呢?我们可以使用另一个递增列。
11.5 . 1.3 可用的选项小结
略。
11.6 得体地处理失败的更新尝试
11.6 . 1 事先为冲突做出计划
DataAdapter对象的ContinueUpdateOnError属性
11.6 . 2 通知用户失败
代码1:
try
{
MyDataAdapter.ContinueUpdateOnError = true;
MyDataAdapter.Update(MyDataTable);
if (MyDataTable.HasErrors)
{
string strMessage;
strMessage = "The following row(s) were not updated " +
"successfully:";
foreach (DataRow row in MyDataTable.Rows)
if (row.HasErrors)
strMessage += "\n\r" + (string) row["ID"] +
row.RowError;
MessageBox.Show(strMessage);
}
else
MessageBox.Show("All updates succeeded");
}
catch (Exception ex)
{
MessageBox.Show("The following exception occurred: \n\r" +
ex.Message);
}
我们可以为用户提交这样的信息:(图略)
代码:
DataTable tbl = CreateFillAndModifyTable();
DataRow row = tbl.Rows[ 0 ];
Console.WriteLine( " Current Balance Due: " + row[ " BalanceDue " ]);
Console.WriteLine( " Original Balance Due: " +
row[ " BalanceDue " , DataRowVersion.Original]);
我们已经知道如何用DataRow访问一行的当前和原来内容,但我们如何从数据库中提取当前内容呢?
11.6 . 3 提取冲突行的当前内容
要提取所需行的当前内容,我们可以使用DataAdapter对象的RowUpdated事件。下面代码确定在更新尝试中DataAdapter是否遇到错误。如果错误是一个并发异常,那么代码会用带参数的查询提取数据库中相应行的内容。
代码:
private void HandleRowUpdated( object sender, OleDbRowUpdatedEventArgs e)
{
if ((e.Status == UpdateStatus.ErrorsOccurred) &&
(e.Errors.GetType == typeof(DBConcurrencyException))
{
ConflictAdapter.SelectCommand.Parameters[0].Value = e.Row["ID"];
int intRowsReturned = ConflictAdapter.Fill(ConflictDataSet);
if (intRowsReturned == 1)
e.Row.RowError = "The row has been modified by another user.";
else
e.Row.RowError = "The row no longer exists in the database.";
e.Status = UpdateStatus.Continue;
}
}
注意:如果没有将Status改变为Continue或SkipCurrentRow,那么在更新尝试失败时DataAdapter会自动将文字附加到RowError属性上。
这个代码段提取数据库中相应行的当前内容并加入到单独的DataSet中,这样您就可以在更新尝试之后对数据进行检查。现在您有了构建上图信息的所有数据。
11.6 . 4 如果第一次没有成功
用户可能不希望重新查询数据库并重新对新数据进行同样的改变才能重新提交这些改变。我们如何使用ado.net模型简化这一过程?
失败的原因在于DataAdapter在并发检查中使用的数据与数据库中这行的当前内容不匹配。在刷新DataRow的内容前,欧文们不能向数据库存储在DataRow中的改变。
只要我们可以改变DataRow的original 值,就可以顺利提交改变。
使用DataSet对象的Merge方法导入“New Original”值
在前面代码片段中,我们捕获了DataAdapter对象的RowUpdated事件。如果当前行没有成功更新,那么代码就捕获数据库中相应行的内容加入到ConflictDataSet的新DataSet中。假设主DataSet称为MainDataSet,我们可以用下面一行代码将ConflictDataSet的内容合并到MainDataSet中:
MainDataSet.Merge(ConflictDataSet, true );
这段代码不改变主DataSet中的DataRow对象的当前内容。它只是用冲突的DataSet的数据覆盖DataRow对象的原来值。
于是便可以更新了。
注意:不能得到有关在数据库中不再存在的行的“New Original”信息。如果一个更新尝试因为行在数据库中不再存在而失败,那么就不能用这种方法刷新行中的原来值。如果希望向数据库重新添加DataRow对象的当前内容,那么可以用从DataTable对象的Rows集合中删除这个DataRow再重新添加它。这样会使DataRow对象的RowState改变为Added。