当前位置: 首页 > news >正文

《WINDOWS 环境下32位汇编语言程序设计》第18章 ODBC数据库编程

早在1970年,关系型数据库的理论就已经被提出。在计算机软件的发展历史上,数据库的发展和操作系统的发展几乎是同步的。在MS-DOS操作系统出现后的几年内,Oracle公司就已经推出了第一个通用的数据库产品。世界十大软件厂商的排名中一直有数据库厂商的身影,在20世纪80年代,生产dBase III数据库的Asnton-Tale公司是全球第三大独立软件公司。现在,生产Oracle数据库的Oracle公司是仅次于微软公司的全球第二大独立软件公司。

虽然汇编程序员平时的工作很少和数据库打交道,但要知道如果能够在汇编语言中访问各种数据库,那么就能显著地增加汇编语言的使用范围。本章将介绍如何使用ODBC接口访问各种类型的数据库,并具体分析访问Oracle、SQL Server等主流数据库时的差异和注意事项。

18.1 基础知识

18.1.1 数据库接口的发展历史

在DOS时代,PC机上流行的数据库是dBase数据库。现在,市场上流行的数据库产品种类繁多,大型的数据库产品有Oracle,DB2,Sybase,Informix和SQL Server等,小型的数据库产品有Access,MySQL,Foxpro和Paradox等。

早期的数据库并没有提供开发接口,比如,dBase数据库的开发只能使用数据库自己的IDE环境和脚本语言,开发完毕后,在dBase软件中对脚本进行解释执行。要在C语言或者汇编语言中使用dBase数据库的话,只能绕过dBase软件直接对DBF文件进行读写,这样不但要对文件进行解码编码,也因此放弃了数据库软件带来的数据查询、管理、统计等各种功能。

后期的数据库则提供了一些编程接口,其中的一种是使用预编译器的SQL语句嵌入模式,比如,Oracle数据库的Pro*C和SQL Server的ESQL。以Pro*C为例,如果在C代码中访问Oracle数据库,访问数据库的语句可以用伪码书写,Pro*C预编译器会将这部分语句翻译成对Oracle接口DLL的调用语句,然后与源代码中其余的标准C代码一并存成*.c文件后,再交给C编译器处理。另一种是API接口,如Oracle的OCI接口,API类型的编程接口比预编译方式要方便得多。这些由数据库产品提供的编程接口并没有统一的标准,不同数据库的接口截然不同,带来的后果就是程序的通用性很差,一旦需要换一种数据库,原有的代码就完全无法使用。

为了能用同样的方法访问不同的数据库,一些软件厂商提出通用的数据库接口标准,如微软的ODBC、DAO、RDO、OLE DB和ADO接口,还有Borland公司的BDE接口等,这些接口对各类数据库的驱动程序进行了封装,应用程序只需通过标准的语法访问接口程序,接口程序会根据情况调用不同的驱动程序来访问相关的数据库。这样从应用程序的角度看,访问各种数据库(包括符合规范的未知数据库)的方法是一样的。

读者肯定或多或少听说过其中的几种接口,那么这么多的数据库接口有什么不同呢?又是哪种接口最适合于汇编语言使用呢?接下来我们来看看这两个问题的答案。

这要从数据库访问技术的演变过程谈起。当市场上出现众多的数据库产品之后,Borland和微软都制定了数据库访问的规范,其中微软推出了ODBC(Open DataBase Connectivity)规范,而Borland公司推出了BDE(Borland Database Engine)规范。在推出的初期,BDE的性能比ODBC要好,但随着微软对ODBC的改进,以及对操作系统的垄断,ODBC标准最终占据了多数市场。到现在,BDE只是Borland产品线上的数据库访问标准,只有C++ Builder和Delphi等开发工具在一直使用BDE接口。

微软的ODBC接口是作为一组API函数提供的,应用程序通过API访问ODBC接口,ODBC接口再通过相关数据库的驱动程序访问数据库。当需要访问一种新的数据库时,只需使用新的数据库驱动程序替代旧的驱动程序,应用程序即可照常运行,而无须修改代码。

由于ODBC使用比较底层的API接口,凡是可以调用API的语言都可以使用ODBC接口,这样,C、VC++ 和Win32汇编等语言都能方便地使用ODBC接口,但是在VB、ASP中的VBScript等比较高级的语言中使用API却是非常麻烦的事情,所以微软设计了DAO接口来供这些语言访问数据库。

由于DAO(Data Access Objects)是面向对象的接口,所以它可以很方便地在VB中使用,但DAO在内部通过Jet引擎来访问数据库,Jet引擎本来是专门为访问Access数据库设计的。虽然用它访问Access数据库时很快也很有效,但访问非Access数据库时(如SQL Server和Oracle),Jet引擎将通过ODBC访问数据库(过程如图18.1所示)。所以,访问除了Access之外的数据库时,DAO的速度比较慢。

                                                      图18.1 DAO接口的架构过程图

为了克服这种缺陷,微软新设计了RDO(Remote Data Objects)接口,与DAO类似,RDO也是一个面向对象的接口,但RDO直接建立于ODBC之上而不是Jet引擎之上,不管访问何种类型的数据库,它都直接和ODBC接口对话,相当于在图18.1中去掉了Jet引擎这一层,从而使性能得到了提高。

ODBC (Open Database Connectivity)开放数据库互连

ODBC、DAO和RDO接口只能用来访问关系型数据库,但是应用程序也经常需要访问非关系型的数据源,如Microsoft Exchange Server中的邮件、文本和图形、目录服务数据,甚至自定义的数据对象等。为此,微软设计了OLE DB接口。

OLE DB接口也建立于ODBC之上,这个接口对关系型的数据库和非关系型的数据源提供了一致的访问。当访问关系型的数据库时,它仍然调用ODBC接口,对于非关系型的数据源,它使用与数据源相对应的驱动程序。这些驱动程序被称为OLE DB Provider。

与ODBC、DAO和RDO接口相比,OLE DB解决了非关系型数据的访问方式,使访问的数据类型得到了很大的扩展,但从编程的角度看,OLE DB使用的是COM接口而不是API接口,COM接口是一种二进制接口,调用的过程中大量地使用了指针变量,这对C和VC++并不成问题,但由于VB和VBScript等高级语言不提供指针数据类型,所以无法直接调用OLE DB。

所以,在此基础上微软又推出了另一个数据访问对象模型:ADO(ActiveX Data Objects),由于ADO接口也是面向对象的,所以可以被VB使用。如图18.2所示,ADO接口是以OLE DB为基础而封装的,它为VB等高级语言提供了OLE DB的所有功能。ADO接口和OLE DB接口的关系就类似于RDO接口和ODBC接口的关系。

                                                           图18.2 ADO接口的架构图

看到这里,读者一定明白了第一个问题的答案。这些接口的差别在于层次、功能和接口封装方式的不同。具体使用哪种接口要看需要访问的数据类型,以及使用的语言能支持哪种接口封装方式,如VB程序访问关系型数据库时,可以使用ADO、DAO或者RDO接口,但是要访问非关系型的数据时,就只能使用ADO接口了。而VC++程序既可以使用ADO接口,也可以使用OLE DB或者最底层的ODBC接口。

对于第二个问题,由于汇编语言是一种比较底层的语言,仅提供了对API的访问,虽然通过编程也能访问COM接口和ActiveX对象,但这属于舍近求远的使用方法,所以在Win32汇编中可选的是使用ODBC或者BDE接口。但是BDE不是Windows操作系统的内置组件,要想在某台计算机上运行使用了BDE接口的应用程序,就必须首先安装BDE环境,整个BDE环境的文件有10MB左右,所以从软件的兼容性和易用性考虑,使用BDE并不是一个明智的选择。ODBC则是Windows 98及以上版本的标准组件,所以本书选择了ODBC作为数据库编程的接口(Win95系统需单独安装ODBC组件)。

现在再来详细看看ODBC接口的组成。ODBC接口的架构如图18.3所示,当应用程序访问某种类型的数据库时,首先将相关的信息告诉ODBC管理器,这些信息包括使用的驱动程序名称,需要访问的数据库名称,用户名和密码等。ODBC管理器根据这些信息选择合适的驱动程序连接到指定的数据库后,就可以使用SQL语句进行数据库操作了。

                                                              图18.3 ODBC接口的架构图

Windows操作系统中附带了大量常见数据库的ODBC驱动程序,如SQL Server,Access,Excel,Foxpro,Paradox,以及dBase数据库的驱动程序等,由于Oracle、Sybase和DB2等大型数据库相对比较复杂,所以这些数据库的驱动程序并没有随操作系统提供,要访问这些数据库的话,必须首先在系统中安装这些数据库的客户端软件,以及对应的ODBC驱动程序后才能使用ODBC接口访问数据库。

因为本书的例子中访问的是Access数据库,所以读者在开始编程前并不需要安装额外的驱动程序,但如果读者要开发访问Oracle数据库的应用程序时,就必须首先安装Oracle客户端和ODBC驱动程序后才能进行程序的调试,开发出来的应用程序拷贝到其他计算机上运行时,该计算机上也必须安装Oracle客户端和ODBC驱动程序。

18.1.2 SQL语言

1.什么是SQL语言

结构化查询语言(SQL /Structured Query Language)是数据库系统的通用语言,利用它,用户可以用几乎同样的语句在不同的数据库系统中执行同样的操作。例如,不管是在Oracle、SQL Server,还是Foxpro数据库中,如果需要从一张表中获取所有数据,使用的都是“select * from表名”这样的语句。如果没有各数据库通用的SQL语言,实现ODBC这样的标准数据库接口是难以想像的。

SQL是一个ISO官方标准,到现在为止,SQL总共出了三代标准,除了最早的SQL/86标准之外,1992年发布的标准被称为SQL/2或者SQL/92标准,最新的标准是1999年制定的SQL/3标准,也被称为SQL/99标准。

在SQL标准中,操作数据库的语句按照用途被分为下面四个大类:

● 数据查询语言(DQL/Data query language)——这部分只包含用于查询的select语句。

● 数据操作语言(DML/Data manipulation language)——这部分包含对数据进行维护的语句,如增加数据的insert语句、删除数据的delete语句和修改数据的update语句。

● 数据定义语言(DDL/Data definition language)——这部分包含对表结构、索引等各种对象进行维护的语句,如创建各种对象时用的create语句、修改对象的alter语句和删除对象的drop语句等。

● 数据控制语言(DCL/Data control language)——这部分包含和权限控制、事务控制等相关的语句。

SQL标准是数据库语言中的官方语言,各种数据库均实现了SQL标准中的大部分语句,但是由于各种数据库的功能毕竟有很大的不同,所以除了“官方语言”之外,各种数据库中又存在少量的“方言”,这些方言反映了数据库对SQL标准的扩展,也反映了对SQL标准没有涉及的部分的处理方式。比如,同样是取数据库的日期,SQL Server和Sybase中使用的是getdate()函数,Oracle数据库中使用的是sysdate变量,而Access中使用的却是now()函数。再比如,Oracle 9i数据库的DML语句中比SQL/99标准多了一个功能强大的merge语句。

要对某种数据库进行编程的时候,读者不仅要对标准的SQL语句有所了解,而且对数据库中特定的语法也要有所了解,只有同时掌握了“官方语言”和“方言”,才能得心应手地发挥数据库的所有功能。当熟悉了某种数据库的编程后,一旦需要换一种数据库,那么只需对“方言”部分再进行熟悉就可以了。

本书已经假定读者对SQL语言有了相当的了解,所以不再对SQL语言的细节做介绍。如果读者对SQL语言的语法还不是很熟悉,请首先进行相关的学习。

2.在ODBC中使用SQL语言

看到这里,读者脑海里一定有个问题,那就是:前面不是刚说过要用Win32汇编语言通过ODBC接口访问数据库吗,怎么后面又说操作数据库用的是SQL语言呢?

其实两者并不矛盾,操作数据库的确是用SQL语言,而汇编语言只不过是将SQL语句以字符串的方式传递给数据库进行处理而已,传递时用的“通道”就是ODBC接口。其实,无论是用ADO接口还是ODBC接口,应用程序向数据库提交的命令都是一个SQL语句字符串,接口在这里起的都是应用程序和数据库之间的桥梁作用。

由于应用程序只管向ODBC接口发送SQL语句字符串,这个字符串是用“官方语言”写的还是以“方言”写的就没必要关心了,只要数据库能“听”懂就行,所以SQL“方言”并不影响程序调用ODBC接口的方式,举例来说,要从Oracle数据库中获取时间,程序要向数据库发送“select sysdate from dual”字符串;而当数据库换成Sybase的时候,程序的汇编源代码并不需要有什么改变,只需将字符串改为“select getdate()”即可。

在ODBC程序的调试中,读者经常感到困惑的是出错原因定位的问题。比如,执行了一个查询语句后,却没有得到预期的数据,这时往往搞不清楚究竟是SQL语句写错了还是汇编代码写错了。知道了ODBC接口的作用后,这个问题就很好解决,那就是调试的时候必须首先保证SQL语句的正确性。

各种数据库都提供了附带的开发工具,如Oracle数据库中有sqlplus,SQL Server中有“查询分析器”,在这些工具中可以输入SQL语句并进行执行,当需要在程序中使用某个SQL语句时,建议读者首先在这些开发工具中验证一下SQL语句的正确性,将SQL语句在开发工具中调试完毕后,再原封不动地拷贝到汇编源代码中定义成字符串,这样就可以专心调试汇编代码的错误。

另外,有些SQL语句在调试的时候没有发现错误,但随着程序的使用,在某些情况下却会出错。比如说,表中某个字段的长度定义为10个字节,大部分情况下执行insert语句都是成功的,但某些情况下出错却并不一定是汇编代码的错误,有可能是用户输入了超过10个字节的数据,造成insert语句因为字段内容太长而失败;也有可能是表中定义了唯一索引,插入的数据因为重复而失败。这时首先要做的事情是把执行失败的语句显示出来并重新拷贝到数据库的开发工具中去执行看看,是不是汇编代码的错误就能立即见分晓。

18.1.3 ODBC程序的流程

如图18.4所示,使用ODBC接口访问数据库的操作总共分为6个步骤,它们分别是连接到数据库、初始化语句句柄、执行SQL语句、处理语句执行结果、事务控制,以及断开连接。根据具体的应用,部分步骤可以多次运行(如步骤3和步骤4),而部分步骤可能被省略(如步骤5)。在整个过程中需要用到近20个函数,这些函数的用法将在下面的内容中逐一介绍。

                                                      图18.4 使用ODBC接口的流程

