.Net-Com双向数据交换的实现(RecordSet与.Net DataSet的转化)
文前说明:
鉴于许多人发Mail询问我关于本文的所涉及的技术问题,由于我无法及时进行一一回复,因此请直接在评论的地方说明即可。
另:请转载的人注明出处,以方便其他人能够及时向我反馈问题。谢谢。
一:前言
.Net平台的普及和应用为开发人员带来了极大的便利,然而这一切的强大功能都是建立在.Net平台的基础上的,而目前Win32平台中可以说基本上是COM元件的天下,如果能在使用.Net强大功能的基础上又复用之前的成果,便相当的重要了。推倒重来,用新的平台和语言再次开发之前已经开发过产品,类库(Class Library),框架(Framework),插件等等,是需要无比的勇气的。即便你有这样的勇气,时间和风险你也得准备去承担。好了,现在那些“没有”勇气或有勇气但能审时度势的人站出来吧,让我们讨论一下如何重用之前的成果。
二:概述
.Net为何无法直接使用之前的COM元件,而需要对COM进行包装?这主要是因为非托管对象(1)模型(如COM)和托管对象模型(.Net平台上的对象)存在一些差异:
(1) 数据类型,方法签名和异常处理机制
(2) 生命周期管理方式不同,COM元件的调用端需要自己管理对象的生命周期,而托管对象的生命周期由.Net中的公共语言运行时库统一管理
(3) COM对象有绝对的位置(地址),这也是Win32中可以通过内存地址直接访问对象的原因所在;而托管对象没有绝对的地址,公共语言运行时库可能根据需要会移动对象
为了对开发人员隐藏这些差异,公共语言运行时库通过提供包装类为托管对象和非托管对象之间的数据交换提供了桥梁,如下图(请参见微软开发文档中的互操作性专题)
大部分的企业应用都是建立在数据库的基础上的,因此.Net中复用COM元件的必要性很大一部分是与COM元件进行数据交换。数据的很大一部分表现为诸如ADO中的RecordSet(记录集)这样的行集对象;而ADO.Net中的使用的是DataSet的,因此与COM元件的数据交换就演变成ADO中的RecordSet和ADO.Net中DataSet的数据交换,接下来就讨论一下两者的数据转换。
三:ADO RecordSet向ADO.Net DataSet的转换
.Net中默认提供了将RecordSet转换为DataSet的功能,用户需要使用DataAdapter来完成这个功能,以下是方法原型:
//
// 摘要:
// 使用指定的 System.Data.DataTable 和 ADO 对象,在 System.Data.DataTable 中添加或刷新行,以便与
// ADO Recordset 或 Record 对象中的行相匹配。
//
// 参数:
// dataTable:
// 一个要用记录和架构(如果需要)填充的 System.Data.DataTable。
//
// ADODBRecordSet:
// 一个 ADO Recordset 或 Record 对象。
//
// 返回结果:
// 已对 System.Data.DataTable 成功刷新的行数。这不包括受不返回行的语句影响的行。
public int Fill(DataTable dataTable, object ADODBRecordSet);
//
// 摘要:
// 使用指定的 System.Data.DataSet、ADO 对象和源表名称,添加或刷新 System.Data.DataSet 中的行,使之与 ADO
// Recordset 或 Record 对象中的行相匹配。
//
// 参数:
// srcTable:
// 用于表映射的源表。
//
// dataSet:
// 要用记录和架构(如果需要)填充的一个 System.Data.DataSet。
//
// ADODBRecordSet:
// 一个 ADO Recordset 或 Record 对象。
//
// 返回结果:
// 已在 System.Data.DataSet 中成功添加或刷新的行数。这不包括受不返回行的语句影响的行。
public int Fill(DataSet dataSet, object ADODBRecordSet, string srcTable);
以下示例描述了这种转换
/// <summary>
/// 将指定的Recordset填充到指定的TDataTable对象中,返回填充的行数
/// </summary>
/// <param name="table">待被填充的TDataTable对象</param>
/// <param name="rec">待填充的Recordset</param>
/// <returns>填充的行数,失败返回</returns>
public static int FillDataTableByRecordset(ref DataTable table, ADODB.Recordset rec)
{
table.Clear();
table.BeginLoadData();
try
{
//如果原生Recordset是关闭的,那么就打开它
if (rec.State == 0)
rec.Open(Type.Missing, Type.Missing, CursorTypeEnum.adOpenStatic, LockTypeEnum.adLockOptimistic, -1);
OleDbDataAdapter adapter = new OleDbDataAdapter();
//添加MissingSchemaAction,如果table中已经有字段了,那么如果不相符合就出错,否则添加
if (table.Columns.Count > 0)
adapter.MissingSchemaAction = MissingSchemaAction.Error;
else
adapter.MissingSchemaAction = MissingSchemaAction.AddWithKey;
//填充DataTable
return adapter.Fill(table, rec);
}
finally
{
if (rec.State != 0)
rec.Close();
table.EndLoadData();
}
}
关于DataAdapter的一些属性设置,如MissingSchemaAction,请查阅MSDN,合理的设置这些属性对转换操作的正确执行至关重要。
四:ADO.Net DataSet向ADO RecordSet的转换
.Net Framework对原生的RecordSet默认提供了向后的兼容,换句话说,提供了RecordSet向ADO.Net DataSet转换的方法,但是没有提供相反的逆向操作方法;然而如上面所述,我们很有可能需要用到数据的逆向交换,以下就描述一下如何进行这种转换。
应该如果作这种转换呢?显然,既然DataAdapter的Fill(…)可以将RecordSet转换为DataSet,那么我们只要对Fill方法进行的操作进行逆向操作就可以了(只要Microsoft不阻止,比如一些必要的方法是内部使用的),使用Reflector追踪Fill方法
Fill-》FillFromADODB-》FillFromRecordset
private int FillFromRecordset(object data, UnsafeNativeMethods.ADORecordsetConstruction recordset, string srcTable, out bool incrementResultCount)
{
IntPtr ptr1;
incrementResultCount = false;
object obj1 = null;
try
{
Bid.Trace("<oledb.ADORecordsetConstruction.get_Rowset|API|ADODB>\n");
obj1 = recordset.get_Rowset(); //从RecordSet中获取Rowset
Bid.Trace("<oledb.ADORecordsetConstruction.get_Rowset|API|ADODB|RET> %08X{HRESULT}\n", 0);
Bid.Trace("<oledb.ADORecordsetConstruction.get_Chapter|API|ADODB>\n");
ptr1 = recordset.get_Chapter();//从RecordSet中获取Chapter
Bid.Trace("<oledb.ADORecordsetConstruction.get_Chapter|API|ADODB|RET> %08X{HRESULT}\n", 0);
}
catch (Exception exception1)
{
if (!ADP.IsCatchableExceptionType(exception1))
{
throw;
}
throw ODB.Fill_EmptyRecordSet("ADODBRecordSet", exception1);
}
if (obj1 != null)
{
CommandBehavior behavior1 = (MissingSchemaAction.AddWithKey != base.MissingSchemaAction) ? CommandBehavior.Default : CommandBehavior.KeyInfo;
behavior1 |= CommandBehavior.SequentialAccess;
OleDbDataReader reader1 = null;
try
{
ChapterHandle handle1 = ChapterHandle.CreateChapterHandle(ptr1);
reader1 = new OleDbDataReader(null, null, 0, behavior1);
reader1.InitializeIRowset(obj1, handle1, ADP.RecordsUnaffected);//初始化Rowset,显然是通过OleDB进行操作了
reader1.BuildMetaInfo();//构建元数据信息
incrementResultCount = 0 < reader1.FieldCount;
if (incrementResultCount)
{
if (data is DataTable)
{
return base.Fill((DataTable) data, reader1); //调用父类方法进行操作
}
return base.Fill((DataSet) data, srcTable, reader1, 0, 0);
}
}
finally
{
if (reader1 != null)
{
reader1.Close();
}
}
}
return 0;
}
从上面面的FillFromRecordSet方法中可以看出来,DataReader从Recordset中获取Rowset和Chapter来填充(初始化)自己,然后DataTable再从DataReader中读取数据。因此如果逆向完成转换就是初始化RecordSet,从DataTable中逐行获取数据,使用OleDB填充RecordSet;以下是两种方式的示意图。这里舍弃了中间的DataReader类,因为我们仅仅是作填充,DataReader可以在方法中直接处理掉。
转换时有以下几个步骤:
(1) 判断DataTable中当前Row的状态,对于Modified(修改)和Delete(删除)状态,则使用旧(Original)的数据填充tagDBBINDING结构数组。对于新增的则用当前值进行填充
(2) 使用(1)的数据对RowSet进行InsertRow操作,插入一行
(3) 如果当前Row的状态是新增,那么直接更新
(4) 如果当前Row的状态是删除,那么先更新刚才插入的行,然后对这一行在执行删除操作
(5) 如果当前Row的状态是修改的,那么先更新刚才插入的行,然后用当前Row的当前(Current)数据填充tagDBBINDING结构数组,最后再执行更新操作
以下是代码示例:
/// <summary>
/// 根据创建时指定的DataTable写出Recordset
/// </summary>
/// <returns></returns>
public Recordset WriteToRecordset()
{
if (this.dataTable == null || rowsetUpdate == null || innerAccessors == null)
return null;
if (bindings.Length > 1)
throw new InvalidOperationException("目前不支持多个Accessor,待改进");
else//以下部分仅支持一个内部accessor,以后需要扩充功能
{
try
{
IntPtr accessorPtr = innerAccessors[0];
foreach(DataRow row in this.dataTable.Rows)
{
switch(row.RowState)
{
case DataRowState.Deleted:
case DataRowState.Modified:
FullFillBindings(row, 0, DataRowVersion.Original);
break;
default:
FullFillBindings(row, 0, DataRowVersion.Current);
break;
}
IntPtr phRow = IntPtr.Zero;
SysUtils.HResultCheck(rowsetUpdate.InsertRow(this.chapter, accessorPtr, (HandleRef)bindings[0], ref phRow));
int ptrSize = IntPtr.Size * 1;//请不要用常量传进去,因为对于不同平台,IntPtr的大小是不固定的
IntPtr prghRows = Marshal.AllocCoTaskMem(ptrSize);
SafeNativeMethods.ZeroMemory(prghRows, ptrSize); //清空内存,这边可以不必,主要是为了指针数组内存的清空
Marshal.WriteIntPtr(prghRows, 0, phRow);
try
{
if (isBatched && (row.RowState == DataRowState.Unchanged || row.RowState == DataRowState.Deleted || row.RowState == DataRowState.Modified))
{
SysUtils.HResultCheck(rowsetUpdate.Update(this.chapter, 1, prghRows, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero));
}
else if (!isBatched) //非LockBatchOptimistic时一定更新
SysUtils.HResultCheck(rowsetUpdate.Update(this.chapter, 1, prghRows, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero));
if (row.RowState == DataRowState.Deleted)
{
IntPtr rowStatus;
SysUtils.HResultCheck(rowsetUpdate.DeleteRows(this.chapter, 1, prghRows, out rowStatus));
if (rowStatus.ToInt32() != 0) //这边可能发生DBROWSTATUS_S_MULTIPLECHANGES状态,请查看msdn
throw OleDbDataProviderHelper.BadRowStatus(rowStatus.ToInt32());
}
if(row.RowState == DataRowState.Modified)
{
FullFillBindings(row, 0, DataRowVersion.Current);
SysUtils.HResultCheck(rowsetUpdate.SetData(phRow, accessorPtr, (HandleRef)bindings[0]));
}
}
finally
{
SysUtils.HResultCheck(rowset.ReleaseRows(1, prghRows, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero));
Marshal.FreeCoTaskMem(phRow);
Marshal.FreeCoTaskMem(prghRows);
prghRows = IntPtr.Zero;
}
}
}
catch(HResultCheckException E)
{
throw DataProviderHelper.TraceException(E);
}
catch(SystemException)//测试使用,尽量不要捕捉SystemExcetion级别及其以上的异常
{
throw;
}
}
return innerRecordset;
}
/// <summary>
/// 根据DataRow对象,为指定位置的DBBinding赋值
/// </summary>
/// <param name="row"></param>
/// <param name="bindingIndex"></param>
/// <param name="rowVersion"></param>
private void FullFillBindings(DataRow row, int bindingIndex, DataRowVersion rowVersion)
{
OLEDBBindings Bindings = this.bindings[bindingIndex];
UnsafeNativeMethods.tagDBBINDING[] dbBindingArray = Bindings.DBBinding;
foreach(UnsafeNativeMethods.tagDBBINDING binding in dbBindingArray)
{
int ordinal = binding.iOrdinal.ToInt32();
Bindings.CurrentIndex = this.dataOrdinalToBindingIndex[ordinal - 1];
object val = row[Bindings.ColumnName, rowVersion];
Bindings.Value = val;
}
}
对上述操作进行封装
/// <summary>
/// 将指定的DataTable转换为Win32中的Recordset表示形式
/// </summary>
/// <param name="table">待转换的DataTable对象</param>
/// <param name="cursorType">Recordset打开时使用的CursorType</param>
/// <param name="lockType">Recordset打开时使用的LockType</param>
/// <returns>返回ADODB.RecordSet</returns>
public static ADODB.Recordset GetRecordsetFromDataTable(DataTable table, ADODB.CursorTypeEnum cursorType, ADODB.LockTypeEnum lockType)
{
OleDbDataWriter dbWriter = new OleDbDataWriter();
try
{
dbWriter.Initialize(table, lockType == LockTypeEnum.adLockBatchOptimistic);//初始化Writer
dbWriter.BuildMetaInfo();//构建元信息
return dbWriter.WriteToRecordset();//写入到Recordset
}
catch
{
throw;
}
finally
{
dbWriter.Close();
}
}
/// <summary>
/// 将指定的DataTable转换为Win32中的Recordset表示形式
/// </summary>
/// <param name="table">待转换的DataTable对象</param>
/// <returns>返回ADODB.Recordset</returns>
/// <remarks>Recordset将使用adOpenStatic的CursorType和adLockOptimistic的LockType打开</remarks>
public static ADODB.Recordset GetRecordsetFromDataTable(DataTable table)
{
return GetRecordsetFromDataTable(table, CursorTypeEnum.adOpenStatic, LockTypeEnum.adLockBatchOptimistic);
}
五:说明
(1) 非托管和托管的含义请参见Net MSDN上的互操作性一章
(2) ADO.Net中DataTable和ADO RecordSet中的转换需要用到OleDB中的很多知识,这些东西可以参考MSDN,好像没有什么书籍有叙述这些东西的(如果有,麻烦告知一下)
(3) 以上功能已实现为公司使用的COM-NET开发平台(Framwork)中的数据转换模块。