VC数据库编程总结(一)

一、访问数据库技术方法................................................................................................. 2

1、几种是数据库访问方法比较.................................................................................. 2

2、VC数据库编程几种方法....................................................................................... 2

(1)通用方法................................................................................................... 2

1. ODBC连接.............................................................................................. 2

2. MFC ODBC连接...................................................................................... 3

3. DAO连接................................................................................................ 5

4. OLE DB连接........................................................................................... 5

使用OLEDB编写数据库应用程序................................................................ 6

使用OLE DB读写SQL Server...................................................................... 9

5. OLE DB Templates连接.......................................................................... 20

6. ADO连接............................................................................................... 21

用ADO进行数据库编程............................................................................ 22

(2)Oracle专用方法....................................................................................... 29

1. OCI(Oracle Call Interface)访问................................................................. 29

2. Oracle Object OLE C++ Class Library....................................................... 29

3. 使用OTL进行数据库编程..................................................................... 30

OTL编程实例............................................................................................ 30

二、据库语句优化.......................................................................................................... 33

SQL语句优化的原则............................................................................................... 33

一些常用的SQL语句............................................................................................... 34

三、数据库优化.............................................................................................................. 36

通用优化................................................................................................................. 36

优化SQL Server数据库............................................................................................ 37

Oracle SQL性能优化................................................................................................ 46

DB2数据库优化....................................................................................................... 48

四、冷备份与热备份、双机热备与容错........................................................................... 54

一、冷备份.............................................................................................................. 54

二、热备份.............................................................................................................. 55

双机热备的实现模式................................................................................................ 56

双机容错的工作原理................................................................................................ 57

双机容错环境下Oracle数据库的具体应用................................................................ 59

双机容错的实现................................................................................................ 59

需要注意的问题................................................................................................ 60

 

数据库编程总结

当前各种主流数据库有很多,包括Oracle, MS SQL Server, Sybase, Informix, MySQL, DB2, Interbase / Firebird, PostgreSQL, SQLite, SAP/DB, TimesTen, MS ACCESS等等。数据库编程是对数据库的创建、读写等一列的操作。数据库编程分为数据库客户端编程与数据库服务器端编程。数据库客户端编程主要使用ODBC API、ADO、ADO.NET、OCI、OTL等方法;数据库服务端编程主要使用OLE DB等方法。数据库编程需要掌握一些访问数据库技术方法,还需要注意怎么设计高效的数据库、数据库管理与运行的优化、数据库语句的优化。

 

一、访问数据库技术方法
数据库编程分为数据库客户端编程与数据库服务器端编程。数据库客户端编程主要使用ODBC API、ADO、ADO.NET、OCI、OTL等方法;数据库服务端编程主要使用OLE DB等方法。

 

1、几种是数据库访问方法比较
ODBC API是一种适合数据库底层开发的编程方法,ODBC API提供大量对数据源的操作,ODBC API能够灵活地操作游标,支持各种帮定选项,在所有ODBC相关编程中,API编程具有最高的执行速度。

DAO提供了很好的数据库编程的对象模型.但是,对数据库的所有调用以及输出的数据都必须通过Access/Jet数据库引擎,这对于使用数据库应用程序,是严重的瓶颈。

OLE DB提供了COM接口,与传统的数据库接口相比,有更好的健壮性和灵活性,具有很强的错误处理能力,能够同非关系数据源进行通信。

ADO最主要的优点在于易于使用、速度快、内存支出少和磁盘遗迹小。

ADO.NET 是利用数据集的概念将数据库数据读入内存中,然后在内存中对数据进行操作,最后将数据集数据回写到源数据库中。

OTL 是 Oracle, Odbc and DB2-CLI Template Library 的缩写,是一个C++编译中操控关系数据库的模板库,OTL中直接操作Oracle主要是通过Oracle提供的OCI接口进行,进行操作DB2数据库则是通过CLI接口来进行,至于MS的数据库和其它一些数据库,则OTL只提供了ODBC来操作的方式。当然Oracle和DB2也可以由OTL间接使用ODBC的方式来进行操纵。具有以下优点:跨平台;运行效率高,与C语言直接调用API相当;开发效率高,起码比ADO.net使用起来更简单,更简洁;部署容易,不需要ADO组件,不需要.net framework 等。

 

2、VC数据库编程几种方法
VC数据库编程几种方法,包括ODBC连接、MFC ODBC连接、DAO连接、OLE DB、OLE DB Templates连接、ADO、Oracle专用方法(OCI(Oracle Call Interface)访问、Oracle Object OLE C++ Class Library )。

(1)通用方法
1. ODBC连接
ODBC(Open DataBase Connectivity)是MSOA的一部分,是一个标准数据库接口。它提供对关系数据库访问的统一接口,实现对异构数据源的一致访问。

ODBC数据访问由以下部分组成:

<1>句柄(Handles):ODBC使用句柄来标识ODBC环境、连接、语句和描述器.

<2>缓存区(Buffers):

<3>数据类型(Data types)

<4>一致性级别(Conformance levels)

用ODBC设计客户端的一般步骤:

<1>分配ODBC环境

<2>分配连接句柄

<3>连接数据源

<4>构造和执行SQL语句

<5>获得查询结果

<6>断开数据源的连接

<7>释放ODBC环境

ODBC API是一种适合数据库底层开发的编程方法,ODBC API提供大量对数据源的操作,ODBC API能够灵活地操作游标,支持各种帮定选项,在所有ODBC相关编程中,API编程具有最高的执行速度.因此,ODBC API编程属于底层编程。

 

2. MFC ODBC连接
MFC ODBC是MFC对ODBC进行的封装,以简化对ODBC API的 调用,从而实现面向对象的数据库编程接口.

MFC ODBC的封装主要开发了CDatabase类和CRecordSet类

(1) CDatabase类

CDatabase类用于应用程序建立同数据源的连接。CDatabase类中包含一个m_hdbc变量,它代表了数据源的连接句柄。如果要建立CDatabase类的实例,应先调用该类的构造函数,再调用Open函数,通过调用,初始化环境变量,并执行与数据源的连接。在通过Close函数关闭数据源。

CDatabase类提供了对数据库进行操作的函数及事务操作。

(2) CRecordSet类

CRecordSet类定义了从数据库接收或者发送数据到数据库的成员变量,以实现对数据集的数据操作。

CRecordSet类的成员变量m_hstmt代表了定义该记录集的SQL语句句柄,m_nFields为记录集中字段的个数,m_nParams为记录集所使用的参数个数。

CRecordSet的记录集通过CDatabase实例的指针实现同数据源的连接,即CRecordSet的成员变量m_pDatabase.

MFC ODBC编程更适合于界面型数据库应用程序的开发,但由于CDatabase类和CRecordSet类提供的数据库操作函数有限,支持的游标类型也有限,限制了高效的数据库开发。在编程层次上属于高级编程。

应用实例:

1.打开数据库

CDatabase database;

database.OpenEx( _T( "DSN=zhuxue" ),CDatabase::noOdbcDialog);//zhuxue为数据源名称

2.关联记录集

CRecordset recset(&database);

3.查询记录

CString sSql1="";

sSql1 = "SELECT * FROM tablename" ;

recset.Open(CRecordset::forwardOnly, sSql1, CRecordset::readOnly);

int ti=0;

CDBVariant var;//var可以转换为其他类型的值

while (!recset.IsEOF())

{

//读取Excel内部数值

recset.GetFieldValue("id",var);

jiangxiang[ti].id=var.m_iVal;

recset.GetFieldValue("name", jiangxiang[ti].name);

ti++;

recset.MoveNext();

}

recset.Close();//关闭记录集

4.执行sql语句

CString sSql="";

sSql+="delete * from 院系审核";//清空表

database.ExecuteSQL(sSql);

sSql也可以为Insert ,Update等语句

5.读取字段名

sSql = "SELECT * FROM Sheet1" ;  //读取的文件有Sheet1表的定义,或为本程序生成的表.

// 执行查询语句

recset.Open(CRecordset::forwardOnly, sSql, CRecordset::readOnly);

int excelColCount=recset.GetODBCFieldCount();//列数

CString excelfield[30];

//得到记录集的字段集合中的字段的总个数

for( i=0;i<excelColCount;i++)

{

CODBCFieldInfo fieldinfo;

recset.GetODBCFieldInfo(i,fieldinfo);

excelfield[i].name =fieldinfo.m_strName;//字段名

}

6.打开excel文件

CString sDriver = "MICROSOFT EXCEL DRIVER (*.XLS)"; // Excel安装驱动

CString sSql,sExcelFile; //sExcelFile为excel的文件路径

TRY

{

// 创建进行存取的字符串

sSql.Format("DRIVER={%s};DSN='';FIRSTROWHASNAMES=1;READONLY=FALSE;CREATE_DB=/"%s/";DBQ=%s",sDriver, sExcelFile, sExcelFile);

// 创建数据库 (既Excel表格文件)

if( database.OpenEx(sSql,CDatabase::noOdbcDialog) )

{

//可以把excel作为一个数据库操作

}

}

catch(e)

{

TRACE1("Excel驱动没有安装: %s",sDriver);

AfxMessageBox("读取失败,请检查是否定义数据区Sheet1");

}

 

3. DAO连接
DAO(Data Access Object)是一组Microsoft Access/Jet数据库引擎的COM自动化接口.DAO直接与Access/Jet数据库通信.通过Jet数据库引擎,DAO也可以同其他数据库进行通信。DAO还封装了Access数据库的结构单元,通过DAO可以直接修改Access数据库的结构,而不必使用SQL的数据定义语言(DDL)。

