创建可更新的提供程序

Visual C++ 支持可更新的提供程序,或可以更新(写入)数据存储的提供程序。 本主题讨论如何使用 OLE DB 模板创建可更新的提供程序。

本主题假定从可运行的提供程序开始。 创建可更新的提供程序需要两个步骤。 必须首先确定提供程序如何更改数据存储;具体而言,是立即更改还是延迟更改直到发出更新命令。 “使提供程序可更新”部分介绍了在提供程序代码中需要进行的更改和设置。

接下来,必须确保提供程序包含用以支持使用者可能请求的任何内容的所有功能。 如果使用者想要更新数据存储,提供程序必须包含用于将数据保留到数据存储的代码。 例如,可以使用 C 运行时库或 MFC 对数据源执行此类操作。 “写入数据源”部分介绍了如何写入数据源、处理 NULL 和默认值以及设置列标志。

注意

UpdatePV 是可更新的提供程序的示例。 UpdatePV 与 MyProv 相同,但其支持更新。

使提供程序可更新

使提供程序可更新的关键在于了解你希望提供程序对数据存储执行哪些操作及提供程序执行这些操作的方式。 具体而言,主要问题是要立即更新数据存储,还是延迟更新(批处理)直到发出更新命令。

必须首先决定是从 IRowsetChangeImpl 继承还是从 IRowsetUpdateImpl 行集类继承。 根据所选的实现内容,这三种方法的功能将受到影响:SetDataInsertRowsDeleteRows

  • 如果继承自 IRowsetChangeImpl,则调用这三种方法会立即更改数据存储。

  • 如果继承自 IRowsetUpdateImpl,则这些方法将延迟更改数据存储直到调用 UpdateGetOriginalDataUndo。 如果更新涉及多个更改,则在批处理模式下执行这些更改(请注意,批处理更改可能会增加大量内存开销)。

请注意,IRowsetUpdateImpl 派生自 IRowsetChangeImpl。 因此,IRowsetUpdateImpl 提供更改功能及批处理功能。

