ado 是目前在windows環(huán)境中比較流行的客戶端數(shù)據(jù)庫編程技術(shù)。ado是建立在ole db底層技術(shù)之上的高級(jí)編程接口,因而它兼具有強(qiáng)大的數(shù)據(jù)處理功能(處理各種不同類型的數(shù)據(jù)源、分布式的數(shù)據(jù)處理等等)和極其簡(jiǎn)單、易用的編程接口,因而得到了廣泛的應(yīng)用。而且按微軟公司的意圖,ole db和ado將逐步取代 odbc和dao。現(xiàn)在介紹ado各種應(yīng)用的文章和書籍有很多,本文著重站在初學(xué)者的角度,簡(jiǎn)要探討一下在vc++中使用ado編程時(shí)的一些問題。我們希望閱讀本文之前,您對(duì)ado技術(shù)的基本原理有一些了解。
一、在vc++中使用ado編程
ado實(shí)際上就是由一組automation對(duì)象構(gòu)成的組件,因此可以象使用其它任何automation對(duì)象一樣使用ado。ado中最重要的對(duì)象有三個(gè):connection、command和recordset,它們分別表示連接對(duì)象、命令對(duì)象和記錄集對(duì)象。如果您熟悉使用mfc中的odbc類(cdatabase、crecordset)編程,那么學(xué)習(xí)ado編程就十分容易了。
使用ado編程時(shí)可以采用以下三種方法之一:
1、使用預(yù)處理指令#import
#import "c:/program files/common files ystem/ado/msado15.dll" /
no_namespace rename("eof", "endoffile")
但要注意不能放在stdafx.h文件的開頭,而應(yīng)該放在所有include指令的后面。否則在編譯時(shí)會(huì)出錯(cuò)。
程序在編譯過程中,vc++會(huì)讀出msado15.dll中的類型庫信息,自動(dòng)產(chǎn)生兩個(gè)該類型庫的頭文件和實(shí)現(xiàn)文件msado15.tlh和msado15.tli(在您的debug或release目錄下)。在這兩個(gè)文件里定義了ado的所有對(duì)象和方法,以及一些枚舉型的常量等。我們的程序只要直接調(diào)用這些方法就行了,與使用mfc中的coledispatchdriver類調(diào)用automation對(duì)象十分類似。
2、使用mfc中的cidispatchdriver
就是通過讀取msado15.dll中的類型庫信息,建立一個(gè)coledispatchdriver類的派生類,然后通過它調(diào)用ado對(duì)象。
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))
{...}
以上三種方法,第一和第二種類似,可能第一種好用一些,第三種編程可能最麻煩。但可能第三種方法也是效率最高的,程序的尺寸也最小,并且對(duì)ado的控制能力也最強(qiáng)。
據(jù)微軟資料介紹,第一種方法不支持方法調(diào)用中的默認(rèn)參數(shù),當(dāng)然第二種方法也是這樣,但第三種就不是這樣了。采用第三種方法的水平也最高。當(dāng)你需要繞過ado而直接調(diào)用ole db底層的方法時(shí),就一定要使用第三種方法了。
ado編程的關(guān)鍵,就是熟練地運(yùn)用ado提供的各種對(duì)象(object)、方法(method)、屬性(property)和容器(collection)。另外,如果是在ms sql或oracle等大型數(shù)據(jù)庫上編程,還要能熟練使用sql語言。
二、使用#import方法的編程步驟
這里建議您使用#import的方法,因?yàn)樗讓W(xué)、易用,代碼也比較簡(jiǎn)潔。
1、 添加#import指令
打開stdafx.h文件,將下列內(nèi)容添加到所有的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++擴(kuò)展的一些預(yù)處理指令、宏等的定義,用于com編程時(shí)使用。
2、定義_connectionptr型變量,并建立數(shù)據(jù)庫連接
建立了與數(shù)據(jù)庫服務(wù)器的連接后,才能進(jìn)行其他有關(guān)數(shù)據(jù)庫的訪問和操作。ado使用connection對(duì)象來建立與數(shù)據(jù)庫服務(wù)器的連接,所以它相當(dāng)于mfc中的cdatabase類。和cdatabase類一樣,調(diào)用connection對(duì)象的open方法即可建立與服務(wù)器的連接。
數(shù)據(jù)類型 _connectionptr實(shí)際上就是由類模板_com_ptr_t而得到的一個(gè)具體的實(shí)例類,其定義可以到msado15.tlh、comdef.h 和comip.h這三個(gè)文件中找到。在msado15.tlh中有:
_com_smartptr_typedef(_collection, __uuidof(_collection));
經(jīng)宏擴(kuò)展后就得到了_connectionptr類。_connectionptr類封裝了connection對(duì)象的idispatch接口指針,及一些必要的操作。我們就是通過這個(gè)指針來操縱connection對(duì)象。類似地,后面用到的_commandptr和_recordsetptr類型也是這樣得到的,它們分別表示命令對(duì)象指針和記錄集對(duì)象的指針。
(1)、連接到ms sql server
注意連接字符串的格式,提供正確的連接字符串是成功連接到數(shù)據(jù)庫服務(wù)器的第一步,有關(guān)連接字符串的詳細(xì)信息參見微軟msdn library光盤。
本例連接字符串中的server_name,database_name,user_name和password在編程時(shí)都應(yīng)該替換成實(shí)際的內(nèi)容。
_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對(duì)象的open方法中的連接字符串參數(shù)必須是bstr或_bstr_t類型。另外,本例是直接通過ole db provider建立連接,所以無需建立數(shù)據(jù)源。
(2)、通過odbc driver連接到database server連接字符串格式與直接用odbc編程時(shí)的差不多:
_bstr_t strconnect="dsn=datasource_name; database=database_name; uid=user_name; pwd=password;";
此時(shí)與odbc編程一樣,必須先建立數(shù)據(jù)源。
3、定義_recordsetptr型變量,并打開數(shù)據(jù)集
定義_recordsetptr型變量,然后通過它調(diào)用recordset對(duì)象的open方法,即可打開一個(gè)數(shù)據(jù)集。所以recordset對(duì)象與mfc中的crecordset類類似,它也有當(dāng)前記錄、當(dāng)前記錄指針的概念。如:
_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對(duì)象的open方法非常重要,它的第一個(gè)參數(shù)可以是一個(gè)sql語句、一個(gè)表的名字或一個(gè)命令對(duì)象等等;第二個(gè)參數(shù)就是前面建立的連接對(duì)象的指針。此外,用connection和command對(duì)象的execute方法也能得到記錄集,但是只讀的。
4、讀取當(dāng)前記錄的數(shù)據(jù)
我認(rèn)為讀取數(shù)據(jù)的最方便的方法如下:
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變量?jī)?nèi)。例中的fields是recordset對(duì)象的容器,getitem方法返回的是field對(duì)象,而value則是field對(duì)象的一個(gè)屬性(即該字段的值)。通過此例,應(yīng)掌握操縱對(duì)象屬性的方法。例如,要獲得field 對(duì)象的value屬性的值可以直接用屬性名value來引用它(如上例),但也可以調(diào)用get方法,例如:
cstring sname=(char*)(_bstr_t)(m_precordset->fields->getitem
(_variant_t("name"))->getvalue());
從此例還可以看到,判斷是否到達(dá)記錄集的末尾,使用記錄集的adoeof屬性,其值若為真即到了結(jié)尾,反之則未到。判斷是否到達(dá)記錄集開頭,則可用bof屬性。
另外,讀取數(shù)據(jù)還有一個(gè)方法,就是定義一個(gè)綁定的類,然后通過綁定的變量得到字段值(詳見后面的介紹)。
5、修改數(shù)據(jù)
方法一:
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、添加記錄
新記錄添加成功后,即自動(dòng)成為當(dāng)前記錄。addnew方法有兩種形式,一個(gè)含有參數(shù),而另一個(gè)則不帶參數(shù)。
方法一(不帶參數(shù)):
// 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);
}
這種方法弄完了還要調(diào)用update()。
方法二(帶參數(shù)):
_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);
這種方法不需要調(diào)用update,因?yàn)樘砑雍螅琣do會(huì)自動(dòng)調(diào)用它。此方法主要是使用safearray挺麻煩。
方法三:就是用定義綁定類的方法(詳見后面的介紹)。
7、刪除記錄
調(diào)用recordset的delete方法就行了,刪除的是當(dāng)前記錄。要了解delete的其它用法請(qǐng)查閱參考文獻(xiàn)。
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、使用帶參數(shù)的命令
command對(duì)象所代表的就是一個(gè)provider能夠理解的命令,如sql語句等。使用command對(duì)象的關(guān)鍵就是把表示命令的語句設(shè)置到commandtext屬性中,然后調(diào)用command對(duì)象的execute方法就行了。一般情況下在命令中無需使用參數(shù),但有時(shí)使用參數(shù),可以增加其靈活性和效率。
(1). 建立連接、命令對(duì)象和記錄集對(duì)象
本例中表示命令的語句就是一個(gè)sql語句(select語句)。select語句中的問號(hào)?就代表參數(shù),如果要多個(gè)參數(shù),就多放幾個(gè)問號(hào),每個(gè)問號(hào)代表一個(gè)參數(shù)。
_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
要注意命令對(duì)象必須與連接對(duì)象關(guān)聯(lián)起來才能起作用,本例中將命令對(duì)象的activeconnection屬性設(shè)置為連接對(duì)象的指針,即為此目的:
cmd1->activeconnection = conn1;
(2). 創(chuàng)建參數(shù)對(duì)象,并給參數(shù)賦值
// 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 );
用命令對(duì)象的方法來創(chuàng)建一個(gè)參數(shù)對(duì)象,其中的長(zhǎng)度參數(shù)(第三個(gè))如果是固定長(zhǎng)度的類型,就填-1,如果是字符串等可變長(zhǎng)度的就填其實(shí)際長(zhǎng)度。parameters是命令對(duì)象的一個(gè)容器,它的append方法就是把創(chuàng)建的參數(shù)對(duì)象追加到該容器里。append進(jìn)去的參數(shù)按先后順序與sql語句中的問號(hào)從左至右一一對(duì)應(yīng)。
(3). 執(zhí)行命令打開記錄集
// open recordset object
rs1 = cmd1->execute( &vtempty, &vtempty2, adcmdtext );
但要注意,用command和connection對(duì)象的execute方法得到的recordset是只讀的。因?yàn)樵诖蜷_recordset之前,我們無法設(shè)置它的locktype屬性(其默認(rèn)值為只讀)。而在打開之后設(shè)置locktype不起作用。
我發(fā)現(xiàn)用上述方法得到記錄集rs1后,不但rs1中的記錄無法修改,即使直接用sql語句修改同一表中任何記錄都不行。
要想能修改數(shù)據(jù),還是要用recordset自己的open方法才行,如:
try{
m_precordset->open((idispatch *) cmd1, vtmissing,
adopenstatic, adlockoptimistic, adcmdunspecified);
}
catch (_com_error &e)
{
::messagebox(null,"mytable表不存在。","提示",mb_ok │ mb_iconwarning);
}
recordset對(duì)象的open方法真是太好了,其第一個(gè)參數(shù)可以是sql語句、表名字、命令對(duì)象指針等等。
9、響應(yīng)ado的通知事件
通知事件就是當(dāng)某個(gè)特定事件發(fā)生時(shí),由provider通知客戶程序,換句話說,就是由provider調(diào)用客戶程序中的一個(gè)特定的方法(即事件的處理函數(shù))。所以為了響應(yīng)一個(gè)事件,最關(guān)鍵的就是要實(shí)現(xiàn)事件的處理函數(shù)。
(1). 從connectioneventsvt接口派生出一個(gè)類
為了響應(yīng)_connection的通知事件,應(yīng)該從connectioneventsvt接口派生出一個(gè)類:
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). 實(shí)現(xiàn)每一個(gè)事件的處理函數(shù)(凡是帶raw_前綴的方法都把它實(shí)現(xiàn)了):
stdmethodimp cconnevent::raw_infomessage(
struct error *perror,
eventstatusenum *adstatus,
struct _connection *pconnection)
{
*adstatus = adstatusunwantedevent;
return s_ok;
};
有些方法雖然你并不需要,但也必須實(shí)現(xiàn)它,只需簡(jiǎn)單地返回一個(gè)s_ok即可。但如果要避免經(jīng)常被調(diào)用,還應(yīng)在其中將adstatus參數(shù)設(shè)置為adstatusunwantedevent,則在本次調(diào)用后,以后就不會(huì)被調(diào)用了。
另外還必須實(shí)現(xiàn)queryinterface, addref, 和release三個(gè)方法:
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). 開始響應(yīng)通知事件
// 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). 停止響應(yīng)通知事件
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;
在連接關(guān)閉之后做這件事。