摘要本文在介绍了ODBC(开放性数据库连接,Open DataBase Co nnectivity)运行机制的基础上,着重讨论了VisualC++2.0下利用ODBC API及利用MFC进行ODBC编程的两种方法.关键词ODBC,SQL,数据源,文档/视图结构,DBMS一、ODBC的发展背景在传统的数据库领域,数据库应用程序通常是指在特定的数据库管理系统(DBMS)的支持下,用特定的内嵌式结构化查询语言(SQL)开发的.这样的数据库应用程序存在如下的缺点:(1)它往往需要一个庞大的数据库管理系统的支持,对用户的软、硬件要求高;(2)它通常只能处理一种格式的数据库文件.与传统的数据库应用程序的实现方法相比,Microsoft的开放性数据库连接(ODBC)标准则提供了一种新的途径:它建立了一组规范,并提供了一组高层应用程序调用接口和一套基于动态链接库(DLL)的运行支持环境.用这样一组接口规范开发的应用程序,使用标准的函数和结构化查询语言(SQL)对数据库进行操作,不必关心"数据源"(DataSourc e)来自何种数据库管理系统DBMS,所有的数据库的底层操作都是由相应的ODBC驱动程序(ODBCDriver)完成.只要有了相应的ODBC驱动程序,应用程序处理的对象-数据源就可以非常广泛,既可以是本机的某种数据库格式的文件,如FoxPro的*.dbf文件,也可以是远程数据库文件,如MicrosoftSQLServer等.二、ODBC的运行机制ODBC的体系构架包括五个部分:ODBC管理器(ODBCAdministrator)、应用程序(Application)、ODBC驱动程序管理器(ODBCDriver Mana ger)、ODBC驱动程序(ODBCDriver)和数据源(Data Source).图1表示了ODBC各部分之间的关系.ODBC管理器在整个ODBC运行机制中起配置环境、登录信息的作用,它被安装在Control Pannel里(ODBCINST.CPL).通过该工具,可以用来配置、增添和删除数据源,也可以用来删除、安装ODBC驱动程序.OD BC管理器把数据源和ODBC驱动程序的信息记录在ODBC.INI、ODBCINST.INI和ODBCISAM.INI中,或者登录在系统数据库中,ODBC的其他部件通过读取这些信息,相互作用,应用程序就能够实现对已登录数据库的共享.应用程序(Application)的主要任务是通过调用标准的ODBC函数, 提交SQL语句并返回结果,对结果进行处理.ODBC驱动程序管理器的作用是根据应用程序的要求,调用不同的ODBC驱动程序.ODBC驱动程序的作用是实现ODBC函数调用,对指定的数据源执行SQL语句,并把结果返回给应用程序.有时候,为了符合特定的数据库管理系统的语法,ODBC驱动程序还会对应用程序的要求作适当修改.这里,ODBC驱动程序的作用与运行在Windows下的打印机驱动程序的作用非常相似.数据源,由用户要访问的数据及与之相关的操作系统、数据库管理系统和用于访问数据库管理系统所需的网络平台组成.对应用程序而言,ODBC驱动程序管理器和ODBC驱动程序就像一个实现ODBC函数调用的整体单元,应用程序感觉不到它们之间的分工合作关系.整个ODBC的结构是基于一种独特的动态链接库DLL而存在的,它使得系统完全模块化了.三、利用ODBC API的C语言ODBC编程下面以一个典型ODBC数据库访问程序的伪代码为例,说明用ODBCAPI的C语言进行ODBC编程的一般步骤.该程序从用户接受SQL查询语句,然后获取结果,集中各行各列的数据.HENV henvHDBChdbcHSTMThstmtint nColsSQLALLocEnv(&henv)/*环境申请,获得一个环境句柄*/SQLAllocConnect(henv,&hdbc)/*连接申请,获得数据库连接句柄*/SQLDriverConnect(hdbc,…)/*与具体的ODBC驱动程序和数据源联系*/SQLAllocStmt(hdbc,&hstmt)/*语句申请,获得一个语句句柄*/Input SQLStatement/*从用户接受SQL语句*/SQLExecDirect(hstmt,SQLStatement,…)/*执行SQL语句*/SQLNumResultCols(hstmt,&nCols)/*获取结果集中列数*/for each column{/*获得每列的信息,并与相应的C变量联系起来*/SQLDescribeCol(hstmt,col,ColName,…)/*获取当前列的信息*/ SQLBindCol(hstmt,col,…,variable,…)/*与相应变量联系起来*/}for each row{/*获取结果集中每行每列的数据,并作相应的处理*/SQLFetch(hstmt)/*将当前行的数据存储在SQLBindCol指定的变量中,当前行在结果集中后向下滚动一行*/foreach column{process field data from column variable}}SQLfreeStmt(hstmt,…)/*释放SQL语句句柄*/SQLDisconnect(hdbc)/*断开当前的连接*/SQLFreeConnect(hdbc)/*释放当前数据库连接句柄*/SQLFreeEnv(henv)/*释放环境句柄*/当编写C程序对ODBC数据库进行访问时,必须了解三个非常重要的概念:环境(environment)、连接(connection)、语句(statement).和其他的C语言Windows程序一样,它们都是通过句柄(handle)来访问的.首先需要一个环境句柄,如上面伪代码所示,它建立了应用程序与ODBC系统之间的联系,一个应用程序通常只有一个环境句柄.下一步需建立一个或多个链接,它建立了驱动程序与数据源之间的特定组合关系.如果在函数SQLDriverConnect(hdbc,…,strConnect ,…)中提供了如下的输入字符串,DSN=COMPONEN;FIL=FOXPRO2.6而且ODBC管理器中路径及配置正确的话,该函数就能自动建立起连接来.环境和连接获得后,最后还需要一个SQL语句来实施具体的数据库操作,它可以是如下形式的查询语句:SELECT NAME,AGE,SEX,GRADE FROMSTUDENT WHERE SEX=′F′也可以是如下的更新语句:UPDATE STUDENTSETAGE=′24′WHERE NAME=′JIANG QIN G SONG′四、利用MFC的C++语言ODBC编程VisualC++2.0对ODBCAPI进行了封装,封装后,最重要的数据库MFC类有CDatabase、CRecordset和CRecordView类.尤其是CRecordset类, 它一方面与CDatabase类的对象进行数据交换,另一方面又与CRecordView类进行数据交换,是CDatabase类和CRecordView类之间的桥梁.它们之间的作用关系如图2所示.实际上,CDatabase类的每个对象代表了一个数据源的连接,适宜于对数据源下的某个表格进行整体操作;CRecordset类的每个对象代表了记录的集合,这个集合是对指定数据表格按预定的查询条件获得的,它适宜于对所选的记录集合进行操作;CRecordView类的每个对象是与之相联系CRecordset类对象的外部表现形式,它的作用是把rdset类对象的变化以一些标准Windows控制的形式表现出来,是主要的用户界面.在应用编程时,根据问题的复杂程度和要求,可采用两种不同的方法来实现.如果仅仅是对某指定的数据库进行一些简单的操作,如向数据库中追加、删除、更改一些特定的记录等,并且不需要显示数据库的这些变化的话,就可以采用第一种方法,仅仅利用CDatabase类的相关功能来实现.这种方法只需两步就可完成:1.调用CDatabase类的打开(Open)函数,进行数据源的实际连接.2.调用CDatabase类的成员函数ExecuteSQL,执行一条SQL语句,实现特定的数据库操作.下面InsertRecord函数是实现向数据源compone n的表COMPONEN中插入一行记录,记录的TYPE字段的值为"pump"的最简代码.void InsertRecord(){CDatabase m-db;m-db.Open(NULL,FALSE,FALSE,″ODBC;DSN=componen″);m-db.ExecuteSQL(″insertinto COMPONEN(TYPE)value(′pump ′)″);m-db.Close();然而,在实际编程中,这种情况很少,多数数据库应用程序不仅要实现复杂得多的数据库操作,还需要显示变化了的数据库的情况,这样就需要用到VisualC++的文档/视图结构,需要CDatabase、CRecordset 、CRecordView和CDocument类之间相互配合来实现.下面以VisualC++2.0的AppWizard和ClassWizard产生的一个实际工程test.mak为例,说明这些类是如何相互作用的.工程test.mak的数据源是componen的COMPONEN表,该表的结构如表1所示.test.exe要实现的功能是按默认的条件查询COMPONEN表,滚动记录集,并在屏幕上显示当前记录集中的当前记录.有关的代码如下所示:表1COMPONEN的结构class CTestSet:public Crecordset{public:CTestSet(CDatabase*pDatabase=NULL);DECLARE-DYNAMIC(CTestSet)//{{AFX-FIELD(CTestSet,CRecordset)CStringm-NAME;CString m-CATALOGID;CStringm-SIZE;CStringm-ORDERID;CString m-PRODUCER;//}}AFX-FIELD//{{AFX-VIRTUAL(CTestSet)public:virtual CStringGetDefaultConnect();//Default connection stringvirtual CString GetDefault//default SQLfor Recordsetvirtual void DoFieldExchange(CFieldExchange*pFX);//RFX support//}}AFX-VIRTUAL……};CStringCTestSet::GetDefaultConnect(){return-T(″ODBC;DSN=componen;″);}CStringCTestSet::GetDefaultSQL(){return-T(″COMPONEN″);}void CTestSet::DoFieldExchange(CFieldExchange*pFX){//{{AFX-FIELD-MAP(CTestSet)pFX->SetFieldType(CFieldExchange::outputColumn);RFX-Text(pFX,″NAME″,m-NAME);RFX-Text(pFX,″CATALOGID″,m-CATALOGID);RFX-Text(pFX,″SIZE″,m-SIZE);RFX-Text(pFX,″ORDERID″,m-ORDERID);RFX-Text(pFX,″PRODUCER″,m-PRODUCER);/*以下是参数连接部分,m-nParams必须先被赋值为2.这段代码仅仅是为了说明如何参数化查询条件而设置,test.mak中没有这几行代码.pFX->SetFieldType(CFieldExchange::param);RFX-Text(pFX,″NAMEParam″,m-strNAMEParam,20);RFX-Text(pFX,″CATALOGIDParam″,m-strCATALOGIDParam,10);*///}}AFX-FIELD-MAP}class CTestDoc:public Cdocument{……public:CTestSet m-testSet;public:……};class CTestView:public CRecordView{……public://{{AFX-DATA(CTestView)enum{IDD=IDD-TEST-FORM};CTestSet*m-pSet;//}}AFX-DATA//Attributespublic:CTestDoc*GetDocument();……public://{{AFX-VIRTUAL(CTestView)public:virtual CRecordset*OnGetRecordset();protected:virtual void DoDataExchange(CDataExchange*pDX(;//DDX/DDVsupportvirtual void OnInitialUpdate();//called firsttime after construct//}}AFX-VIRTUAL};void CTestView::DoDataExchange(CDataExchange*pDX){CRecordView::DoDataExchange(pDX);//{{AFX-DATA-MAP(CTestView)DDX-FieldText(pDX,IDC-EDIT-CATALOGID,m-pSet->m-CATALOGID,m-pSet);DDX-FieldText(pDX,IDC-EDIT-NAME,m-pSet->m-NAME,m-pSet);DDX-FieldText(pDX,IDC-EDIT-ORDERID,m-pSet->m-ORDERID,m-pSet);DDX-FieldText(pDX,IDC-EDIT-PRODUCER,m-pSet->m-PRODUCER,m-pSet);DDX-FieldText(pDX,IDC-EDIT-SIZE,m-pSet->m-SIZE,m-pSet);//}}AFX-DATA-MAP}CRecordset*CTestView::OnGetRecordset(){return m-pSet;}CTestDoc*CTestView::GetDocument(){ASSERT(m-pDocument->IsKindOf(RUNTIME-CLASS(CTestDoc)));return(CTestDoc*)m-pDocument;}void CTestView::OnInitialUpdate(){m-pSet=&GetDocument()->m-testSet;CRecordView::OnInitialUpdate();}分析以上代码要从CTestView类开始.依照VC标准的文档/视图结构的相互作用关系,记录视图类CRecordView的导出类CTestView,在显示前要调用它的成员函数OnInitialUpdate(),弄清函数CTestView::O nInitialUpdate()在这里的行为是理解整个代码的关键.下面是CTest View::OnInitialUpdate()函数调用关系图:CTestView::OnInitialUpdate()CRecordView::OnInitialUpdate()//调用基类的OnInitialUpdate()函数CTestView::OnGetRecordset()//得到一个CRecordset类的指针CRecordset::Open()//利用前面得到的指针访问Open函数CTestSet::GetDefaultConnect()//得到默认连接字符串CDatabase::Open()//利用默认连接字符串与数据源连接CTestSet::GetDefaultSQL()//得到默认SQL语言,执行它CFormView::OnInitialUpdate()//调用CForm View::OnInitial UpdateCTestView::DoDataExchange()//与Windows编辑控制联系起来通过以上分析,我们知道,表面上只有CRecordset、CRecordView和CDocument类的导出类之间相互作用,实际上,由于CRecordset::Ope n()函数要构造一个CDatabase类指针,并通过默认连接字符串与特定的数据源连接,仍然是四个类之间相互作用的,只不过是VC的标准框架将这种关系封装起来罢了.补充说明几点:·对当前记录集中当前记录的滚动功能,应用程序中不需要任何代码支持,VC的标准框架默认实现了.·CRecordset::Open()是如何构造一个完整的SQL查询语句、如何理解CRecordset类工作机制的关键.一个完整的SQL查询语句具有下面的普遍形式:SELECT[ALL|DISTINCT]columnlist FROMtablelist[WHEREsearchcondition][ORDER BYcolumnlist[ASC|DESC]][]内的内容表示是可选的,|表示"或者".CRecordset::Open()构造这样的SQL查询语句的伪代码如下所示:CRecordset::Open(…,lpszSQL…){IF(lpszSQL!=NULL)strSQL=lpszSQLELSEstrSQL=CRecordSet::GetDefaultSQL()IF(strSQL不以"SELECT"或"CALL}"开头){strSQL="SELECTcolumnlist FROMtablelist"其中,columnlist与CRecordSet::DoFieldExchange()中列名保持数量和顺序上的一致;tablelist是前面strSQL的值}IF(m-strFilter!=NULL)strSQL+="WHERE"+m-strFilterIF(m-strSort!=NULL)strSQL+="ORDER BY"+m-strSort}其中,strSQL是保存SQL语句的变量、m-strFilter、m-strSort是CRecordSet类的公有成员变量.CRecordSet类对象中包含的数据就是符合strSQL中查询条件的记录的集合.·程序可以动态改变SQL语句中的查询条件,即参数化WHERE字句和ORDERBY字句.在变量m-strFilter、m-strSort中将要参数化的部分以"?"代替.在调用打开函数CRecordSet::Open()前(如在构造函数中) ,将参数变量的个数赋值给变量m-nParams,并在CRecordSet::DoField Exchange()中按参数出现的先后顺序定义好,如CTestSet::DoFieldEx change()代码中注释部分所示.五、结束语从以上讨论中可知,ODBC应用程序与传统的数据库应用程序相比, 确实有自己独特的优点,它不仅编程接口十分简单方便,更主要的是它的运行机制非常适合与应用程序的其他功能完美地融合.笔者最近利用了ODBC技术,通过ODBC系统读取FoxPro2.6数据库文件中的记录,能够实现液压原理图中元件明细表的自动填充功能,非常方便快捷.。