支持提供程序可更新性

  1. 在行集类中,从 IRowsetChangeImplIRowsetUpdateImpl 继承。 这些类提供用于更改数据存储的相应接口:

    添加 IRowsetChange

    使用以下形式将 IRowsetChangeImpl 添加到继承链:

    IRowsetChangeImpl< rowset-name, storage-name >
    

    此外,将 COM_INTERFACE_ENTRY(IRowsetChange) 添加到行集类中的 BEGIN_COM_MAP 部分。

    添加 IRowsetUpdate

    使用以下形式将 IRowsetUpdate 添加到继承链:

    IRowsetUpdateImpl< rowset-name, storage>
    

    注意

    应从继承链中删除 IRowsetChangeImpl 行。 上述指令的这一例外必须包含 IRowsetChangeImpl 代码。

  2. 将以下内容添加到 COM 映射 (BEGIN_COM_MAP ... END_COM_MAP):

    如果实现 添加到 COM 映射
    IRowsetChangeImpl COM_INTERFACE_ENTRY(IRowsetChange)
    IRowsetUpdateImpl COM_INTERFACE_ENTRY(IRowsetUpdate)
    如果实现 添加到属性集映射
    IRowsetChangeImpl PROPERTY_INFO_ENTRY_VALUE(IRowsetChange, VARIANT_FALSE)
    IRowsetUpdateImpl PROPERTY_INFO_ENTRY_VALUE(IRowsetUpdate, VARIANT_FALSE)
  3. 在命令中,将以下内容添加到属性集映射 (BEGIN_PROPSET_MAP ... END_PROPSET_MAP):

    如果实现 添加到属性集映射
    IRowsetChangeImpl PROPERTY_INFO_ENTRY_VALUE(IRowsetChange, VARIANT_FALSE)
    IRowsetUpdateImpl PROPERTY_INFO_ENTRY_VALUE(IRowsetChange, VARIANT_FALSE)PROPERTY_INFO_ENTRY_VALUE(IRowsetUpdate, VARIANT_FALSE)
  4. 在属性集映射中,还应包含以下所有设置,如下所示:

    PROPERTY_INFO_ENTRY_VALUE(UPDATABILITY, DBPROPVAL_UP_CHANGE |
      DBPROPVAL_UP_INSERT | DBPROPVAL_UP_DELETE)
    PROPERTY_INFO_ENTRY_VALUE(CHANGEINSERTEDROWS, VARIANT_TRUE)
    PROPERTY_INFO_ENTRY_VALUE(IMMOBILEROWS, VARIANT_TRUE)
    
    PROPERTY_INFO_ENTRY_EX(OWNINSERT, VT_BOOL, DBPROPFLAGS_ROWSET |
      DBPROPFLAGS_READ, VARIANT_TRUE, 0)
    PROPERTY_INFO_ENTRY_EX(OWNUPDATEDELETE, VT_BOOL, DBPROPFLAGS_ROWSET |
      DBPROPFLAGS_READ, VARIANT_TRUE, 0)
    PROPERTY_INFO_ENTRY_EX(OTHERINSERT, VT_BOOL, DBPROPFLAGS_ROWSET |
      DBPROPFLAGS_READ, VARIANT_TRUE, 0)
    PROPERTY_INFO_ENTRY_EX(OTHERUPDATEDELETE, VT_BOOL, DBPROPFLAGS_ROWSET |
      DBPROPFLAGS_READ, VARIANT_TRUE, 0)
    PROPERTY_INFO_ENTRY_EX(REMOVEDELETED, VT_BOOL, DBPROPFLAGS_ROWSET |
      DBPROPFLAGS_READ, VARIANT_FALSE, 0)
    

    可以通过在 Atldb.h 中查找属性 ID 和值来查找这些宏调用中使用的值(如果 Atldb.h 不同于联机文档,则 Atldb.h 将取代文档)。

    注意

    OLE DB 模板需要许多 VARIANT_FALSEVARIANT_TRUE 设置;OLE DB 规范规定可读/写它们,但 OLE DB 模板只能支持一个值。

    如果实现 IRowsetChangeImpl

    如果实现 IRowsetChangeImpl,则必须在提供程序上设置以下属性。 这些属性主要用于通过 ICommandProperties::SetProperties 请求接口。

    • DBPROP_IRowsetChange:设置此属性将自动设置 DBPROP_IRowsetChange

    • DBPROP_UPDATABILITY:用于在 IRowsetChange 上指定支持的方法的位掩码:SetDataDeleteRowsInsertRow

    • DBPROP_CHANGEINSERTEDROWS:使用者可以对新插入的行调用 IRowsetChange::DeleteRowsSetData

    • DBPROP_IMMOBILEROWS:行集将不对插入或更新的行重新排序。

    如果实现 IRowsetUpdateImpl

    如果实现 IRowsetUpdateImpl,除了为前面列出的 IRowsetChangeImpl 设置所有属性外,还必须在提供程序上设置以下属性:

    • DBPROP_IRowsetUpdate

    • DBPROP_OWNINSERT:必须为 READ_ONLY AND VARIANT_TRUE。

    • DBPROP_OWNUPDATEDELETE:必须为 READ_ONLY AND VARIANT_TRUE。

    • DBPROP_OTHERINSERT:必须为 READ_ONLY AND VARIANT_TRUE。

    • DBPROP_OTHERUPDATEDELETE:必须为 READ_ONLY AND VARIANT_TRUE。

    • DBPROP_REMOVEDELETED:必须为 READ_ONLY AND VARIANT_TRUE。

    • DBPROP_MAXPENDINGROWS

    注意

    如果支持通知,可能还具有一些其他属性;请参阅有关 IRowsetNotifyCP 的部分了解此列表。

写入数据源

若要读取数据源,请调用函数 Execute。 若要写入数据源,请调用函数 FlushData。 (一般情况下,刷新表示将对表或索引所做的修改保存到磁盘。)

FlushData(HROW, HACCESSOR);

通过行句柄 (HROW) 和访问器句柄 (HACCESSOR) 参数可以指定要写入的区域。 通常,一次写入一个数据字段。