DAO的体系结构如下: 

DAO封装的类:

(1)CdaoWorkspace:对DAO工作区(数据库处理事务管理器)的封装

(2)CdaoDatabase:对DAO数据库对象的封装,负责数据库连接.

(3)CdaoRecordset:对DAO记录集对象的封装,代表所选的一组记录.

(4)CdaoTableDef:对表定义对象的封装,代表基本表或附加表定义.

(5)CdaoQueryDef:对查询对象的封装,包含所有查询的定义.

(6)CdaoException:DAO用于接收数据库操作异常的类.

(7)CDaoFieldExchange

DAO提供了很好的数据库编程的对象模型.但是,对数据库的所有调用以及输出的数据都必须通过Access/Jet数据库引擎,这对于使用数据库应用程序,是严重的瓶颈。

DAO相对于ODBC来说,属于高层的数据库接口. 

 

4. OLE DB连接
OLE DB对ODBC进行了两方面的扩展:一是提供了数据库编程的OLE接口即COM,二是提供了一个可用于关系型和非关系型数据源的接口。

OLE DB提供了COM接口,与传统的数据库接口相比,有更好的健壮性和灵活性,具有很强的错误处理能力,能够同非关系数据源进行通信。 

与ODBC API一样,OLE DB也属于底层的数据库编程接口,OLE DB结合了ODBC对关系数据库的操作功能,并进行扩展,可以访问非关系数据库。

OLE DB访问数据库的原理如下: 

OLE DB程序结构:

OLE DB由客户(Consumer)和服务器(Provider)。客户是使用数据的应用程序,它通过OLE DB接口对数据提供者的数据进行访问和控制。OLE DB服务器是提供OLE DB接口的软件组件。根据提供的内容可以分为数据提供程序(Data Provider)和服务提供程序(Service Provider)。

程序结构原理图如下:

<1>数据提供程序

数据提供程序拥有自己的数据并把数据以表格的形式呈现给使用者使用.

<2>服务提供程序

服务提供程序是数据提供程序和使用者的结合。它是OLE DB体系结构中的中间件,它是OLE DB数据源的使用者和数据使用程序的提供者

<3>数据使用程序

数据使用程序对存储在数据提供程序中的数据进行使用和控制.

OLE DB开发程序的一般步骤:

<1>初始化COM环境

<2>连接数据源

<3>打开对话

<4>执行命令

<5>处理结果

<6>清除对象

 

应用实例:

使用OLEDB编写数据库应用程序
1  概述

OLE DB的存在为用户提供了一种统一的方法来访问所有不同种类的数据源。OLE DB可以在不同的数据源中进行转换。利用OLE DB,客户端的开发人员在进行数据访问时只需把精力集中在很少的一些细节上,而不必弄懂大量不同数据库的访问协议。

OLE DB是一套通过COM接口访问数据的ActiveX接口。这个OLE DB接口相当通用,足以提供一种访问数据的统一手段,而不管存储数据所使用的方法如何。同时,OLE DB还允许开发人员继续利用基础数据库技术的优点,而不必为了利用这些优点而把数据移出来。

2  使用ATL使用OLE DB数据使用程序

由于直接使用OLE DB的对象和接口设计数据库应用程序需要书写大量的代码。为了简化程序设计,Visual C++提供了ATL模板用于设计OLE DB数据应用程序和数据提供程序。

利用ATL模板可以很容易地将OLE DB与MFC结合起来,使数据库的参数查询等复杂的编程得到简化。MFC提供的数据库类使OLE DB的编程更具有面向对象的特性。Viual C++所提供用于OLE DB的ATL模板可分为数据提供程序的模板和数据使用程序的模板。

使用ATL模板创建数据应用程序一般有以下几步骤:

1)创建应用框架

2)加入ATL产生的模板类

3)在应用中使用产生的数据访问对象

3 不用ATL使用OLE DB数据使用程序

利用ATL模板产生数据使用程序较为简单,但适用性不广,不能动态适应数据库的变化。下面我们介绍直接使用MFC OLE DB类来生成数据使用程序。

模板的使用

OLE DB数据使用者模板是由一些模板组成的,包括如下一些模板,下面对一些常用类作一些介绍。

1)会话类

CDataSource类

CDataSource类与OLE DB的数据源对象相对应。这个类代表了OLE DB数据提供程序和数据源之间的连接。只有当数据源的连接被建立之后,才能产生会话对象,可以调用Open来打开数据源的连接。

CSession类

CSession所创建的对象代表了一个单独的数据库访问的会话。一个用CDataSource类产生的数据源对象可以创建一个或者多个会话,要在数据源对象上产生一个会话对象,需要调用函数Open()来打开。同时,会话对象还可用于创建事务操作。

CEnumeratorAccessor类

CEnumeratorAccessor类是用来访问枚举器查询后所产生的行集中可用数据提供程序的信息的访问器,可提供当前可用的数据提供程序和可见的访问器。

2)访问器类

CAcessor类

CAccessor类代表与访问器的类型。当用户知道数据库的类型和结构时,可以使用此类。它支持对一个行集采用多个访问器,并且,存放数据的缓冲区是由用户分配的。

CDynamicAccessor类

CDynamicAccessor类用来在程序运行时动态的创建访问器。当系统运行时,可以动态地从行集中获得列的信息,可根据此信息动态地创建访问器。

CManualAccessor类

CManualAccessor类中以在程序运行时将列与变量绑定或者是将参数与变量捆定。

3)行集类

CRowSet类

CRowSet类封装了行集对象和相应的接口,并且提供了一些方法用于查询、设置数据等。可以用Move()等函数进行记录移动,用GetData()函数读取数据,用Insert()、Delete()、SetData()来更新数据。

CBulkRowset类

CBulkRowset类用于在一次调用中取回多个行句柄或者对多个行进行操作。

CArrayRowset类

CArrayRowset类提供用数组下标进行数据访问。

4)命令类

CTable类

CTable类用于对数据库的简单访问,用数据源的名称得到行集,从而得到数据。

CCommand类

CCommand类用于支持命令的数据源。可以用Open()函数来执行SQL命令,也可以Prepare()函数先对命令进行准备,对于支持命令的数据源,可以提高程序的灵活性和健壮性。

在stdafx.h头文件里,加入如下代码。

#include <atlbase.h>

extern CComModule _Module;

#include <atlcom.h>

#include <atldbcli.h>

#include <atldbsch.h> // if you are using schema templates

在stdafx.cpp文件里,加入如下代码。

#include <atlimpl.cpp>

CComModule _Module;

决定使用何种类型的存取程序和行集。

获取数据

在打开数据源,会话,行集对象后就可以获取数据了。所获取的数据类型取决于所用的存取程序,可能需要绑定列。按以下步骤。

1、用正确的命令打开行集对象。

2、如果使用CManualAccessor,在使用之前与相应列进行绑定。要绑定列,可以用函数GetColumnInfo,如下所示:

// Get the column information

ULONG ulColumns  = 0;

DBCOLUMNINFO* pColumnInfo  = NULL;

LPOLESTR pStrings  = NULL;

if (rs.GetColumnInfo(&ulColumns, &pColumnInfo, &pStrings) != S_OK)

AfxThrowOLEDBException(rs.m_pRowset, IID_IColumnsInfo);

struct MYBIND* pBind = new MYBIND[ulColumns];

rs.CreateAccessor(ulColumns, &pBind[0], sizeof(MYBIND)*ulColumns);

for (ULONG l=0; l<ulColumns; l++)

rs.AddBindEntry(l+1, DBTYPE_STR, sizeof(TCHAR)*40, &pBind[l].szValue, NULL, &pBind[l].dwStatus);

rs.Bind();

3、用while循环来取数据。在循环中,调用MoveNext来测试光标的返回值是否为S_OK,如下所示:

while (rs.MoveNext() == S_OK)

{

// Add code to fetch data here

// If you are not using an auto accessor, call rs.GetData()

}

4、在while循环内,可以通过不同的存取程序获取数据。

1)如果使用的是CAccessor类,可以通过使用它们的数据成员进行直接访问。如下所示:

2)如果使用的是CDynamicAccessor 或CDynamicParameterAccessor 类,可以通过GetValue或GetColumn函数来获取数据。可以用GetType来获取所用数据类型。如下所示:

while (rs.MoveNext() == S_OK)

{

// Use the dynamic accessor functions to retrieve your

// data

ULONG ulColumns = rs.GetColumnCount();

for (ULONG i=0; i<ulColumns; i++)

{

rs.GetValue(i);

}

}

3)如果使用的是CManualAccessor,可以指定自己的数据成员,绑定它们。就可以直接存取。如下所示:

while (rs.MoveNext() == S_OK)

{

// Use the data members you specified in the calls to

// AddBindEntry.

wsprintf("%s", szFoo);

}

决定行集的数据类型

在运行时决定数据类型,要用动态或手工的存取程序。如果用的是手工存取程序,可以用GetColumnInfo函数得到行集的列信息。从这里可以得到数据类型。

4  总结

由于现在有多种数据源,,想要对这些数据进行访问管理的唯一途径就是通过一些同类机制来实现,如OLE DB。高级OLE DB结构分成两部分:客户和提供者。客户使用由提供者生成的数据。

就像其它基于COM的多数结构一样,OLE DB的开发人员需要实现很多的接口,其中大部分是模板文件。

