ODBC 数据库访问——一种模板化方法






4.80/5 (19投票s)
2004年11月22日
17分钟阅读

91590

2944
一个模板类库,能够快速生成客户端数据库代码。
引言
本文是关于使用模板化方法编写 C++ 中 ODBC 数据库访问的一系列简短文章中的第一篇。本期将创建一个类集,允许通用访问和更新 ODBC 数据源。该代码提供了一个 API,允许程序员读取表、遍历表之间的链接以及更新表,包括级联更新。此外,还实现了一个事务框架,允许程序员无论数据库平台如何,都能完全提交或回滚功能。此外,还有大量的空间可以扩展该框架以处理更复杂的问题,例如树形遍历。
背景
无论何时,当您使用 C++ 在 Windows 上通过 ODBC 编写数据库访问代码时,似乎大部分时间都花在了编写样板代码上。Microsoft 曾试图通过生成向导来帮助您编写 CRecordset 的自定义版本,以减轻这种负担,但我仍然觉得这个过程充其量只是令人沮丧。
MFC 中的 CRecordset 和 CDatabase 是一对有用的类;不幸的是,在许多复杂的应用程序中,它们存在根本性的缺陷。CDatabase 提供了一个基本的事务框架;然而,据我所知,这只是委托给数据库,对于支持事务回滚的数据库来说是可以的,但如果我们使用的是像 MS Access 这样简单的数据库,我们就没有这个功能。CRecordset 存在更根本的问题。通过将遍历数据集的概念与将数据写入数据集的概念结合起来,我们得到了一个易于使用的类,但在数据何时会被写回数据库方面几乎没有控制。在像树形遍历这样更复杂的应用程序中,当数据库表包含指向自身的自引用外键时,这可能尤其成问题。在多用户环境中确保树的一致性几乎是不可能的。此外,CRecordset 没有任何真正的事务框架支持,因此很难加入预插入和后插入操作等概念。
在我最近的一个工作项目中,我使用了一个自定义构建的框架,它解决了许多这些问题;然而,由于该系统使用了自定义类而不是 CRecordset,因此花费了大量时间来生成样板代码。我讨厌编写样板代码,所以我决定尝试使用模板编写自己的数据库访问库。
工作原理
该系统依赖于许多库,包括 MFC、Boost 和 Loki。在使用该系统之前,您必须拥有所有这些库,并将其设置在您的包含路径和链接路径中。
本质上,整个系统围绕四个类层次结构。它们是 DatabaseDef、TableDef、ColumnDef 和 PersistentObject。
DatabaseDef 是类层次结构的关键入口点。它从一个字符串模板化,乍一看可能有点奇怪。尽管如此,我有一个非常充分的理由。从公共 API 来看,DatabaseDef 实际上非常简单。它允许我们设置和获取 DSN、开始或结束事务、执行 SQL 或将项添加到更改队列。它支持事务框架背后的两个关键概念。第一个概念是事务本身。BeginTransaction、CommitTransaction 和 RollbackTransaction 是支持此概念的关键函数。第二个概念是预提交操作和后提交操作。当您调用 CommitTransaction 时,会发生三件事:所有预提交操作将按照您添加的顺序完成;更改队列将按照自定义顺序处理;后提交操作将按照您添加的顺序完成。将 DatabaseDef 模板化只是为了让我能够将其变成一个单例。这简化了从表或持久对象回溯到数据库的访问。它还允许其他核心类从数据库类严格类型化,从而提高数据库类型的安全性。如果您仔细检查该类,您会注意到 DatabaseDef 通过一个旨在支持在 UPDATE 和 CREATE 查询期间进行自定义参数绑定的包装器类,从 MFC 的 CDatabase 聚合。
template <char * DBNAME> class DatabaseDef { public: // Access the singleton through DatabaseDef::MySingleton::Instance() typedef Loki::SingletonHolder< DatabaseDef<DBNAME> > MySingleton; friend struct Loki::CreateUsingNew< DatabaseDef<DBNAME> >; public: // Public API // Set and get the DSN string void SetDSN( const std::string &dsn ); const std::string &GetDSN() const; // Access the transaction framework void BeginTransaction(); void CommitTransaction(); void RollbackTransaction(); // Get access to the CDatabase object CDatabaseWrapper *GetDatabase(); // Add an entry to the change queue void AddToChangeQueue( ChangeQueueEntryPtr entry ); // Add an entry to the pre-commit list void AddToPreCommitList( PreCommitActionPtr entry ); // Add an entry to the post-commit list void AddToPostCommitList( PostCommitActionPtr entry ); // Execute sql void ExecuteSQL( const std::string &sql ); };
TableDef 本质上等同于 CRecordset 从数据库获取数据的部分。它包含两层继承,顶层是一个纯虚接口类,便于使用该类,第二层是实际的模板化类。有三个模板参数被使用,即 DatabaseDef 类、一个表示表名的字符串以及一个列定义类型列表。类型列表是系统的巧妙之处。它在编译时封装了表的结构,这意味着您可以避免编写样板代码来从数据库中获取列值并将其返回给用户。为了使这个概念起作用,有必要进行一些模板元编程,但通常我依赖 Loki 库来为我完成这项工作。我唯一进行的自定义元编程是一个 FindPrimaryKey 类,用于从列定义的类型列表中提取主键。TableDef 类的公共接口再次清楚地描述了该类的用途。有一组导航函数,与 CRecordset 非常相似。我们还可以强制重新查询数据集。字段值和列定义都可以按索引提取,并且我们可以自动提取主键。最后,有一个 API 允许从特定行创建持久对象,或者允许生成新行。同样,这个类背后的基本引擎实际上是 MFC 类 CRecordset,主要是因为 CRecordset 在从数据库中提取数据方面实际上做得相当好。
template < class DBDEF, char *TABLENAME, class COLDEFS > class TableDef : public TableDefInterface { public: typedef typename boost::shared_ptr< TableDef< DBDEF, TABLENAME, COLDEFS > > _ptr; static _ptr GetTableDef(); static _ptr GetTableDef( const WhereClause &where ); public: virtual ~TableDef(); // Navigation functions virtual bool IsBOF() const; virtual bool IsEOF() const; virtual void First(); virtual void Last(); virtual void Next(); virtual void Prev(); // Force a requery of the dataset virtual void Requery(); // Access the data virtual const SQLValue &GetFieldValue( unsigned int index ) const; virtual const ColumnDefInterface &GetColumnDef( unsigned int index ) const; // Return the number of columns in this class static unsigned int GetNumberOfColumns(); // Return the first column def with the primary key set static ColumnDefInterface *GetPrimaryKey(); static unsigned int GetPrimaryKeyColumnIndex(); public: // API for create persistent objects on this table typedef typename PersistentObject<DBDEF, TABLENAME, COLDEFS>::_ptr persistentObjectPtr; persistentObjectPtr NewRow(); persistentObjectPtr NewRow( const PersistentObjectPtr &dependentEntry ); persistentObjectPtr AccessRow(); };
PersistentObject 是将数据写入数据库的关键类。它由 TableDef 进行工厂生成。PersistentObject 实际上由三层继承组成;顶层接口是许多函数的纯虚接口。接下来的两个级别定义了各种模板化功能,中间级别提供了仅与数据库相关的功能,无需表名或列定义即可运行。
PersistentObject 是一个有趣的类,因为它实际上被设计成可以以多种方式扩展。要完全理解持久对象,您需要查看它可以存在的四种不同状态。当持久对象首次创建时,它通常处于瞬态状态。这意味着当事务提交到数据库时,它将不执行任何操作。其余状态为插入、删除和更新。插入状态通过 TableDef 中的适当工厂函数自动设置,并且当列被更新时,更新状态会自动设置。删除状态是唯一必须由程序员显式设置的状态。一旦持久对象从瞬态状态移出,它会自动将自身添加到更改队列。通过使用 Boost 智能指针,可以确保持久对象的生命周期一直持续到更改队列被处理。持久对象实现了事务处理系统的最终层,即插入、删除和更新的预操作和后操作。这些可以通过简单的继承在特定持久对象上实现,并提供一种强大的方式来实现树形遍历等功能。例如,当树的头部对象被更新时,树对象可能希望存储树下的所有内容。与基于有缺陷的 CRecordset 接口不同,它直接生成必要的 SQL 并将其发送到数据库。所需的 SQL 是从列定义自动生成的,系统还会自动绑定二进制对象的参数,这些对象无法直接包含在 SQL 语句中。
template< class DBDEF, char *TABLENAME, class COLDEFS > class PersistentObject : public PersistentObjectDB<DBDEF> { public: typedef typename boost::shared_ptr< PersistentObject<DBDEF, TABLENAME, COLDEFS> > _ptr; public: virtual ~PersistentObject(); // Override these functions to perform // specific actions before and after database access virtual void PerformPreInsertActions() {} virtual void PerformPostInsertActions() {} virtual void PerformPreDeleteActions() {} virtual void PerformPostDeleteActions() {} virtual void PerformPreUpdateActions() {} virtual void PerformPostUpdateActions() {} // Accessors and mutators for the columns and field values virtual const SQLValue &GetFieldValue( unsigned int index ) const; virtual const ColumnDefInterface &GetColumnDef( unsigned int index ) const; virtual void SetFieldValue( unsigned int index, const SQLValue &value ); // Return the number of columns in this class static int GetNumberOfColumns(); };
核心四类中的最后一类是 ColumnDef 类。同样,这个类是用三个类的继承层次结构实现的;然而,与持久对象类(其中层次结构中只有最底层的类是具体的类)不同,该层次结构中有两个类是具体的。同样,有一个接口类,然后下面是常用的列定义,它将列映射到一个值,并识别列是否是表的主键。在该类下方是 ColumnJoinDef 类。这个类定义了表中的外键,并为 TableDef 中的元编程提供了基础,以自动遍历 join 来返回来自 join 另一侧的数据。它还提供了支持持久对象框架内的级联插入、更新和删除的功能。
class ColumnDefInterface { public: template <class TABLEDEF> typename TABLEDEF::_ptr GetTableDef() const; template <class TABLEDEF> typename TABLEDEF::persistentObjectPtr GetPersistentObject( const PersistentObjectPtr &dependentEntry ) const; protected: virtual WhereClause GetWhereClause() const };
template < char *COLNAME, class COLTYPE, bool PRIMARYKEY = false > class ColumnDef : public ColumnDefInterface { public: virtual const COLTYPE &GetValue() const; void SetValue( const COLTYPE &value ); };
template < char *COLNAME, class COLTYPE, class TABLEDEF > class ColumnJoinDef : public ColumnDef<COLNAME, COLTYPE> { protected: virtual WhereClause GetWhereClause() const; };
框架中剩余的类结构相对容易理解。我将以半逻辑顺序介绍这些类,解释它们的作用。DBException 是一个非常简单的异常类,用于处理框架内的所有错误。我将其保持简单,因为我知道大多数想要使用此框架的人可能会对其进行自定义,以便在他们的应用程序中更合适地工作。ExpressionNode 是一个简单表达式树的起点。目前,它仅用于描述简单的 WHERE 子句,最终将用于支持在数据库上自定义创建复杂的 SQL 查询。QueryNode 是一个查询树的起点,同样旨在扩展以支持复杂的 SQL 查询。目前,此树中唯一的具体节点是 WhereClause。有一对访问者,ExpressionSQLGenVisitor 和 QuerySQLGenVisitor,它们将树折叠成 SQL。当我在框架中编写查询部分时,它们同样设计用于扩展,但目前用于生成 TableDef 中使用的 SELECT SQL。表达式树和访问者基于我以前的一系列文章中生成的代码以及 Loki 访问者。事务通过 ChangeQueueEntry、PreCommitAction 和 PostCommitAction 类来支持。后一类是抽象基类,旨在供程序员继承以生成预提交或后提交操作。ChangeQueueEntry 提供自定义排序和持久对象的简单容器。通过 SQLParameterBinder、SQLBinaryParameterBinder 和 CDatabaseWrapper 类进行参数的后期绑定。最后,一些继承自 SQLValue 的类为 C++ 中的基本类型提供了空值支持。
使用代码
使用模板定义数据库结构
源文件中包含的测试代码基于 Microsoft Northwind 应用程序,该应用程序是 MS Access XP 的示例。假设已经配置了一个名为 Test 的 DSN。为了生成用于访问该应用程序中表的类,需要以下代码
// Assume that dbString, CategoriesCol1 ... and Categories are all // strings with external linkage // Set up the database, and initialise the DSN typedef DatabaseDef< dbString > DBDef; DBDef::MySingleton::Instance().SetDSN( "Test" ); // Define the categories table typedef ColumnDef< CategoriesCol1, SQLLong, true > ColCategories1; typedef ColumnDef< CategoriesCol2, SQLString > ColCategories2; typedef ColumnDef< CategoriesCol3, SQLString > ColCategories3; typedef ColumnDef< CategoriesCol4, SQLBinary > ColCategories4; typedef TYPELIST_4( ColCategories1, ColCategories2, ColCategories3, ColCategories4 ) CategoriesColumns; typedef TableDef< DBDef, Categories, CategoriesColumns > CategoriesTable; typedef PersistentObject< DBDef, Categories, CategoriesColumns > CategoriesPersistentObject; // Carry out a transaction DBDef::MySingleton::Instance().BeginTransaction(); // TODO: Perform the transaction DBDef::MySingleton::Instance().CommitTransaction();
关于此代码的一些说明
- dbString 只是数据库的一个唯一标识符。
- 单例实现使用 Loki 库。
- ColumnDef 用于定义表中的所有常规列。
- 表的主键的最后一个参数必须为 true。
- ColumnJoinDef 用于定义外键;它有一个额外的模板参数,指定依赖表。
- 使用的类型列表实现再次来自 Loki,大部分模板元编程也来自该库。
访问表
访问表仅仅是获取表定义,并使用标准函数遍历记录读取字段。由于不执行任何更新,因此无需在事务中进行。
// Query the products table ProductsTable::_ptr products( ProductsTable::GetTableDef() ); // Iterate over all records while ( !products->IsEOF() ) { // Output some fields SQLLong idx = products->GetFieldValue( 0 ); SQLString name = products->GetFieldValue( 1 ); cout << (long) idx << ", " << (string) name << endl; // Move to the next record products->Next(); }
表的级联访问
级联访问涉及从对应于正确外键的列定义生成表定义。系统会自动生成一个 SELECT 语句,其中外键用作依赖表主键的 WHERE 子句。
// Query the products table, and prepare // to query the suppliers and categories tables ProductsTable::_ptr products( ProductsTable::GetTableDef() ); SuppliersTable::_ptr suppliers; CategoriesTable::_ptr categories; // Iterate over the products while ( !products->IsEOF() ) { // Output some fields SQLLong idx = products->GetFieldValue( 0 ); SQLString name = products->GetFieldValue( 1 ); cout << (long) idx << ", " << (string) name << endl; // Get the cascaded entries for suppliers and categories suppliers = products->GetColumnDef( 2 ).GetTableDef<SuppliersTable>(); categories = products->GetColumnDef( 3 ).GetTableDef<CategoriesTable>(); // Output some data about suppliers while ( !suppliers->IsEOF() ) { SQLLong idxS = suppliers->GetFieldValue( 0 ); SQLString nameS = suppliers->GetFieldValue( 1 ); cout << " Suppliers: " << (long) idxS << ", " << (string) nameS << endl; suppliers->Next(); } // Output some data about categories while ( !categories->IsEOF() ) { SQLLong idxC = categories->GetFieldValue( 0 ); SQLString nameC = categories->GetFieldValue( 1 ); cout << " Categories: " << (long) idxC << ", " << (string) nameC << endl; categories->Next(); } // Move to the next record products->Next(); }
更新表
更新表几乎和读取它一样简单。当您找到要更新的行时,从表中创建一个持久对象并编辑值。当事务提交时,数据将被更新。
ProductsTable::_ptr products( ProductsTable::GetTableDef() ); while ( !products->IsEOF() ) { SQLLong idx = products->GetFieldValue( 0 ); SQLString name = products->GetFieldValue( 1 ); cout << (long) idx << ", " << (string) name << endl; ProductsPersistentObject::_ptr prods = products->AccessRow(); SQLString nameNew( (string) name + "_temp" ); prods->SetFieldValue( 1, nameNew ); products->Next(); }
向表中插入行
这同样很简单。创建一个表定义,从表中创建一个新的持久对象,并设置其值。
// Get the categories table CategoriesTable::_ptr categories( CategoriesTable::GetTableDef() ); // Generate a new persistent object CategoriesPersistentObject::_ptr cats = categories->NewRow(); // Insert some new data cats->SetFieldValue( 0, SQLLong( 10001 ) ); cats->SetFieldValue( 1, SQLString( "temp" ) ); cats->SetFieldValue( 2, SQLString( "temp_temp" ) );
删除表中的行
删除行也很容易。在表定义上迭代到正确的行,创建一个持久对象并调用 Remove 方法。
CategoriesTable::_ptr categories( CategoriesTable::GetTableDef( WhereClause( ColumnDefNode( ColCategories1( SQLLong( 10001 ) ), "=" ) ) ) ); while ( !categories->IsEOF() ) { CategoriesPersistentObject::_ptr cats = categories->AccessRow(); cats->Remove(); categories->Next(); }
级联更新
当通过使用 GetDependentPersistentObject 模板函数级联更新时,我们确保更改队列条目按正确的顺序处理。除此之外,语法与用于非级联更新的语法相同。
// Query the products table, and prepare to query the suppliers and categories tables ProductsTable::_ptr products( ProductsTable::GetTableDef() ); SuppliersTable::_ptr suppliers; CategoriesTable::_ptr categories; // Iterate over the products while ( !products->IsEOF() ) { // Output some fields SQLLong idx = products->GetFieldValue( 0 ); SQLString name = products->GetFieldValue( 1 ); cout << (long) idx << ", " << (string) name << endl; ProductsPersistentObject::_ptr prods = products->AccessRow(); // Get the cascaded entries for suppliers and categories suppliers = products->GetColumnDef( 2 ).GetTableDef<SuppliersTable>(); categories = products->GetColumnDef( 3 ).GetTableDef<CategoriesTable>(); // Output some data about suppliers while ( !suppliers->IsEOF() ) { SQLLong idxS = suppliers->GetFieldValue( 0 ); SQLString nameS = suppliers->GetFieldValue( 1 ); cout << " Suppliers: " << (long) idxS << ", " << (string) nameS << endl; // Perform a cascaded update if the product id is 4 if ( (long) idx == 4 ) { SuppliersPersistentObject::_ptr supps = prods->GetDependentPersistentObject<SuppliersTable>( 2 ); SQLString nameNew( (string) nameS + "_new" ); supps->SetFieldValue( 1, nameNew ); } suppliers->Next(); } // Output some data about categories while ( !categories->IsEOF() ) { SQLLong idxC = categories->GetFieldValue( 0 ); SQLString nameC = categories->GetFieldValue( 1 ); cout << " Categories: " << (long) idxC << ", " << (string) nameC << endl; // Perform a cascaded update if the product id is 4 if ( (long) idx == 4 ) { CategoriesPersistentObject::_ptr cats = prods->GetDependentPersistentObject<CategoriesTable>( 3 ); SQLString nameNew( (string) nameC + "_new" ); cats->SetFieldValue( 1, nameNew ); } categories->Next(); } // Move to the next record products->Next(); }
级联插入
级联插入类似于级联更新。
// Get the products table ProductsTable::_ptr products( ProductsTable::GetTableDef() ); // Generate a new persistent object ProductsPersistentObject::_ptr prods = products->NewRow(); // Insert some new data prods->SetFieldValue( 0, SQLLong( 10001 ) ); prods->SetFieldValue( 1, SQLString( "temp" ) ); prods->SetFieldValue( 2, SQLLong( 10002 ) ); prods->SetFieldValue( 3, SQLLong( 10003 ) ); // Get the dependent persistent objects and insert data into them SuppliersPersistentObject::_ptr supps = prods->GetDependentPersistentObject<SuppliersTable>( 2 ); supps->SetFieldValue( 1, SQLString( "temp_supp" ) ); CategoriesPersistentObject::_ptr cats = prods->GetDependentPersistentObject<CategoriesTable>( 3 ); cats->SetFieldValue( 1, SQLString( "temp_cats" ) );
级联删除
级联删除类似于级联更新。
// Get the correct record ProductsTable::_ptr products( ProductsTable::GetTableDef( WhereClause( ColumnDefNode( ColProducts1( SQLLong( 10001 ) ), "=" ) ) ) ); while ( !products->IsEOF() ) { // Remove the record and its dependent entries // Note that no check has been made of whether these dependent entries are // used elsewhere - however, a database exception will be thrown if they // are ProductsPersistentObject::_ptr prods = products->AccessRow(); prods->Remove(); SuppliersPersistentObject::_ptr supps = prods->GetDependentPersistentObject<SuppliersTable>( 2 ); supps->Remove(); CategoriesPersistentObject::_ptr cats = prods->GetDependentPersistentObject<CategoriesTable>( 3 ); cats->Remove(); products->Next(); }
异常处理
使用框架时,您必须注意两种可能抛出的不同异常。如果 TemplateDB 框架失败,它将抛出 DBException。如果 Microsoft 框架中出现问题,将抛出 CDBException。目前,这需要在调用应用程序中捕获,并且会导致更改队列失效和当前事务失败。如果程序员的意图是数据库异常是有效代码,那么需要在 DatabaseDef::CommitTransaction 函数中包含一个简短的异常处理程序。
关注点
export 关键字
使用模板编写代码库可能会引发一些非常晦涩的问题。我不得不处理的一个问题是循环包含依赖。在传统的库中,通过大量使用前向声明并将大部分代码移到 CPP 文件中,可以轻松避免循环包含。不幸的是,由于许多编译器尚未实现 export 关键字,您无法将代码移到 CPP 文件中,因为它被模板化了。这导致出现循环包含依赖的可能性绰绰有余。让我举个例子:TableDef 包含一个 ColumnDef 类的列表,用于描述表中的列。当您想遍历二级键以获取另一个表时,ColumnDef 必须通过工厂生成 TableDef。总而言之,这给我带来了很多烦恼。据我所知,Microsoft、Borland 和 GCC 都不支持 export 关键字,我希望他们将来能重新考虑并努力支持这个非常有用的概念。
自动转换模式
SQLValue 类层次结构展示了一种有趣的模式,我称之为 Auto-Cast 模式。据我所知,它在 C++ 中很独特,是我工作中的一名程序员的发明。Auto-Cast 模式涉及在基类 SQLValue 中实现虚拟转换运算符,这些运算符默认为错误,然后在派生类中重写它们以实际将数据转换为所需的基础类型。它允许使用指向基类的指针或引用,就像它们是基础类型本身一样进行多态使用。这是一种避免每次使用 SQLValue 时进行动态转换的绝佳方法,尤其是在您对其具体类型绝对确定时。不幸的是,这种模式有一个致命的弱点。您必须非常小心将 SQLValue 用作多态函数的参数,或在进一步的隐式转换可能导致差异的情况下使用。例如,CLongBinary 指针类型可以被隐式转换为布尔值,从而与 SQLBool 类混淆。在这些情况下,使用简单的静态转换显式转换 SQLValue 可以消除任何混淆。
模板的字符串参数
ANSI 标准 C++ 允许在模板中使用许多不同类型的参数。其中之一是可以在编译时求值的常数整型。这些很少见,但可能非常有用。大多数对模板不熟悉的人,在第一次看到代码时,往往会问为什么要使用整型模板参数,而整数值可以存储为 const 成员变量。事实上,在几乎所有情况下,使用 const 成员变量可能更好。关键的例外是当我们希望能够区分由此参数产生的类类型时。通常,这在模板元编程中变得很重要,但在其他时候也很重要。通过比较下面的两个类,可以看到这两种编程范例之间的区别
// A templatised version template < unsigned int I > class A { public: unsigned int GetParam() { return I; } }; // Using a const member variable class B { public: B( unsigned int i ) : m_i( i ) {} unsigned int GetParam() { return m_i; } private: const unsigned int m_i }; // Functionally A and B are remarkably similar, the // key difference is that A<1> and A<2> are different types // whereas B(1) and B(2) are the same type
字符串常量是常数整型;然而,标准还有一个额外的要求,使得这并不简单。要使字符串常量用作模板参数,它必须具有外部链接。其语法是什么并不明显,但一旦我们理解了语法,使用此类类型的模板参数就会非常有用。在此框架中,我使用了字符串模板参数来标识数据库、表和各个列名。
// Tucked away in a configuration cpp file, // we would have the following global variable defined char dbString[] = "TestDB";
// Then when we wanted to instantiate the template we // would need the following code extern char dbString[]; typedef DatabaseDef< dbString > DBDef;
可移植性
我创建的框架仍然严重依赖 MFC 的 CDatabase 和 CRecordset 类。任何移植到其他平台或打算在没有 MFC 的情况下使用,都需要替换这些类。尽管值得注意的是,通过仅使用 CRecordset 读取记录,此移植将大大简化。
更重要的是,通过使用 ODBC,该框架应该可以相对轻松地移植到多个数据库平台。话虽如此,根据过去的经验,在不同平台之间可能会出现一些问题,例如,对普通字符串和宽字符串的使用不一致、布尔参数格式不同以及其他类似问题。我使用 MS Access 测试了该框架,并且我认为该系统可以轻松地在 SQL Server 上运行。其他平台可能会导致更严重的问题。
使用模板元编程访问表的主键
使用元编程技术从列定义的类型列表中提取主键的方法很有趣。我们可以使用 IsPrimaryKey const static bool 来识别单个列定义是否为主键。它从模板参数初始化。然后可以使用 FindPrimaryKey 模板来提取主键列。它的用法很简单:FindPrimaryKey<COLDEFS>::Result 将从列定义的类型列表中返回主键列的类型。详细检查 FindPrimaryKey 模板,我们可以看到模板的核心基于编译时 IfThenElse 构造。IfThenElse 模板默认有一个与第二个模板参数对应的单个 typedef,但当第一个模板参数为 false 时,它有一个与第三个模板参数对应的 typedef 的部分特化。在使用中,我们传递三个模板参数,模板将根据第一个参数是 true 还是 false 返回第二个或第三个参数。然后,我们生成一个模板,该模板使用类型列表中第一个元素的 IsPrimaryKey 值来返回第一个元素,或者使用相同的模板递归地检查类型列表的其余部分。在 ANSI 标准 C++ 中,如果模板仅与包含主键的类型列表一起使用,则不必终止此递归;但是,Microsoft 编译器不接受这一点,因此我们包含模板的部分特化来终止递归。
template <class TList> struct FindPrimaryKey { typedef typename TList::Head Head; typedef typename TList::Tail Tail; private: // Main template IfThenElse handles the default - ie true template<bool C, class T, class F> struct IfThenElse { // True variant typedef typename T Result; }; // Partial specialisation for the alternate case - false template<class T, class F> struct IfThenElse<false, T, F> { // False variant typedef typename F Result; }; // Main template returns the Head if it is the PrimaryKey, // or recurses onto the Tail template<class TList1> struct In { typedef typename TList1::Head Head; typedef typename TList1::Tail Tail; typedef typename IfThenElse<Head::IsPrimaryKey, Head, typename In<Tail>::Result>::Result Result; }; // Partial specialisation to end recursion - // returns the Head of the list by default template<> struct In< ::Loki::NullType > { typedef typename Head Result; }; public: typedef typename In<TList>::Result Result; };
结论
总之,使用 C++ 为 Windows 编写数据库应用程序并非易事。MFC 类 CDatabase 和 CRecordset 缺少一些关于事务框架的关键功能。如果您决定编写自己的框架,通常需要编写大量的样板代码。即使使用 MFC 类,这也很有必要,尽管 Visual Studio 提供了一个向导来简化这个过程。为了避免编写样板代码,同时仍提供事务处理框架,我开发了一个模板化代码库,用于通过 ODBC 快速生成数据库访问代码。编写此代码存在一些困难,尤其是缺少 export 关键字导致了许多循环包含依赖问题。尽管如此,结果是一套强大且可扩展的类。
我打算在不久的将来扩展此框架,通过扩展表达式和查询树领域的工作,以支持以编程方式生成复杂的 SQL 查询。此外,应该有可能使框架通用化,以应对不同数据库平台的特殊性。目前,该框架仅在 MS Access 上进行了测试,尽管根据以往的经验,它应该可以轻松地在 SQL Server 上运行。
历史
- 2004 年 11 月 22 日:版本 1 发布。