FlushData 方法以数据最初存储格式写入数据。 如果不重写此函数,提供程序将正常运行,但不会将更改刷新到数据存储。

何时刷新

每当需要将数据写入数据存储时,提供程序模板都会调用 FlushData;这通常是(但并非总是)调用以下函数的结果:

  • IRowsetChange::DeleteRows

  • IRowsetChange::SetData

  • IRowsetChange::InsertRows(如果要在行中插入新数据)

  • IRowsetUpdate::Update

工作原理

使用者发出需要刷新的调用(例如 Update),此调用传递给提供程序,提供程序将始终执行以下操作:

  • 每当绑定状态值时调用 SetDBStatus

  • 检查列标志。

  • 调用 IsUpdateAllowed

这三个步骤有助于提供安全性。 然后提供程序调用 FlushData

如何实现 FlushData

若要实现 FlushData,需要考虑几个问题:

确保数据存储可以处理更改。

处理 Null 值。

设置默认值。

若要实现自己的 FlushData 方法,需要:

  • 转到行集类。

  • 在行集类中,放入以下项的声明:

    HRESULT FlushData(HROW, HACCESSOR)
    {
        // Insert your implementation here and return an HRESULT.
    }
    
  • 提供 FlushData 的实现。

正确实现 FlushData 将仅存储实际更新的行和列。 可以使用 HROW 和 HACCESSOR 参数来确定要存储以进行优化的当前行和列。

通常,最大挑战在于使用自己的本机数据存储。 如果可能,请尝试执行以下操作:

  • 使数据存储写入方法尽可能简单。

  • 处理 NULL 值(可选但建议这么做)。

  • 处理默认值(可选但建议这么做)。

最好是在数据存储中为 NULL 和默认值指定实际的值。 最好能推断出这些数据。 如果不能,建议不使用 NULL 和默认值。

以下示例演示如何在 UpdatePV 示例的 RUpdateRowset 类中实现 FlushData(请参阅示例代码中的 Rowset.h):

///////////////////////////////////////////////////////////////////////////
// class RUpdateRowset (in rowset.h)
...
HRESULT FlushData(HROW, HACCESSOR)
{
    ATLTRACE2(atlTraceDBProvider, 0, "RUpdateRowset::FlushData\n");

    USES_CONVERSION;
    enum {
        sizeOfString = 256,
        sizeOfFileName = MAX_PATH
    };
    FILE*    pFile = NULL;
    TCHAR    szString[sizeOfString];
    TCHAR    szFile[sizeOfFileName];
    errcode  err = 0;

    ObjectLock lock(this);

    // From a filename, passed in as a command text,
    // scan the file placing data in the data array.
    if (m_strCommandText == (BSTR)NULL)
    {
        ATLTRACE( "RRowsetUpdate::FlushData -- "
                  "No filename specified\n");
        return E_FAIL;
    }

    // Open the file
    _tcscpy_s(szFile, sizeOfFileName, OLE2T(m_strCommandText));
    if ((szFile[0] == _T('\0')) ||
        ((err = _tfopen_s(&pFile, &szFile[0], _T("w"))) != 0))
    {
        ATLTRACE("RUpdateRowset::FlushData -- Could not open file\n");
        return DB_E_NOTABLE;
    }

    // Iterate through the row data and store it.
    for (long l=0; l<m_rgRowData.GetSize(); l++)
    {
        CAgentMan am = m_rgRowData[l];

        _putw((int)am.dwFixed, pFile);

        if (_tcscmp(&am.szCommand[0], _T("")) != 0)
            _stprintf_s(&szString[0], _T("%s\n"), am.szCommand);
        else
            _stprintf_s(&szString[0], _T("%s\n"), _T("NULL"));
        _fputts(szString, pFile);

        if (_tcscmp(&am.szText[0], _T("")) != 0)
            _stprintf_s(&szString[0], _T("%s\n"), am.szText);
        else
            _stprintf_s(&szString[0], _T("%s\n"), _T("NULL"));
        _fputts(szString, pFile);

        if (_tcscmp(&am.szCommand2[0], _T("")) != 0)
            _stprintf_s(&szString[0], _T("%s\n"), am.szCommand2);
        else
            _stprintf_s(&szString[0], _T("%s\n"), _T("NULL"));
        _fputts(szString, pFile);

        if (_tcscmp(&am.szText2[0], _T("")) != 0)
            _stprintf_s(&szString[0], _T("%s\n"), am.szText2);
        else
            _stprintf_s(&szString[0], _T("%s\n"), _T("NULL"));
        _fputts(szString, pFile);
    }

    if (fflush(pFile) == EOF || fclose(pFile) == EOF)
    {
        ATLTRACE("RRowsetUpdate::FlushData -- "
                 "Couldn't flush or close file\n");
    }

    return S_OK;
}