图中的步骤1和6将在18.2节中介绍,步骤3到步骤5将在18.3节中介绍,最后在18.4节中有一个综合的例子,用来详细演示整个流程。

但是,本章介绍的内容仅是ODBC接口的基本功能,其他例如多记录集查询、利用光标修改记录、高性能光标等内容都没有涉及,由于篇幅所限,也没有列出所介绍函数的所有参数或返回代码的说明。如果读者有兴趣继续深入,请参考MSDN或者Microsoft Press出版的《Microsoft ODBC 3.0 Programmer's Reference》。

18.2 连接数据库

18.2.1 连接和断开数据库

1.分配环境句柄和连接句柄

ODBC接口标准的最早版本是1994年提出的(称为ODBC 1.0版),到现在已经经过了多次的升级,最新的版本是ODBC 3.0,后期的版本比前期版本增加了一些新的函数。为了程序的兼容性,程序的开始部分需要首先指定使用哪个版本,以便ODBC接口决定支持的函数集合,这部分的工作即是环境的初始化工作。

环境的初始化工作只需进行一次,如图18.4的步骤1所示,这部分工作包括分配环境句柄、分配连接句柄,以及对这些句柄进行一些适当的属性设置。

在ODBC 3.0版本之前,分配环境句柄、连接句柄和语句句柄的函数是独立的,它们分别是SQLAllocEnv、SQLAllocConnect和SQLAllocStmt。但在ODBC 3.0版本中,这些函数的功能全部由SQLAllocHandle函数来实现,该函数的原型如下:

invoke SQLAllocHandle,dwHandleType,dwInputHandle,lpOutputHandle

第一个参数dwHandleType用于指定需要分配的句柄类型,取值可以是下面的常量之一:

● SQL_HANDLE_ENV——分配环境句柄(Environment handle)

● SQL_HANDLE_DBC——分配连接句柄(Connection handle)

● SQL_HANDLE_STMT——分配语句句柄(Statement handle)

● SQL_HANDLE_DESC——分配描述符句柄(Descriptor handle)

第二个参数dwInputHandle指定要分配句柄的“父”句柄,分配语句句柄和描述符句柄时,父句柄必须是连接句柄;要分配连接句柄的话,父句柄必须是环境句柄;而环境句柄是最高层次的句柄,所以分配环境句柄时该参数指定为SQL_HANDLE_NULL。第三个参数lpOutputHandle是一个指针,指向一个双字变量,如果句柄分配成功的话,函数会将句柄返回到这个双字中。

句柄分配成功后,需要对句柄的各种属性进行适当的设置,对环境句柄、连接句柄和语句句柄的属性进行设置的函数是不同的,它们分别是SQLSetEnvAttr、SQLSetConnectAttr和SQLSetStmtAttr函数,这三个函数的用法几乎一模一样

invoke  SQLSet???Attr,hHandle,dwAttribute,ValuePtr,StringLength

函数名中的???代表Env、Connect或者Stmt,函数的第一个参数hHandle是需要设置的句柄。第二个参数dwAttribute是一个常数,指定需要设置的属性类型,读者可以从MSDN中查看各种句柄可以设置的具体属性列表。

第三个参数ValuePtr和第四个参数StringLength的取值取决于属性类型,如果某个类型属性的取值是一个32位常数,那么ValuePtr就直接表示为该常数,而StringLength参数被忽略;如果属性的取值要用一个字符串或者一串二进制数据来表示,那么ValuePtr就被解释为指向字符串或二进制值的指针,这时StringLength参数表示字符串或者二进制数据的长度。

对于环境句柄,必须设置的属性值是将要使用的ODBC接口的版本号,这时的属性类型是SQL_ATTR_ODBC_VERSION,要使用3.0版本接口的话,ValuePtr取值为SQL_OV_ODBC3。其他可选的属性值有用于连接池设置的SQL_ATTR_CONNECTION_POOLING和SQL_ATTR_CP_MATCH属性,具体的用法读者可以参考MSDN。

对于连接句柄,有些属性必须在发起连接前进行设置,如SQL_ATTR_LOGIN_TIMEOUT(连接的超时时间),有些则必须在连接成功后进行设置,如SQL_ATTR_TRANSLATE_LIB和SQL_ATTR_TRANSLATE_OPTION(用于字符集转换)等,而大部分属性则可以在任何时刻进行设置,如SQL_ATTR_ACCESS_MODE(连接的方式是只读还是读写)、SQL_ATTR_AUTOCOMMIT(事务的提交方式)等。在大多数情况下,我们没必要对连接句柄的属性进行设置,让其保持默认值即可。

环境句柄、连接句柄的分配和属性设置的代码举例如下,在完成这些步骤后,就可以用连接句柄来发起连接了:

.data?
hEnv    dd ?      ; 环境句柄
hConn   dd ?      ;连接句柄
.codeinvoke SQLAllocHandle,SQL_HANDLE_ENV,SQL_NULL_HANDLE,addr hEnv.if ax != SQL_SUCCESS && ax != SQL_SUCCESS_WITH_INFOjmp _Error.endifinvoke SQLSetEnvAttr,hEnv,SQL_ATTR_ODBC_VERSION,SQL_OV_ODBC3,0.if ax != SQL_SUCCESS && ax != SQL_SUCCESS_WITH_INFOjmp _Error.endifinvoke SQLAllocHandle,SQL_HANDLE_DBC,hEnv,addr hConn.if ax != SQL_SUCCESS && ax != SQL_SUCCESS_WITH_INFOjmp      _Error.endif...;连接数据库,并执行SQL语句..._Error:出错处理

读者可以注意到,在上面的代码中,返回值的判断方法有些特别。其实代码中的方法正是绝大多数ODBC函数的返回值检测方法,这里有两点非常重要。

首先,所有ODBC函数返回值的类型是SQLSMALLINT,也就是汇编中的word类型,所以判断ODBC函数是否执行成功,就要对ax而不是eax进行比较判断。

第二,ODBC函数执行错误的话,返回的错误代码可能有很多种,但是表示成功的代码却有两种,其中SQL_SUCCESS表示函数执行成功,SQL_SUCCESS_WITH_INFO表示函数执行成功但带有非致命的错误。返回这两种代码后,程序都应该按照执行成功进行处理。

无论函数的调用是成功还是失败,我们都可以通过调用SQLGetDiagRec或SQLGetDiagField函数来获得更多的信息,它们的用法和Win32 API中的GetLastError很相似。

2.连接到数据库

一旦分配了环境句柄和连接句柄,并进行了适当的属性设置后,就可以调用SQLConnect或SQLDriverConnect函数来连接到数据库了。

要连接到数据库,就需要将与连接相关的信息告诉ODBC接口,如使用的驱动程序名称、数据库名称、登录数据库的用户名、密码,以及其他一些信息。有两种方法可以指定这些信息,那就是通过DSN或者在函数中直接指定这些信息。

DSN(Data Source Name/数据源)其实就是上述信息的集合,比如,一个连接到Access数据库的数据源内容可能是这样的:

[ODBC]
DRIVER=Microsoft Access Driver (*.mdb)
UID=myusername
PWD=mypassword
DefaultDir=C:\Test
DBQ=C:\Test\Test.mdb

将这些信息保存并命名后,就是一个数据源。根据命名方式和保存位置的不同,可以将数据源分为三种:用户DSN、系统DSN和文件DSN。

因为用户DSN保存在注册表的HKEY_CURRENT_USER\Software\ODBC\ODBC.INI主键下,所以它只对当前登录用户可见,换一个用户登录到系统后,就没法看到这些DSN了(读者一定还记得第15章中介绍过HKEY_CURRENT_USER根键是根据不同用户映射到不同内容中去的);而系统DSN则保存在注册表的HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBC.INI主键下,所以系统中所有的用户都可以看到它,包括NT中的服务都可以使用系统DSN;文件DSN则将信息保存在一个单独的文件中,能访问到该文件,就可以使用文件中的信息。

DSN可以在“控制面板”的“数据源(ODBC)”栏目中创建,打开该栏目后,会弹出如图18.5所示的对话框,读者可以看到,对话框中有用户DSN、系统DSN、文件DSN等表单,每个表单中都有“添加”、“删除”和“配置”按钮。在对应的表单中单击“添加”就可以开始创建对应类型的DSN。

                                                    图18.5 “控制面板”的“数据源”栏目

创建DSN的过程如图18.6所示,单击“添加”按钮后,系统首先弹出图中1所示的对话框,让用户选择使用的驱动程序名称,选择完毕后,再由驱动程序弹出一个用于指定连接参数的对话框,由于各种数据库的连接参数命名方法各不相同,所以这时弹出的对话框也是各不相同的,如图中的2是选择Oracle数据库时的对话框,图中的3是指定了SQL Server数据库时的对话框,而图中的4是指定了Access数据库时的对话框。在对话框中输入所需的信息(包括DSN的名称、数据库名、用户名和密码等)后再存盘,DSN即创建完毕。

                                                           图18.6 创建DSN的过程

假如创建的是用户DSN或者系统DSN,那么现在我们就可以用SQLConnect函数来连接到数据库了(要用文件DSN的话,必须使用SQLDriverConnect函数)。SQLConnect函数的用法非常简单:

invoke SQLConnect,hConn,lpDSN,dwDSNLength,\lpUserName,dwNameLength,lpPassword,dwPasswordLength

其中hConn参数是在前面的例子中申请的连接句柄,lpDSN指向一个包含DSN名称的字符串,dwDSNLength指定为字符串的长度,比如,DSN的名称是“Test”时,字符串就定义为“Test”,dwDSNLength参数就是4。

在创建DSN的时候,用户可以直接在对话框中指定连接数据库使用的用户名和密码,那么后面的四个参数可以全部指定为NULL。有时为了安全起见,在创建DSN的时候可以不指定用户名和密码,这样就要在调用SQLConnect函数时在参数中指定这些信息,这时lpUserName和lpPassword参数指向用户名和密码字符串,dwNameLength和dwPasswordLength则是两个字符串的长度(当然数据库可以匿名连接的时候,即使DSN中没有保存用户名和密码,在调用函数的时候这四个参数也可以指定为NULL)。

SQLConnect函数的最大缺点在于连接一个数据库之前必须创建它的DSN。假如我们开发了一个应用程序并分发给用户后,却要求每个用户在使用前必须在自己的计算机上创建指定的数据源,那将是非常扫兴的事情。所以,我们更多使用SQLDriverConnect函数来连接到数据库。

SQLDriverConnect函数的用法相对复杂一点,却可以通过参数直接指定所有和连接相关的信息,所以在运行前不必先创建DSN。该函数的用法如下:

invoke SQLDriverConnect,hConn,hWnd,lpConnString,dwLength,lpOutConnString,\dwOutBufferSize,lpOutConnStringLength,dwDriverCompletion

hConn是连接句柄。hWnd是应用程序窗口的句柄,因为函数可能会弹出一个模态对话框来要求用户输入一些信息,该对话框会以hWnd作为父窗口句柄,如果这个参数被置为NULL,那么对话框将没有父窗口句柄,用户就可能在关闭对话框之前切换到主窗口中去。

lpConnString是指向连接字符串的指针,字符串中包含了连接到数据库的所有信息,如驱动程序名称,用户名和密码等。当连接到不同的数据库时,连接字符串的格式是不同的,但是不要着急,下面的18.2.2节中马上会详细介绍连接字符串的写法,dwLength参数则用来指定连接字符串的长度(长度可以不包括字符串结尾的0字符)。

lpOutConnString指向一个缓冲区,如果函数执行成功的话,将在缓冲区中返回一个完整的连接字符串。这听起来使人困惑,连接字符串不是已经在前面的参数中提供了吗?事实上,我们提供的连接字符串可能会不完整,这时,ODBC驱动程序会提示用户输入更多信息,并根据所有已知的信息,以及默认的信息创建一个完整的连接字符串并将其放入缓冲区。即使我们提供的连接字符串已经可以工作了,这个缓冲区中也会返回更详细的字符串。dwOutBufferSize参数则用来指定缓冲区的长度。

lpOutConnStringLength是一个指针,指向一个双字变量,函数在缓冲区中返回完整的连接字符串后,在这个双字变量中返回字符串的长度。

最后一个参数dwDriverCompletion用来指示ODBC驱动程序是否提示用户输入更多信息。它可以是以下取值之一:

● SQL_DRIVER_PROMPT——ODBC驱动程序将弹出一个对话框提示用户输入信息。驱动程序将利用这些信息来创建连接字符串。

● SQL_DRIVER_COMPLETE或SQL_DRIVER_COMPLETE_REQUIRED——仅当用户提供的连接字符串不完全时,ODBC驱动程序才会弹出对话框来提示用户输入缺少的信息。

● SQL_DRIVER_NOPROMPT——不管信息是否足够,ODBC驱动程序都不会提示用户输入信息。

用SQLDriverConnect函数连接到数据库的例子如下:

.const
strConn  db "DRIVER=Microsoft Access Driver(*.mdb);DBQ=c:\test.mdb",0
.data?
szBuffer    db 1024 dup(?)
dwLength    dd ?
.code.....   ;前面例子中分配环境句柄和连接句柄的语句invoke SQLDriverConnect,hConn,hWnd,addr strConn,\sizeof strConn,addr szBuffer,sizeof szBuffer,\addr dwLength,SQL_DRIVER_COMPLETE.if ax == SQL_SUCCESS || ax == SQL_SUCCESS_WITH_INFO; 连接成功.else; 连接失败.endif

不管是使用SQLConnect函数还是SQLDriverConnect函数,如果函数返回SQL_SUCCESS或者SQL_SUCCESS_WITH_INFO,那就表示已经成功连接到数据库,接下来就可以执行SQL语句了。

3.断开和数据库的连接

在成功连接到数据库后,我们就可以进行查询及其他操作了,这些将在18.3节中具体讨论,现在假设我们已完成了这些操作,那就需要断开数据库的连接并释放各种资源。

断开数据库使用SQLDisconnect函数,这个函数只需要一个参数:连接句柄:

invoke   SQLDisconnect, hConn

