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

CCommand 和 CDynamicAccessor 的改进版本

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.68/5 (9投票s)

2002年11月21日

9分钟阅读

viewsIcon

155451

downloadIcon

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,我不会解释 ConnectionStatement。它们的作用相当明显。

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 的包装器
© . All rights reserved.