首页 > 开发 > 综合 > 正文

Visual C++ ADO数据库编程入门(上)

2024-07-21 02:05:49
字体:
来源:转载
供稿:网友

   ado 是目前在windows环境中比较流行的客户端数据库编程技术。ado是建立在ole db底层技术之上的高级编程接口,因而它兼具有强大的数据处理功能(处理各种不同类型的数据源、分布式的数据处理等等)和极其简单、易用的编程接口,因而得到了广泛的应用。而且按微软公司的意图,ole db和ado将逐步取代 odbc和dao。现在介绍ado各种应用的文章和书籍有很多,本文着重站在初学者的角度,简要探讨一下在vc++中使用ado编程时的一些问题。我们希望阅读本文之前,您对ado技术的基本原理有一些了解。

  一、在vc++中使用ado编程

  ado实际上就是由一组automation对象构成的组件,因此可以象使用其它任何automation对象一样使用ado。ado中最重要的对象有三个:connection、command和recordset,它们分别表示连接对象、命令对象和记录集对象。如果您熟悉使用mfc中的odbc类(cdatabase、crecordset)编程,那么学习ado编程就十分容易了。

  使用ado编程时可以采用以下三种方法之一:

  1、使用预处理指令#import

#import "c:/program files/common files ystem/ado/msado15.dll" /
no_namespace rename("eof", "endoffile")
  但要注意不能放在stdafx.h文件的开头,而应该放在所有include指令的后面。否则在编译时会出错。
程序在编译过程中,vc++会读出msado15.dll中的类型库信息,自动产生两个该类型库的头文件和实现文件msado15.tlh和msado15.tli(在您的debug或release目录下)。在这两个文件里定义了ado的所有对象和方法,以及一些枚举型的常量等。我们的程序只要直接调用这些方法就行了,与使用mfc中的coledispatchdriver类调用automation对象十分类似。

  2、使用mfc中的cidispatchdriver

  就是通过读取msado15.dll中的类型库信息,建立一个coledispatchdriver类的派生类,然后通过它调用ado对象。

  3、直接用com提供的api

  如使用如下代码:

clsid clsid;
hresult hr = ::clsidfromprogid(l"adodb.connection", &clsid);
if(failed(hr))
{...}
::cocreateinstance(clsid, null, clsctx_server, iid_idispatch, (void **)
&pdispatch);
if(failed(hr))
{...}
  以上三种方法,第一和第二种类似,可能第一种好用一些,第三种编程可能最麻烦。但可能第三种方法也是效率最高的,程序的尺寸也最小,并且对ado的控制能力也最强。

  据微软资料介绍,第一种方法不支持方法调用中的默认参数,当然第二种方法也是这样,但第三种就不是这样了。采用第三种方法的水平也最高。当你需要绕过ado而直接调用ole db底层的方法时,就一定要使用第三种方法了。

  ado编程的关键,就是熟练地运用ado提供的各种对象(object)、方法(method)、属性(property)和容器(collection)。另外,如果是在ms sql或oracle等大型数据库上编程,还要能熟练使用sql语言。


  二、使用#import方法的编程步骤

  这里建议您使用#import的方法,因为它易学、易用,代码也比较简洁。

  1、 添加#import指令

  打开stdafx.h文件,将下列内容添加到所有的include指令之后:

#include <icrsint.h> //include support for vc++ extensions
#import "c:/program files/common files ystem/ado/msado15.dll" /
no_namespace rename("eof", "adoeof")
  其中icrsint.h文件包含了vc++扩展的一些预处理指令、宏等的定义,用于com编程时使用。

  2、定义_connectionptr型变量,并建立数据库连接

  建立了与数据库服务器的连接后,才能进行其他有关数据库的访问和操作。ado使用connection对象来建立与数据库服务器的连接,所以它相当于mfc中的cdatabase类。和cdatabase类一样,调用connection对象的open方法即可建立与服务器的连接。

  数据类型 _connectionptr实际上就是由类模板_com_ptr_t而得到的一个具体的实例类,其定义可以到msado15.tlh、comdef.h 和comip.h这三个文件中找到。在msado15.tlh中有:

_com_smartptr_typedef(_collection, __uuidof(_collection));
  经宏扩展后就得到了_connectionptr类。_connectionptr类封装了connection对象的idispatch接口指针,及一些必要的操作。我们就是通过这个指针来操纵connection对象。类似地,后面用到的_commandptr和_recordsetptr类型也是这样得到的,它们分别表示命令对象指针和记录集对象的指针。

  (1)、连接到ms sql server

  注意连接字符串的格式,提供正确的连接字符串是成功连接到数据库服务器的第一步,有关连接字符串的详细信息参见微软msdn library光盘。

  本例连接字符串中的server_name,database_name,user_name和password在编程时都应该替换成实际的内容。

