数据更新接口与延迟更新

在日常使用中,更新数据库数据经常使用delete 、update等SQL语句进行,但是OLEDB接口提供了额外的接口,来直接修改和更新数据库数据。

OLEDB数据源更新接口

为何不使用SQL语句进行数据更新

常规情况下,使用SQL语句比较简单,利用OLEDB中执行SQL语句的方法似乎已经可以进行数据库的任何操作,普通的增删改查操作似乎已经够用了。确实,在某种情况下,这些内容已经够了,能够执行SQL语句并得到结果集已经够了,但是某些情况下并不合适使用SQL语句。
SQL数据执行过程
SQL语句的执行一般经过这样几个步骤:

  1. 数据库通过sql语句对SQL语句进行分析,生成一些可以被数据库识别的步骤,在这里我们叫它计划任务
  2. 数据库根据计划任务中的相关操作,调用对应的核心组件来执行SQL语句中规定的操作。
  3. 将操作得到的结果集返回到应用程序

我们可以简单的将SQL语句理解为一种运行在数据库平台上的一个脚本语言,它与一般的脚本语言一样需要对每句话进行解释执行。根据解释的内容调用对应的功能模块来完成用户的请求。如果我们能够跳过SQL语句的解释,直接调用对应的核心组件,那么就能大幅度的提升程序的性能。OLEDB中的数据更新的相关接口就是完成这个操作的。
至此我们可能有点明白为什么不用SQL语句而是用OLEDB的相关接口来实现对应的更新操作。主要是为了提高效率。

数据修改的相关接口

数据修改的相关接口主要是IRowsetChange,接口中提供的主要方法如下:

  • DeleteRows: 删除某行
  • InsertRows: 插入行
  • SetData: 更新行

通过这些接口就可以直接对数据库进行相关的增删改操作,而由于查询的操作条件复杂特别是涉及到多表查询的时候,所以OLEDB没有提供对应的查询接口。

更新数据

更新数据需要IRowsetChange接口,而打开该接口需要设置结果集的相关属性。
一般需要设置下面两个属性:

  1. DBPROP_UPDATABILITY:该属性表示是否允许对数据进行更新,它是一个INT型的数值,该属性有3个标志的候选值:DBPROPVAL_UP_CHANGE,允许对数据进行更新,也就是打开SetData方法;DBPROPVAL_UP_DELETE:允许对数据进行删除,打开的是DeleteRows方法。DBPROPVAL_UP_INSERT,允许插入新行,该标志打开InsertRows方法。这3个值一般使用或操作设置对应的标识位。
  2. DBPROP_IRowsetUpdate:该属性是一个BOOL值,表示是否打开IRowsetChange接口。如果要使用IRowsetChange接口,需要将该属性设置为TRUE。

它们属于属性集DBPROPSET_ROWSET。使用命令对象来设置
设置完属性后,调用Execute执行SQL语句并获取到接口IRowsetChange。
PS:使用IRowsetChange接口有一个特殊的要求,必须使用IRowsetChange替代IRowset等接口,作为创建并打开结果集对象的第一个接口。也就是说Execute方法中的最后一个表示结果集对象的参数必须是IRowsetChange。

数据更新模式

一般来说,使用OLEDB的接口对数据库中的数据进行操作时,操作的结果是实时的反映到数据库中的。
对于一般的应用程序来说。采用数据更新的接口虽然在一定程度上解决的效率的问题,但是使用实时更新的模式仍然有一些问题:

  1. 修改立即反映到数据库中,不利于数据库中数据完整性维护和数据安全
  2. 如果是网络中的数据库,会形成很多小的网络数据包传输,造成网络传输效率低下。

因此OLEDB提供了另外一种更新模式——延迟更新

延迟更新

延迟更新本质上提供了一种将所有更新都在本地中缓存起来,最后再一口气将所有更新都一次性提交的机制,它与数据库中的事务不同,事务是将一组操作组织起来,当其中某一步操作失败,那么回滚事务中的所有数据,强调的是一个完整性维护,而延迟提交并不会自己校验某一步是否错误,它强调的是将某些更改一次性的提交。
延迟提交与实时提交有下面几个优点:

  1. 当多个客户端都在修改数据库中的数据时,有机会将某些客户端对数据的修改通知到其他客户端。
  2. 可以合并对一行数据多列的修改并一次提交到数据源上
  3. 网络数据库中可以将对不同表的不同操作合并成一个大的网络数据包,提高网络的使用效率。
  4. 当更新不合适的时候有机会进行回滚

打开延迟更新的接口

