65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.38/5 (4投票s)

2004年7月1日

CPOL

11分钟阅读

viewsIcon

97717

downloadIcon

515

在SQL CE数据库和ATL OLE DB消费者模板上管理大型数据类型。

Sample Image - atl_ole_db_blob_ppc.jpg

引言

这是关于在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支持两种不同的存储对象:ISequentialStreamILockBytes。顾名思义,第一个存储对象以顺序方式读写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正确编译是必要的。

© . All rights reserved.