VC++实战OLEDB编程(八)——行状态与延迟更新(转)

在之前的七篇文章中,我们连续的掌握了从连接数据库到取出数据,再到修改数据的整个过程,同时也看到了一个相对较完整的例子,如果认真阅读并实践前面系列文章的话,那么作为OLEDB编程入门的内容算是都掌握了,当然所有已掌握的内容都是将要学习的新内容的基础,因此之后的文章都要求对前面的内容有熟练的掌握和深刻的理解。

OK,下面我们开始新内容的学习。在第六篇文章中,我们介绍了如何修改、新增、删除记录的方法,对于通常的修改任务,这些操作是足够了,但是有一个严重的问题,就是这些操作是无法还原的,也即这些操作是立刻就被提交到数据库存储的。显然这样的修改方式无论对用户,还是数据库系统来说都是一种野蛮的方式。因为,对于用户来说,修改数据并不意味着新修改的值就是完全正确的,或者说用户没有机会去检查确认修改的内容,而这个内容就已经被立刻反映到数据库中了,显然这对于用户自己以及使用同一数据库的所有用户来说是很不负责任的行为;其次,对于数据库系统来说(或者更广泛的说是应用软件与DBMS组成的大系统),这样的修改被立刻提交的方式显然是一种非常大的系统资源浪费,因为假设用户要修改一行记录中的4个字段值时,耗时的数据库写入保存操作就被执行了4次,如果在一个网络环境中这同时还意味着需要传输4个个头不大的独立数据包,这对于网络资源来说也是一种浪费(当然BLOB型数据是个例外);至少基于以上这样的理由,我们都可以认为,这种立即提交修改的方式都是一种很不好的方式。很多时候我们需要一种缓冲的机制,当用户完全确认一组修改以后,再一次性的提交到数据库中,显然此时我们假设的4个分离的操作就被合并成一个操作,这样可以节约大量的时间和其它资源。如果对这个问题理解还不够深刻,可以去参考一下相关的DBMS系统原理方面的书籍或者系统优化方面的资料。

在OLEDB规范中完整制订了关于这种重要的缓冲方式操作的相关接口和方法。实现这种方式的主要接口就是IRowsetUpdate,这个接口也是Rowset对象的一个可选接口,使用方法跟IRowsetChange接口相类似,要打开这个接口,也需要设置Rowset对象的一个名为DBPROP_IRowsetUpdate的属性(通过ICommandProperties的SetProperties方法),这个属性值是一个VT_BOOL型值,意即打开或关闭IRowsetUpdate接口,默认的这个接口是被关闭的。这个属性值也需要在打开或创建Rowset对象之前进行设置,下面的例子演示了如何设置这个属性,并最终打开IRowsetUpdate接口:

DBPROPSET ps[1];

DBPROP prop[3];

ZeroMemory(ps,1 * sizeof(DBPROPSET));

ZeroMemory(prop, 3 * sizeof(DBPROP));

prop[0].dwPropertyID = DBPROP_UPDATABILITY;

prop[0].vValue.vt = VT_I4;

prop[0].vValue.lVal=DBPROPVAL_UP_CHANGE     //打开Update属性

     |DBPROPVAL_UP_DELETE                    //打开Delete属性

     |DBPROPVAL_UP_INSERT;                   //打开Insert属性

prop[0].dwOptions = DBPROPOPTIONS_REQUIRED;

prop[0].colid = DB_NULLID;

prop[1].dwPropertyID = DBPROP_IRowsetUpdate;

prop[1].vValue.vt = VT_BOOL;

prop[1].vValue.boolVal=VARIANT_TRUE;        

prop[1].dwOptions = DBPROPOPTIONS_REQUIRED;

prop[1].colid = DB_NULLID;

ps[0].guidPropertySet = DBPROPSET_ROWSET;       //注意属性集合的名称

ps[0].cProperties = 2;

ps[0].rgProperties = prop;

       ......

GRS_COM_CHECK(pICommandText->QueryInterface(IID_ICommandProperties

,(void**)& pICommandProperties));

//注意必须在Execute前设定属性

GRS_COM_CHECK(pICommandProperties->SetProperties(1,ps));

