OLEDB 数据变更通知

除了之前介绍的接口,OLEDB还定义了其他一些支持回调的接口,可以异步操作OLEDB对象或者得到一些重要的事件通知,从而使应用程序有机会进行一些必要的处理。其中较有用的就是结果集对象的变更通知接口。通过这个接口可以及时得到结果集被增删改数据变化的情况,并有机会进行必要的数据合法性审核。

数据变更通知的接口是IRowsetNotify,数据源对象要求的异步通知事件接口是IDBAsynchNotify。

标准COM的回调方式

为了更好的理解OLEDB的回调,先回忆一下标准COM的回调方式。
COM组件除了提供函数供应用程序主动调用这种方式外,还提供了回调这种方式,这种方式由用户实现相应的接口,然后由COM组件来调用,这样我们就可以知道COM组件的运行状态,同时能针对一些情况进行处理,比如处理内存耗尽,获取用户输入等等。
要支持事件回调的COM组件必须提供IConnectionPointContainer接口,调用者调用IConnectionPointContainer接口的FindConnectPoint接口,通过回调事件的IID找到特定的事件挂载点,然后调用接口的Advise方法将挂载点与对应的回调函数关联起来(一个事件可以对应多个回调函数)这样当事件发生时就可以调用对应的回调函数。
这个机制有点类似于QT中的信号和槽函数机制,QT中的事件是实现定义好的,可以直接使用而这里是需要通过事件ID找到具体事件,拥有事件后,与QT步骤类似,都是需要将事件与对应的回调函数绑定。

IRowsetNotify接口

对于OLEDB结果集来说,最重要的事件接口是IRowsetNotify,该接口提供三个重要的通知函数:

  • OnFieldChange:列数据发生变更
  • OnRowChange: 行发生变化,尤其是删除或者插入行
  • OnRowsetChange:修改数据被提交

通过这些事件函数具体实现时设置不同的返回值可以控制结果集对象对修改做出的响应,比如:返回S_OK表示接受这个修改,返回S_FALSE明确拒绝接受这个修改。这样就给了一个最终反悔的机制。
这些函数有两个重要的参数:

  • DBREASON: 发生变化的原因
  • DBEVENTPHASE:事件被触发的阶段
    通过对这两个参数组合的判定,可以准确的判断出结果集中数据变化的动态追踪及情况

DBREASON 参数的相关值

  • DBREASON_ROW_ASYNCHINSERT:异步插入
  • DBREASON_ROWSET_FETCHPOSITIONCHANGE:结果集的行指针发生变化,当调用类似 IRowset::GetNextRows or IRowset::RestartPosition时触发
  • DBREASON_ROWSET_RELEASE:当结果集被释放的时候触发
  • DBREASON_ROWSET_CHANGED:数据库中某些元数据发生变化时触发,这里是指描述数据库表字段的一些信息发生变化,比如表字段的大小,类型这些数据,要修改这些数据需要用户具有一定的权限,一般情况下不会触发这个原因
  • DBREASON_COLUMN_SET:当行数据被设置时触发(这里只是已存在的行数据被设置,不包括新增行),一般调用SetData时会触发
  • DBREASON_COLUMN_RECALCULATED:当列的值发生变更时触发,一般是调用SetData
  • DBREASON_ROW_ACTIVATE:当用户修改行指针导致行的状态由未激活变为激活时触发
  • DBREASON_ROW_RELEASE:当调用ReleaseRows释放某些行句柄的时候触发
  • DBREASON_ROW_DELETE:当行被删除时触发
  • DBREASON_ROW_FIRSTCHANGE:当某些行的某列被设置新值后又改变了当前行指针的指向时,它会被第一时间触发,并且它的触发会早于DBREASON_COLUMN_SET,这个事件只会在使用延迟更新的时候才会产生。
  • DBREASON_ROW_INSERT:在插入新行的时候触发
  • DBREASON_ROW_UNDOCHANGE:当调用Undo放弃修改的时候触发
  • DBREASON_ROW_UNDOINSERT:当调用Undo放弃插入新行的时候触发
  • DBREASON_ROW_UNDODELETE:当调用Undo放弃删除的时候触发
  • DBREASON_ROW_UPDATE:当调用Update进行更新的时候触发

DBEVENTPHASE

这个参数表示当前执行的状态,一般操作数据结果集有5个状态,分别对应这样的5个值:

  • DBEVENTPHASE_OKTODO:准备好了去做,当应用程序需要操作结果集的时候会发送一个DBEVENTPHASE_OKTODO到监听程序(在这暂时就理解为OLEDB的数据源),监听程序收到后不会立马去执行该动作,而是会返回S_OK表示它知道了这个请求,或者返回S_FALSE拒绝这个请求
  • DBEVENTPHASE_ABOUTTODO:当数据源针对 DBEVENTPHASE_OKTODO返回S_OK时,应用程序会给一个信号,告知数据源可以进行执行动作之前最后的准备工作,这部完成之后,数据源会异步的执行相关请求操作
  • DBEVENTPHASE_DIDEVENT:当数据源执行完这次的请求之后会到这个状态,此时数据库表的数据已经更新
  • DBEVENTPHASE_FAILEDTODO:当之前的某一步发生错误时会进入这个状态,此时会产生回滚,将数据还原到最开始的状态。

