ODBC(Open Database Connectivity,开放数据库互连)是微软公司开放服务结构(WOSA,Windows Open Services Architecture)中有关数据库的一个组成部分,它建立了一组规范,并提供了一组对数据库访问的标准API(应用程序编程接口)。这些API利用SQL来完成其大部分任务。ODBC本身也提供了对SQL语言的支持,用户可以直接将SQL语句送给ODBC。一个基于ODBC的应用程序对数据库的操作不依赖任何DBMS,不直接与DBMS打交道,所有的数据库操作由对应的DBMS的ODBC驱动程序完成。也就是说,不论是FoxPro、Access还是Oracle数据库,均可用ODBC API进行访问。由此可见,ODBC的最大优点是能以统一的方式处理所有的数据库。MFC提供的ODBC类对较复杂的ODBC API进行了封装,提供了简化的调用接口,从而大大方便了数据库应用程序的开发。程序员不必了解ODBC API和SQL的具体细节,利用ODBC类即可完成对数据库的大部分操作。为了说明如何通过ODBC类操作数据库,本实例给出一个数据库的应用实例,它实现了对数据库中的课程表的浏览、编辑、修改、添加等基本操作。需要注意的是,在编译运行该程序时,请先注册代码中的数据源,并将该数据源命名为"Student Regritration"。程序编译运行后的界面效果如图一所示:
图一、操作数据库的应用程序界面
一、实现方法
一个完整的ODBC由下列几个部件组成:(1)应用程序(Application);(2)ODBC管理器(Administrator),该程序位于Windows 95控制面板(Control Panel)的32位ODBC内,其主要任务是管理安装的ODBC驱动程序和管理数据源;(3)驱动程序管理器(Driver Manager),驱动程序管理器包含在ODBC32.DLL中,对用户是透明的。其任务是管理ODBC驱动程序,是ODBC中最重要的部件(4)ODBC API;(5)ODBC 驱动程序,是一些DLL,提供了ODBC和数据库之间的接口;(6)数据源。数据源包含了数据库位置和数据库类型等信息,实际上是一种数据连接的抽象。各部件之间的关系如图2所示:
图二、ODBC部件关系图
应用程序要访问一个数据库,首先必须用ODBC管理器注册一个数据源(或动态注册一个数据源),管理器根据数据源提供的数据库位置、数据库类型及ODBC驱动程序等信息,建立起ODBC与具体数据库的联系。这样,只要应用程序将数据源名提供给ODBC,ODBC就能建立起与相应数据库的连接。
在ODBC中,ODBC API不能直接访问数据库,必须通过驱动程序管理器与数据库交换信息。驱动程序管理器负责将应用程序对ODBC API的调用传递给正确的驱动程序,而驱动程序在执行完相应的操作后,将结果通过驱动程序管理器返回给应用程序。
MFC的ODBC类主要包括:(1)CDatabase类,主要功能是建立与数据源的连接。(2)CRecordset类,该类代表从数据源选择的一组记录(记录集),程序可以选择数据源中的某个表作为一个记录集,也可以通过对表的查询得到记录集,还可以合并同一数据源中多个表的列到一个记录集中,通过该类可对记录集中的记录进行滚动、修改、增加和删除等操作。(3)CRecordView类,提供了一个表单视图与某个记录集直接相连,利用对话框数据交换机制(DDX)在记录集与表单视图的控件之间传输数据。该类支持对记录的浏览和更新,在撤销时会自动关闭与之相联系的记录集。(4)CFieldExchange类:支持记录字段数据交换(DFX),即记录集字段数据成员与相应的数据库的表的字段之间的数据交换。该类的功能与CDataExchange类的对话框数据交换功能类似。(5)CDBException类,代表ODBC类产生的异常。
1、CDatabase类
概括地讲,CDatabase针对某个数据库,它负责连接数据源;CRecordset针对数据源中的记录集,它负责对记录的操作;CRecordView负责界面,而CFieldExchange负责CRecordset与数据源的数据交换。
要建立与数据源的连接,首先应构造一个CDatabase对象,然后再调用CDatabase的Open()成员函数。Open()函数负责建立连接,其函数原型为:
virtual BOOL Open( LPCTSTR lpszDSN, BOOL bExclusive = FALSE, BOOL bReadOnly =
FALSE, LPCTSTR lpszConnect = "ODBC;", BOOL bUseCursorLib = TRUE );
函数中参数lpszDSN指定了数据源名(构造数据源的方法将在后面介绍),在lpszConnect参数中也可包括数据源名,此时lpszDSN必需为NULL,若在函数中未提供数据源名且使lpszDSN为NULL,则会显示一个数据源对话框,用户可以在该对话框中选择一个数据源。参数bExclusive说明是否独占数据源,由于目前版本的类库还不支持独占方式,故该参数的值应该是FALSE,这说明数据源是被共享的。参数bReadOnly若为TRUE则对数据源的连接是只读的。参数lpszConnect指定了一个连接字符串,连接字符串中可以包括数据源名、用户帐号(ID)和口令等信息,字符串中的"ODBC"表示要连接到一个ODBC数据源上。参数bUseCursorLib若为TRUE,则会装载光标库,否则不装载,快照需要光标库,动态集不需要光标库。 若连接成功,函数返回TRUE,若返回FALSE,则说明用户在数据源对话框中按了Cancel按钮。若函数内部出现错误,则框架会产生一个异常。下面是一个调用Open()函数的例子:
CDatabase m_db; //在文档类中嵌入一个CDatabase对象
//连接到一个名为"Student Registration"的数据源
m_db.Open("Student Registration");
//在连接数据源的同时指定了用户帐号和口令
m_db.Open(NULL,FALSE,FALSE,"ODBC;DSN=Student
Registration;UID=ZYF;PWD=1234");
要从一个数据源中脱离,可调用函数Close()。在脱离后,可以再次调用Open()函数来建立一个新的连接。调用IsOpen()函数可判断当前是否有一个连接,调用GetConnect()可返回当前的连接字符串。需要注意的是CDatabase的析构函数会自动调用Close()函数,所以只要删除了CDatabase对象就可以与数据源脱离。
2、CRecordset类
CRecordset类代表一个记录集。该类是MFC的ODBC类中最重要、功能最强大的类。
在多任务操作系统或网络环境中,多个用户可以共享同一个数据源。共享数据的一个主要问题是如何协调各个用户对数据源的修改。例如,当某一个应用改变了数据源中的记录时,别的连接至该数据源的应用应该如何处理。对于这个问题,基于MFC的ODBC应用程序可以采取几种不同的处理办法,这将由程序采用哪种记录集决定。
记录集主要分为快照(Snapshot) 和动态集(Dynaset)两种,CRecordset类对这两者都支持。这两种记录集的不同表现在它们对别的应用改变数据源记录采取了不同的处理方法。
快照型记录集提供了对数据的静态视。快照是个很形象的术语,就好象对数据源的某些记录照了一张照片一样。当别的用户改变了记录时(包括修改、添加和删除),快照中的记录不受影响,也就是说,快照不反映别的用户对数据源记录的改变。直到调用了CRecordset::Requery重新查询后,快照才会反映变化。对于象产生报告或执行计算这样的不希望中途变动的工作,快照是很有用的。需要指出的是,快照的这种静态特性是相对于别的用户而言的,它会正确反映由本身用户对记录的修改和删除,但对于新添加的记录直到调用Requery后才能反映到快照中。动态集提供了数据的动态视,当别的用户修改或删除了记录集中的记录时,会在动态集中反映出来:当滚动到修改过的记录时对其所作的修改会立即反映到动态集中,当记录被删除时,MFC代码会跳过记录集中的删除部分。对于其它用户添加的记录,直到调用Requery时,才会在动态集中反映出来。本身应用程序对记录的修改、添加和删除会反映在动态集中。当数据必须是动态的时侯,使用动态集是最适合的。例如,在一个火车票联网售票系统中,显然应该用动态集随时反映出共享数据的变化。
在记录集中滚动,需要有一个标志来指明滚动后的位置(当前位置)。ODBC驱动程序会维护一个光标,用来跟踪记录集的当前记录,可以把光标理解成跟踪记录集位置的一种机制。光标库(Cursor Library)是处于ODBC驱动程序管理器和驱动程序之间的动态链接库(ODBCCR32.DLL)。光标库的主要功能是支持快照以及为底层驱动程序提供双向滚动能力,高层次的驱动程序不需要光标库,因为它们是可滚动的。光标库管理快照记录的缓冲区,该缓冲区反映本程序对记录的修改和删除,但不反映其它用户对记录的改变,由此可见,快照实际上相当于当前的光标库缓冲区。
应注意的是,快照是一种静态光标(Static Cursor)。静态光标直到滚动到某个记录才能取得该记录的数据。因此,要保证所有的记录都被快照,可以先滚动到记录集的末尾,然后再滚动到感兴趣的第一个记录上,这样做的缺点是滚动到末尾需要额外的开销,会降低性能。
与快照不同,动态集不用光标库维持的缓冲区来存放记录。实际上,动态集是不使用光标库的,因为光标库会屏蔽掉一些支持动态集的底层驱动程序功能,动态集是一种键集驱动光标(Keyset-Driven Cursor),当打开一个动态集时,驱动程序保存记录集中每个记录的键。只要光标在动态集中滚动,驱动程序就会通过键来从数据源中检取当前记录,从而保证选取的记录与数据源同步。从上面的分析中可以看出,快照和动态集有一个共同的特点,那就是在建立记录集后,记录集中的成员就已经确定了,这就是为什么两种记录集都不能反映别的用户添加记录的原因。
CRecordset类代表一个记录集,用户在操作数据库的时候,一般需要用ClassWizard创建一个CRecordset的派生类,ClassWizard可以为派生的记录集类创建一批数据成员,这些数据成员与记录的各字段相对应,被称为字段数据成员或域数据成员。域数据成员用来保存某条记录的各个字段,它们是程序与记录之间的缓冲区,域数据成员代表当前记录,当在记录集中滚动到某一记录时,框架自动地把记录的各个字段拷贝到记录集对象的域数据成员中。当用户要修改当前记录或增加新记录时,程序先将各字段的新值放入域数据成员中,然后调用相应的CRecordset成员函数把域数据成员设置到数据源中。
不难看出,在记录集与数据源之间有一个数据交换问题,CRecordset类使用"记录域交换"(Record Field Exchange,缩写为RFX)机制自动地在域数据成员和数据源之间交换数据。RFX机制与对话数据交换(DDX)类似,CRecordset的成员函数DoFieldExchange负责数据交换任务,在该函数中调用了一系列RFX函数,当用户用ClassWizard加入域数据成员时,ClassWizard会自动在DoFieldExchange中建立RFX。
记录集的建立实际上主要是一个查询过程,SQL的SELECT语句用来查询数据源,在建立记录集时,CRecordset会根据一些参数构造一个SELECT语句来查询数据源,并用查询的结果创建记录集,明白这一点对理解CRecordset至关重要。
要建立记录集,首先要构造一个CRecordset派生类对象,然后调用Open()成员函数查询数据源中的记录并建立记录集。在Open()函数中,可能会调用GetDefaultConnect()和GetDefaultSQL()函数。具体代码如下:
CRecordset( CDatabase* pDatabase = NULL);
参数pDatabase指向一个CDatabase对象,用来获取数据源。如果pDatabase为NULL,则会在Open()函数中自动构建一个CDatabase对象。如果CDatabase对象还未与数据源连接,那么在Open函数中会建立连接,连接字符串由成员函数GetDefaultConnect()提供。
virtual CString GetDefaultConnect( );
该函数返回缺省的连接字符串,Open()函数在必要的时侯会调用该函数获取连接字符串以建立与数据源的连接,一般需要在CRecordset派生类中覆盖该函数并在新版的函数中提供连接字符串。
virtual BOOL Open( UINT nOpenType = AFX_DB_USE_DEFAULT_TYPE,
LPCTSTR lpszSQL = NULL, DWORD dwOptions = none );
该函数使用指定的SQL语句查询数据源中的记录并按指定的类型和选项建立记录集。参数nOpenType说明了记录集的类型。如果要求的类型驱动程序不支持,则函数将产生一个异常。参数lpszSQL是一个SQL的SELECT语句,或是一个表名。函数用lpszSQL来进行查询,如果该参数为NULL,则函数会调用GetDefaultSQL()获取缺省的SQL语句。参数dwOptions可以是一些选项的组合,具体参见MSDN。若函数调用了CDatabase::Open且返回FALSE,则函数返回FALSE。
virtual CString GetDefaultSQL( );
Open()函数在必要时会调用该函数返回缺省的SQL语句或表名以查询数据源中的记录。一般需要在CRecordset派生类中覆盖该函数并在新版的函数中提供SQL语句或表名。
上面介绍过,在建立记录集时,CRecordset会构造一个SELECT语句来查询数据源。如果在调用Open时只提供了表名,那么SELECT语句还缺少选择列参数rfx-field-list,那么选择列的信息从DoFieldExchange中的RFX语句里提取。
建立记录集后,用户可以随时调用Requery()成员函数来重新查询和建立记录集。Requery()有两个重要用途:一是使记录集能反映用户对数据源的改变;二是按照新的过滤或排序方法查询记录并重新建立记录集。在调用Requery之前,可调用CanRestart()来判断记录集是否支持Requery()操作。要记住Requery只能在成功调用Open()后调用,所以程序应调用IsOpen来判断记录集是否已建立。函数的原型如下:
virtual BOOL Requery( );
返回TRUE表明记录集建立成功,否则返回FALSE。
CRecordset类还有两个公共数据成员m_strFilter和m_strSort用来设置对记录的过滤和排序。在调用Open()或Requery()前,如果在这两个数据成员中指定了过滤或排序,那么Open()和Requery()将按这两个数据成员指定的过滤和排序来查询数据源。成员m_strFilter用于指定过滤器。m_strFilter实际上包含了SQL的WHERE子句的内容,但它不含WHERE关键字。使用m_strFilter的一个例子为:
m_pSet->m_strFilter="CourseID='MATH101'";
//只选择CourseID为MATH101的记录
if(m_pSet->Open(CRecordset::snapshot, "Section"))
成员m_strSort用于指定排序。m_strSort实际上包含了ORDER BY子句的内容,但它不含ORDER BY关键字。m_strSort的一个例子为:
m_pSet->m_strSort="CourseID DESC"; //按CourseID的降序排列记录
m_pSet->Open();
事实上,Open()函数在构造SELECT语句时,会把m_strFilter和m_strSort的内容放入SELECT语句的WHERE和ORDER BY子句中。如果在Open的lpszSQL参数中已包括了WHERE和ORDER BY子句,那么m_strFilter和m_strSort必需为空。
调用无参数成员函数Close()可以关闭记录集。在调用了Close()函数后,程序可以再次调用Open()建立新的记录集。CRecordset的析构函数会调用Close()函数,所以当删除CRecordset对象时记录集也随之关闭。
CRecordset提供了几个成员函数用来在记录集中滚动,如下所示。当用这些函数滚动到一个新记录时,框架会自动地把新记录的内容拷贝到域数据成员中。
void MoveNext( ); //前进一个记录
void MovePrev( ); //后退一个记录
void MoveFirst( ); //滚动到记录集中的第一个记录
void MoveLast( ); //滚动到记录集中的最后一个记录
void SetAbsolutePosition( long nRows );
该函数用于滚动到由参数nRows指定的绝对位置处。若nRows为负数,则从后往前滚动。例如,当nRows为-1时,函数就滚动到记录集的末尾。注意,该函数不会跳过被删除的记录。
virtual void Move( long nRows, WORD wFetchType = SQL_FETCH_RELATIVE );
该函数功能强大。通过将wFetchType参数指定为SQL_FETCH_NEXT、SQL_FETCH_PRIOR、SQL_FETCH_FIRST、SQL_FETCH_LAST和SQL_FETCH_ABSOLUTE,可以完成上面五个函数的功能。若wFetchType为SQL_FETCH_RELATIVE,那么将相对当前记录移动,若nRows为正数,则向前移动,若nRows为负数,则向后移动。
如果在建立记录集时选择了CRecordset::skipDeletedRecords选项,那么除了SetAbsolutePosition外,在滚动记录时将跳过被删除的记录,这一点对象FoxPro这样的数据库十分重要。如果记录集是空的,那么调用上述函数将产生异常。另外,必须保证滚动没有超出记录集的边界。调用IsEOF()和IsBOF()可以进行这方面的检测。