在断开连接后,还需要将连接句柄和环境句柄释放,这两个操作可以用SQLFreeHandle函数来完成。这个函数是ODBC 3.0版本提供的函数,在这以前,释放不同的句柄要分别由SQLFreeConnect、SQLFreeEnv及SQLFreeStmt函数来完成。SQLFreeHandle函数的用法如下:

invoke SQLFreeHandle,dwHandleType,hHandle

dwHandleType是要释放的句柄类型,取值和申请时使用的值相同,也就是SQL_HANDLE_DBC、SQL_HANDLE_ENV或SQL_HANDLE_STMT。hHandle参数就是要释放的句柄。一般来说,扫尾工作的代码如下所示:

invoke   SQLDisconnect,hConn
invoke   SQLFreeHandle,SQL_HANDLE_DBC,hConn
invoke   SQLFreeHandle,SQL_HANDLE_ENV,hEnv

请注意上面代码的先后顺序。在释放环境句柄前,所有该句柄上的连接句柄必须首先被释放,否则释放环境句柄的SQLFreeHandle函数会返回SQL_ERROR。同样,在释放连接句柄前,必须首先用SQLDisconnect函数将连接断开,否则SQLFreeHandle函数会返回SQL_ERROR并且连接仍然被保持有效。

ODBC规范要求ODBC驱动程序必须是多线程安全的,也就是说,应用程序可以在同一个环境句柄上申请多个连接句柄,然后在多个线程中,同时用这些连接句柄连接到不同的数据库(甚至是不同类型的数据库)。

另外,应用程序也可以在同一个连接句柄上申请多个语句句柄,然后在多个线程中,同时用这些语句句柄执行不同的SQL语句。

18.2.2 连接字符串

用SQLDriverConnect函数连接到数据库的时候,所有与连接数据库相关的信息都被放在连接字符串中,本节将具体讨论连接字符串的用法。

1.连接字符串的格式

连接字符串的写法必须符合一定的格式,它由一系列的“属性名称=属性取值”字符串组合而成,每组属性定义之间用分号隔开:

属性1=value;属性2=value; ... ;属性n=value

连接字符串中定义的属性分为两部分,其中的一部分是由ODBC管理器解释的,而另一部分则由驱动程序介绍。

由ODBC管理器解释的属性如下所示:

● DSN——使用系统DSN或者用户DSN来连接到数据库时,指定DSN的名称。

● FILEDSN——使用文件DSN来连接到数据库时,指定DSN的名称。

● DRIVER——使用指定的驱动程序来连接到数据库时,指定驱动程序的名称。

可以看到,在SQLDriverConnect函数中不仅可以使用系统DSN或用户DSN方式连接到数据库,也能使用文件DSN,以及直接指定驱动程序方式,但是三者不能同时指定,只能选择其中的一种。

假如使用DSN方式连接到数据库,DSN的名称是Test,连接时使用的用户名和密码是myusername和mypassword,那么连接字符串可以写成:

szConn  db  "DSN=Test;UID=myusername;PWD=mypassword",0

如果使用文件DSN方式连接到数据库,那么可以使用FILEDSN属性,假设文件DSN的文件名是Test.dsn,连接字符串就可以写成:

szConn  db  "FILEDSN=Test.dsn;UID=myusername;PWD=mypassword",0

使用上面的两种方法前必须首先创建DSN,比较麻烦,最方便的方法是直接指定驱动程序名称来连接到数据库,这就要用到DRIVER属性,假如使用的是Access数据库的驱动程序,那么连接字符串可以写成:

szConn db "DRIVER=Microsoft Access Driver (*.mdb);", \"DBQ=C:\Test\Test.mdb;UID=myusername;PWD=mypassword",0

在字符串中,DRIVER属性指定了驱动程序的名称,DBQ属性指定了数据库的文件名。请注意DRIVER属性是由ODBC管理器解释的,而其他的属性是由驱动程序解释的。

看到这里,读者一定有个问题:DSN是自己创建的,使用的时候当然知道名称,但是怎么知道驱动程序的名称是什么呢?其实驱动程序的列表可以在“控制面板”的“数据源(ODBC)”栏目中看到。如图18.7所示,在“驱动程序”一栏中,我们可以看到本机上安装的所有ODBC驱动程序。在安装了新的数据库后,列表中会出现新数据库的驱动程序名称。要使用其中的某个驱动程序的时候,只要将驱动程序的名称抄过来就可以了。

                                                     图18.7 本机上已安装的ODBC驱动程序

需要注意的是,同一种数据库在不同的版本下驱动程序的名称也可能不同,如Oracle 8i版本的驱动名称是“Oracle ODBC Driver”,但到9i版本时的驱动名称却变成了“Oracle in OraHome92”;Sybase 11版本的驱动名称是“Sybase System 11”,估计到12版的时候就会变成“Sybase System 12”了。

一个完善的应用程序应该考虑到这一点,即使程序不打算支持多种数据库,也应该具有通过参数设置来改变驱动程序名称的能力,以便应对上述情况。

书写驱动程序名称的时候,注意不要将名称中的空格忽略掉,否则名称就无法匹配了。比如,使用Access数据库驱动程序时,名称中的“(*.mdb)”,以及左括号前的一个空格都必须原封不动地抄回来,否则连接时会因无法找到驱动程序而出错。

上面介绍的DSN、FILEDSN、DRIVER属性是由ODBC管理器解释的,ODBC管理器在看到这些属性后,才能找出对应的驱动程序并把其余的属性值传递给它。连接字符串中的其他属性则是原封不动地交给驱动程序解释的。

由驱动程序解释的属性值的定义就没这么统一了,比如,连接Oracle和Access数据库的时候,数据库名称都可以用DBQ属性来指定;但连接Sybase数据库时,数据库却是由SRVR和DB两个属性来共同指定的,其中SRVR指定数据库服务器的IP地址,DB指定数据库的名称;到使用SQL Server数据库时,用于指定数据库名称的却是SERVER和DATABASE这两个属性,其中SERVER指定数据库服务器IP地址,DATABASE属性指定数据库名称。

另外,各种数据库还有自己特定的属性值,比如,Oracle数据库中用MTS属性来表示是否打开Microsoft Transaction Server(MTS)的支持,其他数据库中并没有MTS这样的模块,也就不会有MTS这样的属性值。

要获取连接字符串中各种数据库特有的属性值定义,最好的方法是查看各种数据库的ODBC驱动程序说明文档,一般来说,这些文档可以在数据库系统的帮助文件或用户手册中找到。

2.连接到未知的数据库

虽然在数据库的帮助文档中可以找到ODBC连接字符串的定义方式,但还是有读者经常为连接字符串的写法感到困扰,因为在海量的帮助文档中找到合适的资料的确不是件容易的事情。实在无法找到帮助文档的时候,还有两种方法可以大致搞清楚驱动程序所需的属性值的定义方式。

第一个方法是利用SQLDriverConnect函数的dwDriverCompletion参数。我们知道,当dwDriverCompletion参数指定为SQL_DRIVER_COMPLETE_REQUIRED的时候,驱动程序会提示用户输入缺少的信息。这样,我们可以仅仅在连接字符串中指定驱动程序名称,然后在驱动程序弹出的对话框中输入相关的信息。如图18.8所示,不同数据库的驱动程序弹出的对话框有所不同(图中的1和2分别是Oracle和SQL Server数据库驱动程序弹出的提示框),但是根据上面的提示输入相关的信息并没有什么难度。

                                               图18.8 驱动程序的提示对话框

信息输入完毕并成功进行连接后,函数会在lpOutConnString指定的缓冲区中返回完整的连接字符串,将字符串中属性值的定义方式与在对话框中输入的信息进行比较,就可以发现各种属性的定义方法,以后就能根据我们的发现写出正确的连接字符串来了。上面的方法只能对比出一些必需的属性的定义方式,返回的连接字符串中还有很多定义了默认值的属性值,这些属性的含义就不得而知了。

第二种方法则可以得到更详细的信息,我们可以利用DSN的创建对话框来输入信息(图18.6中所示),这时要输入的信息比图18.8中所示的对话框更加详细,DSN创建完毕并命名为xxx后,在SQLDriverConnect函数中用“DSN=xxx”作为连接字符串,连接成功后,函数同样会在lpOutConnString指定的缓冲区中返回完整的连接字符串。将连接字符串的定义方式和在DSN的创建对话框中输入的信息进行对比,就能得到更详细的属性定义方法。

有了这两种方法,就是遇到了未知的数据库驱动程序,我们仍然可以试出这种数据库的ODBC连接字符串的定义方式。

18.3 数据的管理

18.3.1 执行SQL语句

如图18.4中的第2步所示,连接建立后,我们就可以在这个连接上进行各种操作了,这时读者可以发挥自己的想象,用简单或复杂的SQL语句去完成各种工作。(在作者所处的部门中,经常有同事写一些单条语句长度达几个KB的SQL语句去完成很复杂的工作,这些SQL语句就被戏称为“惊天地泣鬼神”的SQL语句!)

1.分配语句句柄

在让数据库执行SQL语句之前,必须首先分配一个语句(Statement)句柄并对它的属性进行适当的设置。分配语句句柄需要用到前面介绍过的SQLAllocHandle函数,SQLSetStmtAttr函数则用于对句柄的属性进行适当的设置。下面是这个步骤的典型写法:

.data?
hStmt dd  ?
.code...                ;申请环境句柄,申请连接句柄,并连接到数据库的操作invoke  SQLAllocHandle,SQL_HANDLE_STMT,hConn,addr hStmt.if ax != SQL_SUCCESS && ax != SQL_SUCCESS_WITH_INFOjmp Error     ;错误处理.endifinvoke SQLSetStmtAttr,hStmt,SQL_ATTR_CURSOR_TYPE,SQL_CURSOR_STATIC,0...                ;执行SQL语句

一般仅对语句句柄的SQL_ATTR_CURSOR_TYPE属性(游标类型)进行设置,由于语句的游标默认是SQL_CURSOR_FORWARD_ONLY类型的,只能前移而不能随机移动,在使用中有诸多不便,所以一般将其设置为SQL_CURSOR_STATIC类型(静态游标,可以随机移动)。

2.直接执行SQL语句

现在可以进行如图18.3所示的步骤——执行SQL语句了,通过语句句柄执行的SQL语句可以是Select语句,或者是Insert、Delete和Update等DML语句,也可以是“create table test(name char(10))”这样的创建表的语句。

实际上,凡是连接到的目标数据库的语法中支持的语句(也就是前面所称的“官方语言”和“方言”的总和),都可以通过语句句柄来执行,执行语句的函数仅起到“传声筒”的作用。比如,在Oracle数据库中用call语句来调用存储过程,假设现在Oracle数据库中建了一个Proc1存储过程,参数是一个字符串,那么连接到这个Oracle数据库后,就可以通过语句句柄执行“call Proc1('test')”这样的SQL语句。

有两个函数可以用来执行SQL语句,它们是SQLExecDirect和SQLExecute函数。前者直接执行一个SQL语句,后者则执行编译过的语句,这样在多次调用同一个语句的时候效率比较高。

SQLExecDirect函数的用法非常简单:

invoke SQLExecDirect,hStmt,lpSqlText,dwTextLength

hStmt是语句句柄,lpSqlText则是指向要执行的SQL语句字符串的指针,dwTextLength.参数是SQL语句字符串的长度(不包括字符串结尾的0字符)。

函数执行成功时,在ax中返回SQL_SUCCESS_WITH_INFO或者SQL_SUCCESS,但有个例外,如果执行的SQL语句是select语句并且返回的结果集为空时,即使函数的执行是成功的,函数也会返回SQL_NO_DATA代码;如果执行失败,函数根据情况返回SQL_ERROR或SQL_INVALID_HANDLE等错误代码。

在应用中,如果多次执行类似于下面(1)和(2)所示的语句,一般先用(3)所示的字符串来定义SQL语句,然后在执行前用wsprintf函数将具体参数代进去生成完整的语句,再用SQLExecDirect函数去执行:

(1)  insert into address(id,name,phone) values(1,'张三','010-88888888')
(2)  insert into address(id,name,phone) values(2,'李四','021-12345678')
(3)  insert into address(id,name,phone) values(%d,'%s','%s')

这样,多次执行相似的语句时,从应用程序的角度来看,每次需要先用wsprintf等字符串处理函数组合出完整的SQL语句;从数据库的角度来看,每次收到SQL语句时,都要经过语法分析、预编译等步骤后,才能开始执行。不管从哪个角度来看,用SQLExecDirect函数执行SQL语句的效率都是比较低的。

3.编译执行SQL语句

如果需要多次执行相同(或者相似)的SQL语句,那就应该使用编译执行的方法。“相同”的语句指字面上一模一样的语句,包括语句中的参数都是相同的,“相似”的语句指语法相同但是语句中参数的值有所不同的语句,如上面例子中的(1)和(2)语句,这两条语句在语法分析和预编译的阶段完全可以用下面的语句来代替,只要在执行的时候将两个参数的值替换进去即可。

insert into address(id,name,phone) values(?,?,?)

按下面的步骤可以将SQL语句以先编译、后执行的方式执行:

(1)通过调用函数SQLPrepare来对语句进行语法分析和预编译操作。

(2)对于“相同”的语句,直接转第3步;对于“相似”的语句,用SQLBindParameter函数将参数的具体取值绑定到语句中。

(3)调用SQLExecute函数来执行语句。

一旦第1步成功完成后,第2步和第3步即可重复进行,这样从数据库的角度看,就不必每次对同样的SQL语句进行语法分析和预编译的操作,使效率大为提高;从应用程序的角度来看,不必每次使用字符串处理函数来组合SQL语句,也方便得多。

由于SQLPrepare函数与SQLExecDirect使用相同的三个参数,所以这里不再写出函数原型。而SQLExecute函数的用法更加简单,它只有语句句柄这样一个参数,比较复杂的是用于参数绑定的SQLBindParameter函数。

SQLBindParameter函数的用法如下:

invoke SQLBindParameter,hStmt,dwParamNumber,dwInputOutputType, \dwValueType,dwParamType,dwColumnSize,dwDecimalDigits, \lpParamValue,dwBufferLength,lpStrLenOrInd

每次调用SQLBindParameter函数可以将语句中的一个参数(也就是用问号表示的地方)绑定到一个缓冲区地址中,前面例句中有三个参数,就需要循环调用三次SQLBindParameter函数,绑定完毕后调用SQLExecute函数来执行语句的时候,ODBC接口就会自动去绑定的缓冲区地址中取出数据,并代到SQL语句中去执行。