GRS_COM_RELEASE(pICommandProperties);   //没有用了 就释放掉

GRS_COM_CHECK(pICommandText->Execute(NULL

,IID_IRowsetChange

,NULL

,NULL

,(IUnknown**)&pIRowsetChange));

//取得IRowsetChange接口

GRS_COM_CHECK(pIRowsetChange->QueryInterface(IID_IRowset

,(void**)&pIRowset));

    //取得IRowsetUpdate接口

GRS_COM_CHECK(pIRowsetChange->QueryInterface(IID_IRowsetUpdate

,(void**)&pIRowsetUpdate));

......

//修改数据 (注意修改操作实际是修改了缓冲,没有Update时并没有被保存)

GRS_COM_CHECK(pIRowsetChange->SetData(rghRows[iRow]

,phAccessor,pCurData));

.......

//删除一些行

GRS_COM_CHECK(pIRowsetChange->DeleteRows(NULL

,1

,&rghRows[iRow]

,&pdwStatus[0]));

.......

//插入一些行

GRS_COM_CHECK(pIRowsetChange->InsertRow(NULL

,phAccessor

,pNewData

,&hNewRows));

    ......

    //最终确认了所有的修改之后就可以使用Update方法提交

//到此上面所有的修改才被一次性提交到数据库,也即保存修改

DWORD                 dwUpdateRows            = 0;

HROW*                 phUpdateRows            = NULL;

DBROWSTATUS*        pUpdateStatus          = NULL;

//提交所有的修改 (至此上面的所有变更操作才被真正保存或称作提交)

GRS_COM_CHECK(pIRowsetUpdate->Update(NULL

,0

,NULL

,&dwUpdateRows

,&phUpdateRows

,&pUpdateStatus));

    //下面这个可以提交新插入的行,注意hNewRows代表的行,与上一种方法是互为补充的一种方式

GRS_COM_CHECK(pIRowsetUpdate->Update(NULL

,1

,&hNewRows

,NULL

,NULL

,&pUpdateStatus));

    //最终我们只需要释放pUpdateStatus的内存

//当然在释放之前我们可以查看下每行的详细状态

    CoTaskMemFree(pUpdateStatus);

    以上的代码中最终使用了两种方式来调用Update方法来提交(COMMIT)我们操作,让数据库保存我们的修改结果。这两种方式都很重要,分别用于不同的目的,第一种方式表示提交所有的被修改的行,包括被删除,新增的行;第二种方式就是只提交指定的行,这里是通过指定行句柄实现的。

    在第一种调用方法中,使用phUpdateRows参数返回了被修改行的行句柄(包括哪些已经被释放的行句柄),以方便我们知道这次提交操作中有哪些行被修改了,这里要注意一个啰嗦的问题那就是内存泄露,当我们同时提供了这两种方式需要的参数,也就是我们同时提供了提交哪些行的行句柄数组,以及要求返回被修改行的行句柄时,返回的行句柄只是我们传入句柄的一个简单复制,此时我们只需要释放数组本身的内存即可:

    CoTaskMemFree(phUpdateRows);

    但是如果我们没有提供提交行句柄数组,即完全像第一种方式调用Update时,这时返回的句柄数组是所有在结果集中被修改过的行(包括删除行新增行)的句柄,而且这些句柄都要被释放一次,因为其引用计数被自动做了增一处理,所以为了防止内存泄露,必须进行一次ReleaseRows操作:

    GRS_COM_CHECK(pIRowset->ReleaseRows(dwUpdateRows,phUpdateRows,NULL,NULL,NULL));

    接着phUpdateRows句柄数组本身的内存还是要使用CoTaskMemFree正确释放。

    最后Update方法返回的最后一个参数pUpdateStatus是每一个被提交行的最终状态,为了正确分辨哪些行提交成功了,哪些有问题,这个状态还是非常有用的,他跟前面的两个数组参数的序号是一一对应的,数据提供者在实现Update方法时严格的保证了这种一一对应的机制。

    最终无论哪种调用Update方式,都要释放pUpdateStatus数组的内存:

CoTaskMemFree(pUpdateStatus);