下面是数据状态迁移图,这个图很形象的展示了在某个操作执行过程中的各种状态变化
状态迁移图

结果集对象事件通知接口的使用方法

  1. 定义一个派生自IRowsetNotify接口的类,并实现其接口中的所有方法
  2. 设置结果集对象属性集DBPROPSET_ROWSET中的DBPROP_IConnectionPointContainer属性为VARIANT_TRUE
  3. 获得结果集对象
  4. 调用IRowset::QueryInterface方法得到IConnectionPointContainer接口指针
  5. 调用IConnectionPointContainer::FindConnectionPoint方法得到IRowsetNotify接口对应的IConnectionPoint接口指针
  6. 实例化一个第一步中创建的类
  7. 调用IConnectionPoint::Advise并传递该对象指针
  8. 对结果集对象进行操作,此时如果事件条件成立,结果集对象会调用该对象的相应方法通知调用者触发了什么事件

详细的内容可以参考MSDN IRowsetNotify

例子

最后来看使用的具体例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class CCOMRowsetNotify:
public IRowsetNotify
{
public:
CCOMRowsetNotify(void);
virtual ~CCOMRowsetNotify(void);

protected:
virtual HRESULT FindConnectionPointContainer(IUnknown *pIUnknown, REFIID rrid, IConnectionPoint* &pIcp);
public:
virtual HRESULT Addvise(IUnknown *pIUnknown, REFIID rrid);
virtual HRESULT UnAddvise(IUnknown *pIUnknown, REFIID rrid);

public:
virtual STDMETHODIMP_(ULONG) AddRef(void);
virtual STDMETHODIMP_(ULONG) Release(void);
virtual STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);

virtual STDMETHODIMP OnFieldChange (IRowset *pRowset, HROW hRow, DBORDINAL cColumns, DBORDINAL rgColumns[], DBREASON eReason, DBEVENTPHASE ePhase,BOOL fCantDeny);
virtual STDMETHODIMP OnRowChange (IRowset *pRowset, DBCOUNTITEM cRows,const HROW rghRows[], DBREASON eReason, DBEVENTPHASE ePhase, BOOL fCantDeny);
virtual STDMETHODIMP OnRowsetChange (IRowset *pRowset, DBREASON eReason, DBEVENTPHASE ePhase, BOOL fCantDeny);

protected:
ULONG m_uRef;
DWORD m_dwCookie;
};

使用时首先定义一个派生自IRowsetNotify的类,并实现所有的接口方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
if (!OpenTable(pIOpenRowset, pIRowsetChange))
{
COM_PRINTF(_T("打开表失败\n"));
goto __CLEAN_UP;
}

RowsetNotify.Addvise(pIRowsetChange, IID_IRowsetNotify);

HRESULT CCOMRowsetNotify::FindConnectionPointContainer(IUnknown *pIUnknown, REFIID rrid, IConnectionPoint* &pIcp)
{
IConnectionPointContainer* pICpc = NULL;
HRESULT hr = pIUnknown->QueryInterface(IID_IConnectionPointContainer,(void**)&pICpc);
if(FAILED(hr))
{
COM_PRINTF(_T("通过IRowset接口获取IConnectionPointContainer接口失败,错误码:0x%08X\n"),hr);
return hr;
}
hr = pICpc->FindConnectionPoint(rrid,&pIcp);
if(FAILED(hr))
{
COM_PRINTF(_T("获取IConnectionPoint接口失败,错误码:0x%08X\n"),hr);
COM_SAFE_RELEASE(pIcp);
return hr;
}

return hr;
}

HRESULT CCOMRowsetNotify::Addvise(IUnknown *pIUnknown, REFIID rrid)
{
IConnectionPoint *pIcp = NULL;
HRESULT hRes = FindConnectionPointContainer(pIUnknown, rrid, pIcp);
if (S_OK != hRes)
{
return hRes;
}

hRes = pIcp->Advise(dynamic_cast<IRowsetNotify*>(this), &m_dwCookie);
COM_SAFE_RELEASE(pIcp);
return hRes;
}

上述代码先打开数据结果集,然后调用类对象的Addvise方法传入IID_IRowsetNotify接口指针,在方法Addvise中做的主要操作是首先使用传入的接口指针查找到接口IConnectionPointContainer,然后利用IConnectionPointContainer接口的FindConnectionPoint方法找到对应的挂载点,最后调用IConnectionPointContainer的Advise方法将对应的类对象挂载到挂载点上,这样在后面操作结果集时就会调用对应的On函数,完成对应事件的处理

最后放上完整代码的链接
点击这里获取完整代码