函数的hStmt参数是语句句柄,dwParamNumber是要绑定的参数序号,序号从1开始计算,例如前面例子中的SQL语句有三个参数,那么三次调用SQLBindParameter函数时这个参数分别指定为1、2、3。

dwInputOutputType参数表明绑定的参数是用来输入还是输出的,“输入”是将参数中的值代到SQL语句中去执行,“输出”指函数将在操作结束时将结果放入参数中。大多数情况下,参数是以输入方式使用的,而输出参数经常用于存储过程执行后的结果返回。dwInputOutputType参数的取值可以是下面三个数值之一:SQL_PARAM_INPUT_OUTPUT、SQL_PARAM_INPUT和SQL_PARAM_OUTPUT。

SQL语句会对数据库中的某个表进行操作,接下来的三个参数是用来指定表中字段的定义的:其中dwParamType参数用来指定字段的类型,取值是SQL_带头的常量(如表18.1所示),dwColumnSize参数指定字段的长度,如果字段类型是数值型的话,dwDecimalDigits参数指定小数部分的位数。假设上面例子中的address表是这样定义的:

create table address(id       integer,     // 4字节的长整数name     char(10),    // 10字节的字符型phone    char(50))    // 50字节的字符型

那么绑定id字段时,这三个参数分别指定为SQL_INTEGER、4、0,绑定name字段时,这三个参数分别指定为SQL_CHAR、10、0。

函数的最后四个参数用来指定和缓冲区中数据相关的信息,其中dwValueType参数指定了缓冲区中存放数据的类型,可能的类型是一组以SQL_C_开头的常数(如表18.1所示)。lpParamValue参数是一个指向缓冲区的指针,dwBufferLength参数表示缓冲区的长度,lpStrLenOrIndPtr是指向一个双字的指针,双字中包含以下数值之一:

● 缓冲区中的数据的实际长度。

● SQL_NTS——表示缓冲区中的数据是一个以0结尾的字符串(Null-Terminated String),这时由接口自动计算长度。

● SQL_NULL_DATA——表示参数取值为NULL。

● SQL_DEFAULT_PARAM——表示参数取值为存储过程的默认值,它仅适用于已定义了默认参数值的存储过程。

● SQL_DATA_AT_EXEC——参数的数据将由SQLPutData传送,由于数据可能太大无法放入内存(比如,整个文件的数据),那么我们告诉ODBC驱动程序我们将用SQLPutData替代。

请注意dwBufferLength参数和lpStrLenOrIndPtr指向的双字中数据的区别,前者是缓冲区的最大长度,而后者是缓冲区中数据的实际长度。例如,绑定上面例子中的name字段时,由于name字段最长是10个字节的,为了能容纳字符串尾部的0,我们可以分配一个11字节的缓冲区,这时dwBufferLength参数指定为11;现在将字符串“张三”放入缓冲区,那么lpStrLenOrIndPtr指向的双字中的值指定为4——这才是数据的实际字节数。

还是以上面的语句为例,绑定参数的实际代码举例如下,注意为了简单起见,代码中忽略了对错误的判断:

.const
szSQL db 'insert into address(id,name,phone) values(?,?,?)',0
.data?
dwParam1        dd      ?                ;存放id字段的缓冲区
szParam2        db      11 dup (?)       ;存放name字段的缓冲区
szParam3        db      51 dup (?)       ;存放phone字段的缓冲区
dwSize1         dd      ?                ;存放id字段数据的长度
dwSize2         dd      ?                ;存放name字段数据的长度
dwSize3         dd      ?                ;存放phone字段数据的长度
.code...invoke  lstrlen,addr szSQLinvoke  SQLPrepare,hStmt,addr szSQL,eaxinvoke  SQLBindParameter,hStmt,1,SQL_PARAM_INPUT,SQL_C_ULONG,\SQL_INTEGER,4,0,addr dwParam1,4,addr dwSize1invoke  SQLBindParameter,hStmt,2,SQL_PARAM_INPUT,SQL_C_CHAR,\SQL_CHAR,10,0,addr szParam2,11,addr dwSize2invoke  SQLBindParameter,hStmt,3,SQL_PARAM_INPUT,SQL_C_CHAR,\SQL_CHAR,50,0,addr szParam3,51,addr dwSize3...

绑定参数完毕后,在dwParam1、szParam2和szParam3这三个缓冲区中分别放入数值1,字符串“张三”和字符串“010-88888888”,在dwSize1、dwSize2、dwSize3中分别放入数值4、4和12,然后再执行SQLExecute函数,即可插入第一条记录。

接下来将三个缓冲区中的数据换成数值2、字符串“李四”和字符串“021-12345678”(由于三个数据的长度刚好和前面的相同,所以就不必改变dwSize1等的取值了),再执行SQLExecute函数,即可插入第二条记录。如果还要插入成千上万条记录,只要重复这个步骤就可以了。

多次执行相同(或相似)的SQL语句时,采用编译执行的方法效率很高,但对于只执行一次或数次的SQL语句来说,编译执行并没有优势,这时使用直接执行更为方便。另外,编译执行比较适合应用程序内置的SQL语句,如果执行的是用户实时输入的SQL语句,那么只能使用直接执行的方式了。

5.参数数据类型的转换

现在回过头来思考一个问题:dwValueType参数是不是有点多余呢?因为dwParamType参数中已经指定了字段的数据类型,只要在缓冲区中放入和字段同类型的数据就可以了呀,为什么非要用dwValueType参数重新指定一次数据类型呢?

其实这个参数并不多余,当缓冲区中的数据类型和字段数据类型是一致的情况下,这两个参数的确是重复的,但是当两者指定的数据类型不一致时,ODBC接口可以自动将数据进行转换后再交给数据库进行处理,这样可以带来很多方便。

上面的例子中,假如原始id数据是字符串类型的,但是数据库表中的对应字段却是整数型的,那么我们可以将转换的工作交给接口去完成,这时只需在绑定时将dwValueType参数指定为SQL_C_CHAR类型即可:

invoke SQLBindParameter,hStmt,1,SQL_PARAM_INPUT,SQL_C_CHAR,\SQL_INTEGER,4,0,addr szParam1,10,addr dwSize1

然后,在绑定到id字段的缓冲区szParam1中放入字符串“1”,在dwSize1中放入字符串的长度1,函数会自动将其转换成数值1后再交给数据库处理。

缓冲区中的数据类型在ODBC接口中以一系列的SQL_C_开头的常量来定义(称为C数据类型),而数据库表中的字段类型则以SQL_开头的常量来定义(称为SQL数据类型),两者的对应关系以及在汇编中的定义方式如表18.1所示,当绑定时dwParamType参数和dwValueType参数指定的类型一致时,接口不进行数据类型转换,不一致时则自动进行转换。

                                    表18.1 ODBC接口中部分数据类型的对应关系

表中列出的只是最常用的部分数据类型,全部数据类型的列表,以及不同数据类型之间转换的注意事项请参考MSDN中的ODBC部分。

在汇编语言中,遇到整数类型的字段时,一般将缓冲区中的整数指定为SQL_C_ULONG类型,遇到字符串类型的字段时,将缓冲区中的字符串数据指定为SQL_C_CHAR类型。除此之外,通常将数据以字符串方式表现并指定为SQL_C_CHAR类型,然后由接口进行转换。比如表示浮点数时用“1.23”类型的字符串,表示日期时用“2005-01-01”类型的字符串,这样可以避免一些因不了解数据存储格式而造成的错误。

5.重新绑定参数

如果想用同样的语句句柄来执行新的语句,那么没必要将其关闭并重新分配一个语句句柄,只需用SQL_RESET_PARAMS参数来调用SQLFreeStmt函数来解除与参数的绑定就可以了:

invoke   SQLFreeStmt,hStmt, SQL_RESET_PARAMS

接下来就可以使用原来的语句句柄来执行新的SQL语句了。

18.3.2 执行结果的处理

SQL语句执行完毕后,程序需要对执行的结果进行处理,不同类型语句的结果处理方式是不同的,DQL(select)语句执行后需要获取查询结果,DML(insert/update/delete)语句需要获取语句修改的记录行数,而DDL及DCL(创建或删除表、索引等对象,以及权限控制)等语句只需检测语句是否执行成功即可。在大部分的情况下,程序中执行的是预定义的SQL语句,也就是说写程序的时候已经知道SQL语句的类型,所以在执行语句后可以直接转到相关的处理流程中去。

当执行的是DDL、DCL等语句的时候(也就是图18.4中的步骤4C),这时只需检测SQLExecDirect或SQLExecute函数的返回值是否是SQL_SUCCESS_WITH_INFO或SQL_SUCCESS即可,如果是则表示语句执行成功,否则表示语句执行失败。

当执行的是DML语句时(也就是图18.4中的步骤4B),如果语句执行成功,则可以用SQLRowCount函数获取语句修改的记录行数,这样可以判断语句的执行是否符合预期的结果,比如希望删除1行记录,虽然delete语句执行成功,但通过SQLRowCount函数发现删除掉的却有10行时,就不是预期的结果了,SQLRowCount函数的用法是:

invoke SQLRowCount,hStmt,lpdwRows

hStmt参数是执行了SQL语句的语句句柄,lpdwRows参数是指向一个双字的指针,函数将在这个双字中返回insert/update/delete语句修改的记录行数。如果执行的不是DML语句,那么函数在双字中返回-1(根据MSDN文档,这种情况下返回值由数据库的驱动程序自主定义,但迄今为止,所有数据库的ODBC驱动程序返回的值都是-1)。

当执行的是select语句时,我们就有很多事情要做了,将整个结果集的数据取回来是件比较复杂的事情,这个问题将留到下一节再详细讨论。在这里先看看另一个经常遇到的问题:虽然大部分的情况程序中执行的是预定义的SQL语句,但在有些程序中,程序执行的是用户自由输入的SQL语句(比如18.4节中的例子程序),这时我们怎么知道该进入哪种流程进行处理呢?

当然,读者可能会说,检测一下输入的SQL语句的第一个单词不就行了吗?当然不行,比如,检测到insert单词的时候,当然可以认为是遇到DML语句了,但是Oracle 9i数据库的“方言”中,merge语句也是DML语句,以后也不会排除各种数据库的“方言”中出现各种新的语句来,所以靠分析关键字是不准确的。

这时,我们要用到SQLNumResultCols函数,并将这个函数和SQLRowCount等函数组合起来进行判断即可。

SQLNumResultCols函数用于获取SQL语句执行后返回的结果集的列数,比如,执行了“select id,name from address”,由于结果集中有id和name两个列,那么我们会得到2;而执行的是“select * from address”时,表中定义了多少列,就会得到对应的列数。

SQLNumResultCols函数的用法是:

.data?
dwRecordCols dd ?
.codeinvoke SQLNumResultCols,hStmt,addr dwRecordColsand    dwRecordCols,0ffffh

函数的第一个参数hStmt是执行了SQL语句的语句句柄,第二个参数是指向一个word的指针,函数执行后,在这个word中返回结果集中的列数;如果执行的不是select语句,那么显然语句不会产生结果集,这时在word中返回的是0。为了在其他语句中方便使用,一般将第二个参数指向的变量定义成一个dword而不是word,这时要注意在函数执行后将dword的高16位清零,否则容易造成不必要的错误(如上面例子中的代码)。

如果用SQLNumResultCols函数得到的列数大于0,那么执行的肯定是select语句;列数等于0则表示执行的不是select语句,这时要用SQLRowCount函数继续进行判断。

请读者注意区分没有结果集和结果集中没有数据的情况,非select语句执行后,结果是“没有结果集”;而select语句执行后没有得到任何符合条件的记录时(也就是返回的记录是0条),称为“结果集中没有数据”,这时SQLNumResultCols函数返回的还是正确的列数。

结合上面几个函数的功能,可以动态判断执行的语句类型,这在执行了用户自由输入的SQL语句后就显得非常有用:

.data?
hStmt   dd      ?
dwCols  dd      ?
dwRows  dd      ?
.codeinvoke SQLExecDirect,hStmt,addr szSQL,sizeof szSQL.if ax!=SQL_SUCCESS&&ax!=SQL_SUCCESS_WITH_INFO&&ax!=SQL_NO_DATAjmp Error    ; 语句执行失败.endifinvoke SQLNumResultCols,hStmt,addr dwColsand dwCols,0ffffh.if dwCols...              ; 执行的是select语句,在这里获取结果集中的数据.elseinvoke SQLRowCount,hStmt,addr dwRows.if dwRows == -1...                ; 执行的是DDL或DCL语句.else...                ; 执行的是DML语句.endif.endif

在执行SQLExecDirect函数后,注意对返回值要检测SQL_NO_DATA,因为这时也表示函数是执行成功的。然后执行SQLNumResultCols函数,如果得到的结果集列数大于0,则表示执行的是select语句,这时可以进行获取结果集中的数据的操作了。

如果得到的结果集列数为0,则再执行SQLRowCount函数来检测语句影响的行数,得到的结果不是-1,则表示执行的是DML语句;是-1的话就表示执行的是DDL或DCL语句了。

18.3.3 获取结果集中的数据

当成功执行了select语句时,下一步就是将查询到的结果集提取出来,“结果集”这个名词在前面已经多次提及,那么究竟什么是结果集呢?

我们知道,select语句从表中返回一些数据,这些数据可能由多条记录组成,每条记录中可能有多个字段。如图18.9所示,一般将返回结果的集合称为结果集(RecordSet),结果集中的一条记录称为行(Row),结果集中的某个字段的集合称为列(Column),一行中某个列的内容(也就是行和列的交叉点)就称为字段(Field)。另外,由于程序一般以每次一行为单位访问结果集中的数据,所以语句句柄中有一个指示当前行位置的指针(就像文件句柄中的当前位置指针一样),这个指针被称为游标(Cursor),当执行的是select语句时,游标默认被打开,并且位于结果集的第一行之前。注意游标不是指向第一行,第一行的行号为1,第一行之前则称为BOF/Begin of File,而最后一行之后称为EOF/End of File。

                                                     图18.9 结果集、行、列和字段

游标的类型和读取记录的方式有很大关系,默认情况下,游标只能在结果集中向前移动(SQL_CURSOR_FORWARD_ONLY类型),若想回到上一行,则必须先关闭游标然后重新打开(即重新执行语句),然后再从结果集的开始移动直到到达需要的行。静态游标则是可滚动的,它可以随机访问结果集,如果需要随机访问记录,则必须在语句执行前将语句的SQL_ATTR_CURSOR_TYPE属性设置为SQL_CURSOR_STATIC。

1.列缓冲区的绑定

如图18.4的步骤4A所示,获取结果集中数据的步骤是用多次调用SQLBindCol函数将结果集中的每个列绑定到指定的缓冲区地址,然后使用SQLFetch或SQLFetchScroll函数将当前行中每个列的数据传递到绑定的缓冲区中,重复调用SQLFetch函数,就能一行一行地取回数据,直到取出所有的行为止。在所有数据获取完毕后,用SQLCloseCursor函数关闭游标后,即可将结果集释放,语句句柄就能用来重新执行其他语句。

SQLBindCol函数的用法是:

invoke SQLBindCol,hStmt,dwColNumber,dwTargetType,lpTargetValue, \dwBufferLength,lpStrLenOrInd

函数的第一个参数hStmt是执行了SQL语句的语句句柄,dwColNumber是结果集中要绑定的列序号,序号从1开始计数,比如,对于“select id,name from address”语句返回的结果集,绑定id列的时候序号指定为1,绑定name列的时候序号指定为2。

dwTargetType参数指定了传送至缓冲区中的数据的类型,类型可以是表18.1中的C数据类型(也就是以SQL_C_带头的预定义值)。lpTargetValue参数则是指向缓冲区的指针,以后调用SQLFetch函数来获取数据时,函数会将列的类型转换到符合dwTargetType参数定义的格式后再放入缓冲区。例如,id字段是整数型的,结果集中返回的数据是100,当缓冲区类型指定为SQL_C_ULONG时,那么放到缓冲区中的会是双字100,而缓冲区类型指定为SQL_C_CHAR时,放到缓冲区中的就会是字符串“100”和一个结尾的0字符。

dwBufferLength参数是由lpTargetValue指向的缓冲区的长度,lpStrLenOrInd指向一个双字,以后用SQLFetch函数获取数据的时候,函数会在双字中返回数据的实际长度信息,具体的返回值同SQLBindParameter函数的同名参数的定义。

一次调用SQLBindCol函数只能绑定一个列,如果结果集中有两个列,那就需要调用两次SQLBindCol函数。但是绑定完毕以后,以后每次调用SQLFetch函数,列数据都会传递到指定的缓冲区中,这和SQLBindParameter函数的工作方式类似,用SQLBindParameter绑定输入参数缓冲区的操作也只需进行一次,以后每次调用SQLExecute函数,函数都会去同样的缓冲区中取数据。

用SQLFetch函数获取数据的时候,有必要检查lpStrLenOrInd指向的双字中的返回值,假如返回值是SQL_NULL_DATA的话,表示列数据为空,这时函数不会改写列缓冲区。如果这时直接去读缓冲区,可能会得到上一次调用SQLFetch留下的数据。

读者还应该理解NULL和空字符串之间的区别,以一个字符串指针来类比,空字符串相当于一个有效的指针已经指向了字符串缓冲区,但缓冲区中只有一个0字符;而NULL相当于指针的值就是0,指针本身就是无效的。

当然,我们也可以只绑定结果集中部分的列,那么以后调用SQLFetch函数时就只会返回已绑定的列的数据,其余未绑定的列将被忽略。

当需要取消绑定的时候,只需用SQL_UNBIND参数来调用SQLFreeStmt函数即可:

invoke SQLFreeStmt,hStmt, SQL_UNBIND

2.动态检测列的属性并绑定缓冲区

当执行的select语句是程序中预定义的语句时,程序已经知道结果集中会有多少个列,以及都是哪些列,也肯定知道列的定义(比如,列的宽度,这和缓冲区应该保留多少长度有直接关系)。那么,当执行的是用户自由输入的SQL语句时,程序既不知道语句中会有多少个列,也不知道列的定义,该如何用SQLBindCol函数绑定列缓冲区呢?

我们已经知道可以用SQLNumResultCols函数去检测结果集中列的数量,而每个列的属性可以用SQLDescribeCol函数来获取,包括列的数据类型和长度等各种属性,根据这些属性,就可以决定绑定时的缓冲区类型和长度了,SQLDescribeCol函数的用法是:

invoke SQLDescribeCol,hStmt,dwColNumber,lpColName,dwBufferLength,\lpwNameLength,lpwDataType,lpdwColSize,lpwDecDigits,lpwNullable

参数中的hStmt是语句句柄,每次调用SQLDescribeCol函数可以获取一个列的属性,dwColNumber指定列的编号,编号从1开始计算。

lpColName指向一个字符串缓冲区,dwBufferLength参数指定了这个缓冲区的长度,函数会在缓冲区中返回列的名称。lpwNameLength则是指向一个word的指针,函数在这个word中返回实际拷贝到lpColName缓冲区中的字符串的长度(不包括结尾的0字符)。

后面的一系列参数都是指向一个dword或者word的指针,函数会在这些dword或者word中返回和列相关的属性:

● lpwDataType——在指针指向的word中返回列的数据类型,取值是表18.1中的SQL数据类型定义,如SQL_CHAR、SQL_VARCHAR等。

● lpdwColSize——在指针指向的dword中返回列的宽度。

● lpwDecDigits——如果列的数据类型是带小数位的数值,那么在这个指针指向的word中返回小数的位数。

● lpwNullable——在指针指向的word中返回列中是否允许空值的标志,取值可能是SQL_NO_NULLS或者SQL_NULLABLE,分别表示不允许或允许空值。当驱动程序无法检测的时候,也可能返回SQL_NULLABLE_UNKNOWN。

在使用SQLDescribeCol函数时,要特别注意的是参数中的指针指向的变量类型,大部分指针是指向word变量的,但也有指向dword变量的。

在实际的应用中,经常会用lpdwColSize返回的列宽度来动态申请一块内存,并将申请到的内存绑定到列,这时要特别注意的是,返回的列宽度不一定是以字节为单位的,也有可能是以unicode字符为单位。

例如,Oracle数据库驱动程序返回的列宽度是以字节为单位的,但Access数据库驱动程序返回的列宽度却是Unicode字符数,如果在Access中定义了一个长度为10的字符型字段,用SQLDescribeCol函数检测会得到10,但这个字段中却允许放入10个中文,如果我们申请10字节的缓冲区并绑定到列,那么返回的数据可能会被截掉尾巴,因为10字节的缓冲区中只能放5个中文字符。

用SQLDescribeCol函数检测列宽度并动态申请缓冲区的例子请参考18.4节中的例子。

在实际的使用中,利用SQLDescribeCol函数检测列宽度并动态申请缓冲区内存时,一般申请2*dwColSize+1长度的缓冲区,将列宽度乘以2是为了应对宽度的单位是unicode字符的情况,多出来的1字节是为了放置字符串末尾的0字符。如果能确定访问的数据库的列宽度是以字节为单位的,也可以将2*去掉,这样可以节省内存。

3.提取数据

列缓冲区绑定完毕后,我们可以根据游标的类型分别用SQLFetch或SQLFetchScroll函数来将一行的数据返回到绑定的缓冲区中。

当游标的类型是SQL_CURSOR_FORWARD_ONLY类型时,游标只能前移(增加行号)而不能后移(减少行号),这时可以用SQLFetch函数来获取数据。SQLFetch函数只有一个函数——语句句柄。它首先将游标前移一行,然后将已绑定的列的数据返回到缓冲区中:

invoke SQLFetch,hStmt

如果函数执行成功,并且已经将数据返回到缓冲区中,那么函数返回SQL_SUCCESS或者SQL_SUCCESS_WITH_INFO;如果执行成功,但是游标已经到了结果集的末尾而导致没有数据返回,那么函数返回SQL_NO_DATA;当函数执行失败的时候,将根据情况返回SQL_ERROR等其他代码。

由于游标刚打开的时候位于第一行之前,所以首次执行SQLFetch函数时,游标前移一行后刚好位于第一行,函数将返回第一行的数据,以后多次重复调用SQLFetch函数,即可一行一行地将结果集中的所有行返回,直到函数返回SQL_NO_DATA为止。

当游标的类型是SQL_CURSOR_STATIC类型时,游标可以自由滚动,用SQLFetch函数就体现不出优势来,这时我们可以用功能有所增强的SQLFetchScroll函数:

invoke SQLFetchScroll,hStmt,dwFetchOrientation,dwOffset

SQLFetchScroll函数的返回值和SQLFetch函数相同,但比后者增加了两个参数,函数首先根据这两个参数移动游标,然后再将游标到达的行的数据返回到绑定的列缓冲区中。其中dwFetchOrientation参数表示移动游标的方法,参数的取值可以是以下值之一(假设函数执行前的行号为n,新的行号为m,最大行号为max):

● SQL_FETCH_NEXT——游标前移一行(m=n+1),这时dwOffset参数的值被忽略。选择SQL_FETCH_NEXT值的时候,函数的功能和SQLFetch相同。

● SQL_FETCH_PRIOR——游标后移一行(m=n-1),dwOffset参数的值被忽略。

● SQL_FETCH_FIRST——游标移动到第一行(m=1),dwOffset参数的值被忽略。

● SQL_FETCH_LAST——游标移动到最后一行(m=max),dwOffset参数的值被忽略。

● SQL_FETCH_ABSOLUTE——游标移动到dwOffset参数指定的行(m=dwOffset)。

● SQL_FETCH_RELATIVE——游标从当前行开始,前移由dwOffset参数指定的行数(m=n+dwOffset),如果dwOffset参数为负数,则后移dwOffset个记录。

在计算游标位置的过程中,如果新的行号小于1,则游标移动到第一行之前;如果新的行号大于max,则游标移动到最后一行之后,这两种情况下函数均返回SQL_NO_DATA。

需要注意的是,当dwFetchOrientation的取值不是SQL_FETCH_NEXT的情况下,游标类型必须是可移动的,否则函数会返回错误。

4.检测结果集中数据的行数

在实际应用中,读取结果集前经常需要先得到结果集中的记录行数,以便用来计算显示的页数等信息,在网上能够查到的很多资料中,提供的都是下面两种方法:

第一种方法是用SQLFetch函数遍历一遍结果集并进行计数,然后重新打开结果集(也就是重新执行SQL语句),这种方法有两个缺点,首先是效率问题,当结果集很大的时候,遍历结果集需要花很长时间;另外,两次执行SQL语句的中间,其他程序可能已经改动了记录,造成计数不准确。(当然,用动态游标可以不必重新执行语句,这时不存在第二个问题)。

第二种方法是首先用同样的where条件执行“select count(*) from table where ...”语句,得到计数值后再执行原来的查询语句,这种方法的缺点是,首先,在一个很大的表中执行计数操作可能非常慢;其次,如果执行的语句是用户自由输入的SQL语句,那么动态构建上述用于计数的SQL语句是非常困难的;最后,这种方法还是没法解决两次执行的过程中,记录可能被其他程序修改造成计数值不准确的问题。

实际上,可以用SQLFetchScroll函数配合SQLGetStmtAttr函数来检测结果集中包含的记录行数,这是因为语句句柄的SQL_ATTR_ROW_NUMBER属性中保存有当前行号,用SQLFetchScroll函数将游标移动到最后一行后,再获取的当前行号就是结果集中的总行数:

.data?
dwTemp      dd       ?
dwRecRows   dd       ?         ;记录的总行数
.codeinvoke  SQLFetchScroll,hStmt,SQL_FETCH_LAST,0invoke  SQLGetStmtAttr,hStmt,SQL_ATTR_ROW_NUMBER,\addr dwRecRows,SQL_IS_INTEGER,addr dwTemp

但是并不是所有数据库的ODBC驱动程序都支持SQL_ATTR_ROW_NUMBER属性,例如Access数据库的驱动程序就不支持该属性,也就无法用这种方法检测出正确的结果集行数,但Oracle、SQL Server等大部分数据库驱动程序则支持SQL_ATTR_ROW_NUMBER属性,所以这种方法还是非常实用的。

读者可以根据数据库的具体情况来决定使用哪种方法检测结果集中的记录行数。

5.结果集的释放和语句句柄的释放

在所有数据获取完毕后需要将结果集释放,这可以用SQLCloseCursor函数来实现,该函数关闭游标并释放结果集,这样语句句柄就能用来重新执行其他语句。

SQLCloseCursor函数的用法是:

invoke SQLCloseCursor,hStmt

如果执行的语句不是select语句,那么执行结果不会产生结果集,这时在重用语句句柄之前就没有必要调用SQLCloseCursor函数。

如果语句句柄不需要被用来执行新的SQL语句,那么可以使用SQLFreeHandle函数来将其释放:

invoke SQLFreeHandle,SQL_HANDLE_STMT,hStmt

假如在释放语句句柄之前用SQLDisconnect函数断开了到数据库的连接,那么该连接上的所有语句句柄将被自动释放。

18.3.4 事务处理

1.什么是事务

事务(Transaction)是关系型数据库中的一个重要特征,它指的是被当做单个逻辑单元来执行的一系列操作。举例来说,假如张三的账户中有5000元,现在张三到银行给李四转账1000元,那么转账过程至少包括下面几步和数据库相关的操作:

● 张三的账户余额减少了1000元,新的余额是4000元。

● 张三的账户明细记录增加一笔转出的操作。

● 李四的账户余额增加了1000元。

● 李四的账号明细记录增加一笔转入的操作。

这些操作必须作为单个逻辑单元来执行,也就是说,如果转账操作成功,那么这四步数据库操作必须全部成功;如果其中的某条语句执行失败,那么数据库中被其余语句修改的数据必须全部恢复到执行前的样子,这四步操作的整体就是一个事务。

如果数据库不提供对事务的支持,那么假如在第二步完成后由于系统断电或者别的原因造成后续操作失败,就会出现张三的账户余额已被减少,而李四的账户余额不会增加的情况。时间一长,数据库中的数据必定存在很多不可预测的错误。

数据库对事务的支持表现在下面一些方面:

首先,事务将一组相关操作组合成一个要么全部成功要么全部失败的单元。程序可以在全部操作完成后选择提交(Commit)或者回滚(Rollback)事务,提交操作将事务对数据库所做的修改存盘,回滚则将整个事务所做的修改全部撤销。

其次,并发的事务之间是隔离的,假如应用程序和数据库建立了两个连接,连接A中执行上面的转账事务,连接B中进行查看,那么不管操作已经完成几步,连接B中看到的要么是事务开始前的数据,要么是事务结束后的数据,不能看到中间状态的数据。例如第一步执行后,在连接A中用select语句看张三的余额会是4000,而连接B中看到的还会是5000,只有连接A提交了事务,那么连接B中才会看到所有被修改的数据。

再次,事务间有排他的写入锁定机制,如果在事务中修改了某个表,那么对应的记录将被锁定,在该事务结束前,其他事务将无法修改被锁定的记录。锁定的范围取决于数据库的具体实现,有可能是锁定部分记录,也可能锁定整个表。

最后,一旦被提交或回滚,那就意味着事务已经结束。已经结束的事务无法再次提交或者被回滚。

现在流行的大型数据库全部支持事务机制,MySQL、dBase等大部分小型数据库则不提供对事务的支持。在小型数据库中,Access数据库可以支持事务,但是性能比较差,最显著的差别就在于对锁定机制的处理上,Oracle等大型数据库采用的是行级锁,当某个事务对表中的记录进行了修改后,数据库仅锁定被修改的行,其他事务还是可以修改表中其他的行,但Access数据库仅实现了表级锁,也就是说即使某个事务只修改表中的一条记录,也会引起整个表被锁定,其他事务将无法修改表中的其他记录,这样在需要实现并发事务时,Access数据库基本上不具备使用性。

2.事务的实现

在ODBC接口中,可以通过设置连接句柄的SQL_ATTR_AUTOCOMMIT属性来决定是否打开事务机制,在默认情况下,该属性设置为SQL_AUTOCOMMIT_ON,也就是说,每执行一条语句后,ODBC接口会自动将语句提交,这样就相当于每条语句自动成为一个事务,程序中就无法将多条语句组合成一个事务进行统一提交或回滚了。

当属性设置为SQL_AUTOCOMMIT_OFF时,ODBC接口不会将语句自动提交,需要程序在合适的时候执行SQLEndTran函数将前面执行的语句统一提交或者回滚。两次执行SQLEndTran函数之间执行的SQL语句就组成了一个事务。

SQLEndTran函数的用法是:

invoke SQLEndTran,dwHandleType,hHandle,dwCompletionType

参数dwHandleType指定了需要操作的句柄类型,当该参数指定为SQL_HANDLE_ENV时,参数hHandle必须指定为一个环境句柄;当dwHandleType指定为SQL_HANDLE_DBC时,参数hHandle必须指定为一个连接句柄。

当指定的句柄是环境句柄时,函数对通过该环境句柄分配的所有连接句柄逐一进行操作;指定的句柄是连接句柄时,函数仅对该连接进行提交或回滚操作。

参数dwCompletionType则指定了操作的类型,如果指定为SQL_COMMIT,则函数将事务提交,如果指定为SQL_ROLLBACK,则函数将事务回滚。

在使用SQLEndTran函数的时候需要注意以下事项:

首先,如果希望在程序中实现事务,就必须确认连接的数据库是否提供对事务的支持,如果连接的是MySQL或者dBase等不支持事务的数据库,那么每条语句执行后肯定会被自动提交,SQLEndTran函数形同虚设。

其次,事务处理的具体特征取决于数据库的具体实现,假设我们现在运行了几条update语句后再运行一条create table...语句,然后用SQLEndTran函数进行回滚操作。在Access数据库中执行时,SQLEndTran函数会将前面的update语句连后面的建表语句一起回滚掉。但Oracle数据库规定DDL或者DCL语句会在执行前后隐含执行提交操作,所以在Oracle数据库中进行上述操作时,建表语句已经将前面的update语句提交,相当于结束了事务,即使后面用SQLEndTran函数来回滚,也不可能将前面已经结束的事务再回滚回去。

再次,有些数据库中已经存在提交或回滚事务的SQL语句,如Oracle或SQL Server数据库中可以通过SQLExecDirect函数执行“commit”语句来提交事务,也可以执行“rollback”语句来回滚事务,这些语句的作用和SQLEndTran函数没什么不同,但Access等数据库中却没有这两个语句,所以从程序的兼容性考虑,应该总是使用SQLEndTran函数来结束事务。

最后,由于ODBC驱动程序允许多线程操作,所以程序可以使用同一个连接句柄来分配多个语句句柄,然后在不同的线程中同时执行不同的SQL语句,但是SQLEndTran函数的最小单位却是一个连接,所以在一个线程中执行SQLEndTran函数,会将使用同一个连接句柄的所有线程中的语句提交掉。如果希望在多个线程中实现互不干扰的多个事务,这些线程必须使用独立的连接来执行SQL语句。

18.4 数据库操作的例子

ODBC工作流程的各部分之间是连续的,如环境初始化后才能连接到数据库,连接成功后才能执行SQL语句,select语句执行成功后才能读取结果集,用到的函数相互关联,很难割裂成多个单独的例子来演示,所以本章将一个完整的例子放在这里统一进行演示和分析。

例子程序的代码位于Chapter18\OdbcSample目录中,运行后的界面如图18.9所示。这是一个能使用ODBC驱动程序自由连接到各种类型的数据库,并自由执行用户输入的SQL语句的小程序。

在“ODBC连接字符串”文本框中输入合适的连接字符串,单击“连接”按钮后,如果输入的字符串正确并且数据库正常工作,那么程序即可连接到数据库。并激活SQL语句输入文本框和“执行”、“提交”、“回滚”等按钮,以便开始执行SQL语句。

为了方便演示,目录中已经有一个Access数据库Test.mdb,并在连接字符串文本框中默认填入了“Driver={Microsoft Access Driver (*.mdb)};dbq=test.mdb”,这样读者可以马上连接到这个测试数据库进行操作。如果读者希望连接到其他数据库进行操作,也可以输入其他的连接字符串。

测试数据库Test.mdb中建有两个表:addr_group表中存放通信录分组信息,表中有id,group_name,sort三个字段,分别记录分组id、分组名称和排列顺序;addr表中存放通信录,存放有id,group_id,name,mobile,gender,company,addr,phone,post和memo等字段,读者从字段的名称即可看出其含义。连接到Test.mdb数据库后,读者执行下面两条SQL语句,即可看出两个表中的数据:“select * from addr_group”,“select *from addr”。

例子程序能够连接到任何数据库,并能执行任何的SQL语句,当语句执行后,能自动根据语句类型显示不同的结果:如果执行的是select语句,程序能够自动获取结果集中各个列的名称和宽度,并根据这些信息初始化ListView,然后在ListView中显示结果集中的全部数据;如果用insert,update等语句对记录进行修改,则能用提交或回滚按钮来将操作存盘或者撤销。总之,这个只有几百行代码的例子麻雀虽小,五脏俱全,能够实现绝大多数常用的数据库操作。

目录中的Odbc.asm和Odbc.rc文件是例子的汇编源代码和资源文件。在Odbc.asm中用到了两个通用的模块:_ListView.asm和_RecordSet.asm,这两个文件中包含了一些通用的子程序,可以原封不动地包含在其他汇编源代码中使用。

_ListView.asm模块中包含了3个和列表框控件相关的子程序:_ListViewAddColumn用于在列表框中插入一个列;_ListViewSetItem用于添加一个新的行,并对行中的数据进行修改;_ListViewClear用于清除整个列表框中的数据。这个文件的内容在本节中就不再详细列出并分析了,有兴趣的读者请查看光盘中的文件内容并参考其中的注释进行分析。

_RecordSet.asm模块中则包含了和结果集处理相关的子程序,这部分的功能写成通用的子程序有利于在其他程序中重复使用。

下面,我们来具体看看源代码,并对代码进行分析。

18.4.1 结果集处理模块

在使用ODBC API获取结果集的时候,读者一定发现了一个问题,那就是整个操作流程很烦琐——每次执行SQL语句后,需要根据各字段的长度分配多个缓冲区,然后多次调用SQLBindCol函数来将缓冲区绑定到列中,以后才能使用SQLFetchScroll函数获取数据。

一些应用程序需要执行的SQL语句很多,这样就需要定义非常多的缓冲区变量并伴随着大量的SQLBindCol调用,从编程的主观感受来考虑,这些重复的代码对书写其他代码造成很大的干扰。看看下面ASP中的VBScript语言对结果集的处理语句,那真是方便极了:

Do Until rs.EOFResponse.Write "姓名:" & rs(0) & " 地址:" & rs(1)...rs.MoveNext
Loop
rs.Close

在上面的VBScript代码中,rs是SQL语句执行后产生的结果集对象,获取当前行中第n个列的数据时,只需用rs(n)来引用即可,需要将指针移动到下一行时,使用rs.MoveNext即可。如果能够在汇编中也能这样方便地使用ODBC接口,那么我们就能专注于代码逻辑,而不必在大堆的缓冲区定义语句和SQLBindCol函数上浪费精力了。

当然,汇编中没有这么直接的用法,但我们可以写一些通用的子程序来完成这些功能。_RecordSet.asm文件中就是这样一些子程序,里面的4个子程序完成了类似于上面VBScript代码中的各种功能:

● ODBC_RS结构定义了一个结果集对象。

● _RsOpen子程序自动申请缓冲区内存,并将列绑定到这些缓冲区中。

● _RsGetField获取某个列的数据,类似于上面的rs(n)的用法。

● _RsMoveNext将指针移动到下一行,类似于上面的rs.MoveNext的用法。

● _RsClose释放结果集对象使用的资源,如申请的缓冲区内存等。

这样主程序中就能避免缓冲区分配和绑定等烦琐的操作,保证了代码的清晰简洁。本节的例子Odbc.asm中就用到了这些子程序,读者也可以将这些子程序用在自己的程序中。

_RecordSet.asm文件的内容如下:

;_RecordSet.asm  -----   模拟结果集操作的通用子程序ODBC_RS struct hStmt 	dword ? 			;执行语句用的 StateMent 句柄dwCols 	dword ?			;当前结果集中的列数lpField dword 100 dup(?)	;预留100个列缓冲区的指针dwTemp 	dword ?
ODBC_RS ends assume esi:ptr ODBC_RS ;释放“结果集”——释放为各字段申请的缓冲区内存
_RsClose proc uses esi ebx _lpRs mov esi, _lpRs xor ebx, ebx .while ebx < [esi].dwCols lea eax, [esi + ebx *4 + ODBC_RS.lpField]mov eax, [eax].if eax invoke GlobalFree, eax .endif inc ebx .endw invoke RtlZeroMemory, esi, sizeof ODBC_RS ret 
_RsClose endp ;创建“结果集”——为每个字段预先申请缓冲区,并Bind到语句句柄上
;返回:eax = 0,失败;eax = TRUE,则成功
_RsOpen proc _lpRs, _hStmt local @szName[128]:byte, @dwNameSize, @dwType local @dwSize, @dwSize1, @dwNullable pushad mov esi, _lpRs invoke RtlZeroMemory, esi, sizeof ODBC_RS invoke SQLNumResultCols, _hStmt, addr [esi].dwCols mov eax, _hStmt mov [esi].hStmt, eax ;没有结果集则退出,如果超过100个列则只处理前面100个列and [esi].dwCols, 0ffffh cmp [esi].dwCols, 0 jz _Ret .if [esi].dwCols > 100 mov [esi].dwCols, 100 .endif ;为每个列申请内存并绑定到 statement 句柄xor ebx, ebx .while ebx < [esi].dwCols inc ebx invoke SQLDescribeCol, _hStmt, ebx, \addr @szName, sizeof @szName, addr @dwNameSize, \addr @dwType, addr @dwSize, addr @dwSize1, addr @dwNullable mov eax, @dwSize shl eax, 1 inc eax 		;eax=字段长度*2+1push eax invoke GlobalAlloc, GPTR, eax pop edx 		;edx=字段长度*2+1,eax=缓冲区指针or eax, eax jz _Err lea ecx, [esi+ebx*4+ODBC_RS.lpField - 4]mov [ecx], eax lea ecx, [esi].dwTemp invoke SQLBindCol, _hStmt, ebx, SQL_C_CHAR, eax, edx, ecx .endw 
_Ret:popad ret 
_Err:invoke _RsClose, esi jmp _Ret _RsOpen endp ;获取结果集缓冲区中指定编号字段的内容
;返回:eax = 0,失败
;	   eax > 0,则eax为指向字段内容字符串的指针
_RsGetField proc uses esi _lpRs, _dwFieldId mov esi, _lpRs mov eax, _dwFieldId .if eax < [esi].dwCols lea eax, [esi+eax*4+ODBC_RS.lpField]mov eax, [eax].else xor eax, eax .endif ret 
_RsGetField endp ;返回:eax = TRUE,结果集已经到末尾
;	   eax = FALSE,成功
_RsMoveNext proc uses esi ebx _lpRs mov esi, _lpRs ;********************************************************************;预先清除的缓冲区,否则遇到空字段的时候SQLFetchScroll可能不返回数据,;这样缓冲区中是错误的上一条记录的内容;********************************************************************xor ebx, ebx .while ebx < [esi].dwCols lea eax, [esi+ebx*4+ODBC_RS.lpField]mov eax, [eax].if eax mov byte ptr [eax], 0 .endif inc ebx .endw ;********************************************************************;将游标移动到下一条记录,并将内容获取到字段缓冲区中;********************************************************************invoke SQLFetchScroll, [esi].hStmt, SQL_FETCH_NEXT, 0.if ax == SQL_SUCCESS || ax == SQL_SUCCESS_WITH_INFO xor eax, eax .else xor eax, eax inc eax .endif ret 
_RsMoveNext endp assume esi:nothing 

首先,代码中定义了一个ODBC_RS结构来存放一些中间数据,这个结构可以看成是一个对象。结构中定义了语句句柄hStmt,结果集中列的数量dwCols,还预留了100缓冲区指针,这些指针用来指向分配的缓冲区内存,其中每个列使用一个指针。结构中的dwTemp字段在调用SQLFetchScroll时临时使用。

严格地说,缓冲区指针的数量应该等于结果集中的列数,其数量最好是根据dwCols的值动态申请,但从简化演示代码考虑,这里用了一个很宽余的数量100,可以供最多有100个列的结果集使用,如果读者使用的时候需要操作很大的表,里面的列数大于100个,那么可以增加预留的指针数量。

1._RsOpen子程序——初始化结果集

在执行SQL语句后,在主程序中需要分配一个ODBC_RS结构,然后将指向结构的指针和语句句柄作为参数调用_RsOpen子程序。_RsOpen子程序的工作流程如下:

子程序中首先用SQLNumResultCols函数获取结果集中列的数量,顺便也可以检测执行的是不是select语句,如果得到的列数为0,则表示执行的不是select语句,不存在结果集,这时子程序直接返回错误代码。

接下来就是以列的数量为循环次数的循环,循环中用SQLDescribeCol函数获取列的宽度@dwSize,然后分配2*@dwSize +1字节的内存,并用SQLBindCol函数绑定到列中。分配的字节数是两倍@dwSize大小加一的原因是,有些数据库如Access返回的列宽度是以Unicode字符为单位的,折算到字节数需要乘2,多余的一个字节则用于存放字符串数据的结束0字符。

在调用SQLBindCol函数时,为了简单起见,缓冲区的类型统一被指定成SQL_C_CHAR类型,这样ODBC接口返回的数据将全部是字符串类型的,可以直接用来显示。如果读者需要在缓冲区中存放实际类型的数据,那么请尝试自己修改源代码,预留一些双字变量将SQLDescribeCol函数返回的字段类型保存起来。

2._RsMoveNext子程序——移动到下一条记录

_RsOpen子程序调用完毕后,主程序需要移动结果集指针时可以调用_RsMoveNext子程序,这个子程序首先将所有预先申请的缓冲区中的数据清零,然后调用SQLFetchScroll函数移动指针并获取数据。

在常见的用法中,调用SQLBindCol函数时需要指定一个双字(该函数的最后一个参数),用于返回实际拷贝到缓冲区中的数据的长度(在这里被绑定到ODBC_RS结构的dwTemp字段中),在调用SQLFetchScroll后,判断双字中的返回值不是SQL_NULL_DATA时才表示数据是可用的,如果返回值是SQL_NULL_DATA,那么函数实际上不对缓冲区进行任何操作,这时缓冲区中的数据实际上是上一条记录残留的数据,如果对双字中的返回值不加判断就去使用缓冲区中的数据,那么得到的数据有可能是错误的。

在_RsMoveNext子程序中首先清除缓冲区,就不会存在这个问题,所以程序中没有对双字中的值进行判断。

3._RsGetField子程序——返回某个列的数据

调用_RsMoveNext子程序后,每个列的数据已经返回到了对应的缓冲区中,_RsGetField子程序实际上就是根据列的编号在ODBC_RS结构中查出缓冲区指针,并把指针返回给主程序而已。返回指针而不是将数据拷贝到由主程序提供的缓冲区是基于下面两个考虑:

首先,如果要拷贝数据,那么主程序势必又要分配一个新的缓冲区,如何确定缓冲区的长度又成了问题;其次,分配新缓冲区,以及拷贝数据会带来新的性能开销。所以子程序直接返回ODBC_RS结构中定义的缓冲区指针是最佳的选择。

4._RsClose子程序——释放资源

不需要再使用由ODBC_RS定义的结果集对象的时候,可以用_RsClose子程序来将其释放,释放的过程就是用一个循环将在_RsOpen子程序中申请的缓冲区内存一一释放而已。

理解了这些通用子程序的实现方法后,现在来看看如何在Odbc.asm例子中具体使用。

18.4.2 例子的源代码

Chapter18\OdbcSample目录中的Odbc.rc文件定义了程序的界面,文件的内容如下:

    //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>#include            <resource.h>//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>#define  ICO_MAIN         1000#define  DLG_MAIN         2000#define  IDC_CONN_STR     2001#define  IDC_CONN         2002#define  IDC_DISCONN      2003#define  IDC_SQL          2004#define  IDC_EXEC         2005#define  IDC_LIST         2006#define  IDC_INFO         2007#define  IDC_COMMIT       2008#define  IDC_ROLLBACK     2009//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>ICO_MAIN       icon       "Main.ico"//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>DLG_MAIN DIALOG 51, 78, 465, 237STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU |
WS_THICKFRAMECAPTION "ODBC数据库操作例子"FONT 9, "宋体"{EDITTEXT IDC_CONN_STR, 65, 5, 290, 12, ES_AUTOHSCROLL | WS_BORDER | WS_TABSTOPPUSHBUTTON "连接(&C)", IDC_CONN, 359, 4, 50, 14PUSHBUTTON "断开(&D)", IDC_DISCONN, 412, 4, 50, 14, WS_DISABLED | WS_TABSTOPEDITTEXT IDC_SQL, 40, 22, 261, 12, ES_AUTOHSCROLL | WS_DISABLED | WS_BORDERDEFPUSHBUTTON "执行(&E)", IDC_EXEC, 306, 21, 50, 14, BS_DEFPUSHBUTTON |
WS_DISABLEDPUSHBUTTON "提交(&M)", IDC_COMMIT, 359, 21, 50, 14, WS_DISABLED | WS_TABSTOPPUSHBUTTON "回滚(&R)", IDC_ROLLBACK, 412, 21, 50, 14, WS_DISABLED | WS_TABSTOPCONTROL "", IDC_LIST, "SysListView32", 13 | WS_CHILD | WS_VISIBLE| WS_BORDER | WS_TABSTOP, 2, 56, 460, 179LTEXT "SQL语句", -1, 5, 24, 34, 8LTEXT "ODBC连接字符串", -1, 5, 8, 60, 8LTEXT "", IDC_INFO, 5, 38, 455, 18}

资源文件中定义了如图18.9所示的对话框,这个对话框将被作为程序的主界面来使用,其中的IDC_LIST控件是一个“SysListView32”控件,这是一个列表框控件,其他的控件都是文本框和按钮等最常用的控件。

汇编源代码Odbc.asm的内容如下:

; Odbc.asm   ------   用Odbc操作数据库的例子
; 
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff Odbc.asm
; rc Odbc.rc
; Link /subsystem:windows Odbc.obj Odbc.res
.386
.model flat, stdcall
option casemap:none ; include 数据
include 	c:/masm32/include/windows.inc 
include 	c:/masm32/include/user32.inc 
includelib 	c:/masm32/lib/user32.lib 
include 	c:/masm32/include/kernel32.inc 
includelib 	c:/masm32/lib/kernel32.lib 
include 	c:/masm32/include/comctl32.inc 
includelib 	c:/masm32/lib/comctl32.lib 
include 	c:/masm32/include/odbc32.inc 
includelib 	c:/masm32/lib/odbc32.lib ; equ 数据
ICO_MAIN 	equ 1000 
DLG_MAIN 	equ 2000 
IDC_CONN_STR equ 2001
IDC_CONN 	equ 2002 
IDC_DISCONN equ 2003 
IDC_SQL 	equ 2004 
IDC_EXEC 	equ 2005 
IDC_LIST 	equ 2006 
IDC_INFO 	equ 2007 
IDC_COMMIT 	equ 2008 
IDC_ROLLBACK equ 2009 ; 数据段
.data?
hInstance 	dword ?
hWinMain 	dword ?				;对话框句柄
hListView 	dword ?				;列表框句柄
hEnv 		dword ?				;ODBC环境句柄
hConn 		dword ?				;ODBC连接句柄
szConnString byte 1024 dup(?)		;ODBC连接字符串
szFullString byte 1024 dup(?)		;连接后返回的全字符串
szSQL 		 byte 1024 dup(?)		;输入的准备执行的SQL语句.const 
szDefConnStr 	byte "Driver={Microsoft Access Driver (*.mdb)};dbq=test.mdb",0
szErrConn 	 	byte '无法连接到数据库!',0
szOkCaption 	byte '成功连接到数据库,完整的连接字符串如下:',0
szErrDDL 		byte 'DDL/DCL 语句已成功执行。',0
szErrDML 		byte 'DML 语句已成功执行,Insert/Update/Delete的行数:%d。',0
szErrDQL 		byte '查询语句已经成功执行,得到的结果集如下:',0; 代码段
.code
include _ListView.asm 
include _RecordSet.asm ; 执行SQL语句
_Execute proc local @dwTemp, @dwErrCode local @szSQLState[8]:byte, @szMsg[SQL_MAX_MESSAGE_LENGTH]:byte local @dwRecordCols, @dwResultRows local @szName[128]:byte, @dwNameSize, @dwType, @dwSize, @dwSize1, @dwNullable local @stRs:ODBC_RS, @hStmt invoke SetDlgItemText, hWinMain, IDC_INFO, NULL invoke ShowWindow, hListView, SW_HIDE invoke _ListViewClear, hListView invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr @hStmt .if ax != SQL_SUCCESS && ax != SQL_SUCCESS_WITH_INFO ret .endif invoke SQLSetStmtAttr, @hStmt, SQL_ATTR_CURSOR_TYPE, SQL_CURSOR_STATIC, 0 ;执行 SQL 语句invoke lstrlen, addr szSQL invoke SQLExecDirect, @hStmt, addr szSQL, eax .if ax != SQL_SUCCESS && ax != SQL_SUCCESS_WITH_INFO && ax != SQL_NO_DATA mov @szMsg, 0 invoke SQLGetDiagRec, SQL_HANDLE_STMT, @hStmt, 1, \addr @szSQLState, addr @dwErrCode, addr @szMsg, \sizeof @szMsg, addr @dwTemp invoke SetDlgItemText, hWinMain, IDC_INFO, addr @szMsg jmp _FreeStmt .endif ;语句执行成功,如果是DML语句,则显示语句影响的行数invoke SQLNumResultCols, @hStmt, addr @dwRecordCols and @dwRecordCols, 0ffffh.if !@dwRecordCols invoke SQLRowCount, @hStmt, addr @dwResultRows .if @dwResultRows == -1 		;DDL或DCL语句invoke SetDlgItemText, hWinMain, IDC_INFO, addr szErrDDL .else 							;DML语句invoke wsprintf, addr @szMsg, addr szErrDML, @dwResultRows invoke SetDlgItemText, hWinMain, IDC_INFO, addr @szMsg .endif jmp _FreeStmt .endif ;如果是Select语句,则根据结果集初始化ListView的标题,以便显示invoke SetDlgItemText, hWinMain, IDC_INFO, addr szErrDQL invoke ShowWindow, hListView, SW_SHOW xor ebx, ebx .while ebx < @dwRecordCols inc ebx invoke SQLDescribeCol, @hStmt, ebx, \addr @szName, sizeof @szName, addr @dwNameSize, \addr @dwType, addr @dwSize, addr @dwSize1, addr @dwNullable mov eax, @dwSize 			;列宽度=字符数*8象素mov ecx, 8 mul ecx .if eax > 300 				;最大不超过300象素mov eax, 300 .endif .if eax < 40 				;最小不小于40象素mov eax, 40 .endif lea ecx, @szName 			;将列名称插入列表框invoke _ListViewAddColumn, hListView, ebx, eax, ecx .endw ;将结果集填写到ListView中invoke _RsOpen, addr @stRs, @hStmt xor esi, esi .while TRUE invoke _RsMoveNext, addr @stRs .break .if eax invoke _ListViewSetItem, hListView, esi, -1, 0 	;插入新的一行mov esi, eax xor ebx, ebx 				;循环显示一行中的所有列.while ebx < @dwRecordCols invoke _RsGetField, addr @stRs, ebx .if eax invoke _ListViewSetItem, hListView, esi, ebx, eax .endif inc ebx .endw inc esi 		;行号加1.endw invoke _RsClose, addr @stRs invoke SQLCloseCursor, @hStmt _FreeStmt:invoke SQLFreeHandle, SQL_HANDLE_STMT, @hStmt ret 
_Execute endp ;断开到数据库的连接
_DisConnect proc .if hConn invoke SQLEndTran, SQL_HANDLE_DBC, hConn, SQL_COMMIT invoke SQLDisconnect, hConn invoke SQLFreeHandle, SQL_HANDLE_DBC, hConn .endif .if hEnv invoke SQLFreeHandle, SQL_HANDLE_ENV, hEnv .endif xor eax, eax mov hConn, eax mov hEnv, eax invoke SetDlgItemText, hWinMain, IDC_INFO, NULL invoke SetDlgItemText, hWinMain, IDC_SQL, NULL invoke ShowWindow, hListView, SW_HIDE invoke GetDlgItem, hWinMain, IDC_CONN_STR invoke EnableWindow, eax, TRUE invoke GetDlgItem, hWinMain, IDC_CONN invoke EnableWindow, eax, TRUE invoke GetDlgItem, hWinMain, IDC_SQL invoke EnableWindow, eax, FALSE invoke GetDlgItem, hWinMain, IDC_DISCONN invoke EnableWindow, eax, FALSE invoke GetDlgItem, hWinMain, IDC_COMMIT invoke EnableWindow, eax, FALSE invoke GetDlgItem, hWinMain, IDC_ROLLBACK invoke EnableWindow, eax, FALSE ret 
_DisConnect endp ;连接到数据库
_Connect proc local @dwTemp invoke GetDlgItem, hWinMain, IDC_CONN_STR invoke EnableWindow, eax, FALSE invoke GetDlgItem, hWinMain, IDC_CONN invoke EnableWindow, eax, FALSE ;申请环境句柄和连接句柄invoke SQLAllocHandle, SQL_HANDLE_ENV, SQL_NULL_HANDLE, addr hEnv .if ax != SQL_SUCCESS && ax != SQL_SUCCESS_WITH_INFO jmp _Error .endif invoke SQLSetEnvAttr, hEnv, SQL_ATTR_ODBC_VERSION, SQL_OV_ODBC3, 0 .if ax != SQL_SUCCESS && ax != SQL_SUCCESS_WITH_INFO jmp _Error .endif invoke SQLAllocHandle, SQL_HANDLE_DBC, hEnv, addr hConn .if ax != SQL_SUCCESS && ax != SQL_SUCCESS_WITH_INFO jmp _Error .endif invoke SQLSetConnectAttr, hConn, SQL_ATTR_AUTOCOMMIT, SQL_AUTOCOMMIT_OFF, 0 ;连接到数据库invoke lstrlen, addr szConnString mov ecx, eax invoke SQLDriverConnect, hConn, hWinMain, addr szConnString, ecx, \addr szFullString, sizeof szFullString, addr @dwTemp, SQL_DRIVER_COMPLETE .if ax == SQL_SUCCESS || ax == SQL_SUCCESS_WITH_INFO invoke MessageBox, hWinMain, addr szFullString, addr szOkCaption, MB_OK invoke GetDlgItem, hWinMain, IDC_DISCONN invoke EnableWindow, eax, TRUE invoke GetDlgItem, hWinMain, IDC_COMMIT invoke EnableWindow, eax, TRUE invoke GetDlgItem, hWinMain, IDC_ROLLBACK invoke EnableWindow, eax, TRUE invoke GetDlgItem, hWinMain, IDC_SQL push eax invoke EnableWindow, eax, TRUE pop eax invoke SetFocus, eax .else 
_Error:invoke MessageBox, hWinMain, addr szErrConn, NULL, MB_ICONSTOP or MB_OK invoke _DisConnect .endif ret 
_Connect endp ;主窗口程序
_ProcDlgMain proc uses ebx edi esi hWnd, wMsg, wParam, lParam local @stWsa:WSADATA mov eax, wMsg .if eax == WM_COMMAND mov eax, wParam ;输入连接字符串后才激活“连接”按钮.if ax == IDC_CONN_STR invoke GetDlgItemText, hWnd, IDC_CONN_STR, addr szConnString, sizeof szConnString invoke GetDlgItem, hWnd, IDC_CONN .if szConnString invoke EnableWindow, eax, TRUE .else invoke EnableWindow, eax, FALSE .endif ;输入SQL语句后才激活“执行”按钮.elseif ax == IDC_SQL invoke GetDlgItemText, hWnd, IDC_SQL, addr szSQL, sizeof szSQL invoke GetDlgItem, hWnd, IDC_EXEC .if szSQL invoke EnableWindow, eax, TRUE .else invoke EnableWindow, eax, FALSE .endif ;连接、断开连接、执行按钮的处理.elseif ax == IDC_CONN invoke _Connect .elseif ax == IDC_DISCONN invoke _DisConnect .elseif ax == IDC_EXEC invoke _Execute invoke SendDlgItemMessage, hWnd, IDC_SQL, EM_SETSEL, 0, -1 .elseif ax == IDC_COMMIT invoke SQLEndTran, SQL_HANDLE_DBC, hConn, SQL_COMMIT .elseif ax == IDC_ROLLBACK invoke SQLEndTran, SQL_HANDLE_DBC, hConn, SQL_ROLLBACK .endif .elseif eax == WM_INITDIALOG push hWnd pop hWinMain invoke LoadIcon, hInstance, ICO_MAIN invoke SendMessage, hWnd, WM_SETICON, ICON_BIG, eax invoke GetDlgItem, hWnd, IDC_LIST mov hListView, eax invoke SendMessage, hListView, LVM_SETEXTENDEDLISTVIEWSTYLE, \0, LVS_EX_GRIDLINES or LVS_EX_FULLROWSELECT invoke ShowWindow, hListView, SW_HIDE invoke SendDlgItemMessage, hWnd, IDC_CONN_STR, EM_SETLIMITTEXT, 1024, 0invoke SendDlgItemMessage, hWnd, IDC_SQL, EM_SETLIMITTEXT, 1024, 0 invoke SetDlgItemText, hWnd, IDC_CONN_STR, addr szDefConnStr .elseif eax == WM_CLOSE .if !hEnv && !hConn invoke EndDialog, hWinMain, NULL .endif .else mov eax, FALSE ret .endif mov eax, TRUE ret 
_ProcDlgMain endp ;程序开始 start:
main proc invoke InitCommonControls invoke GetModuleHandle, NULL mov hInstance, eax invoke DialogBoxParam, eax, DLG_MAIN, NULL, offset _ProcDlgMain, 0 invoke ExitProcess, NULL 
main endp 
end main 
;end start

为了使用ODBC API,在程序的开始必须将odbc32.inc和odbc32.lib文件包含进来。另外,_ListView.asm和_RecordSet.asm文件也被包含进来,以便使用其中的子程序。

程序在对话框的WM_INITDIALOG消息中进行一些初始化工作,包括将列表框控件隐藏(当执行的SQL语句是select语句时,才需要将控件显示出来以便显示数据),以及将默认的连接到Test.mdb的连接字符串设置到文本框中。

1.连接到数据库

用户使用默认的连接字符串,或者输入其他连接字符串后,单击“连接”按钮,程序在WM_COMMAND消息的IDC_CONN流程中调用_Connect子程序。

_Connect子程序实现的是图18.4中的步骤1的功能,并做了下面一些补充:首先是将连接句柄的SQL_ATTR_AUTOCOMMIT属性设置成非自动递交模式,以便支持事务功能;其次是在连接成功后用消息框显示SQLDriverConnect函数返回的完整的连接字符串;最后,如果连接成功,程序将激活SQL语句输入框、“执行”、“提交”和“回滚”按钮,并灰化连接字符串输入框和“连接”按钮。

由于调用SQLDriverConnect函数时,使用了SQL_DRIVER_COMPLETE参数,所以读者可以用类似于“Driver=SQL Server”这样的最简化的连接串来连接,等ODBC驱动程序弹出参数输入框后再输入具体的参数,然后再查看函数返回的完整的连接字符串有什么不同。

2.执行SQL语句

现在可以输入SQL语句并执行了,单击“执行”按钮后,程序调用_Execute子程序。_Execute子程序实现的是图18.4中的步骤2、步骤3和步骤4的功能,其中首先申请一个语句句柄,将句柄的光标属性设置成可滚动光标后再用SQLExecDirect函数执行SQL语句,由于SQL语句是用户自由输入的,所以在这个例子中无法演示SQLPrepare,SQLBindParameter和SQLExecute等函数的用法了。

如果程序检测到语句执行失败(返回值不是SQL_NO_DATA,SQL_SUCCESS或者SQL_SUCCESS_WITH_INFO),那么使用SQLGetDiagRec函数来获取具体的出错信息,这个函数的作用类似于Win32 API中的GetLastError函数,它返回的是上一条ODBC API的具体出错原因。

SQLGetDiagRec函数的用法是这样的:

invoke SQLGetDiagRec,dwHandleType,hHandle,dwRecNumber,lpSqlstate,\lpdwNativeError,lpMessageText,dwBufferLength,lpdwTextLength

dwHandleType参数是需要获取错误信息的句柄类型,可以是SQL_HANDLE_ENV,SQL_HANDLE_DBC和SQL_HANDLE_STMT,hHandle参数则指定句柄。

dwRecNumber指定程序从哪个状态编码开始查询错误信息,这个参数一般指定为1。lpSqlState指向一个字符串缓冲区,函数在这里返回错误代码字符串;lpdwNativeError参数指向一个双字,函数在这里返回错误代码;lpMessageText参数指向一个字符串缓冲区,函数在这里返回具体出错信息字符串,dwBufferLength指定了这个缓冲区的长度,最后一个参数指向一个双字,函数返回出错信息字符串后,字符串的长度将被填写到这个双字中。

函数返回了三种错误信息,这些错误信息有什么不同呢?其实这些信息分两类,一类是ODBC规范定义的标准错误信息,另一类是驱动程序返回的本地化错误信息。

lpSqlState指向的缓冲区中返回的是ODBC规范定义的标准错误信息,这是一个5字节长的字符串,当在不同类型的数据库中进行操作并出现同类型的错误时,这里的代码是相同的,比如,01004代表Data truncated,01S02代表Option value changed,HY008代表Operation canceled等,相同的编码有利于书写通用的错误处理代码,详细的代码列表请读者参考MSDN中的相关部分。

lpdwNativeError和lpMessageText指向的变量中返回的则是数据库本地化的数值型错误代码,以及具体的出错原因字符串。比如,同样执行“select * from add”语句(假设add表是不存在的),操作Access数据库时,返回的本地错误代码是-1305,返回的出错原因字符串是“[Microsoft] [ODBC Microsoft Access Driver] Microsoft Jet数据库引擎找不到输入表或查询'add'。确定它是否存在,以及它的名称的拼写是否正确”;而操作的是Oracle数据库时,返回的本地错误代码却是903,返回的出错原因字符串变成了“[oracle][ODBC][Ora] ORA-00903: 无效表名”。

显然,本地化的出错原因字符串能提供更详细的信息,另外,在有些情况下也必须用到本地化的错误编码,例如使用SQL Server或Oracle等C/S数据库时,遇到网络断线后需要重连数据库,否则后面的然后操作肯定会失败,但是仅仅从标准错误信息不足以检测错误是否是由网络引起的,这时就要用到本地化的出错代码了。在实际使用中,如果网络断线,Oracle数据库可能出现3113或3114错误,SQL Server数据库可能出现232和10054错误,这种情况就需要根据具体的数据库进行具体处理了。

现在回过头来看_Execute子程序,语句执行成功的话,程序首先检测执行的SQL语句类型,用到的代码就是18.3.2节最后部分示范的代码,如果检测到是DML语句,程序显示语句修改的记录行数;如果是DDL或DCL等语句,则仅仅显示语句是否执行成功的信息;如果执行的是select语句,则获取结果集并进行处理。

要显示结果集的话,需要先用ShowWindow函数将隐藏的列表框控件显示出来,并根据列名在列表框中添加对应的标题,子程序中用一个循环来完成这个功能,循环中用SQLDescribeCol函数获得每个列的名称和宽度。然后用_ListView.asm文件中的_ListViewAddColumn子程序添加列。

现在,终于可以用到_RecordSet.asm文件中的那些子程序来显示整个结果集了,这部分的代码如下:

invoke  _RsOpen,addr @stRs,@hStmt
xor     esi,esi                   ; esi作为行号,从0开始计算
.while  TRUEinvoke  _RsMoveNext,addr @stRs.break  .if eaxinvoke  _ListViewSetItem,hListView,esi,-1,0 ;插入新的一行mov      esi,eaxxor      ebx,ebx          ; 获取并显示行中所有的列.while  ebx <   @dwRecordColsinvoke  _RsGetField,addr @stRs,ebx.if      eaxinvoke  _ListViewSetItem,hListView,esi,ebx,eax.endifinc      ebx.endwinc      esi               ;行号加1
.endw
invoke  _RsClose,addr @stRs

读者可以看到,使用了封装的子程序后,代码的结构简洁程度可以与VBScript程序相媲美了,原先烦杂的缓冲区分配工作和SQLBindCol函数调用不见了踪影。

在_Execute子程序的最后,程序使用SQLCloseCursor函数关闭结果集,并调用SQLFreeHandle函数释放语句句柄。

3.事务处理

执行了一系列的DML语句后,可以用“提交”或“回滚”按钮来结束事务。这部分功能由这两个按钮的处理代码实现:

mov eax,wMsg
.if eax ==  WM_COMMANDmov eax,wParam.if ax ==    ...; 其他按钮的处理代码.elseif ax == IDC_COMMITinvoke SQLEndTran,SQL_HANDLE_DBC,hConn,SQL_COMMIT.elseif ax == IDC_ROLLBACKinvoke SQLEndTran,SQL_HANDLE_DBC,hConn,SQL_ROLLBACK.endif

这部分的实现非常简单,只是用SQL_COMMIT或者SQL_ROLLBACK参数来调用SQLEndTran函数而已。

读者可以用下面的序列来测试事务功能:

(1)两次运行Odbc.exe文件,这样屏幕上有两个操作窗口。在两个窗口中执行“select*from addr where id=1”语句,可以发现name字段的值都是“张三”。

(2)现在在窗口A中执行“update addr set name='aaa' where id=1”后,然后在同一窗口中用步骤1的select语句查看结果,可以发现name字段的值变成了“aaa”。但是在窗口B中再次执行步骤1的select语句,可以发现name字段的值还是“张三”,说明事务结束之前,其他连接中是看不到中间结果的。

(3)在窗口B中执行“update addr set name='aaa' where id=1”,片刻后程序会提示错误:由于表被锁定造成记录无法更新,说明事务结束前,DML语句将锁定记录。将语句中的where条件改为id=2,程序还是会提示同样的错误,说明Access数据库实现的是表级锁。

(4)在窗口A中单击“提交”按钮,然后在两个窗口中再次查询,name字段的值全部变成了“aaa”。

读者可以做个实验,将_Connect子程序中设置连接句柄属性的SQLSetConnectAttr语句去掉,重新编译链接后再进行上述操作,可以发现每条语句执行后,所做的修改都会被自动提交。

4.断开连接

如果用户单击“断开”按钮,那么程序调用_DisConnect子程序来断开连接,子程序中首先将事务提交,再用SQLDisconnect函数断开到数据库的连接,然后释放连接句柄和环境句柄,最后在设置相关按钮的状态后,子程序返回。

http://www.dtcms.com/a/389865.html

相关文章:

  • linux入门(3)
  • 任意版本GitLens vscode插件破解邪修秘法
  • Redis最佳实践——热点数据缓存详解
  • font简写和CSS的继承性
  • 高性能服务器配置经验指南6——BIT校园网在ubuntu中的自动检查连接状况脚本使用
  • SQL 连接详解:内连接、左连接与右连接
  • C2000基础-TIM介绍及使用
  • Day 06 动作类的初始化类------以B1为例
  • 面试题:对数据库如何进行优化?
  • samurai 点选分割 box分割
  • 计算机架构的总线协议中的等待状态是什么?
  • C++:入门基础(1)
  • ACD智能分配:服务延续和专属客服设置
  • 自监督学习分割
  • 抛弃自定义模态框:原生Dialog的实力
  • LangGraph 简单入门介绍
  • Docker 部署 DzzOffice:服务器 IP 转发功能是否需要开启
  • 无人机避障——卡内基梅隆大学(CMU)CERLAB 无人机自主框架复现
  • 正点原子zynq_FPGA-初识ZYNQ
  • Vue3中对比ref,reactive,shallowRef,shallowReactive
  • 通过Freemark渲染数据到Word里并生成压缩包
  • Vue 项目中使用 AbortController:解决请求取消、超时与内存泄漏问题
  • 设置管家婆服务器开机自动启动
  • ubuntu20 安装 ros2 foxy
  • 二分查找(二分查找算法)
  • 贪心算法应用:超图匹配问题详解
  • Hadoop3.3.5搭建指南(双NN版本)
  • 如何正确写Controller?参数校验、异常处理
  • 线性代数:LU与Cholesky分解
  • 饮用水在线监测设备:实时、精准地捕捉水体中的关键参数,为供水安全提供全方位保障