延迟修改的基本操作就是上面这些操作了,主要就是通过IRowsetChange接口和IRowsetUpdate的相关方法联合作战实现这种延迟提交的操作数据方式。当然也可以为Update的所有参数指定NULL值,象下面这样调用:

GRS_COM_CHECK(pIRowsetUpdate->Update(NULL,0,NULL,NULL,NULL,NULL));

这时带来的唯一好处就是方便(没有啰嗦的资源释放操作),但是这样的调用方法不利于出错时的处理,不好定位错误行的具体位置,因此也是不被推荐的方法。

本质上讲数据延迟提交的最终实现方式无非就是通过为原始数据准备一个新的拷贝放在内存中,让所有的修改都发生在这个拷贝上,最终Update时再真正一次性复制回数据源。由这个本质实现方法我们可以知道这种方式的代价就是至少双倍内存的付出,如果结果集过大时,显然这种方式是无法实现两份这种超大结果集内存拷贝的,同时这样也会给DBMS系统带来极高的压力(如果有多个人都在以这种方式修改时,就不得不准备多份拷贝),鉴于这种情况,OLEDB在DBPROPSET_ROWSET属性集合(也就是Rowset属性集合)中,提供了一个DBPROP_MAXPENDINGROWS属性,来指定一次缓冲修改方式能够修改的最大行数,或者说就是两次Update调用之间,可以调用SetData、InsertRow、DeleteRows的总次数,超过这个次数,这些方法都会返回一个DB_E_MAXPENDCHANGESEXCEEDED错误。可以像下面这样来指定这个属性:

prop[2].dwPropertyID = DBPROP_MAXPENDINGROWS;

prop[2].vValue.vt = VT_I4;

prop[2].vValue.intVal = 10;        

prop[2].dwOptions = DBPROPOPTIONS_REQUIRED;

prop[2].colid = DB_NULLID;

这个属性可以通过pICommandProperties的SetProperties方法来设置,要注意的是并不是我们设置多少,提供者就能提供多大的缓冲能力,我们可以再通过IRowsetInfo的GetProperties方法来探知这种情况,当得到的值是0时,表示数据提供者提供了无限的修改缓冲能力,当然这通常表示你系统有多少内存,它就允许你修改多少。

对于一些具体的DBMS来说,这个值还有可能是一些常数,比如Access数据库,因为它是为轻量级的数据管理任务而设计的,因此它通常只能允许你修改一个数据,我们在直接使用Access的时候也可以明显的感觉到这种方式,在我们修改完一个数据,并将焦点移动到另一个上时,被修改的数据立刻就被保存了。

当然IRowsetUpdate的能力不仅仅是为了“延迟保存”这样简单,它还允许你反悔,撤销所有的修改,这仍然是因为你所做的修改仅仅发生在了数据的第二份缓冲上。要撤销全部的或部分的修改,可以调用IRowsetUpdate的Undo方法来实现,其参数和Update是完全一样的,含义也是一样的,此处就不在赘述了。大家可以自己写代码实验一下。

最后我们需要讨论一个这种缓冲能力的核心问题,也就是被修改的行状态迁移问题。在OLEDB中所有被修改的行(包括新增、删除的行),在没有被提交的情况下,都被称作一种Pending(待决、待定)的状态,其含义就是说这个被缓冲的修改还不知道最终要怎么处理,最终是要放弃修改,还是保存修改都是无法预知或决定的,通常这个结果都是由用户来最终决定的。

如果你有使用PowerBuilder DataWindow的经验,对于这种状态应该是非常熟悉的。下面的图展示了这些行状态,以及调用相应的IRowsetChange和IRowsetUpdate接口方法使对应的行状态发生改变的状态迁移图,看懂这个图非常的重要,这对于一些带有复杂录入界面的MIS系统或其它系统是至关重要的。图如下:

VC++实战OLEDB编程(八)——行状态与延迟更新 - Gamebaby Rock Sun - Gamebaby Rock Sun的博客

这个图是我对OLEDB帮助文档中对应图进行了一个简单的加工得到的,原图如下:

