C++ 中的中级 OLE DB 消费者






4.73/5 (8投票s)
2001年12月5日
9分钟阅读

133347

1713
本文在我之前的文章的基础上,介绍了记录集操作和数据库条目操作。
引言
在我之前关于 C++ 中使用 OLE DB 消费者(OLE DB Consumers in C++)的文章中,我介绍了如何设置数据库以及如何从中读取数据。本文在我之前的文章的基础上,通过允许在数据库表中进行前后移动,并使用 update、insert 和 delete 函数对数据库中的条目进行更改,进一步扩展了内容。这可以通过表和命令来完成,最后一节将展示如何对数据库表运行查询。应该注意的是,尽管上一篇文章使用了 Microsoft 提供的 nwind 演示数据库作为示例,但本文将使用一个在 Access 中构建的小型 Books 数据库,因此不使用专门设置的 ODBC 驱动程序,而是使用 Jet 4.0 驱动程序并直接访问数据库文件。如果您没有此驱动程序并且需要使用其他驱动程序,请参阅本系列中关于设置驱动程序的早期 C++ 文章。
此项目的代码是在 Windows 2000 上使用 Developer Studio 6 和 7 开发的。
使用表访问器
在上一篇文章中,我提到了表访问数据库和命令访问数据库之间的区别。本文将使这两种类型的不同功能更加清晰。首先,表的 [主] 功能由表继承的 CRowset
类提供。
class CBooks : public CTable< CAccessor< CBooksAccessor > >
上面的行中缺失的部分是 CTable
的定义是
CTable< TAccessor, TRowset >
因此,如果您不指定记录集类型,您将免费获得标准的 CRowset
。标准 CRowset
类提供了所有的移动功能。
HRESULT hResult = m_Books.MoveNext(); HRESULT hResult = m_Books.MovePrev();
以及更改数据库的所有功能。
HRESULT hResult = m_Books.SetData(); /// update
HRESULT hResult = m_Books.Delete();
HRESULT hResult = m_Books.Insert();
应该注意的是,插入函数没有任何限制,所以您可以一直按插入按钮,它会插入一个条目,除了自动编号的 id 值外,其他条目数据都相同,直到您想要为止。
这里的主要关注点在于访问器以及如何使用它们来自定义从数据库返回的信息。在上一篇文章中,我们使用了访问器中的标准表绑定方法。本文的标准访问器是
BEGIN_COLUMN_MAP( CBooksAccessor ) COLUMN_ENTRY(1, m_ID ) COLUMN_ENTRY(2, m_Title ) COLUMN_ENTRY(3, m_AuthorsFirstName ) COLUMN_ENTRY(4, m_AuthorsLastName ) COLUMN_ENTRY(5, m_Category ) COLUMN_ENTRY(6, m_price ) END_COLUMN_MAP()
这是将列绑定到类中的数据成员的标准定义,针对表中的每一行。但事实证明,宏 BEGIN_COLUMN_MAP
的定义是
BEGIN_ACCESSOR_MAP(x, 1 ) BEGIN_ACCESSOR( 0, true )
其中 x 代表类名,1 代表当前列表中定义的访问器数量。 BEGIN_ACCESSOR
宏也接受两个变量,第一个是当前访问器的定义编号,第二个是它是否为自动访问器的标志。用通俗的话来说,如果没有指定访问器,则使用哪个访问器。因此,对于表访问器,我们将需要三个不同的访问器:1、用于对表的标准访问;2、用于查看作者;3、用于查看类别。这些可以使用 BEGIN_ACCESSOR
、 END_ACCESSOR
对来指定。
BEGIN_ACCESSOR_MAP( CBooksAccessor, 3 ) /// update the number of accessors BEGIN_ACCESSOR( 0, true ) COLUMN_ENTRY( 1, m_ID ) COLUMN_ENTRY( 2, m_Title ) COLUMN_ENTRY( 3, m_AuthorFirstName ) COLUMN_ENTRY( 4, m_AuthorLastName ) COLUMN_ENTRY( 5, m_Category ) COLUMN_ENTRY( 6, m_price ) END_ACCESSOR() BEGIN_ACCESSOR( 1, false ) COLUMN_ENTRY( 1, m_AuthorsFirstName ) COLUMN_ENTRY( 2, m_AuthorsLastName ) END_ACCESSOR() BEGIN_ACCESSOR( 2, false ) COLUMN_ENTRY( 1, m_Category ) END_ACCESSOR() END_ACCESSOR_MAP()
上面是 CTable
访问器的外观。访问器映射有三个条目,旨在如上所述使用。尽管在此示例中我们获得了附加访问器中的相同数据,但我们可以设置访问器,以便每个类成员只有一个绑定,但这会增加使用类的难度,因为我们需要不断切换访问器才能获得完整的记录集数据。
要在代码中切换访问器,请调用 CAccessor::GetData( x )
,其中 x 等于您希望使用的访问器的编号,但您应该注意,示例中访问器的编号是基于零的,传递给 get data 的数字也是如此。在当前示例中,如果您更改传递给 GetData
的任何数字为 3 或更高,您将遇到一个 ATLASSERT
,它会检查您在 GetData
调用中传递的数字是否不大于传递给 BEGIN_ACCESSOR_MAP
的数字。
一旦使用了 GetData
函数,您就必须小心如何继续。首先,如果您一直使用 MoveNext
函数直到它返回的值不等于 S_OK
。您实际上已经超出了记录集的末尾,这意味着对访问器的任何调用都将失败,因为此时访问器未指向有效记录,所以您需要执行此操作:
m_Books.MoveFirst(); /// get the accessor back to a valid point in the rowset m_Books.GetData( 0 ); /// go back to using the main accessor m_Books.MoveFirst(); /// Move to the first row using this accessor previous GetData call /// postioned you before the first row.
使用命令访问器
CCommand
访问器几乎与表访问器相同,在这种情况下, COLUMN_MAP
中的绑定完全按标准处理。命令访问器拥有与表访问器完全相同的 CRowset
函数。这意味着 CCommand
访问器在演示应用程序中的行为与表访问器完全相同,唯一的明显例外是我获取作者数据和类别数据的方式。这是通过在 CCommandBooks
类本身中添加一些内容来实现的。
CCommand< CDynamicAccessor > m_QueryCommand; HRESULT Query( BSTR bstrSQL ) { CString strString( bstrSQL ); HRESULT hResult = m_QueryCommand.Open( m_session, strString ); if( FAILED( hResult ) ) return hResult; hResult = m_QueryCommand.MoveFirst(); if( FAILED( hResult ) ) return hResult; return S_OK; }
类中声明的单独的 CCommand
变量采用 CDynamicAccessor
,这意味着数据库中的列将不会预先映射,而是需要使用 CDynamicAccessor
函数检索它们。
Query 函数接受一个 SQL 字符串,并使用该字符串在数据库会话上打开一个新命令。请注意,它使用类会话对象访问数据库,而不尝试设置自己的会话。然后,该函数移到查询返回的第一个记录集。
使用 Query 函数的两个函数是 COLEDBConsumer2Dlg::CommandViewCategories
和 COLEDBConsumer2Dlg::CommandViewAuthors
。所有查看作者和类别的方法都编写得一样,首先获取数据,然后将数据放入对象列表中,该列表是 CAccessorDlg
类的一个成员。 CAccessorDlg
类将使用对象列表来填充其列表框,当它在 DoModal()
调用中初始化时。
CComBSTR bstrQuery( _T( "SELECT distinct Books.[Author Last Name], Books.[Author First Name] FROM Books" ); HRESULT hResult = m_CommandBooks.Query( bstrQuery.m_str );
调用查询函数的代码非常简单,使用命令查询方法而不是表方法的关键在于,在作者姓名前使用 distinct 关键字,SQL 语句将只选择作者的一个版本,即使他们有三个或四个数据库条目。
目前有一个小优势,因为我们知道在查询数据库以获取作者姓名和姓氏时,我们总是只会从列中获得字符串。
bstrTemp = static_cast< BSTR >( m_CommandBooks.m_QueryCommand.GetValue( 1 ) );
CDynamicAccessor::GetValue
函数可以接受列编号(此处使用)或列名作为参数。然后,它将返回 void 指针格式的数据,该数据被转换为 BSTR
(记住,它本身就是 OLECHAR
指针),因此此转换没有问题,它只是看起来像一个指向类型被转换为另一种类型。一旦列中的值被转换为字符串,我们就可以按需使用它。
最后,我们关闭 m_CommandBooks.m_QueryCommand
对象并返回主对话框。
演示应用程序
下面的对话框显示了 Accessor 对话框的图片,显示了在使用 CCommand
访问器时按下 View Authors 按钮的结果。
演示应用程序(在页面顶部图片中)包含一个小的复选框。如果您逐步查看代码,您会注意到它充当一个简单的标志。如果选中该框,应用程序将调用函数的 Command Accessor 版本而不是 Table Accessor 版本。这是为了表明两种访问器之间的方法基本相同。主要区别在于 CCommand
访问器通过使用 SQL 语句查看类别和作者,该 SQL 语句允许 Command Accessor 在 SQL 中指定条件,从而使结果表比 Table Accessor 达到的效果更好。
这两种方法都允许对表进行例行维护,如修改、删除和插入数据,每种操作都作为一个单独的函数提供。
由于 testbooks.mdb 文件很小,它被包含在项目 zip 文件中,而不是作为单独的下载。
上面的图片显示了在使用 Table Accessor 时按下 View Categories 按钮的结果。
最后
在使用 CCommand
访问器时,也可以像使用 CTable
访问器一样更改列绑定,尽管 CCommand
访问器可以使用原始 SQL 命令,但我倾向于只使用 ACCESSOR_MAP
宏和 CTable
访问器,而使用 SQL 和 CCommand
访问器。
此外,还有其他技术可以通过 Command 类使用 SQL 语句访问数据,其中一种主要技术是使用 CCommand::Create
函数,它接受一个 CSession
对象和一个 SQL 语句。其思想是您调用 Create,然后调用 CCommand::Prepare
,最后调用 CCommand::Open
函数。我在这个过程中遇到了一些问题,我不断遇到本应等于 NULL 却不等于 NULL 的记录集指针,或者本应不等于 NULL 却等于 NULL 的记录集指针。在 HOWTO 文章“ExecuteParameterized Command Multiple Times with ATL OLEDB Consumer Templates”中有对此方法的示例,应该注意的是,尽管标题如此,它并没有使用向导生成的消费者模板,代码会创建一个访问器,然后完全绕过向导生成的类,并手动完成所有操作。
实现相同效果的另一种方法是降到接口级别,使用 ICommand::Execute
函数。这工作正常,但是从 IRowset
接口检索数据所需的工作量对于小型应用程序来说不是您真正想要的。此技术的示例可以在文章 OLE DB for the ODBC Programmer 的“OLE DB Rowsets”子标题下找到。该示例几乎有一百行代码。
总而言之,本文与前两篇文章数据库更改的原因是 Access 表中内置的关系,这会让我过早地处理多表访问。我将在下一篇 C++ 文章中遇到多表访问。