要使用延迟更新必须申请打开OLEDB的IRowsetUpdate接口,这个申请主要通过设置结果集的DBPROP_IRowsetUpdate属性来实现,这个属性是一个bool类型的值。
同时要打开延迟更新的接口必须先打开IRowsetChange接口,所以它们二者一般都是同时打开的。
另外为了方便操作一般也会将结果集的DBPROP_CANHOLDROWS属性打开,该属性允许在上一次对某行数据的更改未提交的情况下插入新行。如果不设置该属性,那么在调用SetData方法进行更新后就必须调用IRowsetUpdate的Update接口进行提交,否则在提交之前数据库不允许进行Insert操作(但是允许进行SetData操作)
在使用延迟更新的时候需要注意一个问题。当我们使用了DBPROP_CANHOLDROWS属性后,数据源为了维护方便,会额外返回一个第0行的数据。通常改行数据是一个INT型,由数据提供者进行维护,它一般是只读的,如果尝试对它进行修改可能会返回一个错误造成对数据的其他修改操作也失败。在这种情况下,可以考虑建立2个访问器,一个包含第0行,只用来做显示使用,而另外一个更新的访问器不绑定第0行。
一般情况下可以通过检测返回结果集中的列信息中的标志字段来确定哪些列可以进行变更,哪些列是只读列等标志来创建多个不同用途的行访问器
下面是延迟更新的例子:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
BOOL ExecSql(IOpenRowset *pIOpenRowset, IRowsetChange* &pIRowsetChange)
{
COM_DECLARE_BUFFER();
COM_DECALRE_INTERFACE(IDBCreateCommand);
COM_DECALRE_INTERFACE(ICommandText);
COM_DECALRE_INTERFACE(ICommandProperties);

LPOLESTR lpSql = OLESTR("select * from aa26;");
BOOL bRet = FALSE;

//TODO:query出ICommandProperties接口并设置对应属性
//设置属性,允许返回的结果集对数据进行增删改操作
dbProp[0].colid = DB_NULLID;
dbProp[0].dwOptions = DBPROPOPTIONS_REQUIRED;
dbProp[0].dwPropertyID = DBPROP_UPDATABILITY;
dbProp[0].vValue.vt = VT_I4;
dbProp[0].vValue.intVal = DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_DELETE | DBPROPVAL_UP_INSERT;

//设置属性,打开IRowsetUpdate接口实现延迟更新
dbProp[1].colid = DB_NULLID;
dbProp[1].dwOptions = DBPROPOPTIONS_REQUIRED;
dbProp[1].dwPropertyID = DBPROP_IRowsetUpdate;
dbProp[1].vValue.vt = VT_BOOL;
dbProp[1].vValue.boolVal = VARIANT_TRUE;

//设置属性,允许结果集在不提交上次行的更改的情况下插入行
dbProp[2].colid = DB_NULLID;
dbProp[2].dwOptions = DBPROPOPTIONS_REQUIRED;
dbProp[2].dwPropertyID = DBPROP_CANHOLDROWS;
dbProp[2].vValue.vt = VT_BOOL;
dbProp[2].vValue.boolVal = VARIANT_TRUE;

dbPropset[0].cProperties = 3;
dbPropset[0].guidPropertySet = DBPROPSET_ROWSET;
dbPropset[0].rgProperties = dbProp;

hRes = pICommandProperties->SetProperties(1, dbPropset);
COM_SUCCESS(hRes, _T("设置属性失败,错误码为:%08x\n"), hRes);

hRes = pICommandText->SetCommandText(DBGUID_DEFAULT, lpSql);
COM_SUCCESS(hRes, _T("设置SQL失败,错误码为:%08x\n"), hRes);

hRes = pICommandText->Execute(NULL, IID_IRowsetChange, NULL, NULL, (IUnknown**)&pIRowsetChange);
COM_SUCCESS(hRes, _T("执行SQL语句失败,错误码为:%08x\n"), hRes);
bRet = TRUE;
__CLEAR_UP:
SAFE_RELEASE(pIDBCreateCommand);
SAFE_RELEASE(pICommandText);
SAFE_RELEASE(pICommandProperties);

return bRet;
}