VC++实战OLEDB编程(八)——行状态与延迟更新 - Gamebaby Rock Sun - Gamebaby Rock Sun的博客

下面对这个图做一些说明:

首先、这个状态图的开始状态是1号和2号状态,1号状态也是终止状态之一,另一个终止状态是5号状态,如果结果集中的行不能正常的回到终止状态,通常说明有些意外的问题发生了(比如未保存,主键或唯一值冲突,字段值溢出等等情况)。

其次、对于熟悉PB DataWindow的读者来说,Retrieve函数一定不陌生,在OLEDB中,对应但不等价的操作是RefreshRows方法,这里要注意的区别是RefreshRows会保留新插入的行,而PB的Retrieve方法不会这样,它会丢弃新插入的行。

再次、行一旦成为作废状态之后,就没有方法再恢复它们,即使我们还拥有它们HROW句柄也是无能为力,但是如果我们拥有这行记录的本地行拷贝,那么我们可以通过一个InsertRow来恢复它。

最终、从1号状态到2号状态的InsertRow操作其实是一个性质不同的状态迁移操作,他并不是表示同一行的状态转移,而应该理解为一个从“初始的空状态”开始的一个通过InsertRow“无中生有”产生一个新行状态的操作。原图中也是这样画的,而并没有引入一个“新的空状态”,这个地方要仔细的琢磨理解一下。

通过对图的理解,我们可以看出,我们调用IRowsetUpdate和IRowsetChange接口的相关方法时,实际就是控制行的状态在这些不同的状态间迁移,理解这些状态就可以为我们正确识别和要求用户采取正确动作打下基础。比如用户修改了一些数据,那么我们就可以要求用户进行保存操作,如果是新插入的行,我们可以要求用户输入一些必须填写的值再进行保存,如果是删除的行,我们可以要求用户再次确认进行这个危险的操作。

当我们想知道结果集中究竟哪些行发生了变化,或者需要知道具体某行是什么状态时,我们可以调用IRowsetUpdate的GetPendingRows和GetRowStatus方法得到Pending状态的行句柄和某些指定行的状态。它们的原型如下:

HRESULT GetPendingRows (

       HCHAPTER             hReserved,

       DBPENDINGSTATUS      dwRowStatus,

       DBCOUNTITEM         *pcPendingRows,

       HROW               **prgPendingRows,

       DBPENDINGSTATUS    **prgPendingStatus);

HRESULT GetRowStatus(

       HCHAPTER          hReserved,

       DBCOUNTITEM       cRows,

       const HROW        rghRows[],

       DBPENDINGSTATUS   rgPendingStatus[]);

实际调用这两个函数时,通常情况下hReserved参数直接赋值为NULL即可,DBCOUNTITEM其实就是个DWORD值,表示数量。对于GetPendingRows方法来说,后面三个参数都是传出参数,也即返回的数组,这都需要我们在使用完毕后正确的进行释放操作,pcPendingRows参数不需要释放,prgPendingRows除了使用CoTaskMemFree释放外,还需要使用IRowset的ReleaseRows方法来释放行句柄先;最后prgPendingStatus数组中,就保存了每一行的状态,其值就是前面状态图中的那些对应的状态常量值(以DBPENDINGSTATUS_开头的值)。通过得到这些值,我们就可以知道具体的某一行数据处在前面画出的图的哪种状态,以及之后调用函数对这些状态的影响。

而GetRowStatus函数的后面3个参数,需要我们为它来准备内存,它只管朝最后的rgPendingStatus数组中填写HROW数组中指定的对应行的状态。

这两个函数的具体调用有兴趣的读者可以直接使用代码调用进行实验,此处不再举例。