_connectionptr pmyconnect=null;
hresult hr=pmyconnect.createinstance(__uuidof(connection)));
if(failed(hr))return;

_bstr_t strconnect="provider=sqloledb; server=server_name;"
"database=database_name; uid=user_name; pwd=password;";
//connecting to the database server now:
try{pmyconnect->open(strconnect,"","",null);}
catch (_com_error &e)
{
::messagebox(null,e.description(),"警告",mb_ok │ mb_iconwarning);
}
  注意connection对象的open方法中的连接字符串参数必须是bstr或_bstr_t类型。另外,本例是直接通过ole db provider建立连接,所以无需建立数据源。

  (2)、通过odbc driver连接到database server连接字符串格式与直接用odbc编程时的差不多:

_bstr_t strconnect="dsn=datasource_name; database=database_name; uid=user_name; pwd=password;";
  此时与odbc编程一样,必须先建立数据源。

  3、定义_recordsetptr型变量,并打开数据集

  定义_recordsetptr型变量,然后通过它调用recordset对象的open方法,即可打开一个数据集。所以recordset对象与mfc中的crecordset类类似,它也有当前记录、当前记录指针的概念。如:

_recordsetptr m_precordset;
if(!failed(m_precordset.createinstance( __uuidof( recordset )))
{
m_pdoc->m_initialized=false;
return;
}

try{
m_precordset->open(_variant_t("mytable"),
_variant_t((idispatch *)pmyconnect,true), adopenkeyset,
adlockoptimistic, adcmdtable);
}
catch (_com_error &e)
{
::messagebox(null,"无法打开mytable表。","提示",
mb_ok │ mb_iconwarning);
}
  recordset对象的open方法非常重要,它的第一个参数可以是一个sql语句、一个表的名字或一个命令对象等等;第二个参数就是前面建立的连接对象的指针。此外,用connection和command对象的execute方法也能得到记录集,但是只读的。


  4、读取当前记录的数据

  我认为读取数据的最方便的方法如下:

try{
m_precordset->movefirst();
while(m_precordset->adoeof==variant_false)
{
//retrieve column's value:
cstring sname=(char*)(_bstr_t)(m_precordset->fields->getitem
(_variant_t("name"))->value);
short cage=(short)(m_precordset->fields->getitem
(_variant_t("age"))->value);
//do something what you want to do:
......
m_precordset->movenext();
}
}//try
catch (_com_error &e)
{
cstring str=(char*)e.description();
::messagebox(null,str+"/n又出毛病了。","提示",
mb_ok │ mb_iconwarning);
}
  本例中的name和age都是字段名,读取的字段值分别保存在sname和cage变量内。例中的fields是recordset对象的容器,getitem方法返回的是field对象,而value则是field对象的一个属性(即该字段的值)。通过此例,应掌握操纵对象属性的方法。例如,要获得field 对象的value属性的值可以直接用属性名value来引用它(如上例),但也可以调用get方法,例如:

cstring sname=(char*)(_bstr_t)(m_precordset->fields->getitem
(_variant_t("name"))->getvalue());
  从此例还可以看到,判断是否到达记录集的末尾,使用记录集的adoeof属性,其值若为真即到了结尾,反之则未到。判断是否到达记录集开头,则可用bof属性。

  另外,读取数据还有一个方法,就是定义一个绑定的类,然后通过绑定的变量得到字段值(详见后面的介绍)。

  5、修改数据

  方法一:

try{
m_precordset->movefirst();
while(m_precordset->adoeof==variant_false)
{
m_precordset->fields->getitem
(_variant_t("姓名"))->value=_bstr_t("赵薇");
......
m_precordset->update();

m_precordset->movenext();
}
}//try
  改变了value属性的值,即改变了字段的值。

  方法二:

m_precordset->fields->getitem
(_variant_t("姓名"))->putvalue(_bstr_t("赵薇"));
  方法三:就是用定义绑定类的方法(详见后面的介绍)。

  6、添加记录

  新记录添加成功后,即自动成为当前记录。addnew方法有两种形式,一个含有参数,而另一个则不带参数。

  方法一(不带参数):

// add new record into this table:
try{
if(!m_precordset->supports(adaddnew)) return;

m_precordset->addnew();
m_precordset->fields->getitem
(_variant_t("姓名"))->value=_bstr_t("赵薇");
m_precordset->fields->getitem
(_variant_t("性别"))->value=_bstr_t("女");
m_precordset->fields->getitem
(_variant_t("age"))->value=_variant_t((short)20);
m_precordset->fields->getitem
(_variant_t("marry"))->value=_bstr_t("未婚");
m_precordset->update();
}//try
catch (_com_error &e)
{
::messagebox(null, "又出毛病了。","提示",mb_ok │ mb_iconwarning);
}
  这种方法弄完了还要调用update()。

  方法二(带参数):

_variant_t varname[4],narvalue[4];
varname[0] = l"姓名";
varname[1] = l"性别";
varname[2] = l"age";
varname[3] = l"marry";
narvalue[0]=_bstr_t("赵薇");
narvalue[1]=_bstr_t("女");
narvalue[2]=_variant_t((short)20);
narvalue[3]=_bstr_t("未婚");

const int ncrit = sizeof varname / sizeof varname[0];
// create safearray bounds and initialize the array
safearraybound rgsaname[1],rgsavalue[1];
rgsaname[0].llbound = 0;
rgsaname[0].celements = ncrit;
safearray *psaname = safearraycreate( vt_variant, 1, rgsaname );
rgsavalue[0].llbound = 0;
rgsavalue[0].celements = ncrit;
safearray *psavalue = safearraycreate( vt_variant, 1, rgsavalue );
// set the values for each element of the array
hresult hr1=s_ok.hr2=s_ok;
for( long i = 0 ; i < ncrit && succeeded( hr1 ) && succeeded( hr2 );i++)
{
hr1=safearrayputelement(psaname, &i,&varname[i]);
hr2=safearrayputelement(psavalue, &i,&narvalue[i]); }

// initialize and fill the safearray
variant vsaname,vsavalue;
vsaname.vt = vt_variant │ vt_array;
vsavalue.vt = vt_variant │ vt_array;
v_array(&vsaname) = psaname;//&vsaname->parray=psaname;
//see definition in oleauto.h file.
v_array(&vsavalue) = psavalue;

// add a new record:
m_precordset->addnew(vsaname,vsavalue);
  这种方法不需要调用update,因为添加后,ado会自动调用它。此方法主要是使用safearray挺麻烦。

  方法三:就是用定义绑定类的方法(详见后面的介绍)。

 7、删除记录

  调用recordset的delete方法就行了,删除的是当前记录。要了解delete的其它用法请查阅参考文献。

try{
m_precordset->movefirst();
while(m_precordset->adoeof==variant_false)
{
cstring sname=(char*)(_bstr_t)(m_precordset->fields->getitem
(_variant_t("姓名"))->value);
if(::messagebox(null,"姓名="+sname+"/n删除她吗?",
"提示",mb_yesno │ mb_iconwarning)==idyes)
{
m_precordset->delete(adaffectcurrent);
m_precordset->update();
}
m_precordset->movenext();
}
}//try
catch (_com_error &e)
{
::messagebox(null,"又出毛病了。","提示",mb_ok │ mb_iconwarning);
}
  8、使用带参数的命令

  command对象所代表的就是一个provider能够理解的命令,如sql语句等。使用command对象的关键就是把表示命令的语句设置到commandtext属性中,然后调用command对象的execute方法就行了。一般情况下在命令中无需使用参数,但有时使用参数,可以增加其灵活性和效率。

  (1). 建立连接、命令对象和记录集对象

  本例中表示命令的语句就是一个sql语句(select语句)。select语句中的问号?就代表参数,如果要多个参数,就多放几个问号,每个问号代表一个参数。

_connectionptr conn1;
_commandptr cmd1;
parametersptr *params1 = null; // not an instance of a smart pointer.
_parameterptr param1;
_recordsetptr rs1;

try
{
// create connection object (1.5 version)
conn1.createinstance( __uuidof( connection ) );
conn1->connectionstring = bstrconnect;
conn1->open( bstrempty, bstrempty, bstrempty, -1 );
// create command object
cmd1.createinstance( __uuidof( command ) );
cmd1->activeconnection = conn1;
cmd1->commandtext = _bstr_t("select * from mytable where age< ?");
}//try
  要注意命令对象必须与连接对象关联起来才能起作用,本例中将命令对象的activeconnection属性设置为连接对象的指针,即为此目的:

cmd1->activeconnection = conn1;
  (2). 创建参数对象,并给参数赋值

// create parameter object
param1 = cmd1->createparameter( _bstr_t(bstrempty),
adinteger,
adparaminput,
-1,
_variant_t( (long) 5) );
param1->value = _variant_t( (long) 5 );
cmd1->parameters->append( param1 );
  用命令对象的方法来创建一个参数对象,其中的长度参数(第三个)如果是固定长度的类型,就填-1,如果是字符串等可变长度的就填其实际长度。parameters是命令对象的一个容器,它的append方法就是把创建的参数对象追加到该容器里。append进去的参数按先后顺序与sql语句中的问号从左至右一一对应。

  (3). 执行命令打开记录集

// open recordset object
rs1 = cmd1->execute( &vtempty, &vtempty2, adcmdtext );
  但要注意,用command和connection对象的execute方法得到的recordset是只读的。因为在打开recordset之前,我们无法设置它的locktype属性(其默认值为只读)。而在打开之后设置locktype不起作用。

  我发现用上述方法得到记录集rs1后,不但rs1中的记录无法修改,即使直接用sql语句修改同一表中任何记录都不行。

  要想能修改数据,还是要用recordset自己的open方法才行,如:

try{
m_precordset->open((idispatch *) cmd1, vtmissing,
adopenstatic, adlockoptimistic, adcmdunspecified);
}
catch (_com_error &e)
{
::messagebox(null,"mytable表不存在。","提示",mb_ok │ mb_iconwarning);
}
  recordset对象的open方法真是太好了,其第一个参数可以是sql语句、表名字、命令对象指针等等。

  9、响应ado的通知事件

  通知事件就是当某个特定事件发生时,由provider通知客户程序,换句话说,就是由provider调用客户程序中的一个特定的方法(即事件的处理函数)。所以为了响应一个事件,最关键的就是要实现事件的处理函数。

  (1). 从connectioneventsvt接口派生出一个类

  为了响应_connection的通知事件,应该从connectioneventsvt接口派生出一个类:

class cconnevent : public connectioneventsvt
{
private:
ulong m_cref;
public:
cconnevent() { m_cref = 0; };
~cconnevent() {};

stdmethodimp queryinterface(refiid riid, void ** ppv);
stdmethodimp_(ulong) addref(void);
stdmethodimp_(ulong) release(void);
stdmethodimp raw_infomessage(
struct error *perror,
eventstatusenum *adstatus,
struct _connection *pconnection);
stdmethodimp raw_begintranscomplete(
long transactionlevel,
struct error *perror,
eventstatusenum *adstatus,
struct _connection *pconnection);
......
};
   (2). 实现每一个事件的处理函数(凡是带raw_前缀的方法都把它实现了):

stdmethodimp cconnevent::raw_infomessage(
struct error *perror,
eventstatusenum *adstatus,
struct _connection *pconnection)
{
*adstatus = adstatusunwantedevent;
return s_ok;
};
  有些方法虽然你并不需要,但也必须实现它,只需简单地返回一个s_ok即可。但如果要避免经常被调用,还应在其中将adstatus参数设置为adstatusunwantedevent,则在本次调用后,以后就不会被调用了。
另外还必须实现queryinterface, addref, 和release三个方法:

stdmethodimp cconnevent::queryinterface(refiid riid, void ** ppv)
{
*ppv = null;
if (riid == __uuidof(iunknown) ││
riid == __uuidof(connectioneventsvt)) *ppv = this;
if (*ppv == null)
return resultfromscode(e_nointerface);
addref();
return noerror;
}
stdmethodimp_(ulong) cconnevent::addref() { return ++m_cref; };
stdmethodimp_(ulong) cconnevent::release()
{
if (0 != --m_cref) return m_cref;
delete this;
return 0;
}
  (3). 开始响应通知事件

// start using the connection events
iconnectionpointcontainer *pcpc = null;
iconnectionpoint *pcp = null;

hr = pconn.createinstance(__uuidof(connection));
if (failed(hr)) return;

hr = pconn->queryinterface(__uuidof(iconnectionpointcontainer),
(void **)&pcpc);
if (failed(hr)) return;
hr = pcpc->findconnectionpoint(__uuidof(connectionevents), &pcp);
pcpc->release();
if (failed(hr)) return;

pconnevent = new cconnevent();
hr = pconnevent->queryinterface(__uuidof(iunknown), (void **) &punk);
if (failed(hr)) return rc;
hr = pcp->advise(punk, &dwconnevt);
pcp->release();
if (failed(hr)) return;

pconn->open("dsn=pubs;", "sa", "", adconnectunspecified);
  也就是说在连接(open)之前就做这些事。

  (4). 停止响应通知事件

pconn->close();
// stop using the connection events
hr = pconn->queryinterface(__uuidof(iconnectionpointcontainer),
(void **) &pcpc);
if (failed(hr)) return;
hr = pcpc->findconnectionpoint(__uuidof(connectionevents), &pcp);
pcpc->release();
if (failed(hr)) return rc;
hr = pcp->unadvise( dwconnevt );
pcp->release();
if (failed(hr)) return;
  在连接关闭之后做这件事。
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表