当生成一个客户对象时,可以通过ATL对象向导指向一个数据源而创建一个简单的客户。ATL对象向导将会检查数据源并创建数据库的客户端代理。从那里,可以通过OLE DB客户模板使用标准的浏览函数。

当生成一个提供者时,向导提供了一个很好的开端,它们仅仅是生成了一个简单的提供者来列举某一目录下的文件。然后,提供者模板包含了OLE DB支持的完全补充内容。在这种支持下,用户可以创建OLE DB提供者,来实现行集定位策略、数据的读写以及建立书签。

 

应用案例:

使用OLE DB读写SQL Server
在需要对数据库进行操作时,OLE DB总是被认为是一种效率最高但最难的方法。但是以我最近使用OLE DB的经验看来,OLE DB的效率高则高矣,但却一点都不难。说它难恐怕主要是因为可参考的中文资料太少,为了帮助以后需要接触OLE DB的同行,我撰写了这篇文章。本文包含如下内容:

1. OLE DB写数据库;

2. OLE DB读数据库;

3. OLE DB对二进制数据(text、ntext、image等)的处理。

首先来看看对SQL Server进行写操作的代码,有一定VC基础的读者应该可以很顺利地看懂。OLE DB写数据库,就是这么简单!

 

注:

1.以下代码中使用的模板类EAutoReleasePtr<T>与ATL中的CComPtr<T>类似,是一个在析构时自动调用Release的类。CComPtr<T>的代码在ATLBASE.H中定义。

2.以下代码均在UNICODE环境下编译,因为执行的SQL语句必须是UNICODE的。设置工程为UNICODE的方法是:首先在project->settings->C/C++的属性页中的Preprocessor中,删除_MBCS写入UNICODE,_UNICODE。然后在link属性页中Category中选择output,在Entry-Point symbol 中添加wWinMainCRTStartup。

 

EAutoReleasePtr<IDBInitialize> pIDBInitialize;

HRESULT hResult = ConnectDatabase( &pIDBInitialize, _T("127.0.0.1"), _T(“sa”), _T("password") );

if( FAILED( hResult ) )

{

//失败,可能是因为数据库没有启动、用户名密码错等等

return;

}

EAutoReleasePtr<IOpenRowset> pIOpenRowset;

hResult = CreateSession( pIDBInitialize, &pIOpenRowset );

if( FAILED( hResult ) )

{

//出错

return;

}

EAutoReleasePtr<ICommand> pICommand;

EAutoReleasePtr<ICommandText> pICommandText;

hResult = CreateCommand( pIOpenRowset, &pICommand, &pICommandText );

if( FAILED( hResult ) )

{

//出错

return;

}

hResult = ExecuteSQL( pICommand, pICommandText, _T("USE PBDATA") );

if( FAILED( hResult ) )

{

//如果这里失败,那就是SQL语句执行失败。在此处,就是PBDATA还未创建

return;

}

 

// 创建表

ExecuteSQL( pICommand, pICommandText, _T("CREATE TABLE 2005_1(Volume real NOT NULL,ID int NOT NULL IDENTITY)") );

 

// 添加记录

ExecuteSQL( pICommand, pICommandText, _T("INSERT INTO 2005_1 VALUES(100.0)") );

 

//...

 

其中几个函数的代码如下:

 

HRESULT ConnectDatabase( IDBInitialize** ppIDBInitialize, LPCTSTR pszDataSource, LPCTSTR pszUserID, LPCTSTR pszPassword )

{

ASSERT( ppIDBInitialize != NULL && pszDataSource != NULL && pszUserID != NULL && pszPassword != NULL );

 

UINT uTimeout = 15U; // 连接数据库超时(秒)

TCHAR szInitStr[1024];

VERIFY( 1023 >= wsprintf( szInitStr, _T("Provider=SQLOLEDB;Data Source=%s;Initial Catalog=master;User Id=%s;Password=%s;Connect Timeout=%u"), pszDataSource, pszUserID, pszPassword, uTimeout ) );

//Initial Catalog=master指明连接成功后,"USE master"。

EAutoReleasePtr<IDataInitialize> pIDataInitialize;

HRESULT hResult = ::CoCreateInstance( CLSID_MSDAINITIALIZE, NULL, CLSCTX_INPROC_SERVER,

IID_IDataInitialize, ( void** )&pIDataInitialize );

if( FAILED( hResult ) )

{

return hResult;

}

 

EAutoReleasePtr<IDBInitialize> pIDBInitialize;

hResult = pIDataInitialize->GetDataSource( NULL, CLSCTX_INPROC_SERVER, ( LPCOLESTR )szInitStr,

IID_IDBInitialize, ( IUnknown** )&pIDBInitialize);

if( FAILED( hResult ) )

{

return hResult;

}

hResult = pIDBInitialize->Initialize( );

if( FAILED( hResult ) )

{

return hResult;

}

* ppIDBInitialize = pIDBInitialize.Detach( );

return S_OK;

}

 

HRESULT CreateSession( IDBInitialize* pIDBInitialize, IOpenRowset** ppIOpenRowset )

{

ASSERT( pIDBInitialize != NULL && ppIOpenRowset != NULL );

EAutoReleasePtr<IDBCreateSession> pSession;

HRESULT hResult = pIDBInitialize->QueryInterface( IID_IDBCreateSession, ( void** )&pSession );

if( FAILED( hResult ) )

{

return hResult;

}

EAutoReleasePtr<IOpenRowset> pIOpenRowset;

hResult = pSession->CreateSession( NULL, IID_IOpenRowset, ( IUnknown** )&pIOpenRowset );

if( FAILED( hResult ) )

{

return hResult;

}

* ppIOpenRowset = pIOpenRowset.Detach( );

return S_OK;

}

 

HRESULT CreateCommand( IOpenRowset* pIOpenRowset, ICommand** ppICommand, ICommandText** ppICommandText )

{

ASSERT( pIOpenRowset != NULL && ppICommand != NULL && ppICommandText != NULL );

HRESULT hResult;

EAutoReleasePtr<ICommand> pICommand;

{

EAutoReleasePtr<IDBCreateCommand> pICreateCommand;

hResult = pIOpenRowset->QueryInterface( IID_IDBCreateCommand, ( void** )&pICreateCommand );

if( FAILED( hResult ) )

{

return hResult;

}

 

hResult = pICreateCommand->CreateCommand( NULL, IID_ICommand, (IUnknown**)&pICommand );

if( FAILED( hResult ) )

{

return hResult;

}

}

EAutoReleasePtr<ICommandText> pICommandText;

hResult = pICommand->QueryInterface( &pICommandText );

if( FAILED( hResult ) )

{

return hResult;

}

* ppICommand = pICommand.Detach( );

* ppICommandText = pICommandText.Detach( );

return S_OK;

}

 

HRESULT ExecuteSQL( ICommand* pICommand, ICommandText* pICommandText, LPCTSTR pszCommand, LONG* plRowsAffected )

{

ASSERT( pICommand != NULL && pICommandText != NULL && pszCommand != NULL && pszCommand[0] != 0 );

 

HRESULT hResult = pICommandText->SetCommandText( DBGUID_DBSQL, ( LPCOLESTR )pszCommand );

if( FAILED( hResult ) )

{

return hResult;

}

LONG lAffected;

hResult = pICommand->Execute( NULL, IID_NULL, NULL, plRowsAffected == NULL ? &lAffected : plRowsAffected, ( IUnknown** )NULL );

return hResult;

}

 

以上就是写数据库的全部代码了,是不是很简单呢?下面再来读的。

 

// 先用与上面代码中一样的步骤获取pICommand,pICommandText。此处省略

 

HRESULT hResult = pICommandText->SetCommandText( DBGUID_DBSQL, ( LPCOLESTR )_T("SELECT Volume FROM 2005_1 WHERE ID = @@IDENTITY") ); //取我们刚刚添加的那一条记录

if( FAILED( hResult ) )

{

return;

}

 

LONG lAffected;

EAutoReleasePtr<IRowset> pIRowset;

hResult = pICommand->Execute( NULL, IID_IRowset, NULL, &lAffected, ( IUnknown** )&pIRowset );

if( FAILED( hResult ) )

{

return;

}

EAutoReleasePtr<IAccessor> pIAccessor;

hResult = pIRowset->QueryInterface( IID_IAccessor, ( void** )&pIAccessor );

if( FAILED( hResult ) )

{

return;

}

 

// 一个根据表中各字段的数值类型而定义的结构,用于存储返回的各字段的值

struct CLoadLastFromDB

{

DBSTATUS dwdsVolume;

DWORD   dwLenVolume;

float    fVolume;

};

 

// 此处我们只查询了一个字段。如果要查询多个字段,CLoadLastFromDB中要添加相应的字段定义,下面的dbBinding也要相应扩充。dbBinding[].iOrdinal要分别指向各个字段,dbBinding[].wType要根据字段类型赋合适的值。

 

DBBINDING dbBinding[1];

dbBinding[0].iOrdinal    = 1; // Volume 字段的位置,从 1 开始

dbBinding[0].obValue     = offsetof( CLoadLastFromDB, fVolume );

dbBinding[0].obLength   = offsetof( CLoadLastFromDB, dwLenVolume );

dbBinding[0].obStatus   = offsetof( CLoadLastFromDB, dwdsVolume );

dbBinding[0].pTypeInfo   = NULL;

dbBinding[0].pObject     = NULL;

dbBinding[0].pBindExt   = NULL;

dbBinding[0].dwPart    = DBPART_VALUE | DBPART_STATUS | DBPART_LENGTH;

