CCommand 和 CDynamicAccessor 的改进版本






2.68/5 (9投票s)
2002年11月21日
9分钟阅读

155451

2065
本文展示了一种扩展 CCommand 和 CDynamicAccessor 以使 C++ 中的 OLEDB 更容易的方法
引言
对于 C++ 开发者来说,在数据库访问方面有多种技术可供选择。有 ODBC、ADO、DAO、OLEDB,当然还有更多。大多数 C++ 开发者很可能使用过 ADO 或 OLEDB,或者两者都使用过。这两种技术在 C++ 中使用起来都很繁琐。如果你选择 ADO,那么你最好喜欢 VARIANT
。如果你选择 OLEDB,那么你最好喜欢静态和不灵活的访问器——或者——带有 void*
复杂性的动态访问器。
这是我谦逊地尝试让 OLEDB 更易于使用,就像 ADO 在 VB 中那样,但又无需使用变体。当然,如果你喜欢变体,你随时可以修改代码并根据需要包含它们。
我最近在工作中经常使用 Java。我所做的工作以数据库为中心,所以我一直使用 JDBC 作为我的主要工具。我发现 JDBC 为程序员提供的接口非常好。我根据 JDBC 接口 Connection.prepareStatement()
和 ResultSet.getXXX()
(其中 XXX 是类型名)来设计我的解决方案。
我希望你对 C++ 中的 OLEDB 有一点了解。Java 知识不会有坏处,但不是必需的。
JDBC 介绍
作为 Java 数据库程序员,你需要了解的主要类有
- Connection
- 等同于 OLEDB
CSession
- 声明
- 等同于 OLEDB
CCommand
- PreparedStatement
- 在 OLEDB 中没有等效项,这就是我的代码发挥作用的地方
- ResultSet
- 等同于我派生自 OLEDB 类
CDynamicAccessor
假设你已经了解 OLEDB,我不会解释 Connection
和 Statement
。它们的作用相当明显。
PreparedStatement
PreparedStatement
是一个支持 SQL 查询参数化的命令。它允许你编写诸如 SELECT attr FROM table WHERE key1 < ? AND key2 LIKE ?
的查询。正如你所看到的,这个查询本身是不完整的。在 Java 中,你通过函数调用在后面填充问号的值。Java 类还会确保值被适当地引用和转义。这意味着你不必担心字符串中的单引号——所有这些都已处理。
这是一个使用 PreparedStatement
创建完整查询的示例
Connection conn = ...;
PreparedStatement pstmt =
conn.prepareStatement(
"SELECT attr FROM table WHERE key1 < ? AND key2 LIKE ?"
);
pstmt.setInt(1, 1234);
pstmt.setString(2, "Code%");
这段 Java 代码将生成 SQL 查询 SELECT attr FROM table WHERE key1 < 1234 AND key2 LIKE 'Code%'
。
setInt()
和 setString()
函数接受两个参数。第一个参数是参数索引,用于指定哪个参数应该被替换为值。第二个参数是值本身——相当直接。我的类工作方式与此类似,除了一点——我重载了一个单一的方法名,而不是为每个可用类型使用一个名称。
ResultSet
在 Java 中执行查询语句时,你会得到一个结果集。结果集基本上是一个行滚动器,支持“移到开头、移到下一个、移到上一个、移到最后一个”等导航——非常类似于 OLEDB。
要访问列数据,你可以使用函数 getXXX(ordinal)
,其中 ordinal
是整数索引或包含列名的字符串。
这是一个简单的示例,展示如何从查询中获取数据(接续之前的 Java 示例)
ResultSet rs = pstmt.executeQuery();
while(rs.next()) {
System.out.println("attr = " + rs.getString(1));
}
简单吧?一个好处是 ResultSet.getXXX
可以为你强制转换值。例如,getString()
适用于大多数类型,因为大多数类型都可以表示为字符串。
OLEDB 中的访问器
在 OLEDB 中,你必须使用宏魔术来创建静态访问器,将成员变量绑定到每个列。或者,你可以使用 CDynamicAccessor
来访问数据。
静态访问器
静态访问器是访问数据最快的方式,但像往常一样——其中涉及权衡。如果你的表属性发生变化,静态访问器会存在问题。字符串属性的变化尤其成问题。静态访问器中的字符串属性表示为固定大小的字符数组,其大小是数据库指定的大小加上一个终止符。想象一下,如果增加数据库中的字符串大小而忘记更新访问器会发生什么。
动态访问器
动态访问器不需要宏魔法,也不易受数据库更改的影响。由于它们使用动态内存分配来处理数据,因此它们比静态访问器慢一点。但是,如果你能接受这种小的开销,你会发现访问 OLEDB 中的数据可以像 Java 中一样简单(尽管它不会像 Java 那样慢)。
CDynamicAccessor
的标准实现实际上相当严格。它有一个模板化的 GetValue()
函数,乍一看可能很出色。但是,如果你尝试获取一个 2 字节整数并将其存储在一个 4 字节长变量中,它就会断言。因此,模板化版本要求你的参数大小与数据大小匹配。没有尝试强制转换值。
使用模板化的 GetValue()
函数获取字符串几乎是不可能的——字符串指针很少与字符串数据(所有字符大小的总和)大小相同。对于字符串,你必须使用 GetValue()
的 void*
版本。另外值得注意的是,可能存在三种类型的字符串:wchar_t*
、char*
和 BSTR
,因此你必须提前知道它是哪种类型的字符串——你不能假设是其中一种!
我将尝试用我的代码解决这些问题。获取字符串应该很容易——它应该执行必要的转换来适合你,而不是反过来。其他类型也一样。一个 short
可以放入一个 long
中!一个 short
也可以表示为字符串。
CDynamicCommand
CDynamicCommand
公开派生自 CCommand
。但是,它不应被视为多态类型——没有方法是虚的。值得注意的函数如下:
SetCommandTemplate()
SetParamValue()
Open()
SetCommandTemplate
等同于 Java 的 Connection.prepareStatement()
。该函数接受一个字符串,其中包含你的参数化查询。请不要引用字符串参数,SetParamValue
函数会为你处理!
SetParamValue
SetParamValue
等同于 Java 的 PreparedStatement.setXXX()
。该函数接受一个索引,告诉你要设置哪个参数,以及一个值。
在这个函数中,我不仅记录了值,我还将值标记为“已使用”。我稍后会使用这些标记来确定用户是否遗漏了设置参数。
这些函数还会转义字符串中的单引号,以及引用字符串。像 datetime
这样的类型会被转换为它们的字符串表示形式。
打开
Open()
等同于 Java 的 PreparedStatement.executeQuery()
。它将执行查询,并让你滚动浏览结果。有关参数的更多信息,请参阅 MSDN 文档,因为我只是将它们传递给 CCommand::Open()
。
在这个函数中,我将你设置的所有参数汇总起来,并创建完整的 SQL 查询。如果你忘记设置参数,则返回 E_FAIL
。如果一切正常,则使用完整的 SQL 查询调用 CCommand::Open()
。
CDynamicAccessorEx
感兴趣的函数是 template <typename T> GetValue(ordinal, T* ptr)
及其特化。GetValue
的泛型版本将所有内容传递给 CDynamicAccessor::GetValue()
,因此充当回退函数。这意味着如果你尝试获取非特化类型的值,CDynamicAccessorEx
的行为将与 CDynamicAccessor
相同。GetValue
的特化版本将在必要时执行转换。请注意,GetValue()
的字符串版本(除了 CString
)会为你动态分配一个字符串。你必须稍后使用 delete[]
释放字符串。我稍后会为字符串添加固定大小缓冲区版本的 GetValue()
,以减少内存分配和删除的需要。
使用 CDynamicCommand 和 CDynamicAccessorEx 的示例
这些类可以这样使用
CDataSource ds;
ds.Open(...); // Setup data source
CSession session;
session.Open(ds);
CDynamicCommand<CDynamicAccessorEx> cmd;
cmd.SetCommandTemplate(
_T("SELECT Telephone, Mobile, Fax, Email FROM Person "
"WHERE FirstName LIKE ?"));
cmd.SetParamValue(0, "A%");
cmd.Open(session);
HRESULT hr = cmd.MoveFirst();
while(S_OK == hr) {
CString strTel, strMob, strFax, strEmail;
cmd.GetValue(_T("Telephone"), &strTel);
cmd.GetValue(1, &strMob);
cmd.GetValue(_T("Fax"), &strFax);
cmd.GetValue(4, &strEmail);
DoSomething(strTel, strMob, strFace, strEmail);
hr = cmd.MoveNext();
}
cmd.Close();
如你所见,这再简单不过了——几乎和 VB 中的 ADO 或 Java 中的 java.sql.*
一样简单。下载演示项目自己尝试一下。演示项目是一个非常简单的对话框,使用 WTL7 实现。请参阅 MainDlg::OnInitDialog()
。ZIP 中包含一个 Access MDB 文件(不大)。你可能需要更改连接字符串,以便驱动程序能够找到 MDB 文件。
买者自负
- 此代码不完整——暂时将其视为 alpha 版。
- 我尚未做任何支持 BLOB 的工作。
- 我尚未充分测试此代码,无法保证其在生产环境中的适用性。
- 由于动态内存分配,动态访问器在多线程环境中可能会成为主要瓶颈。
- 在调用
CDynamicAccessor::GetValue()
之后,必须释放字符串(char*
、wchar_t*
和BSTR
) - 我不确定
CDynamicCommand
是否应该公开继承自CCommand
,因为这种关系不是严格的“IS-A”关系。我将看看如何让客户端使用CDynamicCommand
作为CCommand
。如果我发现这完全可行,继承将保留,我将在CDynamicCommand
中添加using CCommand::Open
语句。
然而,我的目标是将此代码用于一个处理某种敏感数据的项目。我将在该项目进行过程中不断更新代码。一旦发现并修复错误,我也会及时更新本文。当然,欢迎提交错误报告、修复和/或建议。
许可证授予
你被授予使用代码的许可,无论出于何种目的,但仅限于满足以下条件:
- 你不能期望我提供任何保证。如果你损坏了什么,由你修复。
- 如果有一天我们见面,你可以请我喝我选择的啤酒。这不是强制要求,但这样做会非常好,我将不胜感激。如果你的宗教禁止你购买任何酒精,即使不是为了你自己,一杯可乐也可以。
修订
- 2002-11-22
- 初始版本
- 2002-11-23
- 相当大的更新,包括
GetValue()
,可以处理固定大小的字符串缓冲区- 当列数据是字符串数据时,
GetValue(ordinal, CString*)
更智能。在这种情况下,它使用CString::Get/ReleaseBuffer()
。如果时间允许,其他被强制转换的类型,如果字符长度已定义,也将使用Get/ReleaseBuffer()
函数。 - 更多
GetValue<>()
的特化。所有本机整数和bool
。 - 字符串版本现在可以将大多数值强制转换为字符串。(并非全部……尚未)
- 2002 年 11 月 25 日
- 优化和修复
SetParamValue(size_t, const DATE&)
现在是正确的。SetParamValue()
的整体性能得到优化。GetValue<char/wchar_t>()
中有更多字符串强制转换
2004 年 10 月 26 日
- 各种错误修复
- 添加了 Decimal 类 - OLE 类型 DECIMAL 的包装器