在 Pocket PC 上使用 ATL OLE DB Consumer 模板管理 Blob






4.38/5 (4投票s)
在SQL CE数据库和ATL OLE DB消费者模板上管理大型数据类型。
引言
这是关于在Pocket PC上使用ATL OLE DB消费者模板的第二篇文章。在第一篇文章中,我描述了ATL OLE DB消费者模板如何在Pocket PC平台上进行适配和使用。该文章提供了一个在SQL Server CE 2.0数据库中管理数据的简单示例,并构成了本文所提供内容的基础。
简单的例子并不是您日常业务中遇到的情况。除了您可能在文章代码中发现的所有其他缺点之外,其中一个非常明显:没有提供处理大型数据类型(也称为blob,即二进制大对象)的功能。
对大型数据的需求
我们不能简单地让它们消失:我们的数据库中确实需要blob。客户会要求他们的数据库处理笔记、图片或其他任意的二进制数据。虽然您可以考虑其他处理方式,例如数据库和文件存储的混合场景,但您会失去复制数据的灵活性和简单性,尤其是在使用Microsoft的数据复制机制时。所以,最好将blob存储在数据库中。
但是,还需要处理blob的另一个不太明显的原因是:数据库限制和数据复制转换。当您将桌面数据库复制到SQL CE时,会强制执行一些数据转换,尤其是在ANSI到UNICODE转换方面,以及最重要的是,规避nvarchar
255个字符的大小限制。如果桌面数据库表有一个列,例如nvarchar(512)
,它将自动在Pocket PC上转换为ntext
(一个blob)。这适用于SQL Server CE 2.0,但将来可能会改变。
那么,如果我们必须与它们共存,我们如何管理blob呢?
管理大型数据
通常,大型数据字段通过OLE DB通过存储对象进行管理。数据不会像较小数据类型那样以一个完整的块呈现给您。相反,OLE DB提供程序会给您一个存储对象接口指针,您可以使用它来管理这些大型数据。通过此对象,您可以操作blob数据,将其读入应用程序变量或从应用程序写入blob。让我们看看如何操作。
存储对象类型
SQL Server CE支持两种不同的存储对象:ISequentialStream
和ILockBytes
。顾名思义,第一个存储对象以顺序方式读写blob数据。数据以顺序块的方式读写,这使您可以节省程序内存:您不必在内存中创建完整的blob映像即可使用它。ILockBytes
存储对象允许您执行相同的操作,但使用随机访问方法。
在本文中,我将只使用ISequentialStream
对象。使用ILockBytes
可以留作读者的练习。
绑定Blob
在读写blob之前,它们必须像任何普通列一样通过访问器进行绑定。CDynamicAccessor
的标准实现会在BindColumns
方法中为您完成此操作。该方法将使用以下条件测试是否存在blob
if (m_pColumnInfo[i].ulColumnSize > 1024 || m_pColumnInfo[i].wType == DBTYPE_IUNKNOWN)
通常,blob将被标记为大小远大于1024字节,因此类型测试不相关。如果此测试为真,该方法将创建一个DBOBJECT
对象,指定数据将通过ISequentialStream
指针进行管理。
这意味着,访问器缓冲区将存储一个`ISequentialStream`指针,而不是像非blob列那样具有数据的完整显式表示。读取数据时,此指针将由提供程序自动创建,并由消费者负责释放。写入数据时,情况则完全相反。消费者创建一个`ISequentialStream`对象并提供一个指向它的指针。提供程序使用此指针读取数据并释放它。
因此,读写blob的蓝图似乎相当简单。您不必直接访问数据副本,而是必须通过存储对象指针,该指针由提供程序或消费者创建,具体取决于数据是读取还是写入。但是,像往常一样,生活并非如此简单。如果我们必须管理每个表或查询的多个blob,则此方案不起作用。为什么?请继续阅读。
SQL Server CE 2.0提供程序限制
上述用于绑定blob的方法在SQL Server CE 2.0 OLE DB提供程序上不起作用,因为它一次只能提供一个存储对象。因此,如果您使用默认的BindColumns
行为,则每个行集将无法绑定多个blob列。此限制由DBPROP_MULTIPLESTORAGEOBJECTS
属性揭示,该属性在此特定提供程序上是只读的,并设置为false。
我第一次尝试解决此问题是按引用绑定blob字段。此方法用于桌面ATL的较新版本(7.1),并且适用于大多数提供程序(SQL Server 2000和Jet 4.0都支持它)。您知道吗?SQL Server CE 2.0也不支持这种类型的绑定。
因此,我们需要解决这个问题。经过一番研究,解决方案变得显而易见,尽管不一定简单。
使用多个访问器
绑定每个行集多个blob的解决方案是使用多个访问器。如果您仔细阅读CDynamicAccessor
的代码,您会看到它只使用一个访问器句柄,而不论情况如何。我解决此问题的想法是为每个行集提供更多访问器句柄,使用一个简单的分配:我们使用第一个访问器绑定所有非blob列,并使用所有后续访问器分别绑定一个blob列。因此,例如,如果我们有一个包含5个常规列和2个blob列的行集,我们将在绑定时使用3个访问器句柄。
通过更改派生类CSmartAccessor
中的BindColumns
行为,可以轻松实现这些更改。如果您查看代码(包含在示例中——太长而无法在此处重现),您将看到两个列绑定循环。在第一个循环中,所有非blob列都使用第一个访问器句柄进行绑定。第二个循环将使用其自己的访问器句柄绑定所有blob列。请注意,没有任何blob访问器被标记为自动访问器,这意味着当获取新行时(例如使用MoveNext
),不会自动检索blob列的数据。您必须逐列进行,将blob数据存储在应用程序提供的内存中。
所以,让我们来看看这一切的实际操作。
读取
每当行位置移动时,底层数据都会自动从提供程序提取到访问器缓冲区中。这是在消费者模板中找到的默认实现,请允许我提醒您,它将只为整个行使用一个访问器句柄。
我们的解决方案意味着使用额外的访问器句柄,每个 blob 列一个,由于 SQL CE 提供程序的限制,其数据必须按需加载。因此,虽然非 blob 数据在行位置更新后会立即可用,但 blob 数据必须通过存储对象显式加载,并且在读取任何其他 blob 之前必须显式处理此对象。
现在,这带来了一个非常简单的操作问题。你看,为了通过访问器句柄显式加载BLOB列数据,我们必须知道要使用哪个访问器句柄。BindColumns
方法已经为我们完成了所有的访问器分配工作,所以让我们看看它是如何完成的,以及我们如何找到任何给定BLOB列的访问器句柄。
访问器分配
在分配过程开始之前,该方法计算blob列和访问器句柄的数量。这是代码(请使用本文提供的示例代码来遵循此讨论)
nblobs = 0; for(i = 0; i < m_nColumns; ++i) if(m_pColumnInfo[i].ulColumnSize > m_nblobSize || m_pColumnInfo[i].wType == DBTYPE_IUNKNOWN) ++nblobs; nAccessors = nblobs + 1;
在这段代码下方,您可以看到专门为blob列分配了一个新的DBBINDING
数组。在此之后,有一个循环将所有非blob列绑定到第一个访问器句柄。这段代码与您在CDynamicAccessor
的默认实现中找到的代码非常相似。
接下来的代码将所有blob列绑定到它们自己的访问器句柄。访问器句柄存储在一个数组中,该数组的索引存储在DBCOLUMNINFO
结构的bPrecision
成员中。这就是我们寻找访问器句柄索引的地方。
m_pColumnInfo[i].wType = DBTYPE_IUNKNOWN; m_pColumnInfo[i].ulColumnSize = sizeof(IUnknown*); m_pColumnInfo[i].bPrecision = ++iAccessor; // Accessor number for this blob m_pColumnInfo[i].bScale = 0;
为了方便开发人员,我包含了两个名为`GetBlobAccessor`的方法,它们将返回任何给定blob列的访问器句柄索引。听起来很复杂?其实不然,让我们看一个代码示例。
代码示例
以下代码展示了如何将blob文本字段加载到CString
变量中。
// // Retrieve the description ntext field // if(table.GetblobAccessor(_T("Description"), &nAccessor)) { HRESULT hr; hr = table.GetData(nAccessor); if(FAILED(hr)) return hr; } table.Get(_T("Description"), m_strDesc);
存储对象的释放是在Get
方法中执行的。
插入
在插入新数据或更新现有数据时,我们必须处理一个新的有趣问题。在之前的讨论中,我说用于传输数据的存储对象在读取数据时由提供程序创建,在写入数据时由消费者创建。因此,我们需要创建一个COM存储对象,用blob数据填充它,并将其指针提供给提供程序。这可不是一个简单的任务,对吧?
值得庆幸的是,我得到了Microsoft示例的帮助,解决了这个问题。
CBlobStream类
在研究本文时,我在MSDN上遇到了一个名为**AOTBLOB**的示例。这个小程序展示了如何使用一个辅助类轻松解决写入问题:CISSHelper
。我编写CBlobStream
类时改编了大部分代码,但添加了一个小功能:Release
方法现在按预期工作:它使用我最喜欢的C++代码行之一删除对象
ULONG CblobStream::Release() { if(m_nRef) { --m_nRef; if(m_nRef == 0) { delete this; } } return m_nRef; }
如果您查看示例代码,您会看到引用计数机制已就位,并且第一次增量是在构造函数内部完成的。
如您所见,该类本身派生自ISequentialStream
,因此您可以实际将对象指针提供给提供程序,并且它会正常工作。提供程序甚至会为您调用Release
,因此您应该小心不要静态分配此类对象。
现在,让我们看看这个类是如何工作的。以下代码取自CSmartAccessor
类,具体来说是来自_set_value
的字符串版本的blob部分
IStream* pStream; DBSTATUS dbStatus; ULONG nLength, nActual; CBlobStream* pBlob = NULL; dbStatus = _get_status(nColumn); if(dbStatus == DBSTATUS_S_OK) { // // Release the existing stream // pStream = *(IStream**)_GetDataPtr(nColumn); if(pStream) pStream->Release(); } // // Create a new stream // nLength = wcslen(pszText) * sizeof(TCHAR); pBlob = new CBlobStream; if(pBlob) { HRESULT hr; hr = pBlob->Write(pszText, nLength, &nActual); if(SUCCEEDED(hr)) { *(CBlobStream**)_GetDataPtr(nColumn) = pBlob; _set_status(nColumn, DBSTATUS_S_OK); _set_length(nColumn, nLength); bOk = true; } else { _set_status(nColumn, DBSTATUS_S_ISNULL); _set_length(nColumn, 0); delete pBlob; } }
首先,我们必须确保提供程序创建的存储对象被释放。请注意如何测试它的存在(状态和指针)。
接下来,我们创建CBlobStream
对象,并使用Write
方法用字符串数据填充它。最后,如果最后一次操作成功,我们将指针写入访问器缓冲区,从而将其发送给提供程序。请注意,对象未被删除或释放,因为这是提供程序的责任。
不过,还缺少一件事。我们如何插入包含blob的行?
插入过程
当使用多个blob(和访问器)时,插入行的过程有点不同。本质上,我们必须在第一个访问器(用于绑定非blob列的那个)上使用Insert
方法,并请求新的行句柄。然后,使用此行句柄,我们可以设置所有blob列。让我们看一些代码
// // Use Insert on the non-BLOB accessor, and SetData on all BLOB accessors // hr = table.Insert(0, true); for(i = 1; i < table.GetNumAccessors() && SUCCEEDED(hr); ++i) hr = table.SetData(i);
就这么简单。
更新
我把最好的部分留到了最后。更新数据不需要对代码进行任何更改,因为`SetData`方法将使用所有访问器句柄。我们完成了。
示例应用
随附的示例应用程序是第一篇文章中介绍的应用程序的扩展。它现在允许您编辑类别表,其中每行有两个blob:描述和图像。
在开发此示例期间,对atldbcli_ce.h文件进行了一些小改动。这些改动在此处描述。
注意:正如我在第一篇文章中提到的,由于可能存在的Microsoft版权限制,此文件未在此处提供。
代码更改
打开您根据第一篇文章中给出的说明创建的atldbcli_ce.h文件,并查找CDynamicAccessor
。在成员变量声明中,添加
ULONG m_nBlobSize;
现在,转到构造函数并添加以下行
m_nBlobSize = 1024;
现在,如果您愿意,可以将BindColumns
方法中1024的出现替换为m_nBlobSize
,尽管这不是必需的。此更改对于使新版CSmartAccessor
正确编译是必要的。