dbBinding[0].dwMemOwner = DBMEMOWNER_CLIENTOWNED;

dbBinding[0].eParamIO   = DBPARAMIO_NOTPARAM;

dbBinding[0].cbMaxLen   = 0;

dbBinding[0].dwFlags    = 0;

dbBinding[0].wType    = DBTYPE_R4; // float就是DBTYPE_R4,int就是DBTYPE_I4。参见MSDN

dbBinding[0].bPrecision   = 0;

dbBinding[0].bScale     = 0;

 

HACCESSOR hAccessor = DB_NULL_HACCESSOR;

DBBINDSTATUS dbs[1];

hResult = pIAccessor->CreateAccessor( DBACCESSOR_ROWDATA, 1, dbBinding, sizeof( CLoadLastDataFromDB ), &hAccessor, dbs );

if( FAILED( hResult ) )

{

return;

}

ASSERT( dbs[0] == DBBINDSTATUS_OK );

ULONG uRowsObtained = 0;

HROW  hRows[1];          // 这里我们只查询了最新的那一条记录

HROW* phRows = hRows;

CLoadLastFromDB rmd;

hResult = pIRowset->GetNextRows( NULL, 0, 1, &uRowsObtained, &phRows );

if( SUCCEEDED( hResult ) && uRowsObtained != 0U )

{

hResult = pIRowset->GetData( phRows[0], hAccessor, &rmd );

if( FAILED( hResult ) )

{

ASSERT( FALSE );

}

ASSERT( rmd.dwdsVolume == DBSTATUS_S_OK );

// rmd.fVolume 就是我们要取的值

}

pIRowset->ReleaseRows( uRowsObtained, phRows, NULL, NULL, NULL );

pIAccessor->ReleaseAccessor( hAccessor, NULL );

pIAccessor.Release( );

pIRowset.Release( );

 

读操作也完成了,是不是仍然很简单呢?下面我们再来看看最麻烦的二进制数据(text、ntext、image等)的读写。要实现BLOB数据的读写,我们需要一个辅助的类,定义如下:

 

class CSequentialStream : public ISequentialStream  // BLOB 数据访问类

{

public:

CSequentialStream( );

virtual ~CSequentialStream( );

virtual BOOL Seek( ULONG uPosition );

virtual BOOL Clear( );

virtual ULONG GetLength( ) { return m_uBufferUsed; };

virtual operator void* const( ) { return m_pBuffer; };

STDMETHODIMP_( ULONG ) AddRef( ) { return ++ m_uRefCount; };

STDMETHODIMP_( ULONG ) Release( ) { ASSERT( m_uRefCount != 0U );

-- m_uRefCount;

if( m_uRefCount == 0U ) { delete this; } return m_uRefCount; };

STDMETHODIMP QueryInterface( REFIID riid, LPVOID* ppv );

STDMETHODIMP Read( void __RPC_FAR* pv, ULONG cb, ULONG __RPC_FAR* pcbRead );

STDMETHODIMP Write( const void __RPC_FAR* pv, ULONG cb, ULONG __RPC_FAR* pcbWritten );

void  ResetPosition( ) { m_uPosition = 0U; };

HRESULT PreAllocBuffer( ULONG uSize );

 

private:

ULONG m_uRefCount;   // reference count

void* m_pBuffer;     // buffer

ULONG m_uBufferUsed;  // buffer used

ULONG m_uBufferSize; // buffer size

ULONG m_uPosition;   // current index position in the buffer

};

 

实现如下:

 

CSequentialStream::CSequentialStream( ) : m_uRefCount( 0U ), m_pBuffer( NULL ), m_uBufferUsed( 0U ), m_uBufferSize( 0U ), m_uPosition( 0U )

{

AddRef( );

}

 

CSequentialStream::~CSequentialStream( )

{

Clear( );

}

 

HRESULT CSequentialStream::QueryInterface( REFIID riid, void** ppv )

{

if( riid == IID_IUnknown || riid == IID_ISequentialStream )

{

* ppv = this;

( ( IUnknown* )*ppv )->AddRef( );

return S_OK;

}

* ppv = NULL;

return E_NOINTERFACE;

}

BOOL CSequentialStream::Seek( ULONG uPosition )

{

ASSERT( uPosition < m_uBufferUsed );

m_uPosition = uPosition;

return TRUE;

}

 

BOOL CSequentialStream::Clear( )

{

m_uBufferUsed = 0U;

m_uBufferSize = 0U;

m_uPosition = 0U;

( m_pBuffer != NULL ? CoTaskMemFree( m_pBuffer ) : 0 );

m_pBuffer = NULL;

return TRUE;

}

 

HRESULT CSequentialStream::PreAllocBuffer( ULONG uSize )

{

if( m_uBufferSize < uSize )

{

m_uBufferSize = uSize;

m_pBuffer = CoTaskMemRealloc( m_pBuffer, m_uBufferSize );

if( m_pBuffer == NULL )

{

Clear( );

return STG_E_INSUFFICIENTMEMORY;

}

}

return S_OK;

}

 

HRESULT CSequentialStream::Read( void* pv, ULONG cb, ULONG* pcbRead )

{

( pcbRead != NULL ? ( * pcbRead = 0U ) : 0 );

if( pv == NULL ) { return STG_E_INVALIDPOINTER; }

if( cb == 0U ) { return S_OK; }

ASSERT( m_uPosition <= m_uBufferUsed );

ULONG uBytesLeft = m_uBufferUsed - m_uPosition;

 

if( uBytesLeft == 0U ) { return S_FALSE; } //no more bytes

 

ULONG uBytesRead = ( cb > uBytesLeft ? uBytesLeft : cb );

memcpy( pv, ( BYTE* )m_pBuffer + m_uPosition, uBytesRead );

m_uPosition += uBytesRead;

 

( pcbRead != NULL ? ( * pcbRead = uBytesRead ) : 0 );

return ( cb != uBytesRead ? S_FALSE : S_OK );

}

 

HRESULT CSequentialStream::Write( const void* pv, ULONG cb, ULONG* pcbWritten )

{

if( pv == NULL ) { return STG_E_INVALIDPOINTER; }

( pcbWritten != NULL ? ( * pcbWritten = 0U ) : 0 );

if( cb == 0U ){ return S_OK; }

 

ASSERT( m_uPosition <= m_uBufferUsed );

if( m_uBufferSize < m_uPosition + cb )

{

m_uBufferSize = m_uPosition + cb;

m_pBuffer = CoTaskMemRealloc( m_pBuffer, m_uBufferSize );

if( m_pBuffer == NULL )

{

Clear( );

return STG_E_INSUFFICIENTMEMORY;

}

}

m_uBufferUsed = m_uPosition + cb;

memcpy( ( BYTE* )m_pBuffer + m_uPosition, pv, cb );

m_uPosition += cb;

( pcbWritten != NULL ? ( * pcbWritten = cb ) : 0 );

return S_OK;

}

 

下面我们开始往一个包含ntext字段的表中添加记录。假设这个表(News)的结构为:ID int NOT NULL IDENTITY、Title nchar(80)、 Contents ntext。

 

// 先将记录添加进去,ntext字段留空。我们稍后再更新ntext的内容。

HRESULT hResult = ExecuteSQL( pICommand, pICommandText, _T("INSERT INTO News VALUES('TEST','')") );

 

DBPROP dbProp;

dbPropSet.guidPropertySet = DBPROPSET_ROWSET;

dbPropSet.cProperties    = 1;

dbPropSet.rgProperties   = &dbProp;

DBPROPSET dbPropSet;

dbPropSet.rgProperties[0].dwPropertyID   = DBPROP_UPDATABILITY;

dbPropSet.rgProperties[0].dwOptions   = DBPROPOPTIONS_REQUIRED;

dbPropSet.rgProperties[0].dwStatus     = DBPROPSTATUS_OK;

dbPropSet.rgProperties[0].colid      = DB_NULLID;

dbPropSet.rgProperties[0].vValue.vt     = VT_I4;

V_I4( &dbPropSet.rgProperties[0].vValue ) = DBPROPVAL_UP_CHANGE;

 

EAutoReleasePtr<ICommandProperties> pICommandProperties;

hResult = pICommandText->QueryInterface( IID_ICommandProperties, ( void** )&pICommandProperties );

 

// 设置 Rowset 属性为“可以更新某字段的值”

hResult = pICommandProperties->SetProperties( 1, &dbPropSet );

 

hResult = pICommandText->SetCommandText( DBGUID_DBSQL, ( LPCOLESTR )L"SELECT Contents FROM News WHERE ID = @@IDENTITY" );

 

LONG lAffected;

EAutoReleasePtr<IRowsetChange> pIRowsetChange;

hResult = pICommand->Execute( NULL, IID_IRowsetChange, NULL, &lAffected, ( IUnknown** )&pIRowsetChange );

EAutoReleasePtr<IAccessor> pIAccessor;

hResult = pIRowsetChange->QueryInterface( IID_IAccessor, ( void** )&pIAccessor );

 

struct BLOBDATA

{

DBSTATUS     dwStatus;

DWORD      dwLength;

ISequentialStream* pISeqStream;

};

 

// 有关DBOBJECT、DBBINDING的设置,建议参考MSDN,很容易懂。

DBOBJECT dbObj;

dbObj.dwFlags = STGM_READ;

dbObj.iid   = IID_ISequentialStream;

 

DBBINDING dbBinding;

