在开始新的结果集对象(Rowset)之旅之前,我们再来补充一个关于Command对象的用法,在有些情况下,我们执行的SQL语句只是一个Update、Insert或Delete等操作,有些时候我们可能直接执行就是一个存储过程而已,存储过程可能产生也可能不产生一个结果集,这类没有结果集的SQL语句,我们可以像下面这样来执行:
pICommandText->Execute(NULL,IID_NULL,NULL,NULL,NULL);
这样就不用关心SQL语句的结果集。当然对于返回结果集的查询也可以这样来执行,即直接丢弃结果集,当然这通常是没有意义的。
下面依照惯例,在开始正式的Rowset对象之旅前,让我们首先来认识下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;
}
作为补充,我们同时一看看上一会提到的多结果集对象的接口:
CoType TMultipleResults
{
[mandatory] interface IMultipleResults;
[optional] interface ISupportErrorInfo;
}
从Rowset对象的接口表中,我们可以知道这个对象都有什么接口,以及那些接口是强制的,那些接口是可选的。在Rowset对象中,我们经常用的接口就是IRowset、IColumnInfo和IAccessor,通过这几个接口,才可以访问到我们查询得到的数据结果。同时要注意的就是,最终这个过程并不像前面的接口及方法那么简单,得到可访问的数据就是OLEDB中最复杂的一个过程,如果这个过程彻底理解和掌握了,才能说是掌握了OLEDB编程的基础,当然不要丧气,在这里我将详细、细致且有条理的介绍这个得到数据的过程:
一、得到结果集:
(请参看(三)文中关于得到结果集的方法示例,此处只简单放一个示意性代码)
pICommandText->Execute(NULL,IID_IRowset,NULL,NULL,(IUnknown**)&pIRowset)
二、得到列信息:
得到列信息是一个非常重要的过程,通过列信息我们可以知道结果集的完整数据结构,这为后续的创建访问器,准备数据缓冲奠定了基础。很多时候,在SQL的查询语句中是没有关于数据结构的信息的,这个信息是隐含在数据库中的,而我们查询得到结果集时就必须要知道这个数据结构的信息,否则对数据的访问将无从谈起。要想得到一个结果集确切的数据结构信息,就要使用IColumnsInfo接口,下面的例子演示了如何得到一个结果集完整的列信息,已经如何安全的释放这个列信息:
IColumnsInfo * pIColumnsInfo = NULL;
ULONG cColumns = 0;
DBCOLUMNINFO * rgColumnInfo = NULL;
LPWSTR pStringBuffer = NULL;
HRESULT hr = pIRowset->QueryInterface(IID_IColumnsInfo,
(void**)&pIColumnsInfo));
hr = pIColumnsInfo->GetColumnInfo(&cColumns,
&rgColumnInfo,
&pStringBuffer
));
//如果成功了,那么rgColumnInfo中已经包含了一个关于列信息数据结构的数组,
//数组元素的个数即cColumns 也就是最终的列数
//使用完毕后释放所有的资源及接口
CoTaskMemFree(rgColumnInfo);
CoTaskMemFree(pStringBuffer);
if( NULL != pIColumnsInfo )
{
pIColumnsInfo->Release();
}
上面的实例代码显示了如何从一个结果集查询出IColumnsInfo接口,以及如何得到列信息的完整过程。在这个过程中特别需要注意的就是最后对rgColumnInfo数组和pStringBuffer内存的释放,这里使用了CoTaskMemFree这个COM库函数,通常这样就足够了 ,而在OLEDB的帮助文档中则说要使用IMalloc接口的Free方法来释放,其实二者是等价的。一般的我们使用CoTaskMemFree函数即可,特别说明的是调用这个函数释放内存的时候不需要检查被释放的指针是否为空,它内部有检查是否为空的机制,所以直接调用皆可,这又为我们节约了2-3行代码。
下面让我们来认识下DBCOLUMNINFO这个结构,它的原型如下:
typedef struct tagDBCOLUMNINFO
{
LPOLESTR pwszName;
ITypeInfo *pTypeInfo;
DBORDINAL iOrdinal;
DBCOLUMNFLAGS dwFlags;
DBLENGTH ulColumnSize;
DBTYPE wType;
BYTE bPrecision;
BYTE bScale;
DBID columnid;
}
DBCOLUMNINFO;
pwszName字段即为字段的名称,注意是个UNICODE字符串。如果查询中没有明确为列指定名称时,这个字段即为空,也就是没有列名。
pTypeInfo是一个保留待将来使用的接口,直到MSDAC2.6版为止这个字段都没有被使用,它永远是NULL,我们忽略它即可。
iOrdinal就是字段在结果集中的序号,也就是表示这个列是结果集的第几列,这个字段非常重要。特别注意的是,如果是结果集中的列,那么序号是从1开始的。而序号0是为一些特殊用途保留的,比如我们打开了结果集的书签功能时,那么序号0的列将是结果集的行号。关于书签的内容将在后续的提高部分中介绍。这里大家只要知道1对应第一列,n对应第n列即可。
dwFlags字段描述了列的状态,最重要的状态就是描述该列是否为一个BLOB型字段,或者该列是否可为空等。它的类型为DBCOLUMNFLAGS 是个枚举类型,在MSDN中有关于这个枚举类型的详细描述,本文就不在赘述,只在用到时特意说明。
ulColumnSize字段描述了列的大小,单位为字节。需要注意得是,这个大小仅对字符型列(即wType = DBTYPE_STR和wType = DBTYPE_WSTR)有意义,包括预定义的,或由查询得到的字符串等,而对于其他类型的列该字段则设置为一个~0的值,即所有bit位都为一的值。
wType则说明了结果集列的数据类型,OLEDB中定义了完整的被支持的数据类型,此处将所有类型含义及其值列表出来,方便大家查询:
enum DBTYPEENUM
{
// The following values exactly match VARENUM
// in Automation and may be used in VARIANT.
DBTYPE_EMPTY = 0,
DBTYPE_NULL = 1,
DBTYPE_I2 = 2,
DBTYPE_I4 = 3,
DBTYPE_R4 = 4,
DBTYPE_R8 = 5,
DBTYPE_CY = 6,
DBTYPE_DATE = 7,
DBTYPE_BSTR = 8,
DBTYPE_IDISPATCH = 9,
DBTYPE_ERROR = 10,
DBTYPE_BOOL = 11,
DBTYPE_VARIANT = 12,
DBTYPE_IUNKNOWN = 13,
DBTYPE_DECIMAL = 14,
DBTYPE_UI1 = 17,
DBTYPE_ARRAY = 0x2000,
DBTYPE_BYREF = 0x4000,
DBTYPE_I1 = 16,
DBTYPE_UI2 = 18,
DBTYPE_UI4 = 19,
// The following values exactly match VARENUM
// in Automation but cannot be used in VARIANT.
DBTYPE_I8 = 20,
DBTYPE_UI8 = 21,
DBTYPE_GUID = 72,
DBTYPE_VECTOR = 0x1000,
DBTYPE_FILETIME = 64,
DBTYPE_RESERVED = 0x8000,
// The following values are not in VARENUM in OLE.
DBTYPE_BYTES = 128,
DBTYPE_STR = 129,
DBTYPE_WSTR = 130,
DBTYPE_NUMERIC = 131,
DBTYPE_UDT = 132,
DBTYPE_DBDATE = 133,
DBTYPE_DBTIME = 134,
DBTYPE_DBTIMESTAMP = 135
DBTYPE_HCHAPTER = 136
DBTYPE_PROPVARIANT = 138,
DBTYPE_VARNUMERIC = 139
};
需要特别注意的是,这里的数据类型不是数据库支持的数据类型,或者说并不是所有的数据库都支持这些所有的数据类型,这仅是一个所有可能数据类型的全部概括,当然也有一些数据库中的类型并不在这个列表中,这时往往数据库对应的OLEDB接口提供程序都做了很好的转换,已经转换成了这个列表中所具有的类型,因此不用担心会碰到不支持的数据类型。
bPrecision和bScale字段就不用介绍了,一个是精度,一个是小数位数,他们对于数值型字段是非常重要的。
columnid字段是该列在数据库系统字典表中的id号,这个id号也很重要,当我们需要一些关于列的其他更多信息时往往就需要这个id号。
至此关于列的信息以及列信息的结构体就介绍完了,也许你会很奇怪,为什么这个东西我会啰嗦这么多,这是因为这对我们后续的操作是非常非常重要的。看了接下来的操作你就知道为什么这个会这么重要了。
三、创建一个绑定:
绑定是OLEDB中最重要最核心的概念之一,它是我们访问数据的关键操作,也是比较难理解的一个操作。其实对绑定最简单的理解就是:安排得到数据的内存摆放方式,并将这一方式告诉数据提供者,让它按要求将数据摆放到我们指定的内存中。为了这个目的,我们就需要自己创建一个被称作DBBINDING的数组,首先我们来看下这个结构体的样子:
typedef struct tagDBBINDING
{
DBORDINAL iOrdinal;
DBBYTEOFFSET obValue;
DBBYTEOFFSET obLength;
DBBYTEOFFSET obStatus;
ITypeInfo *pTypeInfo;
DBOBJECT *pObject;
DBBINDEXT *pBindExt;
DBPART dwPart;
DBMEMOWNER dwMemOwner;
DBPARAMIO eParamIO;
DBLENGTH cbMaxLen;
DWORD dwFlags;
DBTYPE wType;
BYTE bPrecision;
BYTE bScale;
} DBBINDING;
乍一看这个结构和前面的DBCOLUMNINFO结构很相似,其实他们的字段大多数确实是一致的,甚至可以直接使用DBCOLUMNINFO对DBBINDING进行赋值,但是二者字段的含义确是完全不同的,只有理解这二者的差别,才能真正玩转OLEDB。首先DBCOLUMNINFO是数据提供者给你的信息,它是固定的,对相同的查询来说,列总是相同的,因此数据提供者返回的DBCOLUMNINFO数组也是固定的。其次DBBINDING是你作为数据消费者创建之后给数据提供者的一个结构数组,它的内容则由你来完全控制,通过这个结构我们可以指定数据提供者最终将数据摆放成我们指定的格式,或者进行指定的数据类型转换。
下面的例子展示了如何完成这个过程:
ULONG cColumns;
DBCOLUMNINFO * rgColumnInfo = NULL;
LPWSTR pStringBuffer = NULL;
IColumnsInfo * pIColumnsInfo = NULL;
ULONG iCol;
ULONG dwOffset = 0;
DBBINDING * rgBindings = NULL;
pIRowset->QueryInterface(IID_IColumnsInfo,(void**)&pIColumnsInfo));
pIColumnsInfo->GetColumnInfo(&cColumns, &rgColumnInfo,
&pStringBuffer));
rgBindings = (DBBINDING*)HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY,
cColumns * sizeof(DBBINDING));
for( iCol = 0; iCol < cColumns; iCol++ )
{
rgBindings[iCol].iOrdinal = rgColumnInfo[iCol].iOrdinal;
rgBindings[iCol].dwPart = DBPART_VALUE|DBPART_LENGTH|DBPART_STATUS;
rgBindings[iCol].obStatus = dwOffset;
rgBindings[iCol].obLength = dwOffset + sizeof(DBSTATUS);
rgBindings[iCol].obValue = dwOffset+sizeof(DBSTATUS)+sizeof(ULONG);
rgBindings[iCol].dwMemOwner = DBMEMOWNER_CLIENTOWNED;
rgBindings[iCol].eParamIO = DBPARAMIO_NOTPARAM;
rgBindings[iCol].bPrecision = rgColumnInfo[iCol].bPrecision;
rgBindings[iCol].bScale = rgColumnInfo[iCol].bScale;
rgBindings[iCol].wType = rgColumnInfo[iCol].wType;
rgBindings[iCol].cbMaxLen = rgColumnInfo[iCol].ulColumnSize;
dwOffset = rgBindings[iCol].cbMaxLen + rgBindings[iCol].obValue;
dwOffset = ROUNDUP(dwOffset);
}
CoTaskMemFree(rgColumnInfo);
CoTaskMemFree(pStringBuffer);
if( pIColumnsInfo )
{
pIColumnsInfo->Release();
}
在上面的例子代码中,主体循环中演示了如何遍历得到的列信息数组,以及如何使用这个列信息创建一个DBBINDING数组。
在代码中,要特别留意的就是下面这四行:
rgBindings[iCol].dwPart = DBPART_VALUE|DBPART_LENGTH|DBPART_STATUS;
rgBindings[iCol].obStatus = dwOffset;
rgBindings[iCol].obLength = dwOffset + sizeof(DBSTATUS);
rgBindings[iCol].obValue = dwOffset+sizeof(DBSTATUS)+sizeof(ULONG);
其中第一句指明数据提供者最终提交数据时必须包含的信息,这里指定了列值(DBPART_VALUE),长度(DBPART_LENGTH)和状态(DBPART_STATUS)共3个信息,列值不用说就是要放字段的最终结果值,长度就是这个字段占用的字节长度,列状态中将存放的是值的状态比如是否为空等。
后面3句则明确的指出了以上三个信息在内存中摆放的偏移位置,通常按习惯,我们要求数据提供者先摆放状态,再摆放长度,最后摆放数据。
循环结束后dwOffset的值就是一行记录数据所需要的内存的大小。
至此一个简单的DBBINDING就创建好了。当然实际中创建这个结构数组时可不是这么简单就行了,还需要考虑很多问题,比如BLOB型的字段如何处理等等。作为基础的了解上面的例子已经足够了,关于DBBINDING更高级的内容我将放在后续的高级内容中。这里先快速给大家打个基础。
四、创建访问器:
再有了绑定结构之后,接下来要做的工作就是通知数据提供者按我们的要求将数据进行一个“格式化”,这个过程就是创建一个访问器。创建访问器就要使用IAccessor接口,同样这个接口也是从IRowset查询的来,代表访问器的标志则是一个类型为HACCESSOR的句柄。
下面的例子代码演示了如何创建一个访问器:
HACCESSOR * phAccessor,
IAccessor * pIAccessor = NULL;
pIRowset->QueryInterface(IID_IAccessor,(void**)&pIAccessor));
pIAccessor->CreateAccessor( DBACCESSOR_ROWDATA,
cColumns,rgBindings,0,phAccessor,NULL));
if( pIAccessor )
{
pIAccessor->Release();
}
上面的例子代码中直接使用了前一个例子中创建的绑定结构,这段例子代码中唯一需要注意的就是最后我们直接释放了IAccessor接口,这也表示创建了访问器后这个接口就没有用了,如果需要我们其实也可以随时从同一IRowset接口再查询出这个接口即可,因为这两个接口表示的结果集对象是同一个。
至此数据提供者也知道了我们要以什么具体的内存格式来得到数据,接下去就是真正的得到数据了。
五、得到数据:
有了前面4步的基础工作,最后我们终于可以正式的得到我们的数据了,为了描述的连贯性这里先将例子代码写出来:
void * pData = NULL;
ULONG cRowsObtained;
HROW * rghRows = NULL;
ULONG iRow;
LONG cRows = 10;//一次读取10行
void * pCurData;
//分配cRows行数据的缓冲,然后反复读取cRows行到这里,然后逐行处理之
pData = HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY, dwOffset * cRows);
while( S_OK == pIRowset->GetNextRows(DB_NULL_HCHAPTER,
0,cRows,&cRowsObtained, &rghRows)) )
{//循环读取数据,每次循环默认读取cRows行,实际读取到cRowsObtained行
for( iRow = 0; iRow < cRowsObtained; iRow++ )
{
pCurData = (BYTE*)pData + (dwOffset * iRow);
pIRowset->GetData( rghRows[iRow],hAccessor,pCurData));
//pCurData中已经包含了结果数据,显示或者进行处理
......
}
if( cRowsObtained )
{//释放行句柄数组
pIRowset->ReleaseRows(cRowsObtained,rghRows,NULL,
NULL,NULL));
}
CoTaskMemFree(rghRows);
rghRows = NULL;
}
HeapFree(GetProcessHeap(),0,pData);
if( pIRowset )
{
pIRowset->Release();
}
上面的例子代码中也直接使用了第三步和第四步中的一些变量,大家要注意每个变量的含义,dwOffset实际在这里已经表示一行数据需要的内存大小。
在这里又多了一个类型HROW,这个类型表示行的句柄,这里使用的是HROW的数组,读出多少行,数组中就有多少行的句柄,然后在使用句柄调用GetData将数据读到指定的内存位置。最后每次都释放了这个数组,因为数据已经读进了我们分配的缓存中,这个句柄数组也就变得没有意义了。最终数据读取完毕,我们也就释放了IRowset接口。
至此进过5个复杂而难于理解的步骤,我们终于读到了我们想要的数据,每一行数据都在那个pCurData中,并且严格按照我们指定的DBBINDING结构中的几个Offset偏移地址摆放,处理行数据内存时,也就按照这几个偏移就的得了具体的每一行每一个列的值。
大体上如何最终得到行数据的基础性内容也就介绍完了,整个的OLEDB基础性的知识也就介绍完了,之后的后续文章中我将继续介绍一些更加细节性的问题,或者是更加深入和专题化的问题,也就是提高部分的内容,在继续后续内容之前,也希望大家对之前的所有的内容都有所了解和掌握。
不管怎么说OLEDB接口虽然功能强大,但其复杂性也是让人望而却步的。通过前面4篇文章的介绍,也只是很基础的内容,同时限于本人的知识能力水平,错漏之处也在所难免,在此也恳请大家对这一系列文章提出宝贵的意见或问题,以便我之后改进提高。最终的目的无非就是让大家都真正的掌握OLEDB这个“神兵利器”,做到“攻无不克,战无不胜!”。