ATL OLE DB 消费者模板架构






4.17/5 (6投票s)
使用 ATL OLE DB 消费者模板探索 SQL CE 2.0 架构信息。
引言
这是第三篇关于在 Pocket PC 上使用 ATL OLE DB 消费者模板的文章。在第一篇文章中,我描述了如何将 ATL OLE DB 消费者模板改编并用于 Pocket PC 平台。第二篇文章第二篇文章展示了如何管理大数据类型,也称为 BLOB。这两篇文章提供的都是管理 SQL Server CE 2.0 数据库中数据的示例,是本文材料的基础。
在这里,我将向您展示如何探索数据库架构信息,特别是针对 SQL CE 2.0 数据库。就像您在第一篇文章中所见,我将不得不改编 ATL 分发文件之一,以便它能在 Windows CE 编译器上正常工作。
启用
如果您还记得我的第一篇文章,我不得不创建一个自定义版本的 atldbcli.h 头文件,并重新排列 stdafx.h 的布局,以便我们可以将 ATL OLE DB 消费者模板与 MFC 一起编译。现在,还有更多相同的工作要做,但现在我们将不得不专注于 atldbsch.h 头文件。
- atldbsch.h 头文件
此文件包含许多类声明,可帮助我们枚举数据库架构信息。大部分信息以行集的形式提供,可以像表一样访问。这些信息包括表、列、索引、约束等的列表。有趣的是,正如您将在本文的讨论中看到的,我们将使用混合技术来访问 SQL CE 2.0 架构信息。这些行集起着非常重要的作用,但正如我们将在本文中看到的,它们还不够。
atldbsch.h 中有许多有趣的类,其中最值得注意的是
CRestrictions
类模板。该模板用于使用辅助访问器类来实例化架构行集类。这些类中的每一个都定义了底层数据结构的结构。例如,要枚举数据库中的所有表,您可以使用CTables
类,它不过是一个typedef
(您可以在 atldbsch.h 文件的末尾找到所有可用的typedef
)。typedef CRestrictions<CAccessor<CTableInfo>,4,&DBSCHEMA_TABLES> CTables;
要理解打开和浏览此类行集时可用的信息,您必须查看
CTableInfo
类的定义。为了方便我们的讨论,我在此处重述它class CTableInfo { public: // Constructors CTableInfo() { memset(this, 0, sizeof(*this)); } // Attributes TCHAR m_szCatalog[129]; TCHAR m_szSchema[129]; TCHAR m_szName[129]; TCHAR m_szType[129]; GUID m_guidTable; TCHAR m_szDescription[129]; // Binding Map BEGIN_COLUMN_MAP(CTableInfo) COLUMN_ENTRY(1, m_szCatalog) COLUMN_ENTRY(2, m_szSchema) COLUMN_ENTRY(3, m_szName) COLUMN_ENTRY(4, m_szType) COLUMN_ENTRY(5, m_guidTable) COLUMN_ENTRY(6, m_szDescription) END_COLUMN_MAP() };
正如您所见,这很简单:记录描述和访问器列映射。这些类中的大多数将按预期运行,有些会失败,而有些则完全不完整。因此,就像我们之前所做的一样,我们必须更改此文件以使其正常工作。让我们看看如何。
- 修改 atldbsch.h 头文件
开始之前,请将 atldbsch.h(位于 ATL include 文件夹中)复制到 atldbsch_ce.h。这将允许您在不修改 SDK 分发版的情况下进行必要的更改。此外,我建议您将此新文件放在不同的目录中,以便在需要重新安装开发工具时不会被删除。
我们需要进行的更改是纠正布尔值的访问方式。布尔值使用
VARIANT_BOOL
类型声明,它不过是一个具有特定语义的无符号短整型:false
表示为 0,true
表示为 ~0。这与布尔常数的习俗定义略有不同,当数据由访问器绑定时会出现问题。解决方案实际上非常简单,它涉及替换访问器列绑定宏。让我们看一个例子。首先找到
CColumnsInfo
类。正如您所见,有两个布尔值:m_bColumnHasDefault
和m_bIsNullable
。如果您往下看,您将看到这些值的绑定条目COLUMN_ENTRY(8, m_bColumnHasDefault)
三行之后,您将找到另一个条目
COLUMN_ENTRY(11, m_bIsNullable)
这就是我们需要更改的内容,以使所有布尔值绑定都能正常工作,而且这是一个简单的更改。例如,我们将第一个绑定更改为
COLUMN_ENTRY_TYPE_SIZE(8, DBTYPE_BOOL, 2, m_bColumnHasDefault)
此列绑定宏现在明确表示我们正在绑定一个长度为两个字节的布尔值。现在,您的任务是在文件中替换所有此类出现的实例。
最后,您应该修改 stdafx.h 文件,并将 atldbsch_ce.h 包含在 atldbcli_ce.h 之后。
... #include "atldbcli_ce.h" #include "atldbsch_ce.h" ...
Using
OLE DB 允许您枚举数据库中的所有内容,从可用的服务器到表列的非常具体的属性。在本文中,我们仅考虑枚举 SQL CE 2.0 架构,因此有一些限制。
要查看支持哪些架构行集,请参阅 SQL CE 2.0 在线文档。正如您所见,我们缺少一些相关的行集。让我们看看如何在没有它们的情况下进行。
- 枚举服务器
枚举服务器不是一个选项,因为每个设备只能有一个服务器,所以服务器要么已安装,要么未安装。要确定服务器是否已安装,您可以使用以下代码
#include <ca_mergex20.h> bool IsSqlCeInstalled() { ISSCEEngine* pEngine; HRESULT hr; ::CoInitializeEx(NULL, COINIT_MULTITHREADED); hr = CoCreateInstance(CLSID_Engine, NULL, CLSCTX_INPROC_SERVER, IID_ISSCEEngine, (LPVOID *) &pEngine); if(SUCCEEDED(hr)) pEngine->Release(); ::CoUninitialize(); return SUCCEEDED(hr); }
不要忘记在项目的库列表中(项目设置/链接选项卡)包含 ca_mergex20.lib。
- 枚举数据库
SQL CE 2.0 数据库存储在单个文件中,并且不像 SQL Server 2000 那样有中央数据库引用,但像 Access 一样。因此,枚举可用数据库只是使用
FindFirstFile
和FindNextFile
函数的一个简单方法,使用“*.sdf”作为搜索通配符。 - 枚举表
数据库表使用
CTables
类进行枚举。以下代码假定您已使用第一篇文章中描述的技术打开了数据库。HRESULT hr; CTables table; hr = table.Open(session, NULL, NULL, NULL, _T("TABLE"));
变量 session 持有一个打开的
CSession
对象。最后一个参数指示 OLE DB 提供程序枚举用户表,跳过系统表。要查看此参数的所有支持值,请参阅 OLE DB 程序员参考中的“TABLES Rowset”主题。枚举表就像在表或查询中一样简单
if(SUCCEEDED(hr)) { for(hr = table.MoveFirst(); hr == S_OK; hr = table.MoveNext()) { // The table name is table.m_szName } }
在表枚举结束时,不要忘记关闭
CTables
对象table.Close();
很简单,不是吗?现在,让我们看看表中列的枚举。
- 枚举表列
枚举表中的列也很简单。SQL CE 2.0 OLE DB 提供程序支持 COLUMNS 行集,该行集由 ATL 通过
CColumns
类实现。逻辑相同HRESULT hr; CColumns column; hr = column.Open(session, NULL, NULL, _T("TableName"));
此代码打开指定表的列行集进行枚举。枚举的执行方式与表一样:使用底层游标。
不幸的是,此架构行集不会暴露您可能需要的所有列信息(请参阅 atldbsch_ce.h 中的
CColumnsInfo
类)。正如您所看到的,一些最有趣的信息缺失了,比如列是否为IDENTITY
列,其种子值和增量值。不幸的是,无法使用架构行集获取此信息。我们必须另辟蹊径。
ITableCreation 接口
要访问扩展的列架构信息,我们必须使用一个特定的 OLE DB 接口:ITableCreation
。如果您查看接口文档,您会发现它只支持一个方法:GetTableDefinition
。这是一个多么强大的方法!只需一次调用,您就可以获得所有列的名称、属性和约束。
为了说明如何使用此方法,请参考示例应用程序中的 CDbTable
类和 LoadSchema
方法。此类封装了整个表的架构信息,包括列、约束和索引。
首先,我们开始声明所需的变量
HRESULT hr; CComPtr<ITableCreation> spTableCreation;
现在,我们实例化一个指向 ITableCreation
接口的指针
hr = session.m_spOpenRowset->QueryInterface(IID_ITableCreation, (void**)&spTableCreation);
现在,我们可以访问列架构
if(SUCCEEDED(hr)) { DBID idTable; ULONG nColumns, nProperties, nConstraints; DBCOLUMNDESC* pColDesc; DBPROPSET* pPropertySet; DBCONSTRAINTDESC* pConstraints; OLECHAR* pStrings; TCHAR szName[129]; hr = spTableCreation->GetTableDefinition(&idTable, &nColumns, &pColDesc, &nProperties, &pPropertySet, &nConstraints, &pConstraints, &pStrings); if(SUCCEEDED(hr)) { AddColumns(pColDesc, nColumns); AddConstraints(pConstraints, nConstraints); AddIndexes(session); CoTaskMemFree(pStrings); } spTableCreation.Release(); }
代码实际上并不复杂。该方法填充了许多由提供程序分配的缓冲区(不包括 pStrings
),这些缓冲区将包含我们需要的所有信息。如您所见,除了 pStrings
之外,我们不关心释放这些缓冲区。访问如此丰富的信息需要一些工作。在这里,我将所有这些工作封装在专门的方法下:AddColumns
用于添加所有列定义,AddConstraints
用于添加主键和外键约束,最后 AddIndexes
用于添加与表相关的所有索引信息。
让我们详细看看每一个。
AddColumns
每个表列由一个
DBCOLUMNDESC
结构描述。如果您在 oledb.h 文件中查看它,您会看到typedef struct tagDBCOLUMNDESC { LPOLESTR pwszTypeName; ITypeInfo __RPC_FAR *pTypeInfo; /* [size_is] */ DBPROPSET __RPC_FAR *rgPropertySets; CLSID __RPC_FAR *pclsid; ULONG cPropertySets; ULONG ulColumnSize; DBID dbcid; DBTYPE wType; BYTE bPrecision; BYTE bScale; } DBCOLUMNDESC;
此结构包含一个属性集数组,该数组描述了列的所有特征。每个属性集都由其自己的 GUID 标识,我们正在查找的列属性可以在
DBPROPSET_COLUMN
属性集中找到。属于此属性集的最相关的属性是DBPROP_COL_AUTOINCREMENT
:告诉我们这是否是IDENTITY
列。SQL CE 将这些列限制为 32 位整数类型(DBTYPE_I4
)。DBPROP_COL_SEED
:此属性包含IDENTITY
列的种子(初始值)值。DBPROP_COL_INCREMENT
:数据库引擎用于递增IDENTITY
列的值存储在此处。DBPROP_COL_NULLABLE
:如果列接受 null 值,则此属性设置为true
。DBPROP_COL_ISLONG
:存储二进制大对象(BLOB
)的列将此属性设置为true
。DBPROP_COL_FIXEDLENGTH
:对于固定长度的列,此属性为true
。
要读取所有这些属性值,我们必须循环遍历属性集数组。我的方法是使用一个中间结构来存储列属性
typedef struct tagCOLUMN { TCHAR szName[129]; DBTYPE wType; DBCOLUMNFLAGS dwFlags; ULONG nSize, nOrdinal; USHORT nPrecision; SHORT nScale; VARIANT_BOOL bIdentity; int nSeed, nIncr; } COLUMN;
某些属性将存储在结构变量中,而其他属性将转换为列标志。请注意,在此示例中,我没有检索列的默认值,但这也可以通过使用
DBPROP_COL_DEFAULT
属性来完成。填充此结构后,将创建一个类型为
CDbColumn
的对象并将其添加到表的列列表中(m_vecCol
成员变量)。这是完整的代码
void CDbTable::AddColumns(DBCOLUMNDESC* pColDesc, ULONG nColumns) { ULONG i; COLUMN col; m_vecCol.reserve(nColumns); // // Loop through all columns // for(i = 0; i < nColumns; ++i) { ULONG cs, cp; DBPROPSET* pPropSet; CDbColumn* pColumn; StrCopyN(col.szName, pColDesc[i].dbcid.uName.pwszName, 128); col.dwFlags = 0; col.wType = pColDesc[i].wType; col.nPrecision = pColDesc[i].bPrecision; col.nOrdinal = i + 1; col.nScale = pColDesc[i].bScale; col.nSize = pColDesc[i].ulColumnSize; col.bIdentity = VARIANT_FALSE; col.nSeed = 0; col.nIncr = 0; pPropSet = pColDesc[i].rgPropertySets; for(cs = 0; cs < pColDesc[i].cPropertySets; ++cs, ++pPropSet) { if(pPropSet->guidPropertySet == DBPROPSET_COLUMN) { DBPROP* pProp = pPropSet->rgProperties; for(cp = 0; cp < pPropSet->cProperties; ++cp, ++pProp) { switch(pProp->dwPropertyID) { case DBPROP_COL_AUTOINCREMENT: col.bIdentity = pProp->vValue.boolVal; break; case DBPROP_COL_SEED: col.nSeed = pProp->vValue.intVal; break; case DBPROP_COL_INCREMENT: col.nIncr = pProp->vValue.intVal; break; case DBPROP_COL_NULLABLE: if(pProp->vValue.boolVal == VARIANT_TRUE) col.dwFlags |= DBCOLUMNFLAGS_ISNULLABLE; break; case DBPROP_COL_ISLONG: if(pProp->vValue.boolVal == VARIANT_TRUE) col.dwFlags |= DBCOLUMNFLAGS_ISLONG; break; case DBPROP_COL_FIXEDLENGTH: if(pProp->vValue.boolVal == VARIANT_TRUE) col.dwFlags |= DBCOLUMNFLAGS_ISFIXEDLENGTH; break; } } } } // // Add the column // pColumn = new CDbColumn(&col); if(pColumn) m_vecCol.push_back(pColumn); } }
AddConstraints
约束有四种不同类型
- 主键
- 外键
- 检查约束
- 唯一约束
如果 SQL CE 2.0 不支持检查约束,那么我们可以跳过它们。本文不会涵盖唯一约束,尽管它们可以轻松添加。
所有约束类型都由
DBCONSTRAINTDESC
结构描述,该结构定义如下typedef struct tagDBCONSTRAINTDESC { DBID __RPC_FAR *pConstraintID; DBCONSTRAINTTYPE ConstraintType; ULONG cColumns; /* [size_is] */ DBID __RPC_FAR *rgColumnList; DBID __RPC_FAR *pReferencedTableID; ULONG cForeignKeyColumns; /* [size_is] */ DBID __RPC_FAR *rgForeignKeyColumnList; OLECHAR __RPC_FAR *pwszConstraintText; DBUPDELRULE UpdateRule; DBUPDELRULE DeleteRule; DBMATCHTYPE MatchType; DBDEFERRABILITY Deferrability; ULONG cReserved; /* [size_is] */ DBPROPSET __RPC_FAR *rgReserved; } DBCONSTRAINTDESC;
约束类型存储在
ConstraintType
成员中,可以是以下值之一DBCONSTRAINTTYPE_UNIQUE
DBCONSTRAINTTYPE_FOREIGNKEY
DBCONSTRAINTTYPE_PRIMARYKEY
DBCONSTRAINTTYPE_CHECK
为了处理主键和外键,我创建了类来存储它们,即
CDbForeignKey
和CDbIndex
(主键实际上是一种特殊的索引)。这些类只是列或列对的列表(有关详细信息,请参阅示例应用程序)。
这是
AddConstraints
的完整代码void CDbTable::AddConstraints(DBCONSTRAINTDESC *pConstraints, ULONG nConstraints) { ULONG iCon; for(iCon = 0; iCon < nConstraints; ++iCon) { DBCONSTRAINTDESC* pCon = pConstraints + iCon; switch(pCon->ConstraintType) { case DBCONSTRAINTTYPE_UNIQUE: // Not handled break; case DBCONSTRAINTTYPE_FOREIGNKEY: { CDbForeignKey* pFky = new CDbForeignKey( pCon->pConstraintID->uName.pwszName, pCon->pReferencedTableID->uName.pwszName, pCon->UpdateRule, pCon->DeleteRule); if(pFky) { ULONG iCol; m_vecFky.push_back(pFky); for(iCol = 0; iCol < pCon->cColumns; ++iCol) { CDbForeignKeyCol* pCol; pCol = new CDbForeignKeyCol( pCon->rgColumnList[iCol].uName.pwszName, pCon->rgForeignKeyColumnList[iCol].uName.pwszName); if(pCol) pFky->AddColumn(pCol); } } } break; case DBCONSTRAINTTYPE_PRIMARYKEY: { ULONG iCol; CDbIndex* pIndex = new CDbIndex( pCon->pConstraintID->uName.pwszName, true); if(pIndex) m_vecIdx.push_back(pIndex); for(iCol = 0; iCol < pCon->cColumns; ++iCol) { CDbColumn* pColumn = FindColumn( pCon->rgColumnList[iCol].uName.pwszName); CDbIndexCol* pIdxCol = new CDbIndexCol( pCon->rgColumnList[iCol].uName.pwszName, 1); if(pColumn) pColumn->SetPrimaryKey(true); if(pIdxCol && pIndex) pIndex->AddColumn(pIdxCol); } } break; case DBCONSTRAINTTYPE_CHECK: // Not supported break; } } }
AddIndexes
最后,我们可以将索引添加到我们的表定义中。不幸的是,这个过程并不像您预期的那样直接。索引使用
CIndexes
类进行枚举,但此列表将包括真正的索引、主键(一种特殊的索引)和所有外键。因此,我们需要在弄清楚我们被报告的对象是真正的索引还是其他东西之前告知它们。CIndexes
类通过在m_bPrimaryKey
成员变量中存储索引是否为主键来帮助我们。不幸的是,外键没有这样的运气——我们必须比较约束名称。此过程在源代码中进行了说明,并在此完全重现
void CDbTable::AddIndexes(CSession &session) { CIndexes idx; HRESULT hr; hr = idx.Open(session, NULL, NULL, NULL, NULL, m_strName); if(hr == S_OK) { while(idx.MoveNext() == S_OK) { CDbIndex* pIdx = NULL; // Skip primary keys. if(idx.m_bPrimaryKey == VARIANT_TRUE) continue; // Skip foreign key indexes. if(FindForeignKey(idx.m_szIndexName)) continue; pIdx = FindIndex(idx.m_szIndexName); if(!pIdx) { pIdx = new CDbIndex(idx.m_szIndexName, false, idx.m_bUnique == VARIANT_TRUE); if(pIdx) m_vecIdx.push_back(pIdx); } if(pIdx) { CDbIndexCol* pCol = new CDbIndexCol(idx.m_szColumnName, idx.m_nCollation); if(pCol) pIdx->AddColumn(pCol); } } idx.Close(); } }
请注意,每个记录都包含索引名称和列名称。要构建多个列索引,我们必须从
CIndexes
读取一个以上的行。这就是为什么我必须使用FindIndex
方法。
示例应用
本文的示例应用程序是 **SqlCeSpy**,一个小型免费工具。在这里,您可以访问完整的源代码,其中包含一些有趣的功能,例如带有分割器的窗口和 CListView
控件的数据缓存。但这些将在其他文章中介绍。
注意:示例仅包含 eVC3 项目文件。