dbBinding.iOrdinal = 1;          // BLOB 字段的位置,从 1 开始

dbBinding.obValue  = offsetof( BLOBDATA, pISeqStream );

dbBinding.obLength = offsetof( BLOBDATA, dwLength );

dbBinding.obStatus = offsetof( BLOBDATA, dwStatus );

dbBinding.pTypeInfo  = NULL;

dbBinding.pObject  = &dbObj;

dbBinding.pBindExt = NULL;

dbBinding.dwPart   =  DBPART_VALUE | DBPART_STATUS | DBPART_LENGTH;

dbBinding.dwMemOwner = DBMEMOWNER_CLIENTOWNED;

dbBinding.eParamIO = DBPARAMIO_NOTPARAM;

dbBinding.cbMaxLen = 0;

dbBinding.dwFlags  = 0;

dbBinding.wType  = DBTYPE_IUNKNOWN;

dbBinding.bPrecision = 0;

dbBinding.bScale   = 0;

 

HACCESSOR hAccessor = DB_NULL_HACCESSOR;

DBBINDSTATUS dbs;

hResult = pIAccessor->CreateAccessor( DBACCESSOR_ROWDATA, 1, &dbBinding, sizeof( BLOBDATA ), &hAccessor, &dbs );

EAutoReleasePtr<IRowset> pIRowset;

hResult = pIRowsetChange->QueryInterface( IID_IRowset, ( void** )&pIRowset );

ULONG uRowsObtained = 0;

HROW* phRows = NULL;

hResult = pIRowset->GetNextRows( NULL, 0, 1, &uRowsObtained, &phRows );

CSequentialStream* pss = new CSequentialStream;

pss->PreAllocBuffer( 1024 );         // 预先分配好内存,并读入数据

pss->Write( pszSomebuffer, 512, NULL );    // pss->Write可以连续调用

pss->Write( pszSomebuffer+512, 512, NULL );

pss->ResetPosition( );

BLOBDATA bd;

bd.pISeqStream = ( ISequentialStream* )pss;

bd.dwStatus  = DBSTATUS_S_OK;

bd.dwLength  = pss->GetLength( );

 

// 将 BLOB 数据写入到数据库

hResult = pIRowsetChange->SetData( phRows[0], hAccessor, &bd );

 

pIAccessor->ReleaseAccessor( hAccessor, NULL );

pIRowset->ReleaseRows( uRowsObtained, phRows, NULL, NULL, NULL );

 

// pss was released by pIRowsetChange->SetData.

 

这样,我们就完成了一条记录的添加。读取BLOB字段的代码跟上面的完全类似,只要把

hResult = pIRowset->GetNextRows( NULL, 0, 1, &uRowsObtained, &phRows );

后面的那些改成下面的代码即可。

 

BLOBDATA bd;

hResult = pIRowset->GetData( phRows[0], hAccessor, &bd );

if( bd.dwStatus == DBSTATUS_S_ISNULL )

{

// 此字段为空

}

else if( bd.dwStatus != DBSTATUS_S_OK || bd.pISeqStream == NULL )

{

// 失败

}

else

{

// 从系统分配的 ISequentialStream 接口读入 BLOB 数据

BYTE szReadBuffer[1024];

for( ULONG uRead = 0U; ; )

{

if( FAILED( bd.pISeqStream->Read( szReadBuffer, 1024, &uRead ) ) )

{

break;

}

//szReadBuffer中就包含了BLOB字段的数据

if( uRead != 1024 )

{

break;

}

}

bd.pISeqStream->Release( );

}

pIAccessor->ReleaseAccessor( hAccessor, NULL );

pIRowset->ReleaseRows( uRowsObtained, phRows, NULL, NULL, NULL );

 

5. OLE DB Templates连接
使用OLE DB接口编程属于最低可能层,代码冗长并且很难维护。因此MS Visual Studio对OLE DB进一步抽象和封装,提供COM OLE DB  Templates这个可行的中间层,从而简化了OLE DB应用程序的编写。

OLE DB Templates编写客户数据库程序方法:

<1>以MFC AppWizard为向导建立应用程序框架,添加OLE DB支持的头文件,然后使用OLE DB类进行数据库应用开发。

<2>以ATL COM AppWizard为向导建立应用程序框架,该框架直接支持OLE DB模板类。

OLE DB Templates包括:Consumer Templates和Provider Templates。

(1) Consumer Templates使用者模板

使用者模板(Consumer Templates)体系结构:

 

(2) Provider Templates服务器模板

服务器模板类体系结构:

 

6. ADO连接
ADO(ActiveX Data Object,ActiveX数据对象)是MS为最新和最强大的数据访问接口OLE DB而设计,是一个便于使用的应用程序层接口。ADO是一种面向对象的、与语言无关的(Language_Neutral)数据访问应用编程接口。它对OLE DB API进行封装,实现对数据的高层访问,同时它也提供了多语言的访问技术,此外,由于ADO提供了访问自动化接口,它也支持脚本语言。ADO最主要的优点在于易于使用、速度快、内存支出少和磁盘遗迹小。ADO是用来访问OLE DB的数据库技术。在模型层次上它基于OLE DB,但在应用上又高于OLE DB,因此它简化了对对象模型的操作,并且不依赖于对象之间的相互层次关系。但是OLE的接口可以数据提供程序、服务提供程序和数据使用程序使用,而ADO所提供的对象只能被数据应用程序使用。并且,ADO对象使用了OLE DB服务提供程序和OLE DB数据提供程序所提供的接口和服务。

(1)ADO访问数据库的结构原理图:

(2)ADO对象模型:

ADO对象模型包括以下关键对象:

<1>Connection对象:在数据库应用里操作数据源都通过该对象,这是数据交换的环境,代表与数据源的一个会话。

<2>Command对象:是一个对数据源执行命令的定义。

<3>Parameter对象:用于制定参数化查询或者存储过程的参数。

<4>Recordset对象:是执行结果集存储到本地的ADO对象。

<5>Field对象:ADO中对列进行操作的对象。

<6>Error对象:对ADO数据操作时发生错误的详细描述。

<7>Property对象:代表一个由提供者定义的ADO对象的动态特征。

ADO对象编程模型:

Parameters

Collection

Execute

Source

Error Collection

(Optional) Active Fields

Connection Collection

 

(3)ADO编程一般步骤:

<1>创建一个Connection对象。

<2>打开数据源,建立同数据源的连接。

<3>执行一个SQL命令。

<4>使用结果集。

<5>终止连接。

(4)ADO的数据库访问规范

引入ADO支持

#import   Program Files/Common Files/System/ado/msado*.dll

初始化和释放ADO环境:

CoInitialize(NULL);

CoUninitialize(); 

 

封装的ADO类A set of ADO classes:

http://blog.csdn.net/byxdaz/archive/2008/06/19/2563174.aspx

ADO编程小结:http://hi.baidu.com/sunkanghome/blog/item/273171f9ffb4735c252df286.html

http://hi.baidu.com/sunkanghome/blog/item/cea70101bdb177031d95839a.html

应用实例:

用ADO进行数据库编程
1. 生成应用程序框架并初始化OLE/COM库环境

创建一个标准的MFC AppWizard(exe)应用程序,然后在使用ADO数据库的InitInstance函数中初始化OLE/COM库(因为ADO库是一个COM DLL库)。

本例为:

BOOL CAdotestDlg::OnInitDialog()

{

::CoInitialize(NULL); //初始化OLE/COM库环境

}

程序最后要调用 ::CoUninitialize();//释放程序占用的COM 资源。

另外:

m_pRecordset->Close(); 注意!!!不要多次关闭!!!!!!!!!!!!

m_pConnection->Close();

m_pRecordset = NULL;

m_pConnection = NULL;

2. 引入ADO库文件

使用ADO前必须在工程的stdafx.h文件最后用直接引入符号#import引入ADO库文件,以使编译器能正确编译。代码如下:

#import "C:/Program Files/common files/system/ado/msado15.dll" no_namespace rename("EOF","adoEOF")

ADO类的定义是作为一种资源存储在ADO DLL(msado15.dll)中,在其内部称为类型库。类型库描述了自治接口,以及C++使用的COM vtable接口。当使用#import指令时,在运行时Visual C++需要从ADO DLL中读取这个类型库,并以此创建一组C++头文件。这些头文件具有.tli 和.tlh扩展名,读者可以在项目的目录下找到这两个文件。在C++程序代码中调用的ADO类要在这些文件中定义。

程序的第三行指示ADO对象不使用名称空间。在有些应用程序中,由于应用程序中的对象与ADO中的对象之间可能会出现命名冲突,所以有必要使用名称空间。如果要使用名称空间,则可把第三行程序修改为: rename_namespace("AdoNS")。第四行代码将ADO中的EOF(文件结束)更名为adoEOF,以避免与定义了自己的EOF的其他库冲突。

3.利用智能指针进行数据库操作

在CaboutDlg头文件中定义两个ADO智能指针类实例,并在对话框中加入一个ListCtrl。

 

class CAdotestDlg : public CDialog

{

_ConnectionPtr m_pConnection;

_RecordsetPtr m_pRecordset;

ClistCtrl m_List;

......

}  

ADO库包含三个智能指针:_ConnectionPtr、_CommandPtr和_RecordsetPtr。

_ConnectionPtr通常被用来创建一个数据连接或执行一条不返回任何结果的SQL语句,如一个存储过程。

