原文檔來自網絡下載下傳,但是失去作者資訊,無法貼原文連結
一、在VC++中使用ADO程式設計
ADO實際上就是由一組Automation對象構成的元件,是以可以象使用其它任何Automation對象一樣使用ADO。ADO中最重要的對象有三個:Connection、Command和Recordset,它們分别表示連接配接對象、指令對象和記錄集對象。如果您熟悉使用MFC中的ODBC類(CDatabase、CRecordset)程式設計,那麼學習ADO程式設計就十分容易了。
使用ADO程式設計時可以采用以下三種方法之一:
1、使用預處理指令#import
#import "C:\Program Files\Common Files\System\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而直接調用OLEDB底層的方法時,就一定要使用第三種方法了。
ADO程式設計的關鍵,就是熟練地運用ADO提供的各種對象(object)、方法(method)、屬性(property)和容器(collection)。另外,如果是在MSSQL或Oracle等大型資料庫上程式設計,還要能熟練使用SQL語言。
二、使用#import方法的程式設計步驟
這裡建議您使用#import的方法,因為它易學、易用,代碼也比較簡潔。
1、 添加#import指令
打開stdafx.h檔案,将下列内容添加到所有的include指令之後:
#include <icrsint.h> //Include support for VC++ Extensions #import "C:\Program Files\Common Files\System\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)、連接配接到MSSQL Server
注意連接配接字元串的格式,提供正确的連接配接字元串是成功連接配接到資料庫伺服器的第一步,有關連接配接字元串的詳細資訊參見微軟MSDNLibraryCD光牒。
本例連接配接字元串中的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類型。另外,本例是直接通過OLEDB Provider建立連接配接,是以無需建立資料源。
(2)、通過ODBCDriver連接配接到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; |
在連接配接關閉之後做這件事。
10、邦定資料
定義一個綁定類,将其成員變量綁定到一個指定的記錄集,以友善于通路記錄集的字段值。
(1). 從CADORecordBinding派生出一個類:
class CCustomRs : public CADORecordBinding { BEGIN_ADO_BINDING(CCustomRs) ADO_VARIABLE_LENGTH_ENTRY2(3, adVarChar, m_szau_fname, sizeof(m_szau_fname), lau_fnameStatus, false) ADO_VARIABLE_LENGTH_ENTRY2(2, adVarChar, m_szau_lname, sizeof(m_szau_lname), lau_lnameStatus, false) ADO_VARIABLE_LENGTH_ENTRY2(4, adVarChar, m_szphone, sizeof(m_szphone), lphoneStatus, true) END_ADO_BINDING() public: CHAR m_szau_fname[22]; ULONG lau_fnameStatus; CHAR m_szau_lname[42]; ULONG lau_lnameStatus; CHAR m_szphone[14]; ULONG lphoneStatus; }; |
其中将要綁定的字段與變量名用BEGIN_ADO_BINDING宏關聯起來。每個字段對應于兩個變量,一個存放字段的值,另一個存放字段的狀态。字段用從1開始的序号表示,如1,2,3等等。
特别要注意的是:如果要綁定的字段是字元串類型,則對應的字元數組的元素個數一定要比字段長度大2(比如m_szau_fname[22],其綁定的字段au_fname的長度實際是20),不這樣綁定就會失敗。我分析多出的2可能是為了存放字元串結尾的空字元null和BSTR字元串開頭的一個字(表示BSTR的長度)。這個問題對于初學者來說可能是一個意想不到的問題。
CADORecordBinding類的定義在icrsint.h檔案裡,内容是:
class CADORecordBinding { public: STDMETHOD_(const ADO_BINDING_ENTRY*, GetADOBindingEntries) (VOID) PURE; }; BEGIN_ADO_BINDING宏的定義也在icrsint.h檔案裡,内容是: #define BEGIN_ADO_BINDING(cls) public: \ typedef cls ADORowClass; \ const ADO_BINDING_ENTRY* STDMETHODCALLTYPE GetADOBindingEntries() { \ static const ADO_BINDING_ENTRY rgADOBindingEntries[] = { ADO_VARIABLE_LENGTH_ENTRY2宏的定義也在icrsint.h檔案裡: #define ADO_VARIABLE_LENGTH_ENTRY2(Ordinal, DataType, Buffer, Size, Status, Modify)\ {Ordinal, \ DataType, \ 0, \ 0, \ Size, \ offsetof(ADORowClass, Buffer), \ offsetof(ADORowClass, Status), \ 0, \ classoffset(CADORecordBinding, ADORowClass), \ Modify}, #define END_ADO_BINDING宏的定義也在icrsint.h檔案裡: #define END_ADO_BINDING() {0, adEmpty, 0, 0, 0, 0, 0, 0, 0, FALSE}};\ return rgADOBindingEntries;} |
(2). 綁定
_RecordsetPtr Rs1; IADORecordBinding *picRs=NULL; CCustomRs rs; ...... Rs1->QueryInterface(__uuidof(IADORecordBinding), (LPVOID*)&picRs)); picRs->BindToRecordset(&rs); |
派生出的類必須通過IADORecordBinding接口才能綁定,調用它的BindToRecordset方法就行了。
(3).rs中的變量即是目前記錄字段的值
//Set sort and filter condition: // Step 4: Manipulate the data Rs1->Fields->GetItem("au_lname")->Properties->GetItem("Optimize")->Value = true; Rs1->Sort = "au_lname ASC"; Rs1->Filter = "phone LIKE '415 5*'"; Rs1->MoveFirst(); while (VARIANT_FALSE == Rs1->EndOfFile) { printf("Name: %s\t %s\tPhone: %s\n", (rs.lau_fnameStatus == adFldOK ? rs.m_szau_fname : ""), (rs.lau_lnameStatus == adFldOK ? rs.m_szau_lname : ""), (rs.lphoneStatus == adFldOK ? rs.m_szphone : "")); if (rs.lphoneStatus == adFldOK) strcpy(rs.m_szphone, "777"); TESTHR(picRs->Update(&rs)); // Add change to the batch Rs1->MoveNext(); } Rs1->Filter = (long) adFilterNone; ...... if (picRs) picRs->Release(); Rs1->Close(); pConn->Close(); |
隻要字段的狀态是adFldOK,就可以通路。如果修改了字段,不要忘了先調用picRs的Update(注意不是Recordset的Update),然後才關閉,也不要忘了釋放picRs(即picRs->Release();)。
(4). 此時還可以用IADORecordBinding接口添加新紀錄
if(FAILED(picRs->AddNew(&rs))) ...... |
11. 通路長資料
在MicrosoftSQL中的長資料包括text、image等這樣長類型的資料,作為二進制位元組來對待。
可以用Field對象的GetChunk和AppendChunk方法來通路。每次可以讀出或寫入全部資料的一部分,它會記住上次通路的位置。但是如果中間通路了别的字段後,就又得從頭來了。
請看下面的例子:
//寫入一張照片到資料庫: VARIANT varChunk; SAFEARRAY *psa; SAFEARRAYBOUND rgsabound[1]; //VT_ARRAY │ VT_UI1 CFile f("h:\\aaa.jpg",CFile::modeRead); BYTE bVal[ChunkSize+1]; UINT uIsRead=0; //Create a safe array to store the array of BYTES while(1) { uIsRead=f.Read(bVal,ChunkSize); if(uIsRead==0)break; rgsabound[0].cElements =uIsRead; rgsabound[0].lLbound = 0; psa = SafeArrayCreate(VT_UI1,1,rgsabound); for(long index=0;index<uIsRead;index++) { if(FAILED(SafeArrayPutElement(psa,&index,&bVal[index]))) ::MessageBox(NULL,"啊,又出毛病了。","提示",MB_OK │ MB_ICONWARNING); } varChunk.vt = VT_ARRAY│VT_UI1; varChunk.parray = psa; try{ m_pRecordset->Fields->GetItem("photo")->AppendChunk(varChunk); } catch (_com_error &e) { CString str=(char*)e.Description(); ::MessageBox(NULL,str+"\n又出毛病了。","提示",MB_OK │ MB_ICONWARNING); } ::VariantClear(&varChunk); ::SafeArrayDestroyData( psa); if(uIsRead<ChunkSize)break; }//while(1) f.Close(); //從資料庫讀一張照片: CFile f; f.Open("h:\\bbb.jpg",CFile::modeWrite│CFile::modeCreate); long lPhotoSize = m_pRecordset->Fields->Item["photo"]->ActualSize; long lIsRead=0; _variant_t varChunk; BYTE buf[ChunkSize]; while(lPhotoSize>0) { lIsRead=lPhotoSize>=ChunkSize? ChunkSize:lPhotoSize; varChunk = m_pRecordset->Fields-> Item["photo"]->GetChunk(lIsRead); for(long index=0;index<lIsRead;index++) { ::SafeArrayGetElement(varChunk.parray,&index,buf+index); } f.Write(buf,lIsRead); lPhotoSize-=lIsRead; }//while() f.Close(); |
12. 使用SafeArray問題
學會使用SafeArray也是很重要的,因為在ADO程式設計中經常要用。它的主要目的是用于automation中的數組型參數的傳遞。因為在網絡環境中,數組是不能直接傳遞的,而必須将其包裝成SafeArray。實質上SafeArray就是将通常的數組增加一個描述符,說明其維數、長度、邊界、元素類型等資訊。SafeArray也并不單獨使用,而是将其再包裝到VARIANT類型的變量中,然後才作為參數傳送出去。在VARIANT的vt成員的值如果包含VT_ARRAY│...,那麼它所封裝的就是一個SafeArray,它的parray成員即是指向SafeArray的指針。SafeArray中元素的類型可以是VARIANT能封裝的任何類型,包括VARIANT類型本身。
使用SafeArray的具體步驟:
方法一:
包裝一個SafeArray:
(1). 定義變量,如:
VARIANT varChunk; SAFEARRAY *psa; SAFEARRAYBOUND rgsabound[1]; |
(2). 建立SafeArray描述符:
uIsRead=f.Read(bVal,ChunkSize);//read array from a file. if(uIsRead==0)break; rgsabound[0].cElements =uIsRead; rgsabound[0].lLbound = 0; psa = SafeArrayCreate(VT_UI1,1,rgsabound); |
(3). 放置資料元素到SafeArray:
for(long index=0;index<uIsRead;index++) { if(FAILED(SafeArrayPutElement(psa,&index,&bVal[index]))) ::MessageBox(NULL,"出毛病了。","提示",MB_OK │ MB_ICONWARNING); } |
一個一個地放,挺麻煩的。
(4). 封裝到VARIANT内:
varChunk.vt = VT_ARRAY│VT_UI1; varChunk.parray = psa; |
這樣就可以将varChunk作為參數傳送出去了。
讀取SafeArray中的資料的步驟:
(1). 用SafeArrayGetElement一個一個地讀
BYTE buf[lIsRead]; for(long index=0;index<lIsRead;index++) { ::SafeArrayGetElement(varChunk.parray,&index,buf+index); } |
就讀到緩沖區buf裡了。
方法二:
使用SafeArrayAccessData直接讀寫SafeArray的緩沖區:
(1). 讀緩沖區:
BYTE *buf; SafeArrayAccessData(varChunk.parray, (void **)&buf); f.Write(buf,lIsRead); SafeArrayUnaccessData(varChunk.parray); |
(2). 寫緩沖區:
BYTE *buf; ::SafeArrayAccessData(psa, (void **)&buf); for(long index=0;index<uIsRead;index++) { buf[index]=bVal[index]; } ::SafeArrayUnaccessData(psa); varChunk.vt = VT_ARRAY│VT_UI1; varChunk.parray = psa; |
這種方法讀寫SafeArray都可以,它直接操縱SafeArray的資料緩沖區,比用SafeArrayGetElement和SafeArrayPutElement速度快。特别适合于讀取資料。但用完之後不要忘了調用::SafeArrayUnaccessData(psa),否則會出錯的。
13. 使用書簽(bookmark )
書簽可以唯一辨別記錄集中的一個記錄,用于快速地将目前記錄移回到已通路過的記錄,以及進行過濾等等。Provider會自動為記錄集中的每一條記錄産生一個書簽,我們隻需要使用它就行了。我們不能試圖顯示、修改或比較書簽。ADO用記錄集的Bookmark屬性表示目前記錄的書簽。
用法步驟:
(1). 建立一個VARIANT類型的變量
_variant_t VarBookmark;
(2). 将目前記錄的書簽值存入該變量
也就是記錄集的Bookmark屬性的目前值。
VarBookmark = rst->Bookmark;
(3). 傳回到先前的記錄
将儲存的書簽值設定到記錄集的書簽屬性中:
// Check for whether bookmark set for a record if (VarBookmark.vt == VT_EMPTY) printf("No Bookmark set!\n"); else rst->Bookmark = VarBookmark; |
設定完後,目前記錄即會移動到該書簽指向的記錄。
14、設定過濾條件
Recordset對象的Filter屬性表示了目前的過濾條件。它的值可以是以AND或OR連接配接起來的條件表達式(不含WHERE關鍵字)、由書簽組成的數組或ADO提供的FilterGroupEnum枚舉值。為Filter屬性設定新值後Recordset的目前記錄指針會自動移動到滿足過濾條件的第一個記錄。例如:
rst->Filter = _bstr_t ("姓名='趙薇' AND 性别=’女’"); |
在使用條件表達式時應注意下列問題:
(1)、可以用圓括号組成複雜的表達式
例如:
rst->Filter = _bstr_t ("(姓名='趙薇' AND 性别=’女’) OR AGE<25"); |
但是微軟不允許在括号内用OR,然後在括号外用AND,例如:
rst->Filter = _bstr_t ("(姓名='趙薇' OR 性别=’女’) AND AGE<25"); |
必須修改為:
rst->Filter = _bstr_t ("(姓名='趙薇' AND AGE<25) OR (性别=’女’ AND AGE<25)"); |
(2)、表達式中的比較運算符可以是LIKE
LIKE後被比較的是一個含有通配符*的字元串,星号表示若幹個任意的字元。
字元串的首部和尾部可以同時帶星号*
rst->Filter = _bstr_t ("姓名 LIKE '*趙*' "); |
也可以隻是尾部帶星号:
rst->Filter = _bstr_t ("姓名 LIKE '趙*' "); |
Filter屬性值的類型是Variant,如果過濾條件是由書簽組成的數組,則需将該數組轉換為SafeArray,然後再封裝到一個VARIANT或_variant_t型的變量中,再賦給Filter屬性。
15、索引與排序
(1)、建立索引
當以某個字段為關鍵字用Find方法查找時,為了加快速度可以以該字段為關鍵字在記錄集内部臨時建立索引。隻要将該字段的Optimize屬性設定為true即可,例如:
pRst->Fields->GetItem("姓名")->Properties-> GetItem("Optimize")->PutValue("True"); pRst->Find("姓名 = '趙薇'",1,adSearchForward); ...... pRst->Fields->GetItem("姓名")->Properties-> GetItem("Optimize")->PutValue("False"); pRst->Close(); |
說明:Optimize屬性是由Provider提供的屬性(在ADO中稱為動态屬性),ADO本身沒有此屬性。
(2)、排序
要排序也很簡單,隻要把要排序的關鍵字清單設定到Recordset對象的Sort屬性裡即可,例如:
pRstAuthors->CursorLocation = adUseClient; pRstAuthors->Open("SELECT * FROM mytable", _variant_t((IDispatch *) pConnection), adOpenStatic, adLockReadOnly, adCmdText); ...... pRst->Sort = "姓名 DESC, 年齡 ASC"; |
關鍵字(即字段名)之間用逗号隔開,如果要以某關鍵字降序排序,則應在該關鍵字後加一空格,再加DESC(如上例)。升序時ASC加不加無所謂。本操作是利用索引進行的,并未進行實體排序,是以效率較高。
但要注意,在打開記錄集之前必須将記錄集的CursorLocation屬性設定為adUseClient,如上例所示。Sort屬性值在需要時随時可以修改。
16、事務處理
ADO中的事務處理也很簡單,隻需分别在适當的位置調用Connection對象的三個方法即可,這三個方法是:
(1)、在事務開始時調用
pCnn->BeginTrans(); |
(2)、在事務結束并成功時調用
pCnn->CommitTrans (); |
(3)、在事務結束并失敗時調用
pCnn->RollbackTrans (); |
在使用事務處理時,應盡量減小事務的範圍,即減小從事務開始到結束(送出或復原)之間的時間間隔,以便提高系統效率。需要時也可在調用BeginTrans()方法之前,先設定Connection對象的IsolationLevel屬性值,詳細内容參見MSDN中有關ADO的技術資料。
三、使用ADO程式設計常見問題解答
以下均是針對MSSQL 7.0程式設計時所遇問題進行讨論。
1、連接配接失敗可能原因
EnterpriseManagemer内,打開将伺服器的屬性對話框,在Security頁籤中,有一個選項Authentication。
如果該選項是WindowsNT only,則你的程式所用的連接配接字元串就一定要包含Trusted_Connection參數,并且其值必須為yes,如:
"Provider=SQLOLEDB;Server=888;Trusted_Connection=yes" ";Database=master;uid=lad;"; |
如果不按上述操作,程式運作時連接配接必然失敗。
如果Authentication選項是SQLServer and Windows NT,則你的程式所用的連接配接字元串可以不包含Trusted_Connection參數,如:
"Provider=SQLOLEDB;Server=888;Database=master;uid=lad;pwd=111;"; |
因為ADO給該參數取的預設值就是no,是以可以省略。我認為還是取預設值比較安全一些。
2、改變目前資料庫的方法
使用Tansct-SQL中的USE語句即可。
3、如何判斷一個資料庫是否存在
(1)、可打開master資料庫中一個叫做SCHEMATA的視圖,其内容列出了該伺服器上所有的資料庫名稱。
(2) 、更簡便的方法是使用USE語句,成功了就存在;不成功,就不存在。例如:
try{ m_pConnect->Execute ( _bstr_t("USE INSURANCE_2002"),NULL, adCmdText│adExecuteNoRecords ); } catch (_com_error &e) { blSuccess=FALSE; CString str="資料庫INSURANCE_2002不存在!\n"; str+=e.Description(); ::MessageBox(NULL,str,"警告",MB_OK │ MB_ICONWARNING); } |
4、判斷一個表是否存在
(1)、同樣判斷一個表是否存在,也可以用是否成功地打開它來判斷,十分友善,例如:
try{ m_pRecordset->Open(_variant_t("mytable"), _variant_t((IDispatch *)m_pConnection,true), adOpenKeyset, adLockOptimistic, adCmdTable); } catch (_com_error &e) { ::MessageBox(NULL,"該表不存在。","提示",MB_OK │ MB_ICONWARNING); } |
(2)、要不然可以采用麻煩一點的辦法,就是在MS-SQL伺服器上的每個資料庫中都有一個名為sysobjects的表,檢視此表的内容即知指定的表是否在該資料庫中。
(3)、同樣,每個資料庫中都有一個名為TABLES的視圖(View),檢視此視圖的内容即知指定的表是否在該資料庫中。
5、類型轉換問題
(1)、類型VARIANT_BOOL
類型VARIANT_BOOL等價于short類型。TheVARIANT_BOOL is equivalent to short. see it's definition below:
typdef short VARIANT_BOOL
(2)、_com_ptr_t類的類型轉換
_ConnectionPtr可以自動轉換成IDspatch*類型,這是因為_ConnectionPtr實際上是_com_ptr_t類的一個執行個體,而這個類有此類型轉換函數。
同理,_RecordsetPtr和_CommandPtr也都可以這樣轉換。
(3)、_bstr_t和_variant_t類
在ADO程式設計時,_bstr_t和_variant_t這兩個類很有用,省去了許多BSTR和VARIANT類型轉換的麻煩。
6、打開記錄集時的問題
在打開記錄集時,在調用Recordset的Open方法時,其最後一個參數裡一定不能包含adAsyncExecute,否則将因為是異步操作,在讀取資料時無法讀到資料。
7、異常處理問題
對所有調用ADO的語句一定要用try和catch語句捕捉異常,否則在發生異常時,程式會異常退出。
8、使用SafeArray問題
在初學使用中,我曾遇到一個傷腦筋的問題,一定要注意:
在定義了SAFEARRAY的指針後,如果打算重複使用多次,則在中間可以調用::SafeArrayDestroyData釋放資料,但決不能調用::SafeArrayDestroyDescriptor,否則必然出錯,即使調用SafeArrayCreate也不行。例如:
SAFEARRAY *psa; ...... //When the data are no longer to be used: ::SafeArrayDestroyData( psa); |
我分析在定義psa指針時,一個SAFEARRAY的執行個體(也就是SAFEARRAY描述符)也同時被自動建立了。但是隻要一調用::SafeArrayDestroyDescriptor,描述符就被銷毀了。
是以我認為::SafeArrayDestroyDescriptor可以根本就不調用,即使調用也必須在最後調用。
9、重複使用指令對象問題
一個指令對象如果要重複使用多次(尤其是帶參數的指令),則在第一次執行之前,應将它的Prepared屬性設定為TRUE。這樣會使第一次執行減慢,但卻可以使以後的執行全部加快。
10、綁定字元串型字段問題
如果要綁定的字段是字元串類型,則對應的字元數組的元素個數一定要比字段長度大2(比如m_szau_fname[22],其綁定的字段au_fname的長度實際是20),不這樣綁定就會失敗。
11、使用AppendChunk的問題
當用AddNew方法剛剛向記錄集内添加一個新記錄之後,不能首先向一個長資料字段(image類型)寫入資料,必須先向其他字段寫入過資料之後,才能調用AppendChunk寫該字段,否則出錯。也就是說,AppendChunk不能緊接在AddNew之後。另外,寫入其他字段後還必須緊接着調用AppendChunk,而不能調用記錄集的Update方法後,才調用AppendChunk,否則調用AppendChunk時也會出錯。換句話說,就是必須AppendChunk在前,Update在後。因而這個時候就不能使用帶參數的AddNew了,因為帶參數的AddNew會自動調用記錄集的Update,是以AppendChunk就跑到Update的後面了,就隻有出錯了!是以,這時應該用不帶參數的AddNew。
我推測這可能是MSSQL 7.0的問題,在MS SQL 2000中則不存在這些問題,但是AppendChunk仍然不能在Update之後。
四、小結
一般情況下,Connection和Command的Execute用于執行不産生記錄集的指令,而Recordset的Open用于産生一個記錄集,當然也不是絕對的。特别Command主要是用于執行參數化的指令,可以直接由Command對象執行,也可以将Command對象傳遞給Recordset的Open。
本文中的代碼片斷均在VC++6.0、Windows NT 4.0 SP6和MS SQL 7.0中調試通過。相信您讀過之後,編寫簡單的資料庫程式應該沒有問題了。當然要編寫比較實用的、複雜一點的程式,還需要對OLEDB、ADO以及資料庫平台再多了解一點,希望您繼續努力,一定會很快成功的!詳細參考資料請參見微軟MSDNJuly 2000CD光牒或MS SQL 7.0線上文檔資料(Books online)。文中難免有錯誤和不妥之處,敬請各位批評指正!