在一些面向業務的應用程序中,很多業務的查詢往往是固定的,其中有些甚至只是根據不同的值改變幾個固定的條件值而已。比如查詢用戶信息,通常都是根據用戶編號或者用戶名稱等固定信息查詢用戶表得到一個結果集。比如查詢語句:Select * From 用戶表 Where 用戶號=’xxxx’。往往在一些程序中,這個語句都是先被組成字符串,然後被執行。之前文章中的例子也是這樣做的,最終的SQL語句都是一個字符串,當這個查詢要反覆被執行的時候,實際上也就是用不同值取代‘xxxx’處的內容然後調用Execute。因爲SQL語句是一種即時解釋即時運行的語句,所以這樣做假如DBMS不優化的話,解析語句的過程就是一個非常大的浪費,因爲類似的語句被翻譯了n遍(n>=2)。現代的DBMS通常都是把SQL語句翻譯成一種叫做“執行計劃”的僞機器碼的形式,然後交由虛擬機或直接由硬件去執行,有時候翻譯的過程是一個比較耗時的過程。
索性的是,現代的DBMS都有一種緩衝執行計劃的能力,在翻譯前先查找有沒有類似語句的執行計劃可以利用,通常常量的值就不去比對了,這時DBMS會發現如下的兩條語句完全可以用同一個執行計劃來完成,這樣就省去了重新翻譯一條語句的過程:
Select * From 用戶表 Where 用戶號=’A’
Select * From 用戶表 Where 用戶號=’B’
從語言的角度講,這兩句中的‘A’和‘B’完全可以看做一個參數,語句本身則可以被認爲是完全相同的,在DBMS內部只需緩衝一個Select * From 用戶表 Where 用戶號=未知常量 語句的等價執行計劃,每次用不同的實際常量值代替未知常量這個佔位符就可以直接運行得到結果,這節約大量的翻譯SQL語句的時間,尤其是查詢再複雜些的時候。
在OLEDB中也提供了類似功能的接口,只不過語句中的常量佔位符需要用一個?(問號)來明確指出,當然能不能這樣使用佔位符最終還是要數據提供者來支持,當前大多數DBMS的OLDDB提供程序都支持這個標準的佔位符。比如上面的語句就可以利用這個佔位符來改寫成:
Select * From 用戶表 Where 用戶號=?
把這個語句直接傳遞給OLEDB的ICommandText接口即可。但是執行的時候就需要傳入真實的參數值。這時具備的好處就是這個命令只需要指定一次,隨後可以Execude很多次,同時在很多DBMS內部,這種命令也直接強制打開了緩衝執行計劃的能力,最終的好處就是整個系統的性能提升,尤其是對一些固定的查詢。這樣的查詢就被稱作“參數化查詢”(可以將一個語句想想成一個函數,常量用參變量來代替)。當然這種佔位符的功能不能被無限擴大化,佔位符不能代替列名錶名等語句要素,它只能用來代替常量,但它可以用來代替Insert語句的VALUES子句中的常量。
要想利用這種能力,就需要用到幾個新的接口ICommandPrepare和ICommandWithParameters,關於這些接口的定義以及方法就不贅述了,可以查閱OLEDB的幫助或者查閱MSDN即可。下面就給出調用的示意代碼,演示如何實現一個參數化查詢:
ICommandPrepare* pICommandPrepare = NULL;
ICommandWithParameters* pICommandWithParameters = NULL;
TCHAR* pSQL = _T("Select * From 用戶表 Where 用戶號=?");
DBPARAMS dbParam = {};
...........
GRS_COM_CHECK(pICommandText->SetCommandText(DBGUID_DEFAULT,pSQL));
GRS_COM_CHECK(pICommandText->QueryInterface(IID_ICommandPrepare
,(void**)&pICommandPrepare));
GRS_COM_CHECK(pICommandPrepare->Prepare(0));
GRS_COM_RELEASE(pICommandPrepare);
GRS_COM_CHECK(pICommandText->QueryInterface(IID_ICommandWithParameters
,(void**)&pICommandWithParameters));
GRS_COM_CHECK(pICommandWithParameters->GetParameterInfo(&iParamCnt
,&pParamInfo,&pStringBuffer));
GRS_COM_RELEASE(pICommandWithParameters);
if(iParamCnt > 0 )
{
GRS_COM_CHECK(pICommandText->QueryInterface(IID_IAccessor
,(void**)&pIAccessor));
rgBindings = (DBBINDING*)GRS_ALLOC(iParamCnt * sizeof(DBBINDING));
for(ULONG i = 0;i<iParamCnt;i++)
{
rgBindings[i].iOrdinal = pParamInfo[i].iOrdinal;
rgBindings[i].dwPart = DBPART_VALUE;
rgBindings[i].obStatus = 0;
rgBindings[i].obLength = 0;
rgBindings[i].obValue = dwOffset;
rgBindings[i].dwMemOwner = DBMEMOWNER_CLIENTOWNED;
rgBindings[i].eParamIO = DBPARAMIO_INPUT;
rgBindings[i].bPrecision = pParamInfo[i].bPrecision;
rgBindings[i].bScale = pParamInfo[i].bScale;
rgBindings[i].wType = pParamInfo[i].wType;
rgBindings[i].cbMaxLen = pParamInfo[i].ulParamSize;
dwOffset = rgBindings[i].cbMaxLen + rgBindings[i].obValue;
dwOffset = GRS_ROUNDUP(dwOffset);
}
GRS_COM_CHECK(pIAccessor->
CreateAccessor(DBACCESSOR_PARAMETERDATA, 1,
rgBindings, iParamCnt * sizeof(DBBINDING),&phAccessor, NULL));
GRS_COM_RELEASE(pIAccessor);
dbParam.cParamSets = 1;
dbParam.hAccessor = phAccessor;
dbParam.pData = GRS_ALLOC(dwOffset);
_tcscpy((TCHAR*)dbParam.pData,_T("aaa"));
}
GRS_COM_CHECK(pICommandText->Execute(NULL
,IID_IRowset,&dbParam,(DBROWCOUNT*)&iRow,(IUnknown**)&pIRowset));
.............
以上代碼中省略的部分可以參閱《七》文章中例子的代碼。
在上面的例子中先得到了一個ICommandPrepare接口,接着就調用了Prepare方法,Prepare方法的只有一個無符號長整形參數,表示預期的執行次數,當不知道的時候傳入0即可,0的含義就是執行多少次由調用者來決定,提供者儘量緩衝這個執行計劃。如果不在需要DBMS緩衝這個查詢的執行計劃了那麼就調用該接口的Unprepare方法顯式的釋放這個查詢的執行計劃。最好在不用的時候都調用下這個釋放操作,以便於DBMS系統釋放相關的資源,不然它就會傻乎乎的一直緩衝着這個執行計劃,如果有很多這樣的執行計劃被緩衝着,最終你會發現數據庫服務器內存會不堪重負。
Prepare之後就是利用ICommandWithParameters的GetParameterInfo方法得到這個參數化查詢的參數信息,這個與IColumnsInfo的GetColumnInfo方法是異曲同工。得到參數信息後,緊接着就是利用這個信息新建一個綁定結構(又見綁定,OLEDB綁定大法是使用OLEDB的不二法門),然後利用這個綁定結構創建一個訪問器HACCESSOR,最後就是準備一個DBPARAMS結構體的數組,放入參數,並調用Execute方法得到IRowset結果集。剩下的操作就是綁定讀取數據了。
以上的操作方式,只是幾種方式中的一種,並且並不是所有的OLEDB提供者都能支持
GetParameterInfo函數,如果不支持的時候,調用者如果知道參數的個數、類型、順序等等的時候就可以自己使用ICommandWithParameters接口的另一個方法SetParameterInfo,用這個方法可以告訴提供者,參數化查詢中有多少個參數,每個參數的類型、順序等等是什麼情況,之後任然需要創建一個參數的綁定結構,並創建HACCESSOR訪問器,當然二者的參數順序必須保持一致。下面的例子演示了使用SetParameterInfo方式進行參數化查詢:
......
GRS_COM_CHECK(pICommandText->QueryInterface(IID_ICommandWithParameters
,(void**)&pICommandWithParameters));
DBPARAMBINDINFO ParamBindInfo[1];
ULONG ParamOrdinals[1];
ZeroMemory(ParamBindInfo,sizeof(DBPARAMBINDINFO));
ParamBindInfo[0].pwszDataSourceType = _T("DBTYPE_STR");
ParamBindInfo[0].pwszName = NULL;
ParamBindInfo[0].ulParamSize = 50 * sizeof(char); //VARCHAR(50)
ParamBindInfo[0].dwFlags = DBPARAMFLAGS_ISINPUT;
ParamOrdinals[0] = 1;
GRS_COM_CHECK(pICommandWithParameters->SetParameterInfo(1
,ParamOrdinals,ParamBindInfo));
......
這樣就設置好了參數,這樣調用的好處是效率高,數據提供者不用深度去解析參數的類型等信息,只需要按照提供的順序安排參數的類型即可。一般參數化查詢時,都是事先知道參數個數、順序、類型等信息的,這樣調用之後,就可以直接創建HACCESSOR,然後調用Execute傳入參數得到結果了。
有些時候,主要用參數化查詢的方式來調用存儲過程,尤其是存儲過程輸出參數的時候更需要使用參數化查詢來得到輸出參數。如果存儲過程有輸出參數時,使用SetParameterInfo時需要注意明確指定DBPARAMBINDINFO 的dwFlags 成員爲DBPARAMFLAGS_ISOUTPUT,同時在創建綁定結構時,還需要明確指定DBBINDING的eParamIO參數爲DBPARAMIO_OUTPUT,如果參數同時用作輸入和輸出時,那麼綁定的時候就要指定成DBPARAMIO_INPUT | DBPARAMIO_OUTPUT。 最後Execute的成功之後,輸出參數就被放在了對應的參數緩衝中。在編寫參數化查詢需要調用的存儲過程SQL語句字符串時,如果存儲過程有幾個參數,就需要明確的使用幾個?佔位符來標明參數的位置,這樣纔可以調用GetParameterInfo來得到參數的具體信息。
最後一種設定參數信息的方式就是ICommandWithParameters接口的MapParameterNames,這個方法的調用方式與IColumnsInfo的MapColumnIDs方式是比較類似的,都是用於不清楚參數順序,而比較明確參數名稱的情況下,在調用存儲過程時,這是一種非常有用的方式,因爲存儲過程的參數都是顯式命名過的參數,此時可以不去關心參數的順序,而只需要按照參數的名稱來設置參數的其他相關信息即可,主要就是參數的類型信息。
至此參數化查詢以及以參數化查詢存儲過程的方式就介紹完了,但是參數化查詢存儲過程的方式沒有給出例子,我想這個留給大家自己去練習吧,有問題的話就可以跟帖討論。
(未完待續)