_CommandPtr返回一个记录集。它提供了一种简单的方法来执行返回记录集的存储过程和SQL语句。在使用_CommandPtr接口时,可以利用全局_ConnectionPtr接口,也可以在_CommandPtr接口里直接使用连接串。_RecordsetPtr是一个记录集对象。与以上两种对象相比,它对记录集提供了更多的控制功能,如记录锁定、游标控制等。

在使用ADO程序的事件响应中OnButton1加入以下代码:

 

void CAdotestDlg::OnButton1()

{

m_List.ResetContent();

m_pConnection.CreateInstance(_uuidof(Connection)); //初始化Connection指针

m_pRecordset.CreateInstance(_uuidof(Recordset));//初始化Recordset指针

try

{

m_pConnection->Open("DSN=ADOTest","","",0); //连接叫作ADOTest的ODBC数据源

//注意:这是连接不需要用户ID或密码的open 函数

// 否则形式为 ->Open("DSN=test;uid=sa;pwd=123;","","",0);

// 执行SQL语句得到一个记录集把其指针赋值给m_pRecordset

CString strSql="select * from middle";

BSTR bstrSQL = strSql.AllocSysString();

m_pRecordset->Open(bstrSQL,(IDispatch*)m_pConnection,adOpenDynamic,adLockOptimistic,adCmdText);

//adOpenDynamic:动态 adLockOptimistic乐观封锁法 adCmdText:文本查询语句

while(!m_pRecordset->adoEOF)//遍历所有记录

{

//取纪录字段值方式之一

_variant_t TheValue; //VARIANT数据类型

TheValue = m_pRecordset->GetCollect("BIG_NAME");//得到字段BIG_NAME的值

if(TheValue.vt!=VT_NULL)

m_List.AddString((char*)_bstr_t(TheValue));

//将该值加入到列表控件中

//取纪录字段值方式之二

// _bstr_t TheValue1=m_pRecordset->Fields->GetItem("BIG_NAME")->Value;

// CString temp=TheValue1.copy();

// m_List.AddString(temp);

//数据类型转换

_variant_t vUsername,vBirthday,vID,vOld;

TRACE("id:%d,姓名:%s,年龄:%d,生日:%s/r/n",

vID.lVal,(LPCTSTR)(_bstr_t)vUsername,vOld.lVal,(LPCTSTR)(_bstr_t)vBirthday);

m_pRecordset->MoveNext();//转到下一条纪录

}

m_pRecordset->Close();

m_pConnection->Close();

}

catch (_com_error e)//异常处理

{

AfxMessageBox(e.ErrorMessage());

}

m_pRecordset->Close(); //注意!!!不要多次关闭!!!!否则会出错

m_pConnection->Close();

m_pRecordset = NULL;

m_pConnection = NULL;

}

程序中通过_variant_t和_bstr_t转换COM对象和C++类型的数据, _variant_t类封装了OLE自治VARIANT数据类型。在C++中使用_variant_t类要比直接使用VARIANT数据类型容易得多。

好,编译后该程序就能运行了,但记住运行前要创建一个叫ADOTest的ODBC数据源。该程序将把表middle中的BIG_NAME字段值显示在列表控件中。

4.执行SQL命令并取得结果记录集

为了取得结果记录集,我们定义一个指向Recordset对象的指针:_RecordsetPtr m_pRecordset;

并为其创建Recordset对象的实例: m_pRecordset.CreateInstance("ADODB.Recordset");

SQL命令的执行可以采用多种形式,下面我们一进行阐述。

(1)利用Connection对象的Execute方法执行SQL命令

Execute方法的原型如下所示:

_RecordsetPtr Connection15::Execute ( _bstr_t CommandText, VARIANT * RecordsAffected, long Options )

其中CommandText是命令字串,通常是SQL命令。

参数RecordsAffected是操作完成后所影响的行数,

参数Options表示CommandText中内容的类型,Options可以取如下值之一:

adCmdText:表明CommandText是文本命令

adCmdTable:表明CommandText是一个表名

adCmdProc:表明CommandText是一个存储过程

adCmdUnknown:未知

Execute执行完后返回一个指向记录集的指针,下面我们给出具体代码并作说明。

_variant_t RecordsAffected;

///执行SQL命令:CREATE TABLE创建表格users,users包含四个字段:整形ID,字符串username,整形old,日期型birthday

m_pConnection->Execute("CREATE TABLE users(ID INTEGER,username TEXT,old INTEGER,birthday DATETIME)",

&RecordsAffected,

adCmdText);

///往表格里面添加记录

m_pConnection->Execute("INSERT INTO users(ID,username,old,birthday) VALUES (1, 'Washington',25,'1970/1/1')",&RecordsAffected,adCmdText);

///将所有记录old字段的值加一

m_pConnection->Execute("UPDATE users SET old = old+1",&RecordsAffected,adCmdText);

///执行SQL统计命令得到包含记录条数的记录集

m_pRecordset =  m_pConnection->Execute("SELECT COUNT(*) FROM users",&RecordsAffected,adCmdText);

_variant_t vIndex = (long)0;

_variant_t vCount = m_pRecordset->GetCollect(vIndex);///取得第一个字段的值放入vCount变量

上两句可以写成— _variant_t vCount = m_pRecordset->GetCollect((_variant_t)((long)0));

m_pRecordset->Close();///关闭记录集

CString message;

message.Format("共有%d条记录",vCount.lVal);

AfxMessageBox(message);///显示当前记录条数

(2)利用Command对象来执行SQL命令

_CommandPtr m_pCommand;

m_pCommand.CreateInstance("ADODB.Command");

_variant_t vNULL;

vNULL.vt = VT_ERROR;

vNULL.scode = DISP_E_PARAMNOTFOUND;///定义为无参数

m_pCommand->ActiveConnection = m_pConnection;///非常关键的一句,将建立的连接赋值给它

m_pCommand->CommandText = "SELECT * FROM users";///命令字串

m_pRecordset = m_pCommand->Execute(&vNULL,&vNULL,adCmdText);///执行命令,取得记录集

在这段代码中我们只是用Command对象来执行了SELECT查询语句,Command对象在进行存储过程的调用中能真正体现它的作用。下次我们将详细介绍。

(3)直接用Recordset对象进行查询取得记录集

实例——

void CGmsaDlg::OnDBSelect()

{

// TODO: Add your control notification handler code here

_RecordsetPtr Rs1;  //定义Recordset对象

_bstr_t Connect("DSN=GMS;UID=sa;PWD=;");//定义连接字符串

_bstr_t Source ("SELECT count(*) FROM buaa.mdb010");  //要执行的SQL语句

::CoInitialize(NULL);  //初始化Rs1对象

HRESUL hr = Rs1.CreateInstance( __uuidof( Recordset ) );

//省略对返回值hr的判断

Rs1->Open( Source,

Connect,

adOpenForwardOnly,

adLockReadOnly,

-1 );

_variant_t temp=Rs1->GetCollect(_variant_t((long)0));

CString strTemp=(char* )(_bstr_t)temp;

MessageBox("OK!"+strTemp);

}

例如

m_pRecordset->Open("SELECT * FROM users",

_variant_t((IDispatch *)m_pConnection,true),

adOpenStatic,

adLockOptimistic,

adCmdText);

Open方法的原型是这样的:

HRESULT Recordset15::Open ( const _variant_t & Source,

const _variant_t & ActiveConnection,

enum CursorTypeEnum CursorType,

enum LockTypeEnum LockType,

long Options )

其中:

①Source是数据查询字符串

②ActiveConnection是已经建立好的连接(我们需要用Connection对象指针来构造一个_variant_t对象)

③CursorType光标类型,它可以是以下值之一,请看这个枚举结构:

enum CursorTypeEnum

{

adOpenUnspecified = -1,///不作特别指定

adOpenForwardOnly = 0,///前滚静态光标。这种光标只能向前浏览记录集,比如用MoveNext向前滚动,这种方式可以提高浏览速度。但诸如BookMark,RecordCount,AbsolutePosition,AbsolutePage都不能使用

adOpenKeyset = 1,///采用这种光标的记录集看不到其它用户的新增、删除操作,但对于更新原有记录的操作对你是可见的。

adOpenDynamic = 2,///动态光标。所有数据库的操作都会立即在各用户记录集上反应出来。

adOpenStatic = 3///静态光标。它为你的记录集产生一个静态备份,但其它用户的新增、删除、更新操作对你的记录集来说是不可见的。

};

④LockType锁定类型,它可以是以下值之一,请看如下枚举结构:

enum LockTypeEnum

{

adLockUnspecified = -1,///未指定

adLockReadOnly = 1,///只读记录集

adLockPessimistic = 2,悲观锁定方式。数据在更新时锁定其它所有动作,这是最安全的锁定机制

adLockOptimistic = 3,乐观锁定方式。只有在你调用Update方法时才锁定记录。在此之前仍然可以做数据的更新、插入、删除等动作

adLockBatchOptimistic = 4,乐观分批更新。编辑时记录不会锁定,更改、插入及删除是在批处理模式下完成。

};

⑤Options可以取如下值之一:

adCmdText:表明CommandText是文本命令

adCmdTable:表明CommandText是一个表名

adCmdProc:表明CommandText是一个存储过程

adCmdUnknown:未知

5. 记录集的遍历、更新

根据我们刚才通过执行SQL命令建立好的users表,它包含四个字段:ID,username,old,birthday

以下的代码实现:打开记录集,遍历所有记录,删除第一条记录,添加三条记录,移动光标到第二条记录,

更改其年龄,保存到数据库。

_variant_t vUsername,vBirthday,vID,vOld;