处理更改

为使提供程序能够处理更改,首先需要确保数据存储(如文本文件或视频文件)具有便于对其进行更改的功能。 如果没有,则应与提供程序项目分开创建该代码。

处理 NULL 数据

最终用户可能会发送 NULL 数据。 将 NULL 值写入数据源中的字段时,会存在一些潜在的问题。 假设有一个可接受城市和邮政编码值的接单应用程序,它可接受任一值,也可接受两个值,但不能同时不接受两个值,因为在这种情况下就无法完成配送。 因此,对于对应用程序有意义的字段,必须限制某些 NULL 值的组合。

作为提供程序开发者,必须考虑如何存储数据、如何从数据存储读取数据,以及如何为用户指定数据。 具体而言,必须考虑如何更改数据源中行集数据的数据状态(例如 DataStatus = NULL)。 确定使用者访问包含 NULL 值的字段时要返回的值。

查看 UpdatePV 示例中的代码;它说明了提供程序如何处理 NULL 数据。 在 UpdatePV 中,提供程序通过在数据存储中写入字符串“NULL”来存储 NULL 数据。 从数据存储中读取 NULL 数据时,它会看到该字符串,然后清空缓冲区,创建 NULL 字符串。 它还会重写 IRowsetImpl::GetDBStatus,如果数据值为空,它会在其中返回 DBSTATUS_S_ISNULL。

标记可为空的列

如果还实现架构行集(请参阅 IDBSchemaRowsetImpl),则该实现应在 DBSCHEMA_COLUMNS 行集(通常在提供程序中由 CxxxSchemaColSchemaRowset 标记)中指定列可为空。

还需要在 GetColumnInfo 版本中指定所有可为空的列包含 DBCOLUMNFLAGS_ISNULLABLE 值。

在 OLE DB 模板实现中,如果未能将列标记为可为空,则提供程序假定它们必须包含值,并且不允许使用者向其发送 null 值。

以下示例演示如何在 UpdatePV 的 CUpdateCommand(请参阅 UpProvRS.cpp)中实现 CommonGetColInfo 函数。 请注意如何为列设置 DBCOLUMNFLAGS_ISNULLABLE 使其为可为空的列。

/////////////////////////////////////////////////////////////////////////////
// CUpdateCommand (in UpProvRS.cpp)

