很久没有更新我的博客了,作为系列文章,OLEDB这个专题的前几篇文章受到了广大网友的极大关注,让我有点受宠若惊了,本想早点完成这个系列,哪怕是后续的文章,无奈杂务缠身,加之原稿和原始代码的丢失,使我不得不又重头建立这些,当然只要有网友的关注,我就会继续将这个话题讨论下去,直到大家都真正的掌握OLEDB这个数据库编程接口,哪怕仅仅是点击率的上涨,对我都是莫大的安慰。
前面的系列文章只是使用OLEDB的基础,这一章中我们将重点讨论对于大二进制数据类型的访问,比如SQL Server中的TEXT型、Image型还有XML类型等。
现在将各种文件图片甚至音像视频文件存入数据库中,并统一进行管理已经不是什么稀奇的事情了,各种开发工具环境都提供了对这些大二进制对象存取的完备支持,在OLEDB中也有对这些数据的完整支持,当然理解起来和使用起来不是那么方便,但它绝对是最底层的接口,执行效率依然是我们选择OLEDB的首要原因。
在OLEDB中对这类数据有专门的称呼BLOB(Binary Large Objects),其他大多数工具中也这样称呼这些数据。实际上如果理解了数据库存放此类数据的具体方式,那么理解对它的访问也比较简单,BLOB型数据与其他的数据类型唯一不同的地方就是绑定和获取数据方式的不同。
首先在绑定时,需要判定结果集中对应的列是否是BLOB型,这可以通过判定DBCOLUMNINFO中的wType或dwFlags标志来判定,具体的可以写如下的判定:
pColInfo[iCol].wType == DBTYPE_IUNKNOWN ||
(pColInfo[iCol].dwFlags & DBCOLUMNFLAGS_ISLONG)
当这里的两个具体条件之一成立时,我们就可以认为当前这个列是BLOB型的。
其次对于此类列数据进行访问时,需要使用一个称为ISequentialStream的接口,这里要注意的是这不是一个OLEDB特有接口,而是一个COM规范的接口,专门用于流式数据访问的接口,它非常的简单,只有两个方法:
HRESULT Read(void *pv,
ULONG cb,
ULONG *pcbRead);
HRESULT Write(void const * pv,
ULONG cb,
ULONG * pcbWritten);
这两个方法都非常简单,一看他们的参数类型已经可以猜到如何调用它们了。
当然说到这里你肯定很奇怪,其它数据类型不就是按照指定的长度准备一块内存,然后让OLEDB接口把数据放到我们指定的内存处就ok了吗?为什么BLOB型非要这些奇怪的Read和Write方法才能读取和存储呢?其实BLOB型的数据在实际的数据库(DBMS)系统中存储的方式与其它的简单类型是非常的不同的,最大的不同就是它们不是被直接放入到“行记录”中,通常对它们的存储使用的是一种相对存储技术,典型的就是在真正的行记录中对应的BLOB字段处只放一个“指针”,而真正的BLOB数据放在表数据的后边,或者专门的磁盘扇区处。注意这里的指针不是内存指针,而是硬盘扇区的指针,通常它是一个64bit值或更长的一个值,因为当前几百个G的硬盘已经不是什么稀奇的东西了,索引它上面的地址必须要用更多的bit。这样存放BLOB数据的根本原因其实是因为此类数据通常是长度不固定的,而且行与行之间的同列数据的尺寸差异非常之大,也可能为空,也可能只有几K,还有可能是几个G,因此为它们安排统一大小的行记录位置显然是非常的不明智的,所以就采用了上面的这种策略。
对于OLEDB来说,对这类数据的访问因为这种存放方式的原因,也需要进行一些特殊的处理。明显的就是无法准确的知道这个数据的实际长度。在使用时,这就需要你具有非常棒的动态内存管理技巧,不然很容易在这各地方造成内存访问异常或者可怕的内存泄露问题。
那么先来看看绑定时我们究竟要做什么不同的工作,示例代码如下:
pBindings[iCol].pBindExt = NULL;
pBindings[iCol].dwFlags = 0;
pBindings[iCol].bPrecision = 0;
pBindings[iCol].bScale = 0;
pBindings[iCol].wType = DBTYPE_IUNKNOWN;
pBindings[iCol].cbMaxLen = 0;
pBindings[iCol].pObject = (DBOBJECT*)HeapAlloc(GetProcessHeap(),0,sizeof(DBOBJECT));
pBindings[iCol].pObject->iid = IID_ISequentialStream;
pBindings[iCol].pObject->dwFlags = STGM_READ;
在上面的例子中有几个明显不同的地方,首先就是wType也就是我们指定的数据类型,是一个IUNKNOWN接口类型,其实这是一个暗示方式,就是告诉OLEDB的提供者我们将对BLOB列采用COM流方式来访问,你直接给我一个COM接口。
接下来,我们为列的大小显式指定了0值,这很重要,不要忽略了,大小指定0并不影响我们后续对数据的访问,因为这里我们使用的是“接口”。
紧接着我们设定了pObject成员,以及此成员的子成员,尤其注意对pObject子成员的赋值。这里对pObject使用了一个堆变量,而不是一个栈变量,这样做是因为很多时候我们使用OLEDB时绑定操作和实际的访问操作,以及最后的释放操作都不是在同一个函数中,因此需要使用堆变量。
对pObject的赋值使用了一个结构叫DBOBJECT,此结构非常简单原型如下:
typedef struct tagDBOBJECT
{
DWORD dwFlags;
IID iid;
} DBOBJECT;
其中的dwFlags成员根据之后的IID变量类型指定一组标志,用于控制实际创建的最终的BLOB数据访问COM接口的行为,目前我们关注的就是3个值:STGM_READ,STGM_WRITE,STGM_READWRITE。这几个值一看已经明白它们的具体含义了,我就不再啰嗦了。
iid成员变量最终指定由OLEDB提供者暴露给我们的访问BLOB数据的COM接口的实际类型,就是我们前面提到的IID_ISequentialStream类型。至此绑定也搞定了。
在绑定之后,OLEDB提供者最终会将一个COM接口的指针放在行集中,因为我们已经指定了类型,那么这个指针实际上就是一个ISequentialStream的指针。下面的例子代码演示了如何访问这个接口:
//pData为指向整个行集开始地址的指针
dwStatus = *(DBSTATUS *)(( BYTE* )pData + pBindings[iCol].obStatus);
ulLength = *(ULONG *)((BYTE *)pData + pBindings[iCol].obLength);
pvValue = (BYTE *)pData + pBindings[iCol].obValue;
if( DBTYPE_IUNKNOWN == pBindings[iCol].wType )
{
ISequentialStream* pISeqStream = *(ISequentialStream**)pvValue;
if(NULL != pISeqStream)
{
const ULONG cBytes = 16 * 4 * 1024; //一次访问64k
ULONG nBufLen = 0;
ULONG cbRead = 0;
BYTE* pBuffer = (BYTE*)HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY,
cBytes);
do
{
hr = pISeqStream->Read(pBuffer + nBufLen,cBytes,&cbRead);
if( 0 < cbRead && SUCCEEDED(hr) )
{
nBufLen += cbRead;
pBuffer = (BYTE*)HeapReAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY,
pBuffer,
nBufLen + cBytes);
}
}while (S_OK == hr);
pISeqStream->Release();
}
最终通过上面的访问代码,pBuffer中就放着对应行的BLOB列的数据了。这段代码中使用了Windows的堆函数进行了内存重分配操作,省却了一个内存拷贝动作,因为HeapReAlloc函数已经帮我们隐含的做了这一切。最后我们立即释放了我们得到的IsequentialStream接口的指针,这个是一定要记住的操作,因为根据OLEDB的规定,这个接口的生命期完全由消费者控制,因此,即使我们释放了对应的IRowset接口或者IAccess接口,OLEDB提供者也不会回收这个ISequentialStream接口,这会造成严重的内存泄露问题,最终导致程序崩溃,因此这个步骤非常的重要,一定要像洗脸刷牙一样的记住它,在我们通过ISequentialStream接口得到我们想要的数据之后,我们就应该释放这个接口了,当然不一定总是立即释放,你可以在合适的地方释放它。
特别要注意的是以上的方法只是简单的从数据库中读出BLOB型数据,而写入这类数据还需要其它的操作,主要用到的接口是IRowsetChange,关于这个接口我将在后面的文章中介绍,此处先不做讲解,重点还是放在BLOB型数据的其他特性上。
通过以上方法我们可以访问一列BLOB数据,通常我们还需要访问多列的BLOB数据,在一个结果集中,但并不是所有的OLEDB提供者都能支持在一行的一个访问器中返回两个以上的BLOB型数据(注意说的是同一个访问器,多个访问器是可以访问多个BLOB的,如果你还没有晕,就请接着往下看,呵呵呵),具体的只需要我们通过查询一个叫做DBPROP_MULTIPLESTORAGEOBJECTS的Rowset属性来判定,代码例子如下:
IRowsetInfo * pIRowsetInfo = NULL;
DBPROPSET * rgPropSets = NULL;
DBPROPID rgPropertyIDs[1];
DBPROPIDSET rgPropertyIDSets[1];
ULONG cPropSets = 0;
rgPropertyIDs[0] = DBPROP_MULTIPLESTORAGEOBJECTS;
rgPropertyIDSets[0].rgPropertyIDs = rgPropertyIDs;
rgPropertyIDSets[0].cPropertyIDs = 1;
rgPropertyIDSets[0].guidPropertySet = DBPROPSET_ROWSET;
hr = pIRowSet->QueryInterface(IID_IRowsetInfo,
(void**)&pIRowsetInfo);
hr = pIRowsetInfo->GetProperties(
1, //cPropertyIDSets
rgPropertyIDSets, //rgPropertyIDSets
&cPropSets, //pcPropSets
&rgPropSets //prgPropSets
);
if( V_VT(&rgPropSets[0].rgProperties[0].vValue) == VT_BOOL
&& V_BOOL(&rgPropSets[0].rgProperties[0].vValue) == VARIANT_TRUE )
{//支持多个BLOB数据
}
通过上面的代码最后一个判断我们就可以知道结果集支不支持多个BLOB列。目前遗憾的是SQL Server系列DBMS(包括最新的2008)都不支持多个BLOB型数据,这需要我们使用一定的编程策略来绕过这一特性。通常可以使用多访问器HACCESSOR多绑定法来实现这样的访问,这是一个非常有用和重要的OLEDB实战技巧,因为很多的OLEDB提供者都会有不支持多个BLOB的情况,因此掌握这个技巧就为访问多个BLOB数据提供了方便。下面的示例代码说明了如何访问多个BLOB数据在一行中:
//定义一个行记录的结构,方便我们后面对数据的访问
struct ROWDATA
{
DBSTATUS dwStatus;
DBLENGTH dwLength;
ISequentialStream* pISeqStream;
};
ROWDATA BLOBGetData0;
ROWDATA BLOBGetData1;
const ULONG cBindings = 1;
//注意我们定义了两个绑定结构
DBBINDING rgBindings0[cBindings];
DBBINDING rgBindings1[cBindings];
HRESULT hr = S_OK;
IAccessor* pIAccessor = NULL;
ICommandText* pICommandText = NULL;
IRowset* pIRowset = NULL;
DBCOUNTITEM cRowsObtained = 0;
//两个访问器定义
HACCESSOR hAccessor0 = DB_NULL_HACCESSOR;
HACCESSOR hAccessor1 = DB_NULL_HACCESSOR;
//两个绑定对应的两个绑定状态
DBBINDSTATUS rgBindStatus0[cBindings];
DBBINDSTATUS rgBindStatus1[cBindings];
HROW* rghRows = NULL;
const ULONG cPropSets = 1;
DBPROPSET rgPropSets[cPropSets];
const ULONG cProperties = 1;
DBPROP rgProperties[cProperties];
const ULONG cBytes = 10;
//结果数据缓存
BYTE pBuffer[cBytes];
ULONG cBytesRead = 0;
//表T_Blob有三列,第一列为int型,第二列为Text型,第三列为image类型
hr = pICommandText->SetCommandText(DBGUID_DBSQL, L"SELECT * FROM T_Blob");
hr = pICommandText->Execute(NULL,
IID_IRowset,
NULL,
NULL,
(IUnknown**)&pIRowset);
//设置两个绑定结构,注意iOrdinal字段序号,一个绑到第2列,一个绑到第3列
rgBindings0[0].iOrdinal = 2;
rgBindings0[0].obValue = offsetof(BLOBDATA, pISeqStream);
rgBindings0[0].obLength = offsetof(BLOBDATA, dwLength);
rgBindings0[0].obStatus = offsetof(BLOBDATA, dwStatus);
rgBindings0[0].pTypeInfo = NULL;
rgBindings0[0].pObject = &ObjectStruct;
rgBindings0[0].pBindExt = NULL;
rgBindings0[0].dwPart = DBPART_VALUE | DBPART_STATUS | DBPART_LENGTH;
rgBindings0[0].dwMemOwner = DBMEMOWNER_CLIENTOWNED;
rgBindings0[0].eParamIO = DBPARAMIO_NOTPARAM;
rgBindings0[0].cbMaxLen = 0;
rgBindings0[0].dwFlags = 0;
rgBindings0[0].wType = DBTYPE_IUNKNOWN;
rgBindings0[0].bPrecision = 0;
rgBindings0[0].bScale = 0;
//第3列
rgBindings1[0].iOrdinal = 3;
rgBindings1[0].obValue = offsetof(BLOBDATA, pISeqStream);
rgBindings1[0].obLength = offsetof(BLOBDATA, dwLength);
rgBindings1[0].obStatus = offsetof(BLOBDATA, dwStatus);
rgBindings1[0].pTypeInfo = NULL;
rgBindings1[0].pObject = &ObjectStruct;
rgBindings1[0].pBindExt = NULL;
rgBindings1[0].dwPart = DBPART_VALUE | DBPART_STATUS | DBPART_LENGTH;
rgBindings1[0].dwMemOwner = DBMEMOWNER_CLIENTOWNED;
rgBindings1[0].eParamIO = DBPARAMIO_NOTPARAM;
rgBindings1[0].cbMaxLen = 0;
rgBindings1[0].dwFlags = 0;
rgBindings1[0].wType = DBTYPE_IUNKNOWN;
rgBindings1[0].bPrecision = 0;
rgBindings1[0].bScale = 0;
hr = pIRowsetChange->QueryInterface(IID_IAccessor, (void**)&pIAccessor);
//创建第一个访问器
hr = pIAccessor->CreateAccessor(DBACCESSOR_ROWDATA,
cBindings,
rgBindings0,
sizeof(BLOBDATA),
&hAccessor0,
rgBindStatus0);
//创建第二个访问器
hr = pIAccessor->CreateAccessor(DBACCESSOR_ROWDATA,
cBindings,
rgBindings1,
sizeof(BLOBDATA),
&hAccessor1,
rgBindStatus1);
//取得行
hr = pIRowset->GetNextRows(NULL, 0, 1, &cRowsObtained, &rghRows);
//用第一个访问器得到第二列数据
hr = pIRowset->GetData(rghRows[0], hAccessor0, &BLOBGetData0);
if (BLOBGetData0.dwStatus != DBSTATUS_S_ISNULL
&& BLOBGetData0.dwStatus == DBSTATUS_S_OK)
{
BLOBGetData0.pISeqStream->Read( pBuffer, cBytes, &cBytesRead);
//pBuffer已经有第二列的数据,可以使用前例中的方法得到全部数据
SAFE_RELEASE(BLOBGetData0.pISeqStream);
}
//用第二个访问器得到第三列数据
hr = pIRowset->GetData(rghRows[0], hAccessor1, &BLOBGetData1);
if (BLOBGetData1.dwStatus != DBSTATUS_S_ISNULL
&& BLOBGetData1.dwStatus == DBSTATUS_S_OK )
{
BLOBGetData1.pISeqStream->Read( pBuffer, cBytes, &cBytesRead);
//pBuffer已经有第三列的数据,可以使用前例中的方法得到全部数据
SAFE_RELEASE(BLOBGetData1.pISeqStream);
}
上面的例子代码很好的演示了如何创建多个绑定结构(注意为了简洁明了,没有加入对hr的错误判定处理部分,实际编码时注意错误处理),多个访问器,以及如何用多个访问器访问同一行记录中的不同字段,上面例子中尤其要理解和掌握的技巧就是多个DBBINDING 、HACCESSOR、CreateAccessor以及多个GetData调用的一对一的关系,同时我们对一行记录只调用了一次GetNextRows,这样就最终实现了一行数据中有多个BLOB字段时的数据访问问题。当然在ADO当中已经对这个问题进行了很好的封装,使用ADO的程序员根本就不知道有这么个限制的存在,看起来用ADO的程序员还是很幸运的。
使用下面的T-SQL脚本创建例子中的表:
CREATE TABLE [T_Blob](
[K_ID] [int] NOT NULL,
[BLOB1] [image] NULL,
[BLOB2] [text] NULL,
CONSTRAINT [PK_T_Blob] PRIMARY KEY CLUSTERED
(
[K_ID] ASC
)WITH (PAD_INDEX = OFF,
STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF,
ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON)
ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
(未完待续)