_RecordsetPtr m_pRecordset;

m_pRecordset.CreateInstance("ADODB.Recordset");

m_pRecordset->Open("SELECT * FROM users",

_variant_t((IDispatch*)m_pConnection,true),

adOpenStatic,

adLockOptimistic,

adCmdText);

while(!m_pRecordset->adoEOF)

{

vID = m_pRecordset->GetCollect(_variant_t((long)0));///取得第1列的值,从0开始计数,

///你也可以直接给出列的名称,如下一行

vUsername = m_pRecordset->GetCollect("username");///取得username字段的值

vOld = m_pRecordset->GetCollect("old");

vBirthday = m_pRecordset->GetCollect("birthday");

///在DEBUG方式下的OUTPUT窗口输出记录集中的记录

if(vID.vt != VT_NULL && vUsername.vt != VT_NULL && vOld.vt != VT_NULL && vBirthday.vt != VT_NULL)

TRACE("id:%d,姓名:%s,年龄:%d,生日:%s/r/n",

vID.lVal,

(LPCTSTR)(_bstr_t)vUsername,

vOld.lVal,

(LPCTSTR)(_bstr_t)vBirthday);

m_pRecordset->MoveNext();///移到下一条记录

}

m_pRecordset->MoveFirst();///移到首条记录

m_pRecordset->Delete(adAffectCurrent);///删除当前记录

///添加三条新记录并赋值

for(int i=0;i<3;i++)

{

m_pRecordset->AddNew();///添加新记录

m_pRecordset->PutCollect("ID",_variant_t((long)(i+10)));

m_pRecordset->PutCollect("username",_variant_t("叶利钦"));

m_pRecordset->PutCollect("old",_variant_t((long)71));

m_pRecordset->PutCollect("birthday",_variant_t("1930-3-15"));

}

m_pRecordset->Move(1,_variant_t((long)adBookmarkFirst));///从第一条记录往下移动一条记录,即移动到第二条记录处

m_pRecordset->PutCollect(_variant_t("old"),_variant_t((long)45));///修改其年龄

m_pRecordset->Update();///保存到库中 

备注:多次查询可把查询过程做成一个函数ExecuteSQL让m_pRecordset获得连接指针m_pConnection查询结果

void ExecuteSQL(_ConnectionPtr  m_pConnection, _RecordsetPtr  m_pRecordset,CString strSql)

{

//执行Select 语句

BSTR bstrSQL = strSql.AllocSysString();    

try

{

m_pRecordset->Open(bstrSQL,(IDispatch*)m_pConnection,adOpenDynamic,adLockOptimistic,adCmdText);

//adOpenDynamic:动态  adLockOptimistic乐观封锁法  adCmdText:文本查询语句

}

catch(_com_error error)

{

CString errorMessage;

errorMessage.Format("%s",(LPTSTR)error.Description());

AfxMessageBox(errorMessage);

}

}   

//出错处理:

3127——没有找到目标表

3092——目标表已经存在

例如:

catch(const _com_error e)

{

AfxMessageBox(e.Description());

long errorCode=e.WCode();

if(3127==errorCode) AfxMessageBox("表不存在");

if(3092==errorCode) AfxMessageBox("表已经存在");

return FALSE;

}

7、ADO.NET

ADO.NET是一组用于和数据源进行交互的面向对象类库。

ADO.NET的主要对象有哪些?

Connection :用于连接到数据库和管理对数据库的事务;

Command :用于对数据库发出SQL命令;

DataReader :用于从数据源读取只进数据记录流;

DataSet :用于对单层数据、XML数据和关系数据进行存储、远程处理和编程;

DataAdapter :用于将数据推入DataSet,并使数据与数据库保持一致;

ADO.NET 2.0 快速入门:

http://tech.e800.com.cn/articles/2009/721/1248143977078_1.html

用VC轻松实现 ADO.net:

http://www.vckbase.com/document/viewdoc/?id=1714

 

(2)Oracle专用方法
1. OCI(Oracle Call Interface)访问
OCI(Oracle Call Interface)是由Oracle提供的一系列用于访问Oracles数据库服 务器的标准接口,它可以使用户将Oracle调用直接嵌入到高级语言中。

使用OCI应用程序访问数据库原理:

在高级语言中使用OCI编程的原理图:

 

用OCI开发Oracle客户端软件的一般流程:

<1>初始化OCI编程环境  

<2>分配必要的句柄,建立服务器连接和一个用户会话

<3>向服务器发出请求,进行必要的数据处理

<4>释放不再需要的语句和句柄

<5>终止会话和连接

 

2. Oracle Object OLE C++ Class Library
这个类库是一个提供编程接口访问Oracle对象服务器的C++类库,它是用OLE的方式实现的。Oracle提供的是一个进程内服务器,也就是服务器将与应用程序在同一个地址空间内, 它以DLL方式提供。应用程序在访问数据库之前必须先加载Oracle对象服务器(OStatup方法),然后与Oracle对象服务器通信,Oracle对象服务器其实是一些组件,它通过Oracle的OCI访问数据库。

 

Oracle对象服务器其实是一些COM组件,它通过Oracle的OCI访问数据库。

 

运用Oracle Objects for OLE C++ Class Library开发的步骤:

1>通过调用OStatup方法初始化类库。  

2>连接数据库。

3>操纵数据库 断开数据库(类库自动为你自动执行)  

4>通过调用OShutdown方法卸载类库。 

 

3. 使用OTL进行数据库编程
OTL 是 Oracle, Odbc and DB2-CLI Template Library 的缩写,是一个C++编译中操控关系数据库的模板库,它目前几乎支持所有的当前各种主流数据库,例如Oracle, MS SQL Server, Sybase, Informix, MySQL, DB2, Interbase / Firebird, PostgreSQL, SQLite, SAP/DB, TimesTen, MS ACCESS等等。OTL中直接操作Oracle主要是通过Oracle提供的OCI接口进行,进行操作DB2数据库则是通过CLI接口来进行,至于MS的数据库和其它一些数据库,则OTL只提供了ODBC来操作的方式。当然Oracle和DB2也可以由OTL间接使用ODBC的方式来进行操纵。

在MS Windows and Unix 平台下,OTL目前支持的数据库版本主要有:Oracle 7 (直接使用 OCI7), Oracle 8 (直接使用 OCI8), Oracle 8i (直接使用OCI8i), Oracle 9i (直接使用OCI9i), Oracle 10g (直接使用OCI10g), DB2 (直接使用DB2 CLI), ODBC 3.x ,ODBC 2.5。OTL最新版本为4.0,参见http://otl.sourceforge.net/,下载地址http://otl.sourceforge.net/otlv4_h.zip

优点:

a. 跨平台

b. 运行效率高,与C语言直接调用API相当

c. 开发效率高,起码比ADO.net使用起来更简单,更简洁

d. 部署容易,不需要ADO组件,不需要.net framework 等

缺点:

a. 说明文档以及范例不足够丰富(暂时性的)

其实现在它提供有377个使用范例可参考,下载地址:http://otl.sourceforge.net/otl4_examples.zip

建立数据源

1.依次点击“开始->控制面板”,打开“控制面板”界面,双击“管理工具”,然后再双击“数据源(ODBC)”,就打开了“ODBC数据源管理器”,选择“系统DSN”。

2.单击“添加”,弹出“创建新数据源”对话框,选择“Microsoft Access Driver(*.mdb)”。

3.点击“完成”,弹出“ODBC Microsoft Access安装”对话框,单击“创建”,开始创建数据库,弹出“新建数据库”对话框,添加数据库名称my_db和选择数据库存放目录,单击“确定”,创建完成,然后添加数据源名:my_db。点击“确定”。

4.然后在系统数据源中就有我们刚才添加的数据源。

5.单击“确定”,完成数据源的创建。

OTL编程实例
下面我们用一个实例来说明:

1. 创建数据表:TestTable ( ColumA int , ColumB varchar(50),ColumC varchar(50) )

2. 插入100条数据,ColumA 为数据的 id 范围:0-99 , ColumB=”Test Data %d” , 其中 %d=id 。

3. 删除表中ColumA 中小于10和大于90的数据。

4. 将ColumA为3的倍数的记录中ColumC更新为ColumB的内容。

具体代码为:

 

#include <iostream>

using namespace std;

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

#define OTL_ODBC // 编译 OTL 4.0/ODBC

// #define OTL_ODBC_UNIX // 如果在Unix下使用UnixODBC,则需要这个宏

#include "otlv4.h" // 包含 OTL 4.0 头文件

otl_connect db; // 连接对象

 

//此函数完成插入100条数据,ComulA为数据的id,范围为0-99,

//ColumB="Test Data %d",其中%d=id

void insert() 

// 向表中插入行

{

// 打开一个通用的流,以模板的方式向表中插入多项数据

otl_stream o(1, // 流的缓冲值必须设置为1

"insert into TestTable values(:f1<int>,:f2<char[50]>,:f3<char[50]>)",

// SQL 语句

db  // 连接对象

);

char tmp1[32];

char tmp2[30];

 

for(int i=0;i<100;++i){

sprintf(tmp1,"Test Data %d",i);

sprintf(tmp2,"");

o<<i<<tmp1<<tmp2;

}

}

 

//此函数完成删除表中ColumA中小于10和大于90的数据

void delete_rows()

{

long rpc=otl_cursor::direct_exec(db,"delete from TestTable where ColumA<10 or ColumA>90");

// rpc是作用效果的返回值,otl_cursor::direct_exec为直接执行sql语句

cout<<"Rows deleted: "<<rpc<<endl;

}

 