在有一些情况下,当某一行处于Pending状态时(尤其是修改的Pending状态下时),我们还会关心某些记录的原始值,比如复杂的关联修改时,我们需要知道有些记录修改是否被允许,或者干脆我们想要记录下所有数据被修改的情况,原始值是什么,新值是什么都记录下来(在有些系统中这种一一对应的记录被称作修改日志),在OLEDB中因为使用了缓冲的修改方式,为我们提供了这种能力(这有点像DataWindow中的访问原始缓冲操作)。通过IRowsetUpdate的GetOriginalData方法就可以得到被修改记录的Original数据,也就是原初数据。这个方法的参数与IRowset的GetData方法参数是一致的,调用也是一致的,二者可以使用一致的Binding结构,访问器也可以是同一个,但是大多数情况下,调用GetOriginalData方法时,会使用一个相对“精简”的绑定结构,比如我们只关心某些核心数据列被修改的情况,就不必为整行提供一个完整的绑定器,只需要为这几列提供一个绑定结构即可,当然这种情况下要重新创建一个访问器句柄(HACCESSOR)。

通过在每次调用Update前,调用GetOriginalData方法得到数据的原初值,并且与当前值进行一个比对保存,就可以得到一个完整的关于记录被修改的日志,这对于一些职责明晰的系统来说是至关重要的能力。当然相对于其它开发工具的类似方法来说,利用VC和OLEDB的直接操作数据内存的方式来实现这个方法有一些非常明显的优势,比如在性能上,在操作的方便性上都是有非常大的优势的,我们可以将内存直接保存到文件中,或者发送到网络上,甚至通过串口保存到专用设备上,再或者将数据通过USB口写到一些专用格式的设备上等等。当然操作内存也是C++语言本身的魅力之一。

还有一些情况下,所有做的修改都要被放弃,同时用户可能需要从头开始重新录入数据,这种情况下,我们可以使用IRowsetRefresh的RefreshVisibleData函数重新从数据源中得到原初的数据,这样就可以从数据源中得到所有数据的原始未修改的值。根据上面的状态图可以知道,这个方法并不影响我们用InsertRow方法插入的新行,既不改变状态,也不提交修改,如果我们也不想要这些新行了,那么根据行状态迁移图,我们调用一下DeleteRows就可以同时将这些新增的行也丢弃。RefreshVisibleData函数的原型如下:

HRESULT RefreshVisibleData (

   HCHAPTER         hChapter,

   DBCOUNTITEM      cRows,

   const HROW       rghRows[],

   BOOL             fOverwrite,

   DBCOUNTITEM     *pcRowsRefreshed,

   HROW           **prghRowsRefreshed,

   DBROWSTATUS    **prgRowStatus);

从参数可以看出,这个函数与Update函数是类似的,其实调用方法和含义也是同Update函数相同的,这里就不再赘述了。需要注意的是,要得到这个接口,需要像使用IRowsetUpdate接口一样,打开一个名叫DBPROP_IRowsetRefresh的VT_BOOL型属性,同样的这个属性也是用ICommandProperties的SetProperties方法,在Execute之前设置。它也是属于DBPROPSET_ROWSET属性集合中的属性之一,目的就是为了控制IRowsetRefresh的开关。

IRowsetRefresh接口还有一个好用的方法就是GetLastVisibleData,这个函数的原型如下:

HRESULT GetLastVisibleData (

   HROW        hRow,

   HACCESSOR   hAccessor,

   void       *pData);

表面上看它的参数与GetData,GetOriginalData都是一致的,甚至它们的调用方法也是相类似的,但是他们的含义却是完全不同的。对于GetData来说,没有什么特殊的,他就是将数据简单的从数据源拿出来,通常他与GetOriginalData的功能是类似的。在延迟提交模式下,也就是我们这篇文章中主要讨论的缓冲修改数据方式中,GetOriginalData和GetLastVisibleData都是非常重要的读取数据的方法,其中GetOriginalData用于得到数据源中当前真实存储的值,而GetLastVisibleData则是得到被修改的数据值,通常如果在两个用户都打开了同一个表的两个不同的Rowset时,GetLastVisibleData可以允许一个用户“偷窥”另一个用户对结果做的修改,也就是说如果一个用户将某个字段的值改成了A,假设该字段数据库中原始的值是B,那么另一个用户调用GetLastVisibleData后将得到A的值,只有当他使用GetOriginalData时才能够得到数据库中的B这个值。显然这为方便我们让几个用户同时协同工作带来了方便。

(未完待续)

转自 : http://gamebabyrocksun.blog.163.com/blog/static/571534632009102945258309/

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值