在 Pocket PC 上使用 ATL OLE DB 消费者模板






4.73/5 (19投票s)
为 Pocket PC 上的 C++ 开发人员启用最快的数据库访问协议。
前言
本文首次发布于 Pocket PC Developer Network。
引言
本文介绍了如何将 ATL OLE DB Consumer Templates 适配并用于 Pocket PC 平台。本文提供的信息适用于 Pocket PC 2002 和 2003 SDK。假设您已具备桌面 OLE DB Consumer Templates 的工作知识 - 您不会通过阅读本文来学习它们。
Microsoft 将 OLE DB 宣传为 Pocket PC 平台上 C++ 开发人员首选的数据库访问方法。尽管 Pocket PC 开发人员还可以选择 ADOCE(3.0 和 3.1),但 Microsoft 不会在 C++ 应用程序中支持它,这意味着如果您使用它,将独自面对问题。此外,ADOCE 基于 OLE DB,因此其性能应该会较慢(更不用说使用令人头疼的 VARIANT
进行数据交换了)。
在 Pocket PC 上使用 OLE DB 的问题在于其明显的复杂性。SQL CE 2.0 SDK 中的 _NorthwindOleDb_ 示例很好地说明了这种复杂性。该示例应用程序直接使用 OLE DB,没有任何封装。简单的表插入需要几十行代码才能完成。局部变量以及编程错误的几率都会增加。至少,调试这样的代码简直是噩梦。
为了封装所有这些复杂性并简化开发人员的生活,绝对需要一个框架。幸运的是,这样的框架已经存在:ATL OLE DB Consumer Template Library。
可用性
尽管 ATL OLE DB Consumer Templates Library 在桌面实现中非常受欢迎,但它尚未移植到 Pocket PC 平台。奇怪的是,这些模板仍然可以在 Pocket PC 2002 和 2003 软件开发工具包中找到,这似乎暗示 Microsoft 允许甚至推荐使用它们。但这并非必然。如果您打开 MFC _includes_ 文件夹中的 _wce.h_ 头文件,您会在第 164 行看到一个有趣的定义
#define _AFX_NO_OLEDB_SUPPORT
同样,在第 198 行(2003 SDK 上为 197 行)
#define __oledb_h__
这意味着在 MFC 下,OLE DB 头文件不会被包含。如果您尝试在包含 _stdafx.h_ _之后_ 包含它,_oledb.h_ 上面的预处理器测试将失败
#ifndef __oledb_h__
Microsoft 似乎在告诉我们不要将 OLE DB 与 MFC 一起使用。正如我们将看到的,有一些理由不使用 OLE DB 和 MFC,但也有方法可以使它们协同工作并带来巨大优势。
在下一节中,我们将探讨为了启用该库在 Pocket PC 上的使用可以做什么。
启用
为了启用该库的使用,我们需要做一些事情。首先,我们必须能够正确地包含头文件。然后,我们将不得不纠正几个阻止文件使用的错误。最后,我们将更新库,添加新类来处理 Pocket PC 数据库编程的一些细节。
包含 OLE DB 头文件
OLE DB 头文件提供了允许您的应用程序使用 OLE DB 接口以及消费模板的基本声明。正如我之前展示的,默认的 MFC 头文件准备阻止 OLE DB 的使用。如果您不包含这些头文件,消费模板将无法编译。
我解决这个问题的方法是更改 _stdafx.h_ 头文件的组织方式。另一种选择是更改 _wce.h_ 头文件,但我认为后一种解决方案的维护性要差得多。如果您需要重新安装开发环境,您将丢失所有更改。
应用程序向导会自动为您创建 _stdafx.h_ 头文件。该文件包含通用包含项,编译器还将其用作预编译头文件的参考。我的更改微不足道:我在标准 MFC 头文件 _之前_ 包含了 OLE DB 头文件(该文件由 2002 SDK 生成)
注意:此过程在 Windows CE .NET(Pocket PC 2003)下略有不同,因为系统中没有 _oledb.dll_ 文件。这种情况有一个变通方法(见下文)。
// stdafx.h : include file for standard system include files, // or project specific include files that are used frequently, but // are changed infrequently // #if !defined(AFX_STDAFX_H__... #define AFX_STDAFX_H__... #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 #if (_WIN32_WCE >= 200) #error This project does not support... #endif #if (_WIN32_WCE <= 211) #error This project can not be built... #endif #define OLEDBVER 0x0210 #define DBINITCONSTANTS // OLEDB Files: // #include <OLEDB.H> #include <OLEDBERR.H> #include <COGUID.H> // SQL CE Files: // #include <SSCEOLEDB.H> // Exclude rarely-used stuff from Windows headers #define VC_EXTRALEAN // MFC core and standard components #include <AFXWIN.H> // MFC extensions #include <AFXEXT.H> #if defined(_AFXDLL) // MFC support for Internet Explorer 4 Common Controls #include <AFXDTCTL.H> #endif #ifndef _AFX_NO_AFXCMN_SUPPORT // MFC support for Windows Common Controls #include <AFXCMN.H> #endif // _AFX_NO_AFXCMN_SUPPORT #include <AFXPRIV.H> // Basic ATL includes // #include <ATLBASE.H> extern CComModule _Module; #include <ATLCOM.H> // ATL OLE DB modified for the Pocket PC // #include "atldbcli_ce.h" //{{AFX_INSERT_LOCATION}} // Microsoft eMbedded Visual C++ will... #endif // !defined(AFX_STDAFX_H__...
正如您所见,所有与 AFX 相关的 `include` 都发生在 OLE DB `include` 之后。这样,我们就可以避免 MFC 的限制。
在 AFX `include` 之后,您会看到一小段 ATL `include`
#include <ATLBASE.H> extern CComModule _Module; #include <ATLCOM.H>
这为包含 OLE DB Consumer 模板头文件铺平了道路。最后一行就是本章的重点
#include "atldbcli_ce.h"
包含 Consumer Templates
我认为 Microsoft 不希望我们包含 ATL Consumer Templates 头文件的一个原因是,它在 eVC3 或 eVC4 下无法编译。
该文件 (_atldbcli.h_) 依赖于标准的 COM 接口声明,即 `IUnknown` 接口,这是我们在移植到 Pocket PC 时遇到的第一个困难。Pocket PC SDK 中的 `IUnknown` 接口声明与其桌面对应项不同。它缺少一个在 _atldbcli.h_ 文件中广泛使用的方法声明,因此无法成功编译。
要检查这一点,请比较桌面版和 Pocket PC 版(2002 和 2003)中的 _unknwn.h_ 头文件。在桌面上,您可以找到以下声明,而在 Pocket PC 上则缺少
template <CLASS Q> HRESULT STDMETHODCALLTYPE QueryInterface(Q** pp) { return QueryInterface(__uuidof(Q), (void **)pp); }
别无选择,只能更改提供的头文件,我将 _atldbcli.h_ 的全部内容复制到一个新文件(_atldbcli_ce.h_)中,并在其中开始进行所有必要的更改和适配。由于 Microsoft 的版权限制,本文未包含此文件。为了规避这个问题,我将描述我所做的所有更改,以使头文件能够在 Pocket PC 上编译和工作。通过遵循这些说明,您也可以修改头文件。
修改 atldbcli.h 头文件
开始之前,请将 _atldbcli.h_(位于 ATL include 文件夹中)复制到 _atldbcli_ce.h_。这将允许您进行必要的更改,而无需修改 SDK 分发版。此外,我建议您将此新文件放在不同的目录中,以便在需要重新安装开发工具时不会被删除。
您需要做的第一个更改是替换所有 `QueryInterface` 调用为 Pocket PC 兼容版本。例如,更改此
HRESULT hr = pUnk->QueryInterface(&spSupportErrorInfo);
改为这样。
HRESULT hr = pUnk->QueryInterface(IID_ISupportErrorInfo, (void**)&spSupportErrorInfo);
如您所见,您必须将接口 IID 作为第一个参数插入,并在第二个参数中添加强制类型转换。我在 Pocket PC 2002 和 2003 SDK 文件中发现了 35 处替换。这些文件除了顶部的版权声明外,基本相同。
进行此更改后,您的新头文件应该可以 _正确编译_,但 _无法正确工作_。
更正 CDynamicAccessor
在使用 `CDynamicAccessor` 类模板时,存在一个需要更正的主要缺陷。该类允许您使用任意行集,并动态为您构建访问器和数据缓冲区(因此得名)。
如果您仔细查看代码,您会发现 ASCII 字符串与 Unicode 字符串的处理方式相同。这会导致 Pocket PC 上一个非常棘手的内存对齐问题。要解决此问题,请将此代码(2002 SDK 的第 2285 行,2003 SDK 的第 2294 行)替换为此
// If column is of type STR or WSTR increase length by 1 // to accommodate the NULL terminator. if (m_pColumnInfo[i].wType == DBTYPE_STR || m_pColumnInfo[i].wType == DBTYPE_WSTR) m_pColumnInfo[i].ulColumnSize += 1;
改为这样。
// If column is of type STR or WSTR increase length by 1 // to accommodate the NULL terminator. if (m_pColumnInfo[i].wType == DBTYPE_STR || m_pColumnInfo[i].wType == DBTYPE_WSTR) m_pColumnInfo[i].ulColumnSize += 1; // // Correct the size if this is wide string // if(m_pColumnInfo[i].wType == DBTYPE_WSTR) m_pColumnInfo[i].ulColumnSize *= sizeof(wchar_t);
这样,访问器将正确地分配一个 Unicode 字符串,避免内存损坏和指针对齐问题。这就完成了使头文件能够投入使用的所需更正。现在,让我们添加一些增强功能。
增强 CAccessorBase 和 CDynamicAccessor
我对这些类的增强之一是跟踪访问器缓冲区大小。一旦知道了它的大小,您就可以根据需要创建访问器缓冲区,并让 `CDynamicAccessor` 用行数据填充它们。这对于缓冲和缓存很有用。
首先,在 `CAccessorBase` 中声明一个新的成员变量
ULONG m_nBufferSize;
现在,在构造函数中初始化它
CAccessorBase() { m_pAccessorInfo = NULL; m_nAccessors = 0; m_pBuffer = NULL; m_nBufferSize = 0; }
现在,我们转向 `CDynamicAccessor`,在变量更改时设置它。从 `Close` 方法开始
void Close() { . . . m_nBufferSize = 0; // Added by JPF 2003-08-11 CAccessorBase::Close(); }
现在,在 `BindColumns` 中
HRESULT BindColumns(IUnknown* pUnk) { . . . // Allocate enough memory for the data buffer and tell the rowset // Note that the rowset will free the memory in its destructor. m_pBuffer = NULL; ATLTRY(m_pBuffer = new BYTE[nOffset]); if (m_pBuffer == NULL) { m_nBufferSize = 0; // Added by JPF 2003-08-11 delete [] pBinding; return E_OUTOFMEMORY; } m_nBufferSize = nOffset; // Added by JPF 2003-08-11 . . . }
我们完成了对这个头文件的更改。现在,我们将看到如何使用这些类,以及需要进行哪些增强才能使 Pocket PC 上的数据库开发更容易。
Windows CE .NET(Pocket PC 2003)的注意事项
Windows CE .NET 不包含 _oledb.dll_,这使得无法为此平台编写 OLE DB 应用程序。幸运的是,Microsoft 发布了一篇知识库文章 825393,其中解释了如何解决此问题,并提供了一个使一切正常工作的头文件。
您需要在 _stdafx.h_ 中做一些小的更改才能适应这一点。这在 eVC4 版本的示例应用程序源代码中有说明。
Using
虽然它们现在已经可以使用了,但我们需要为这些类添加更多功能,以便在实际情况中使用它们:管理 SQL CE 2.0 数据库及其数据。
SQL CE 2.0
我们将编写的第一个扩展是一个针对 SQL CE 2.0 的专用连接对象,通过从 `CDataSource` 派生一个类
// CSqlCeDataSource::Open // // Opens a SQL CE data source // HRESULT CSqlCeDataSource::Open(LPCTSTR pszDbFile) { CDBPropSet propset(DBPROPSET_DBINIT); propset.AddProperty(DBPROP_INIT_DATASOURCE, pszDbFile); return CDataSource::Open(CLSID_SQLSERVERCE_2_0, &propset); }
如果您想指定数据源的其他属性,例如密码或临时文件目录,您将需要添加适当的属性(`DBPROP_SSCE_DBPASSWORD` 和 `DBPROP_SSCE_TEMPFILE_DIRECTORY`)。这些符号在 _ssceoledb.h_(随 SQL CE 2.0 分发)中定义。
打开数据源后,必须创建并打开一个会话。此对象在整个应用程序中将是唯一的,因为 SQL CE 在系统范围内只支持每个数据库一个打开的连接。这是一个代码示例
// // Initialize OLE for OLE DB components // if(!AfxOleInit()) return FALSE; // // Open the data source // hr = g_oleDbSource.Open(_T("\\database.sdf")); if(FAILED(hr)) { AfxMessageBox(_T("Failed to open the data source")); return FALSE; } // // Open a session // hr = g_oleDbSession.Open(g_oleDbSource); if(FAILED(hr)) { AfxMessageBox(_T("Failed to open the session")); return FALSE; }
这段代码设计用于在应用程序类的 `InitInstance` 方法的顶部执行。源对象和会话对象必须在应用程序关闭时关闭,这可以在应用程序的 `ExitInstance` 方法中完成
g_oleDbSession.Close(); g_oleDbSource.Close(); // // Terminate the OLE DB components // AfxOleTerm();
现在应用程序已准备好开始管理 SQL 数据库。我们将首先了解如何执行 SQL 命令。
执行命令
命令是通过实例化 `CCommand` 类模板来执行的。该模板的参数是一个访问器类和一个行集类。_atldbcli.h_ 中有许多此类,如 `CDynamicAccessor` 和 `CRowset`,但它们不一定能满足我们的需求。
`CDynamicAccessor` 类将动态地为底层行集创建访问器。这与 `CAssessor` 不同,后者需要预定义的数据模式和固定结构来接收数据。我们将看到,尽管 `CDynamicAccessor` 非常方便使用,但它本身也存在一些缺点,尤其是在其 C++ 接口方面。
第二个类 `CRowset` 定义了如何插入、检索和更新数据(与访问器类结合使用),以及光标和书签管理。同样,这个类(它封装的 OLE DB 接口)并没有提供我们所需的所有信息,即行数。
计算行集:IRowsetPosition
在许多情况下都需要行集计数信息,尽管您可以使用专用查询来访问它,但如果我们可以将它作为执行命令的副产品来计算,那就太好了。幸运的是,有一个简单的解决方案:`IRowsetPosition` 接口。
SQL CE 2.0 可以提供此接口,它报告行集计数以及当前光标位置。不幸的是,ATL OLE DB Consumer Templates 中没有提供此接口的选项,因此我们必须自己编写。通过从提供的 `CRowset` 派生一个类,这可以非常简单地完成。
遵循 _atldbcli.h_ 的风格,我实现了整个类内联
class CRowsetPosition : public CRowset { public: ~CRowsetPosition() { CRowset::~CRowset(); Close(); } void Close() { CRowset::Close(); if(m_spRowsetPos != NULL) m_spRowsetPos.Release(); } HRESULT GetRecordCount(ULONG *pcRows, HCHAPTER hChapter = DB_NULL_HCHAPTER) { ATLASSERT(m_spRowsetPos != NULL); HRESULT hr = S_OK; hr = m_spRowsetPos->GetRecordCount(hChapter, pcRows); return hr; } HRESULT GetCurrentPosition(ULONG *pulPosition, HCHAPTER hChapter = DB_NULL_HCHAPTER) { ATLASSERT(m_spRowsetPos != NULL); HRESULT hr = S_OK; hr = m_spRowsetPos->GetCurrentPosition(hChapter, pulPosition); return hr; } void SetupOptionalRowsetInterfaces() { CRowset::SetupOptionalRowsetInterfaces(); if (m_spRowset != NULL) { m_spRowset->QueryInterface( IID_IRowsetPosition, (void**)&m_spRowsetPos); } } // // Interface pointer // CComPtr&IRowsetPosition& m_spRowsetPos; };
请注意,`Close` 和 `SetupOptionalRowsetInterfaces` 都调用基类。前者需要关闭 `IRowset`(可能还有 `IRowsetChange`)接口。后者由包含的 `CCommand` 类模板调用,以实例化所有必需的接口。
示例:执行命令
经过所有这些努力,让我们最后执行一个命令。首先,我们声明一个命令对象
CCommand<CDYNAMICACCESSOR, CRowsetPosition> cmd;
现在,我们需要设置它的属性
CDBPropSet propSetCmd(DBPROPSET_ROWSET); // // Use a scrollable cursor (see SQL CE 2.0 books online) // propSetCmd.AddProperty(DBPROP_BOOKMARKS, true); propSetCmd.AddProperty(DBPROP_OWNUPDATEDELETE, false); propSetCmd.AddProperty(DBPROP_OWNINSERT, false); propSetCmd.AddProperty(DBPROP_OTHERUPDATEDELETE, false); propSetCmd.AddProperty(DBPROP_OTHERINSERT, false); propSetCmd.AddProperty(DBPROP_CANFETCHBACKWARDS, true); propSetCmd.AddProperty(DBPROP_QUICKRESTART, true);
最后,我们打开命令
HRESULT hr; CString strSelect = _T("SELECT * FROM Customer"); // // Open the command // hr = cmd.Open(g_oleDbSession, strSelect, &propSetCmd);
正如您所看到的,它相当直接。现在缺的是行数
ULONG nRows = 0; if(hr == S_OK) { hr = cmd.GetRecordCount(&nRows); cmd.MoveFirst(); }
首先,我们检查 `Open` 命令是否成功。如果成功,我们检索行集计数并将光标定位在第一行。现在,我们可以使用 `CDynamicAccessor` 的 `GetValue` 系列方法开始从行集中检索信息。
请注意,无法通过 SQL SELECT
命令更新数据。更新 SQL CE 数据需要单独的章节。
DDL 和 DML 命令
上面的示例展示了如何执行 SQL SELECT
命令。这些命令通常会返回一个行集,其行和列数据可以访问,但不能更改(见下文)。
管理 SQL Server 数据库不仅仅是执行 SELECT
命令。您还可以 INSERT
、DELETE
和 UPDATE
表中的数据。但是,这些命令以及数据定义语言(DDL)命令不会为您返回一个行集来操作,因此无需指定访问器或行集类。Consumer Templates 提供了 `CNoAccessor` 和 `CNoRowset` 来精确满足这些目的。DDL 命令可以这样执行
CCommand<CNOACCESSOR, CNoRowset> cmd; HRESULT hr; hr = cmd.Open(g_oleDbSession, _T("CREATE UNIQUE INDEX ix ON Customer (CustID ASC)")); if(FAILED(hr)) { // Error }
更新数据
有两种方法可以使用 ATL OLE DB Consumer Templates 更新 SQL CE 数据库中的数据:使用 SQL DML(见上文)命令,如 INSERT
、UPDATE
或 DELETE
,或者使用行集类的 `Insert` 和 `Delete` 等方法。
这种类型的数据更新直接使用 OLE DB 接口,并且可以说比 DML 方法更高效、更通用。我们所需要做的就是创建一个可更新的行集,并使用 `CDynamicAccessor` 的 `GetValue` 和 `SetValue` 系列方法。
但这正是我们遇到麻烦的地方:SQL CE 只允许我们使用所谓的“基表光标”来更新表。逻辑很简单:我们打开表,找到所需的行并更新它(对于插入显然不是必需的)。尽管它看起来很简单,但实现起来绝不简单。让我们看看为什么。
ATL OLE DB Consumer Templates 为我们提供了一个用于处理表的类模板 - `CTable`。该模板的操作方式类似于 `CCommand`,但它不处理 SQL 命令:它直接处理单个表,因此 `Open` 方法接收表名作为参数。一种可能的实现是
CTable<CDYNAMICACCESSOR, CRowset> tbl; CDBPropSet propSetTbl(DBPROPSET_ROWSET); HRESULT hr; // // Use a base table cursor // propSetTbl.AddProperty(DBPROP_BOOKMARKS, true); propSetTbl.AddProperty(DBPROP_OWNUPDATEDELETE, true); propSetTbl.AddProperty(DBPROP_OWNINSERT, true); propSetTbl.AddProperty(DBPROP_OTHERUPDATEDELETE, true); propSetTbl.AddProperty(DBPROP_OTHERINSERT, true); propSetTbl.AddProperty(DBPROP_CANFETCHBACKWARDS, true); propSetTbl.AddProperty(DBPROP_QUICKRESTART, true); propSetTbl.AddProperty(DBPROP_IRowsetChange, true); propSetTbl.AddProperty(DBPROP_UPDATABILITY, DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_DELETE | DBPROPVAL_UP_INSERT); hr = tbl.Open(g_oleDbSession, _T("Table"), &propSetTbl);
所以,我们只需要查找特定的行。在这里,我们遇到了一个障碍:使用 `CTable` 无法查找记录。没有可用的接口让我们查找特定行。
实际上,这也没那么糟。事实上,OLE DB 提供了 `IRowsetIndex` 接口来专门实现此目的,但在我们开始使用它之前,我们需要做两件事。首先,我们需要从 `CRowset`(就像我们在 `CRowsetPosition` 中做的那样)派生一个类来支持此接口。其次,我们需要更改 `CTable` 类模板,使其接受一个我们可以用于查找的索引,而不是进行缓慢的完整表扫描。
实现 CRowsetIndex
这个类比我们在本文中看到的要复杂一些。第一个设计选择是决定从哪个行集类派生(`CRowset`、`CRowsetPosition`)。在未得出任何结论的情况下,我决定类用户应该自己做出选择,因此我选择将该类实现为模板。
template <CLASS TBaseRowset="CRowset"> class CRowsetIndex : public TBaseRowset { ... }
因此,通过实例化模板参数,类用户指定从哪个行集类派生。完整的类实现随附代码提供,因为它太长,无法在此完全重现。有几点需要注意。
为了使用 `IRowsetIndex` 接口,我们需要创建一个具有请求索引精确映射的访问器。此访问器在 `BuildBinding` 方法中创建,该方法在设置阶段被调用。
查找一行是一个两步过程。首先,您使用 `SeekValue` 方法指定要匹配的值。此方法接受列名或索引和值。设置完所有值后,调用 `SeekAndGet` 进行实际定位,并将行集数据检索到主访问器。此过程如下所示。
实现 CIndexTable
如前所述,我们需要更改 `CTable` 类模板以支持索引搜索。我们通过一个新的类模板 `CIndexTable` 来实现这一点。
实现 `CIndexTable` 非常直接:打开 _atldbcli_ce.h_ 并完整复制 `CTable` 的定义。将此副本保留在 _atldbcli_ce.h_ 中。
现在,让它看起来像这样
template <CLASS class="" TAccessor="CNoAccessor," TRowset="CRowset"> class CIndexTable : public CAccessorRowset<TACCESSOR, TRowset> { public: // Open a rowset on the passed name HRESULT Open(const CSession& session, LPCTSTR szTableName, LPCTSTR szIndexName, DBPROPSET* pPropSet = NULL) { USES_CONVERSION; DBID idTable, idIndex; idTable.eKind = DBKIND_NAME; idTable.uName.pwszName = (LPOLESTR)T2COLE(szTableName); idIndex.eKind = DBKIND_NAME; idIndex.uName.pwszName = (LPOLESTR)T2COLE(szIndexName); return Open(session, idTable, idIndex, pPropSet); } // Open the a rowset on the passed DBID HRESULT Open(const CSession& session, DBID& dbidTbl, DBID& dbidIdx, DBPROPSET* pPropSet = NULL) { // Check the session is valid ATLASSERT(session.m_spOpenRowset != NULL); HRESULT hr; hr = session.m_spOpenRowset->OpenRowset( NULL, &dbidTbl, &dbidIdx, GetIID(), (pPropSet) ? 1 : 0, pPropSet, (IUnknown**)GetInterfacePtr()); if(SUCCEEDED(hr)) { SetupOptionalRowsetInterfaces(); // If we have output columns then bind if (_OutputColumnsClass::HasOutputColumns()) hr = Bind(); } return hr; } BOOL IsOpen() { return GetInterface() != NULL; } };
最后,让我们看看这些类如何协同工作来更新数据。
CRowset 数据更新原语
到目前为止,我们已经为在 Pocket PC 上使用定制了 ATL OLE DB Consumer Templates。在更改代码并添加了新的便捷类之后,让我们最后看看这些类如何协同工作以实现数据库应用程序的基本目的:访问和更新数据。
为了本讨论的方便,我假设我们正在处理一个名为 `Table` 的表,该表在名为 `id` 的列上有一个名为 `pk_id` 的唯一整数索引。
检索行数据
我们首先通过将光标定位到特定位置来检索表中的行数据,该位置由 `id` 列的值给出。下面的示例代码展示了如何实现这一点。首先,以所有必要的属性打开表。接下来,使用 `SeekValue` 方法指定我们要搜索的 `id` 列值。通过调用 `SeekAndGet` 进行实际定位。此方法将用于执行搜索的索引访问器中的列数作为参数:在本例中为一。如果此方法成功,光标将定位在所需行上,并且行集访问器缓冲区将包含行数据。
CIndexTable<CDYNAMICACCESSOR, CRowsetIndex<CRowset> > tbl; HRESULT hr; CDBPropSet propSetTbl(DBPROPSET_ROWSET); int nID = 1234; // Row ID value TCHAR szID[] = _T("id"); // Row ID column // // Use a base table cursor // propSetTbl.AddProperty(DBPROP_BOOKMARKS, true); propSetTbl.AddProperty(DBPROP_OWNUPDATEDELETE, true); propSetTbl.AddProperty(DBPROP_OWNINSERT, true); propSetTbl.AddProperty(DBPROP_OTHERUPDATEDELETE, true); propSetTbl.AddProperty(DBPROP_OTHERINSERT, true); propSetTbl.AddProperty(DBPROP_CANFETCHBACKWARDS, true); propSetTbl.AddProperty(DBPROP_QUICKRESTART, true); propSetTbl.AddProperty(DBPROP_IRowsetIndex, true); hr = tbl.Open(g_oleDbSession, _T("Table"), _T("pk_id"), &propSetTbl); if(hr == S_OK) { tbl.SeekValue(szID, nID); hr = tbl.SeekAndGet(1); if(hr == S_OK) { tbl.Get(m_szID, m_nID); // // Load the row data here (use GetValue) // } else { // // Seek failed // } tbl.Close(); }
更新行数据
数据更新非常相似,但您需要一套略有不同的属性
// // Use a base table cursor // propSetTbl.AddProperty(DBPROP_BOOKMARKS, true); propSetTbl.AddProperty(DBPROP_OWNUPDATEDELETE, true); propSetTbl.AddProperty(DBPROP_OWNINSERT, true); propSetTbl.AddProperty(DBPROP_OTHERUPDATEDELETE, true); propSetTbl.AddProperty(DBPROP_OTHERINSERT, true); propSetTbl.AddProperty(DBPROP_CANFETCHBACKWARDS, true); propSetTbl.AddProperty(DBPROP_QUICKRESTART, true); propSetTbl.AddProperty(DBPROP_UPDATABILITY, DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_DELETE | DBPROPVAL_UP_INSERT); propSetTbl.AddProperty(DBPROP_IRowsetIndex, true); propSetTbl.AddProperty(DBPROP_IRowsetChange, true); hr = tbl.Open(g_oleDbSession, _T("Table"), _T("pk_id"), &propSetTbl); if(hr == S_OK) { tbl.SeekValue(szID, nID); hr = tbl.Seek(1); if(hr == S_OK) { // // Set the row data here (use SetValue) // } else { // // Seek failed // } tbl.Close(); }
查找序列略有不同,因为我们不一定需要检索行数据。调用 `SeekValue` 后,使用 `Seek` 来定位光标。
确保所有列的状态都已设置。我的方法是将它们全部设置为 `DBSTATUS_S_IGNORE`。这是一种执行此操作的方法
ULONG i; for(i = 0; i < tbl.m_nColumns; ++i) tbl.SetStatus(i, DBSTATUS_S_IGNORE);
当您设置一个列值时,也要将其状态设置为 `DBSTATUS_S_OK`。这样,您可以确保数据提供程序只会设置您想要的值(此改进在 `CSmartAccessor` 类中完成 - 见下文)。
插入新行
在表中插入新行也是一项简单的操作。打开表后,将所有列的状态设置为 `DBSTATUS_S_IGNORE`。如果正在向具有标识列的表中插入行,这一点尤其重要。这些列会自动递增,因此您不应尝试设置它们的值。将这项工作留给数据提供程序。
之后,可以使用 `SetValue` 系列方法设置数据。最后,调用 `Insert` 将新行插入表中。
HRESULT hr = E_FAIL; CDBPropSet propSetTbl(DBPROPSET_ROWSET); // // Use a base table cursor // propSetTbl.AddProperty(DBPROP_BOOKMARKS, true); propSetTbl.AddProperty(DBPROP_OWNUPDATEDELETE, true); propSetTbl.AddProperty(DBPROP_OWNINSERT, true); propSetTbl.AddProperty(DBPROP_OTHERUPDATEDELETE, true); propSetTbl.AddProperty(DBPROP_OTHERINSERT, true); propSetTbl.AddProperty(DBPROP_CANFETCHBACKWARDS, true); propSetTbl.AddProperty(DBPROP_QUICKRESTART, true); propSetTbl.AddProperty(DBPROP_IRowsetChange, true); propSetTbl.AddProperty(DBPROP_UPDATABILITY, DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_DELETE | DBPROPVAL_UP_INSERT); hr = tbl.Open(g_oleDbSession, _T("Table"), _T("pk_id"), &propSetTbl); if(hr == S_OK) { // // Set status to DBSTATUS_S_IGNORE // ULONG i; for(i = 0; i < tbl.m_nColumns; ++i) tbl.SetStatus(i, DBSTATUS_S_IGNORE); // // Set the column data using SetValue() // . . . hr = tbl.Insert(); if(FAILED(hr)) { // // Insert failed // } tbl.Close(); }
删除行
要删除现有行,我们必须首先使用索引来定位它,这与我们加载行的过程基本相同。成功定位行后,我们调用 `Delete` 将其从表中删除。这是一些示例代码
HRESULT hr = E_FAIL; CDBPropSet propSetTbl(DBPROPSET_ROWSET); // // Use a base table cursor // propSetTbl.AddProperty(DBPROP_BOOKMARKS, true); propSetTbl.AddProperty(DBPROP_OWNUPDATEDELETE, true); propSetTbl.AddProperty(DBPROP_OWNINSERT, true); propSetTbl.AddProperty(DBPROP_OTHERUPDATEDELETE, true); propSetTbl.AddProperty(DBPROP_OTHERINSERT, true); propSetTbl.AddProperty(DBPROP_CANFETCHBACKWARDS, true); propSetTbl.AddProperty(DBPROP_QUICKRESTART, true); propSetTbl.AddProperty(DBPROP_IRowsetChange, true); propSetTbl.AddProperty(DBPROP_IRowsetIndex, true); hr = tbl.Open(g_oleDbSession, _T("Table"), _T("pk_id"), &propSetTbl); if(hr == S_OK) { tbl.SeekValue(m_szID, nID); hr = m_table.SeekAndGet(1); if(hr == S_OK) { hr = m_table.Delete(); if(FAILED(hr)) { // // Cannot delete row. // } } else { // // Cannot find row // } m_table.Close(); }
正如您现在可以看到的,这些原语并不难使用,它们的复杂性并不比使用 ADOCE 大,并且具有更快的明显优势。现在我们转向文章开头承诺的内容:增强 `CDynamicAccessor`。
改进 CDynamicAccessor
在使用 `CDynamicAccessor` 一段时间后,您会遇到它的一些缺点,特别是数据如何在访问器缓冲区和您的程序变量之间传输。该类为此目的提供了名为 `GetValue` 和 `SetValue` 的模板方法。列可以通过名称或索引(通常为基于 1 的索引)访问,并通过第二个参数数据类型实现特化。虽然这对于大多数用途来说很方便,但在两种情况下却很不方便:字符串和隐式数据转换。问题是这两个方法都假设您正在以与访问器缓冲区中表示的 _完全相同_ 的二进制格式设置或检索数据。
如果您使用 `CString` 来存储列数据,则无法使用 `GetValue` 或 `SetValue`,因为访问器将字符串存储为扁平的宽字符数组。一种可能的解决方案是
CString strText; strText = (LPCTSTR)tbl._GetDataPtr(nColumn);
`nColumn` 参数是内部列号,可以通过 `TranslateColumnNo`(转换列号)或 `GetInternalColumnNo`(将列名转换为内部列号)来计算。
使用 `GetValue` 或 `SetValue` 也不可能进行隐式类型转换,因为它们假设您知道正在从访问器缓冲区设置或获取数据的二进制格式。
为了规避这些缺点,我创建了 `CSmartAccessor`,一个从 `CDynamicAccessor` 派生的类。该类在示例应用程序中可用,可能没有您期望的所有设置和获取方法,但它仍然是构建自己的一个很好的路线图。
示例应用
示例应用程序管理 _Northwind_ 数据库的 _Customers_ 表。该表使用 RDA 从桌面 SQL Server 版本导入。
示例应用程序有两种版本:一种用于 Pocket PC 2002,另一种用于 Pocket PC 2003。这是由于 Pocket PC 2003 对 OLE DB 的直接支持不足。
该应用程序管理 _Customers_ 表的一个子集。它允许您使用上下文菜单创建、编辑和删除客户。代码说明了管理 SQL CE 数据库的基本技术,并在 Pocket PC 2002 设备和 Pocket PC 2003 模拟器上进行了测试。