//此函数完成将ColumA为3的倍数的记录中ColumC更新为ColumB的内容

void update()

// 更新表

{

otl_stream o(1, // 缓冲值

"UPDATE TestTable "

" SET ColumC=:f2<char[50]> "

" WHERE ColumA=:f1<int>",

// UPDATE 语句

db // 连接对象

);

otl_stream c(1,"select ColumB from TestTable where ColumA=:f3<int>",db);

char temp[10];

for(int i=10;i<91;i++)

{

if(i%3==0)

{

c << i;

c >> temp;

o << temp << i;

}

}

 

}

 

int main()

{

otl_connect::otl_initialize(); // 初始化 ODBC 环境

try{

 

db.rlogon("UID=scott;PWD=tiger;DSN=my_db"); // 连接到 ODBC

//或者使用下面的连接语句方式。

//  db.rlogon("scott/tiger@firebird"); // connect to ODBC, alternative format

// of connect string

 

otl_cursor::direct_exec

(

db,

"drop table TestTable",

otl_exception::disabled // disable OTL exceptions

); // drop table

 

//这里完成表的创建

otl_cursor::direct_exec

(

db,

"create table TestTable(ColumA int, ColumB varchar(50),ColumC varchar(50))"

);  // create table

 

insert(); // insert records into the table

//  update(10); // update records in the table

delete_rows();

update();

 

}

 

catch(otl_exception& p){ // intercept OTL exceptions

cerr<<p.msg<<endl; // print out error message

cerr<<p.stm_text<<endl; // print out SQL that caused the error

cerr<<p.sqlstate<<endl; // print out SQLSTATE message

cerr<<p.var_info<<endl; // print out the variable that caused the error

}

 

db.logoff(); // disconnect from the database

 

return 0;

}

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/yuvmen/archive/2010/09/12/5879385.aspx

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第1篇 基础篇第1章 数据库原理与访问 21.1 数据库基本原理 21.1.1 概述 21.1.2 桌面数据库 31.1.3 对象数据库 31.1.4 关系数据库服务器 51.1.5 选择适用的数据库 51.2 数据库访问技术 61.2.1 概述 61.2.2 ODBC API 71.2.3 ODBC的MFC类 71.2.4 DAO与RDO 81.2.5 OLE DB与ADO 91.3 数据库操纵语言SQL 101.3.1 SQL命令 101.3.2 SQL从句 111.3.3 SQL运算符 111.3.4 SQL合计函数 111.4 小结 12第2章 COM与数据库访问 132.1 COM的基本原理 132.1.1 COM历史 132.1.2 COM结构 142.1.3 COM优势 152.1.4 COM接口 162.1.5 COM与数据库访问 172.1.6 COM与Internet 172.2 ActiveX数据库访问 182.2.1 ActiveX简介 182.2.2 ActiveX数据库访问的支持 182.3 ATL的数据库访问 192.3.1 ATL目标 192.3.2 ATL内容简介 212.3.3 ATL对数据库访问的支持 222.4 小结 22第3章 数据库开发过程 233.1 阶段1:调查与分析 233.2 阶段2:数据建模 243.3 阶段3:功能设计 243.4 阶段4:选择数据库系统 253.5 阶段5:选择数据库访问技术 253.6 阶段6:代码设计 263.7 阶段7:测试与调试 263.8 阶段8:发行产品 26第4章 VC++数据库开发基础 274.1 VC++ 6.0工程创建向导 274.2 VC++ 6.0数据库新建工具 274.3 VC++ 6.0的数据库工程 304.4 小结 33第2篇 实例篇第5章 ODBC API编程 355.1 了解ODBC API 355.2 ODBC API编程步骤 365.2.1 步骤1:连接数据源 365.2.2 步骤2:分配语句句柄 385.2.3 步骤3:准备并执行SQL语句 385.2.4 步骤4:获取结果集 395.2.5 步骤5:提交事务 415.2.6 步骤6:断开数据源连接并释放环境句柄 415.3 ODBC API编程实例 415.3.1 实例概述 415.3.2 实例实现过程 425.3.3 编译并运行ODBCDemo1工程 1095.3.4 ODBCDemo1实例小结 1115.4 本章小结 111第6章 MFC ODBC编程 1126.1 了解MFC ODBC 1126.1.1 CDatabase类 1126.1.2 CRecordSet类 1126.2 MFC ODBC数据库访问技术 1136.2.1 记录查询 1136.2.2 记录添加 1146.2.3 记录删除 1156.2.4 记录修改 1156.2.5 撤销数据库更新操作 1156.2.6 直接执行SQL语句 1156.2.7 MFC ODBC的数据库操作过程 1166.3 MFC ODBC编程实例 1166.3.1 实例概述 1166.3.2 实例实现过程 1176.3.3 编译并运行ODBCDemo2工程 1496.3.4 ODBCDemo2实例小结 1546.4 本章小结 155第7章 DAO数据库编程 1557.1 DAO的数据访问 1557.1.1 DAO对象 1557.1.2 MFC对DAO的支持 1567.1.3 DAO与ODBC的比较 1567.1.4 MFC的DAO类简介 1577.2 DAO编程实例 1607.2.1 实例概述 1607.
编 者 的 话 5 第1篇 基础篇 6 第1章 数据库原理与访问 7 1.1 数据库基本原理 7 1.1.1 概述 7 1.1.2 桌面数据库 7 1.1.3 对象数据库 8 1.1.4 关系数据库服务器 9 1.1.5 选择适用的数据库 9 1.2 数据库访问技术 10 1.2.1 概述 10 1.2.2 ODBC API 10 1.2.3 ODBC的MFC类 11 1.2.4 DAO与RDO 11 1.2.5 OLE DB与ADO 12 1.3 数据库操纵语言SQL 13 1.3.1 SQL命令 13 1.3.2 SQL从句 13 1.3.3 SQL运算符 14 1.3.4 SQL合计函数 14 1.4 小 结 14 第2章 COM与数据库访问 15 2.1 COM的基本原理 15 2.1.1 COM历史 16 2.1.2 COM结构 16 2.1.3 COM优势 17 2.1.4 COM接口 18 2.1.5 COM与数据库访问 19 2.1.6 COM与Internet 19 2.2 ACTIVEX数据库访问 19 2.2.1 ActiveX简介 19 2.2.2 ActiveX数据库访问的支持 20 2.3 ATL的数据库访问 20 2.3.1 ATL目标 20 2.3.2 ATL内容简介 22 2.3.3 ATL对数据库访问的支持 22 2.4 小 结 23 第3章 数据库开发过程 23 3.1 阶段1:调查与分析 24 3.2 阶段2:数据建模 24 3.3 阶段3:功能设计 24 3.4 阶段4:选择数据库系统 25 3.5 阶段5:选择数据库访问技术 25 3.6 阶段6:代码设计 25 3.7 阶段7:测试与调试 26 3.8 阶段8:发行产品 26 第4章 VC++数据库开发基础 26 4.1 VC++ 6.0工程创建向导 26 4.2 VC++ 6.0数据库新建工具 27 4.3 VC++ 6.0的数据库工程 29 4.4 小 结 31 第2篇 实例篇 32 第5章 ODBC API编程 33 5.1 了解ODBC API 34 5.2 ODBC API编程步骤 34 5.2.1 步骤1:连接数据源 34 5.2.2 步骤2:分配语句句柄 36 5.2.3 步骤3:准备并执行SQL语句 36 5.2.4 步骤4:获取结果集 37 5.2.5 步骤5:提交事务 38 5.2.6 步骤6:断开数据源连接并释放环境句柄 39 5.3 ODBC API编程实例 39 5.3.1 实例概述 39 5.3.2 实例实现过程 40 5.3.3 编译并运行ODBCDemo1工程 97 5.3.4 ODBCDemo1实例小结 98 5.4 本 章 小 结 99 第6章 MFC ODBC编程 100 6.1 了解MFC ODBC 100 6.1.1 CDatabase类 100 6.1.2 CRecordSet类 100 6.2 MFC ODBC数据库访问技术 101 6.2.1 记录查询 101 6.2.2 记录添加 102 6.2.3 记录删除 102 6.2.4 记录修改 102 6.2.5 撤销数据库更新操作 103 6.2.6 直接执行SQL语句 103 6.2.7 MFC ODBC的数据库操作过程 103 6.3 MFC ODBC编程实例 104 6.3.1 实例概述 104 6.3.2 实例实现过程 105 6.3.3 编译并运行ODBCDemo2工程 132 6.3.4 ODBCDemo2实例小结 137 6.4 本 章 小 结 137 第7章 DAO数据库编程 138 7.1 DAO的数据访问 138 7.1.1 DAO对象 138 7.1.2 MFC对DAO的支持 139 7.1.3 DAO与ODBC的比较 139 7.1.4 MFC的DAO类简介 139 7.2 DAO编程实例 142 7.2.1 实例概述 142 7.2.2 实例实现过程 143 7.2.3 运行DAODemo工程 167 7.2.4 DAODemo实例小结 171 7.3 小 结 172 第8章 OLE DB客户数据库编程 172 8.1 OLE DB原理 172 8.1.1 OLE DB与ODBC 172 8.1.2 OLE DB的结构 173 8.1.3 OLE DB的优越性 173 8.1.4 OLE DB对象 174 8.1.5 OLE DB客户模板结构 177 8.1.6 OLE DB客户模
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值