int _tmain(int argc, TCHAR* argv[])
{
CoInitialize(NULL);
COM_DECLARE_BUFFER();
COM_DECALRE_INTERFACE(IOpenRowset);
COM_DECALRE_INTERFACE(IRowsetChange);
COM_DECALRE_INTERFACE(IColumnsInfo);
COM_DECALRE_INTERFACE(IAccessor);
COM_DECALRE_INTERFACE(IRowset);
COM_DECALRE_INTERFACE(IRowsetUpdate);

HRESULT hRes = S_FALSE;
DBORDINAL cColumns = 0;
DBCOLUMNINFO* ColumnsInfo = NULL;
OLECHAR *pColumnsName = NULL;
DBBINDING *pDBBindinfo = NULL;
HACCESSOR hAccessor = NULL;
HROW* phRow = NULL;
HROW hNewRow = NULL;
DBCOUNTITEM cRows = 10; //一次读取10行
DBCOUNTITEM dbObtained = 0; //真实读取的行数

LPVOID pInsertData = NULL;
LPVOID pData = NULL;
LPVOID pCurrData = NULL;

DWORD dwOffset = 0;
if(!CreateSession(pIOpenRowset))
{
COM_PRINTF(_T("创建回话对象失败\n"));
goto __CLEAR_UP;
}

if (!ExecSql(pIOpenRowset, pIRowsetChange))
{
COM_PRINTF(_T("执行SQL语句失败\n"));
goto __CLEAR_UP;
}

hRes = pIRowsetChange->QueryInterface(IID_IRowset, (void**)&pIRowset);
COM_SUCCESS(hRes, _T("查询接口IRowsets失败,错误码为:%08x\n"), hRes);

hRes = pIRowset->QueryInterface(IID_IColumnsInfo, (void**)&pIColumnsInfo);
COM_SUCCESS(hRes, _T("查询接口IColumnsInfo失败,错误码为:%08x\n"), hRes);

hRes = pIColumnsInfo->GetColumnInfo(&cColumns, &ColumnsInfo, &pColumnsName);
COM_SUCCESS(hRes, _T("获取结果集列信息失败,错误码为:%08x\n"), hRes);

pDBBindinfo = (DBBINDING*)MALLOC(sizeof(DBBINDING) * (cColumns - 1));
for(int i = 0; i < cColumns; i++)
{
//取消第0行的绑定,防止修改数据时出错
if (ColumnsInfo[i].iOrdinal == 0)
{
continue;
}
pDBBindinfo[i - 1].iOrdinal = ColumnsInfo[i].iOrdinal;
pDBBindinfo[i - 1].bPrecision = ColumnsInfo[i].bPrecision;
pDBBindinfo[i - 1].bScale = ColumnsInfo[i].bScale;
pDBBindinfo[i - 1].cbMaxLen = ColumnsInfo[i].ulColumnSize * sizeof(WCHAR);
if (ColumnsInfo[i].wType == DBTYPE_I4)
{
//整型数据转化为字符串时4个字节不够,需要根据表中的长度来分配对应的空间
//数据库中表示行政单位的数据长度为6个字节
pDBBindinfo[i - 1].cbMaxLen = 7 * sizeof(WCHAR);
}

pDBBindinfo[i - 1].dwMemOwner = DBMEMOWNER_CLIENTOWNED;
pDBBindinfo[i - 1].dwPart = DBPART_STATUS | DBPART_LENGTH | DBPART_VALUE;
pDBBindinfo[i - 1].eParamIO = DBPARAMIO_NOTPARAM;
pDBBindinfo[i - 1].obStatus = dwOffset;
pDBBindinfo[i - 1].obLength = dwOffset + sizeof(DBSTATUS);
pDBBindinfo[i - 1].obValue = dwOffset + sizeof(DBSTATUS) + sizeof(ULONG);
pDBBindinfo[i - 1].wType = DBTYPE_WSTR; //为了方便,类型由数据库进行转化
dwOffset = dwOffset + sizeof(DBSTATUS) + sizeof(ULONG) + pDBBindinfo[i - 1].cbMaxLen;
dwOffset = COM_UPROUND(dwOffset, 8); //8字节对齐
}

//读取数据
hRes = pIRowsetChange->QueryInterface(IID_IAccessor, (void**)&pIAccessor);
COM_SUCCESS(hRes, _T("查询IAccessor接口失败,错误码为:%08x\n"), hRes);
hRes = pIAccessor->CreateAccessor(DBACCESSOR_ROWDATA, cColumns - 1, pDBBindinfo, 0, &hAccessor, NULL);
COM_SUCCESS(hRes, _T("创建访问器失败,错误码为:%08x\n"), hRes);
hRes = pIRowset->GetNextRows(DB_NULL_HCHAPTER, 0, cRows, &dbObtained, &phRow);
COM_SUCCESS(hRes, _T("获取行数据失败,错误码为:%08x\n"), hRes);

pData = MALLOC(cRows * dwOffset);
for (int i = 0; i < dbObtained; i++)
{
pCurrData = (BYTE*)pData + dwOffset * i;
hRes = pIRowset->GetData(phRow[i], hAccessor, pCurrData); //获取当前行
COM_SUCCESS(hRes, _T("获取数据失败, 错误码为:%08x\n"), hRes);

DisplayColumnData(pDBBindinfo, cColumns - 1, pCurrData);

// 将前10行第3列的数据修改为中国
LPOLESTR pUpdateData = (LPOLESTR)((BYTE*)pCurrData + pDBBindinfo[1].obValue);
ULONG* pLen = (ULONG*)((BYTE*)pCurrData + pDBBindinfo[1].obLength);
*pLen = 4;
StringCchCopy(pUpdateData, pDBBindinfo[1].cbMaxLen, _T("中国"));

hRes = pIRowsetChange->SetData(phRow[i], hAccessor, pCurrData);
COM_SUCCESS(hRes, _T("修改数据失败, 错误码为:%08x\n"), hRes);
}

//插入一行数据
pInsertData = MALLOC(dwOffset);
ZeroMemory(pInsertData, dwOffset);

//为了方便直接在原来数据的基础上进行修改
CopyMemory(pInsertData, pData, dwOffset);
DBSTATUS status[4] = {0};
ULONG uSize[4] = {0};
LPOLESTR lpValues[4] = {0};

for (int i = 0; i < 4; i++)
{
status[i] = *(DBSTATUS*)((BYTE*)pInsertData + pDBBindinfo[i].obStatus);
uSize[i] = *(DBSTATUS*)((BYTE*)pInsertData + pDBBindinfo[i].obLength);
lpValues[i] = (LPOLESTR)((BYTE*)pInsertData + pDBBindinfo[i].obValue);
}

StringCchCopy(lpValues[0], pDBBindinfo[0].cbMaxLen, _T("100001"));
StringCchCopy(lpValues[1], pDBBindinfo[1].cbMaxLen, _T("测试"));
hRes = pIRowsetChange->InsertRow(NULL, hAccessor, pInsertData, &hNewRow);
COM_SUCCESS(hRes, _T("插入数据失败, 错误码为:%08x\n"), hRes);

//删除一行数据
hRes = pIRowsetChange->DeleteRows(NULL, 1, &phRow[9], NULL);
COM_SUCCESS(hRes, _T("查询IRowsetChange接口失败,错误码为:%08x\n"), hRes);

hRes = pIRowsetChange->QueryInterface(IID_IRowsetUpdate, (void**)&pIRowsetUpdate);
COM_SUCCESS(hRes, _T("查询IRowsetChange接口失败,错误码为:%08x\n"), hRes);
////提交上述修改
hRes = pIRowsetUpdate->Update(DB_NULL_HCHAPTER, 0, NULL, NULL, NULL, NULL);
COM_SUCCESS(hRes, _T("提交数据更新失败, 错误码为:%08x\n"), hRes);

//取消修改
//hRes = pIRowsetUpdate->Undo(DB_NULL_HCHAPTER, 0, NULL, NULL, NULL, NULL);
COM_SUCCESS(hRes, _T("取消更新失败, 错误码为:%08x\n"), hRes);
pIRowset->ReleaseRows(dbObtained, phRow, NULL, NULL, NULL);
pIRowset->ReleaseRows(1, &hNewRow, NULL, NULL, NULL);

__CLEAR_UP:
SAFE_RELEASE(pIOpenRowset);
SAFE_RELEASE(pIRowsetChange);
SAFE_RELEASE(pIColumnsInfo);
SAFE_RELEASE(pIRowsetUpdate);
SAFE_RELEASE(pIRowset);
SAFE_RELEASE(pIAccessor);

CoTaskMemFree(ColumnsInfo);
CoTaskMemFree(pColumnsName);

FREE(pInsertData);
FREE(pData);
FREE(pDBBindinfo);
CoUninitialize();
return 0;
}

在例子中仍然是首先进行数据库连接,创建出Session对象,创建Command对象,设置结果集对象的相关属性并执行sql语句。但是与之前不同的是,在执行SQL语句时不再返回IRowset接口而是返回IRowsetChange接口。然后利用IRowsetChange接口Query出其他需要的接口。接着仍然是绑定,与之前不同的是,在绑定中加了一个判断。跳过了第0行的绑定,以免它影响到后面的更新操作,然后打印输出对应的查询结果。并且在显示每行数据之后,调用SetData对数据进行更改。
接着准备一个对应的缓冲,放入插入新行的数据。在这为了方便我们直接先拷贝了之前的返回的结果集中的一行的数据,然后再在里面进行修改,修改后调用InsertRows,插入一行数据。
插入操作完成后,调用DeleteRows函数删除一行数据
最后调用IRowsetUpdate接口的Update方法提交之前的更新,或者调用Undo取消之前的更改
源代码查看