前面的系列文章中,我们主要讨论的是如何“获得”或者“读取”数据,关键的步骤就是如何执行一个SQL语句,并读取语句的结果集,包括读取BLOB型数据。
对于一般的任务来说,这些基本的操作已经足够了,因为只要可以执行SQL语句,那么大多数数据库任务就可以很好的完成了,而我们要做的就是以静态或动态的方式拼出SQL语句即可。看起来这很完美,事实上也是这样,在一些应用中,这确实足够了,但是对于一些高级的任务来说,这些还只是开始。
熟悉Powerbuilder的人都知道,它的DataWindow几乎无以替代,开发效率,灵活程度,都是其它开发工具难以企及的,可以说就是因为DataWindow,现在很多的应用系统项目都在使用Powerbuilder。
在DataWindow中有一个重要的特性,就是数据的灵活修改,我们可以设想一下,如果是你来做DataWindow,你要如何实现这种完美的数据增删改存的经典操作方式呢?显然如果只是简单的靠执行SQL来完成这些工作,是不可想象的。因为首先效率就将严重的制约你,你可以想象,用户同时修改了10万条记录,然后一次性提交,你就需要拼接10万条SQL语句执行,中间可能还要出现不可预知的错误,(别问我为什么用户能够修改10万多条的记录,因为很多系统现在都提供了批量操作的方式,10万条记录可以很轻松的就进行修改),这个时候显然这样的拼SQL方式已经无能为力了。而且还有更复杂的应用需求,比如主子表的修改,事务的完整性控制等等,这个时候拼SQL已经变成了“体育运动”,而不是程序开发了。
当然OLEDB的功能也不只到这个层面就结束了,接下来我们就进入数据修改的话题。
首先,让我们复习一下结果集对象(Rowset)的接口:
CoType TRowset
{
[mandatory] interface IAccessor;
[mandatory] interface IColumnsInfo;
[mandatory] interface IConvertType;
[mandatory] interface IRowset;
[mandatory] interface IRowsetInfo;
[optional] interface IChapteredRowset;
[optional] interface IColumnsInfo2;
[optional] interface IColumnsRowset;
[optional] interface IConnectionPointContainer;
[optional] interface IDBAsynchStatus;
[optional] interface IGetRow;
[optional] interface IRowsetChange;
[optional] interface IRowsetChapterMember;
[optional] interface IRowsetCurrentIndex;
[optional] interface IRowsetFind;
[optional] interface IRowsetIdentity;
[optional] interface IRowsetIndex;
[optional] interface IRowsetLocate;
[optional] interface IRowsetRefresh;
[optional] interface IRowsetScroll;
[optional] interface IRowsetUpdate;
[optional] interface IRowsetView;
[optional] interface ISupportErrorInfo;
[optional] interface IRowsetBookmark;
}
从这个接口列表中,我们尤其注意的就是IRowsetChange和IRowsetUpdate这两个接口,顾名思义我们立刻就可以知道这两个接口是用来修改结果集的,具体要怎么做呢?下面我就详细为大家介绍。
根据我们的任务,我们应该很容易发现IRowsetChange这个接口就是我们要关注的焦点。当然先不要急,这个接口的调用并不是想象的只需要调用QueryInterface方法得到接口的指针就可以使用了。
这里有一个主要的原因导致你没法直接这样使用,因为即使数据提供者支持对这些数据的修改,而实际中并不是所有查询得到的结果集都可以直接进行更新,典型的比如很多汇总类查询,复杂的嵌套的子查询等都是没法进行更新的,实际在数据库中他们的有些字段是不存在物理对应的,因此数据提供者无法知道到何处去存储这些值,当然简单的表连接查询通常是可以进行直接的修改的。
根据以上的说明,那么我们要打开某个结果集上的修改特性那么就还需要其他的操作,这个操作就是设置Command对象的相关属性以一种协商的方式与数据提供者进行交互,下面的属性设置例子演示了如何打开使用IRowsetChange接口:
const ULONG cPropSets = 1;
DBPROPSET rgPropSets[cPropSets];
const ULONG cProperties = 1;
DBPROP rgProperties[cProperties];
rgPropSets[0].guidPropertySet = DBPROPSET_ROWSET;
rgPropSets[0].cProperties = cProperties;
rgPropSets[0].rgProperties = rgProperties;
rgPropSets[0].rgProperties[0].dwPropertyID
=DBPROP_UPDATABILITY;
rgPropSets[0].rgProperties[0].dwOptions
=DBPROPOPTIONS_REQUIRED;
rgPropSets[0].rgProperties[0].dwStatus = DBPROPSTATUS_OK;
rgPropSets[0].rgProperties[0].colid = DB_NULLID;
rgPropSets[0].rgProperties[0].vValue.vt = VT_I4;
//这句设定了我们需要打开IRowsetChange接口,并修改得到的数据
V_I4(&rgPropSets[0].rgProperties[0].vValue)
= DBPROPVAL_UP_CHANGE;
//也可以从ICommand接口直接得到ICommandProperties接口
hr=pICommandText->QueryInterface(IID_ICommandProperties,
(void **)&pICommandProperties);
//设置属性
hr=pICommandProperties->SetProperties(cPropSets,
rgPropSets);
//设置SQL语句
hr=pICommandText->SetCommandText(DBGUID_DBSQL,
_T("SELECT * FROM Table_Test"));
//执行,注意直接使用了IRowsetChange得到结果
hr = pICommandText->Execute(NULL,
IID_IRowsetChange,
NULL,
NULL,
(IUnknown**)&pIRowsetChange);
//绑定结果集(代码略,请参阅前面文章)
……
//得到IRowset的接口
hr=pIRowsetChange->QueryInterface(IID_IRowset,
(void **)&pIRowset);
//取得数据
hr = pIRowset->GetNextRows(NULL, 0, 1,
&cRowsObtained, &rghRows);
hr = pIRowset->GetData(rghRows[0],
hAccessor, &DataBuffer);
//修改DataBuffer中相关字段的值(代码略)
……
//使用SetData方法,注意参数和GetData方法一致,
//要求DataBuffer的内存结构也必须是一致的
//特别注意SetData是IRowsetChange的方法
hr=pIRowsetChange->SetData(rghRows[0],
hAccessor, &DataBuffer);
至此修改数据的任务就结束了,其中要注意的地方一个是要在SQL语句执行之前设定属性DBPROP_UPDATABILITY值为DBPROPVAL_UP_CHANGE,这样在执行的时候数据提供者就知道你打算要对得到的数据进行修改操作了;另一个要注意的地方就是SetData的用法,它的参数和GetData方法其实是一致的,那么特别要注意的就是数据缓冲格式此处二者是一致的,同时访问器句柄和行句柄必须一一对应。
以上例子中还有一个调用方式要注意,就是使用Execute方法直接返回IRowsetChange接口,其实这就是理解COM精髓以后的一种方式,也是前面文章中一再强调的对于COM对象,COM接口需要深入理解的地方。这个地方还有一个要千万千万注意的问题就是,这里返回的IRowsetChange接口很有可能为NULL,也就是Execute失败,很多时候你会发疯般的仔细检查SQL语句,却发现SQL语句并没有错误,于是你百思不得其解,便开始诅咒OLEDB的发明者,或者诅咒你的数据库系统开发者,其实很多时候就是我刚才提到的原因,使得数据提供者“拒绝”向你提供一个可以修改的结果集,因为有些数据是你的SQL语句“无中生有”得出来的,而你又企图修改这样的数据,当然这是无论如何也办不到的,结果就只能是失败了。所以一开始我就说要用好OLEDB还需要你对数据库原理SQL语句有一个深入的理解,不然有些问题就会超出OLEDB编程问题的范围。
从上面的关于IRowsetChange的讨论可以看出,这仅仅实现了对已有数据的修改,通常我们操作数据的方式还有新增记录和删除记录,而要彻底的实现这些,就必须对属性DBPROP_UPDATABILITY进行如下设置:
V_I4(&rgPropSets[0].rgProperties[0].vValue)
= DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_DELETE | DBPROPVAL_UP_INSERT;
进行了上面这个设置后,我们不但可以使用IRowsetChange接口的SetData方法,还可以使用DeleteRows,InsertRow,来删除和新增记录(有些时候称作插入记录)。因为例子和前面我们讲的例子差不多,这里只重点的讲解一下DeleteRows和InsertRow的调用方法。
DeleteRows方法的原形如下:
HRESULT DeleteRows (
HCHAPTER hChapter,
DBCOUNTITEM cRows,
const HROW rghRows[],
DBROWSTATUS rgRowStatus[]);
通常的第一个参数我们先不关心它,简单的为它设置为NULL即可,第二和第三个参数其实对于现在的你来说已经不陌生了,就是我们拿到的行集数组参数,cRows是数组元素个数,其实也就是我们总共要删除的行数,rghRows就是行集句柄的数组。最后一个参数大小必须和rghRows大小一致,将返回删除每行数据时的状态,当然如果你不关心返回的状态,那么设置为NULL就可以了。下面的示例代码演示了如何删除一行数据:
DBROWSTATUS rghStatus[1];
hr=pIRowsetChange->DeleteRows(NULL,1,
rghRows,rghStatus);
这里的DBROWSTATUS其实就是DWORD值,详细的值列表及含义你可以查看MSDN,这里就不再赘述了。
接下来我们看看InsertRow方法的原型:
HRESULT InsertRow (
HCHAPTER hChapter,
HACCESSOR hAccessor,
void *pData,
HROW *phRow);
第一个参数我们仍然先设置为NULL即可,这里要注意的就是第二个参数hAccessor,同我们取得数据时创建hAccessor的方式一致,我们要先创建一个hAccessor访问器句柄,当然前提就是我们定义了一个Binding结构,然后用CreateAccessor方法创建访问器,当然pData就是我们要插入的数据的内存了,结构当然就是按照我们创建hAccessor时设置的Binding结构指定的格式,最终与GetData方式需要GetNextRow调用得到HROW不同,因为我们是插入数据,这个指向新记录行的指针就是通过指针的方式返回给我们。作为练习,InsetRow方法的调用示例代码我就不罗列了,又兴趣的读者可以自己做个例子实验一下,有什么问题可以跟帖提问。
这里要特别注意的就是,创建InsertRow需要的hAccessor的Binding结构的赋值方式和GetData需要的hAccessor的Binding结构的赋值方式通常是不同的(有些情况下也可以是相同的,甚至可以使用完全相同的Binding,这完全看应用的需要而定),有些参数我们是不需要设置的,下面的例子演示了如何定义这样的Binding结构:
struct InsertBuffer
{//行集的数据结构
long * pl;
double * pd;
short * pi;
};
static DBBINDING InsertBindings [3] =
{
{
1,
offsetof (InsertBuffer, pl), //数据偏移
0, // 不需要指定长度,
// 因为类型为固定长度的long等类型
0, // 不用指定状态。有时候要插入NULL值时需要为
// 状态字段预留空间,此时就要制定偏移
NULL, // 不指定类型
NULL, // 非BLOB不用指定
NULL,
DBPART_VALUE,
DBMEMOWNER_PROVIDEROWNED,
DBPARAMIO_NOTPARAM,
sizeof (void*),
0,
DBTYPE_I4 | DBTYPE_BYREF,
0,
0
},
{
2,
offsetof (InsertBuffer, pd),
0,
0,
NULL,
NULL,
NULL,
DBPART_VALUE,
DBMEMOWNER_PROVIDEROWNED,
DBPARAMIO_NOTPARAM,
sizeof (void*),
0,
DBTYPE_R8 | DBTYPE_BYREF,
0,
0
},
{
3,
offsetof (InsertBuffer, pi),
0,
0,
NULL,
NULL,
NULL,
DBPART_VALUE,
DBMEMOWNER_PROVIDEROWNED,
DBPARAMIO_NOTPARAM,
sizeof (void*),
0,
DBTYPE_I2 | DBTYPE_BYREF,
0,
0
}
};
上面例子中就演示了如何定义一个用于Insert目的的Binding结构,实际他也就定义了我们要插入的记录行的数据结构,上面的例子使用了一个固定格式的结构InsertBuffer,这就省去了动态设计Binding的麻烦。
另一个需要注意的问题就是InsertRow一次只能插入一行数据,如果要插入多行数据,或者说要“批量插入数据”,那么就需要对这个方法进行循环调用,通常这样的性能是很不能让人忍受的,后面的文章中,我将介绍一种由SQL Server提供的扩展的批量插入数据的方法。
通过前面的讨论,我们经历了完整的对于数据的查询,修改,新增,删除等操作,其实核心的就是要掌握好Binding结构如何定义,因此对于Binding结构的理解和应用是我们用好OLEDB 数据库编程接口的基础和核心。通常的面向业务的应用中,由于数据表通常是固定的,那么我们可以大量的使用固定结构的Binding,而如果我们的应用是面向分析的,那么我们通常需要灵活的多的动态Binding生成方案。这完全是一个理解了Binding的意义之后的创造性的方式,可以参考很多别人的源码来看看如何实现动态Binding,最好的例子就在ATL的CDynamicAccessor中,它实现了一个比较好用的动态Binding器,但是对于多个BLOB字段的情况却没有处理,如果你遇到这种情况的时候,还需要在改进,当然我们是站在巨人的肩膀上。
(未完待续)
转自: http://gamebabyrocksun.blog.163.com/blog/static/571534632009101210628583/?hasChannelAdminPriv=true