ATLCOLUMNINFO* CommonGetColInfo(IUnknown* pPropsUnk, ULONG* pcCols, bool bBookmark)
{
    static ATLCOLUMNINFO _rgColumns[6];
    ULONG ulCols = 0;

    if (bBookmark)
    {
        ADD_COLUMN_ENTRY_EX(ulCols, OLESTR("Bookmark"), 0,
                            sizeof(DWORD), DBTYPE_BYTES,
                            0, 0, GUID_NULL, CAgentMan, dwBookmark,
                            DBCOLUMNFLAGS_ISBOOKMARK)
        ulCols++;
    }

    // Next set the other columns up.
    // Add a fixed length entry for OLE DB conformance testing purposes
    ADD_COLUMN_ENTRY_EX(ulCols, OLESTR("Fixed"), 1, 4, DBTYPE_UI4,
                        10, 255, GUID_NULL, CAgentMan, dwFixed,
                        DBCOLUMNFLAGS_WRITE |
                        DBCOLUMNFLAGS_ISFIXEDLENGTH)
    ulCols++;

    ADD_COLUMN_ENTRY_EX(ulCols, OLESTR("Command"), 2, 16, DBTYPE_STR,
                        255, 255, GUID_NULL, CAgentMan, szCommand,
                        DBCOLUMNFLAGS_WRITE | DBCOLUMNFLAGS_ISNULLABLE)
    ulCols++;
    ADD_COLUMN_ENTRY_EX(ulCols, OLESTR("Text"), 3, 16, DBTYPE_STR,
                        255, 255, GUID_NULL, CAgentMan, szText,
                        DBCOLUMNFLAGS_WRITE | DBCOLUMNFLAGS_ISNULLABLE)
    ulCols++;

    ADD_COLUMN_ENTRY_EX(ulCols, OLESTR("Command2"), 4, 16, DBTYPE_STR,
                        255, 255, GUID_NULL, CAgentMan, szCommand2,
                        DBCOLUMNFLAGS_WRITE | DBCOLUMNFLAGS_ISNULLABLE)
    ulCols++;
    ADD_COLUMN_ENTRY_EX(ulCols, OLESTR("Text2"), 5, 16, DBTYPE_STR,
                        255, 255, GUID_NULL, CAgentMan, szText2,
                        DBCOLUMNFLAGS_WRITE | DBCOLUMNFLAGS_ISNULLABLE)
    ulCols++;

    if (pcCols != NULL)
    {
        *pcCols = ulCols;
    }

    return _rgColumns;
}

默认值

与 NULL 数据一样,你有责任更改默认值。

默认值 FlushDataExecute 会返回 S_OK。 因此,如果不重写此函数,则更改显示成功(将返回 S_OK),但不会传输到数据存储。

UpdatePV 示例中(在 Rowset.h 中),SetDBStatus 方法将处理默认值,如下所示:

virtual HRESULT SetDBStatus(DBSTATUS* pdbStatus, CSimpleRow* pRow,
                            ATLCOLUMNINFO* pColInfo)
{
    ATLASSERT(pRow != NULL && pColInfo != NULL && pdbStatus != NULL);

    void* pData = NULL;
    char* pDefaultData = NULL;
    DWORD* pFixedData = NULL;

    switch (*pdbStatus)
    {
        case DBSTATUS_S_DEFAULT:
            pData = (void*)&m_rgRowData[pRow->m_iRowset];
            if (pColInfo->wType == DBTYPE_STR)
            {
                pDefaultData = (char*)pData + pColInfo->cbOffset;
                strcpy_s(pDefaultData, "Default");
            }
            else
            {
                pFixedData = (DWORD*)((BYTE*)pData +
                                          pColInfo->cbOffset);
                *pFixedData = 0;
                return S_OK;
            }
            break;
        case DBSTATUS_S_ISNULL:
        default:
            break;
    }
    return S_OK;
}

列标志

如果支持对列使用默认值,则需要使用 <提供程序类> SchemaRowset 类中的元数据对其进行设置。 设置 m_bColumnHasDefault = VARIANT_TRUE

你还有责任设置列标志,这些标志通过 DBCOLUMNFLAGS 枚举类型进行指定。 列标志描述列特征。

例如,在 UpdatePVCUpdateSessionColSchemaRowset 类中(在 Session.h 中),将按以下方式设置第一列:

// Set up column 1
trData[0].m_ulOrdinalPosition = 1;
trData[0].m_bIsNullable = VARIANT_FALSE;
trData[0].m_bColumnHasDefault = VARIANT_TRUE;
trData[0].m_nDataType = DBTYPE_UI4;
trData[0].m_nNumericPrecision = 10;
trData[0].m_ulColumnFlags = DBCOLUMNFLAGS_WRITE |
                            DBCOLUMNFLAGS_ISFIXEDLENGTH;
lstrcpyW(trData[0].m_szColumnDefault, OLESTR("0"));
m_rgRowData.Add(trData[0]);

除其他设置外,此代码还指定列支持默认值 0,指定列是可写的,并且指定列中所有数据具有相同的长度。 如果希望列中数据具有可变长度,请不要设置此标志。

另请参阅

创建 OLE DB 提供程序