ADO
?
目 錄
第1章 基礎????1
1.1 引入ADO庫文件????1
1.1.1 版本????1
1.2 初始化OLE/COM庫環境????2
1.3 comdef.h????2
1.3.1 字符串編碼轉換????2
1.3.2 重要的類????3
1.3.3 重要的變量????4
1.3.4 智能指針????4
第2章 _ConnectionPtr????5
2.1 連接數據庫????5
2.2 執行SQL語句????5
2.3 事務處理????6
2.4 斷開連接????7
第3章 _RecordsetPtr????8
3.1 打開記錄集????8
3.1.1 CursorLocation????9
3.1.2 CursorType????9
3.1.3 LockType????10
3.2 遍歷記錄集????11
3.3 關閉記錄集????12
3.4 訪問BLOB????12
3.4.1 寫入????12
3.4.2 讀取????14
3.4.3 更新????15
3.5 書簽????15
3.6 過濾????16
3.7 Find????16
3.8 Sort????17
3.9 Index????17
3.10 綁定數據????18
3.10.1 CADORecordBinding派生類????18
3.10.2 綁定????20
3.10.3 讀取字段????20
3.10.4 添加新記錄????21
第4章 _CommandPtr????23
4.1 執行SQL語句????23
4.1.1 無名參數????23
4.1.2 有名參數????23
4.2 修改BLOB????24
4.3 執行存儲過程????24
4.3.1 無名參數????25
4.3.2 有名參數????26
4.3.3 Refresh????27
4.3.4 游標位置????27
4.4 重復使用命令對象????28
第5章 ADOX????29
5.1 引入庫文件????29
5.1.1 一個BUG????29
5.2 創建數據庫????30
5.3 創建數據表????30
第6章 JRO????31
6.1 VB6.0????31
6.1.1 引用????31
6.1.2 代碼????31
6.2 VC++????32
6.2.1 引入????32
6.2.2 代碼????32
第7章 常見問題????33
7.1 連接失敗的原因????33
7.2 改變當前數據庫????33
7.3 判斷某個數據庫是否存在????33
7.4 判斷某個表是否存在????34
7.5 ADO鎖定整張表????34
7.6 獲取記錄集行數????35
7.7 解決并發沖突????35
?
?
第1章 基礎
1.1 引入ADO庫文件
VC++使用ADO,首先需要導入ADO的類型庫。可以在StdAfx.h里增加如下代碼:
#import "c:/program files/common files/system/ado/msado15.dll" no_namespace rename("EOF","adoEOF")
no_namespace表示不需要命名空間。刪除這個詞,則ADO的接口函數將被放在ADODB命名空間內。也可以用rename_namespace("ADO")將命名空間更改為ADO。
rename("EOF","adoEOF")表示將EOF更改為adoEOF。因為EOF在某些文件里被定義為(-1),如此一來,VARIANT_BOOL EOF;就變成了VARIANT_BOOL (-1);把它當做類成員變量編譯時就會出錯。
VC++編譯器中的預處理器針對這條指令做了哪些工作呢?
1、根據msado15.dll里的類型庫信息生成COM組件的接口文件。具體的就是生成msado15.tlh和msado15.tli,前者是聲明文件,后者是實現文件;
2、替換#import語句為#include "msado15.tlh"。這樣,源文件里就可以使用COM組件的接口了;
3、msado15.tli是COM接口的實現文件,如果沒有它則程序可以編譯但無法連接。不過,msado15.tli被msado15.tlh包含起來了(通過#pragma start_map_region或#include),且它的函數全部為內聯的。因此,不用再單獨編譯msado15.tli。
1.1.1 版本
Windows7 sp1里的msado15.dll,其類型庫版本為 6.1,而Windows XP下msado15.dll的版本為2.x。最關鍵的是新版本的類型庫并不是向下兼容的!
如果在Windows7下編譯代碼,然后在Windows XP下運行,就有可能會出現問題。解決方法有兩個:
1、#import "msado15.dll"中的msado15.dll請使用低版本的,即Windows XP里的msado15.dll;
2、升級Windows XP里的ADO組件。
1.2 初始化OLE/COM庫環境
ADO是一組COM動態庫,所以使用ADO前,必須初始化OLE/COM庫。在MFC應用程序里,一個比較好的方法是在應用程序主類的InitInstance成員函數里初始化OLE/COM庫。
BOOL CMyAdoTestApp::InitInstance()
{
AfxOleInit();????//這就是初始化COM庫
……
}
也可以使用 CoInitialize或CoInitializeEx或OleInitialize初始化COM庫,退出程序前請使用CoUninitialize或OleUninitialize。
1.3 comdef.h
msado15.tlh文件里,語句#include <comdef.h>包含了comdef.h頭文件,這個頭文件里又有如下語句:
... ... ...
#include <comutil.h>
... ... ...
#pragma comment(lib, "comsupp.lib")
comdef.h、comutil.h、comsupp.lib包含了一些重要的變量、函數、類。
1.3.1 字符串編碼轉換
comutil.h頭文件里有兩個函數可用于字符串的編碼轉換:
namespace _com_util
{
BSTR __stdcall ConvertStringToBSTR(const char*pSrc);
char* __stdcall ConvertBSTRToString(BSTR pSrc);
}
_com_util::ConvertStringToBSTR將Ansi字符串轉換為Unicode字符串,返回值記得要調用SysFreeString釋放內存;
_com_util::ConvertBSTRToString 將Unicode字符串轉換為Ansi字符串,返回值記得要調用 delete[] 釋放內存;
這兩個函數的實現代碼在comsupp.lib里。
1.3.2 重要的類
在comutil.h和comdef.h這兩個頭文件里,有三個重要的類:_bstr_t、_variant_t、_com_error。
_bstr_t 封裝了BSTR,_variant_t封裝了VARIANT,_com_error封裝了COM錯誤。
使用_bstr_t和_variant_t可以極大的簡化代碼。以下面的代碼進行說明:
_RecordsetPtr m_pRecordset;
... ... ...
_variant_t vAge = m_pRecordset->GetCollect(_T("Age"));
上面的代碼用來讀取字段Age。查看msado15.tli里的Recordset15::GetCollect函數,可以知道COM組件返回的其實是一個VARIANT,GetCollect函數返回前創建了一個臨時_variant_t對象,將VARIANT封裝了起來并返回給vAge。
vAge析構時將調用VariantClear銷毀COM組件返回的VARIANT。如果Age字段是一個BSTR字符串,VariantClear會調用SysFreeString釋放BSTR字符串。
如果不借助_variant_t,而直接使用VARIANT,會是什么情況呢?那就是必須顯式的調用VariantClear函數,銷毀COM組件返回的每一個VARIANT。這樣代碼就會顯得非常臃腫,而且一旦疏忽就會發生內存泄露。
此外,使用_bstr_t也會相當的方便。
如:代碼_bstr_t s(m_pRecordset->GetCollect(_T("Age")));會自動將GetCollect返回的_variant_t轉換為BSTR字符串。
如:可以使用Ansi字符串構造_bstr_t對象
_bstr_t s1("測試_bstr_t");
如:可以使用Unicode字符串構造_bstr_t對象
_bstr_t s2(L"測試_bstr_t");
如:可以將Ansi字符串或Unicode字符串賦給_bstr_t變量
s1 = "_bstr_t測試";
s2 = L"_bstr_t測試";
如:可以輕松獲得Ansi字符串或Unicode字符串
const char*????????sA????=????(const char*)s1;????????????//返回Ansi字符串
const wchar_t*????sW????=????(const wchar_t*)s1;????????//返回Unicode字符串
需要注意的是:上面的sA、sW所指向的內存由_bstr_t維護。_bstr_t對象s1析構時,sA、sW也就成為了野指針。
1.3.3 重要的變量
在comutil.h里,有全局變量vtMissing,其定義如下:
extern _variant_t vtMissing;
它其實是類型為VT_EMPTY的VARIANT。
1.3.4 智能指針
宏_COM_SMARTPTR_TYPEDEF用來聲明智能指針。
如:智能指針_ConnectionPtr的聲明如下:
_COM_SMARTPTR_TYPEDEF(_Connection, __uuidof(_Connection));
宏展開之后,其實就是:
typedef _com_ptr_t< _com_IIID<_Connection, &__uuidof(_Connection)> > _ConnectionPtr;
_ConnectionPtr的實質就是一個自動維護COM計數的_Connection。
?
第2章 _ConnectionPtr
_ConnectionPtr表示與數據庫的連接。
2.1 連接數據庫
下面的代碼將實例化一個_ConnectionPtr,并連接數據庫
HRESULT hr = S_OK; _ConnectionPtr m_pConnection; {//創建連接 try { //創建 COM 對象,__uuidof(Connection) 可以替換為 "ADODB.Connection" hr = m_pConnection.CreateInstance(__uuidof(Connection)); if(SUCCEEDED(hr)) {//連接 Access 數據庫 Demo.mdb m_pConnection->Open("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=Demo.mdb" ,"","",adModeUnknown); } } catch(_com_error& e) { AfxMessageBox(e.Description()); } }? |
m_pConnection->Open用來連接數據庫。第一個參數用來指定連接字符串;第二、三個參數分別為用戶名和密碼;第四個參數是enum ConnectModeEnum,對數據庫的讀寫進行控制,如:adModeRead表示只讀……
2.2 執行SQL語句
Execute方法將執行SQL語句,其聲明如下:
_RecordsetPtr Connection15::Execute(_bstr_t CommandText
????????????????????????,VARIANT*RecordsAffected
????????????????????????,long Options);
CommandText????????是命令字串,通常是SQL命令
RecordsAffected????是操作完成后所影響的行數
Options????????????????表示CommandText的類型,取值如下
????????????????????adCmdText????????????表明CommandText是文本
????????????????????adCmdTable????????????表明CommandText是表名
????????????????????adCmdProc????????????表明CommandText是存儲過程
????????????????????adCmdUnknown????????未知
Execute執行完后返回一個記錄集。
示例代碼如下:
_variant_t RecordsAffected; //執行SQL命令,創建表格 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); //取得第一個字段的值放入vCount變量 _variant_t vCount = m_pRecordset->GetCollect((_variant_t)((long)0)); m_pRecordset->Close();//關閉記錄集 CString message; message.Format("共有%d條記錄",vCount.lVal); AfxMessageBox(message);///顯示當前記錄條數 |
2.3 事務處理
ADO中的事務處理也很簡單,只需分別在適當的位置調用Connection對象的三個方法即可,這三個方法是:
1、在事務開始時調用
pCnn->BeginTrans();
2、在事務結束并成功時調用
pCnn->CommitTrans();
3、在事務結束并失敗時調用
pCnn->RollbackTrans();
在使用事務處理時,應盡量減小事務的范圍,即減小從事務開始到結束(提交或回滾)之間的時間間隔,以便提高系統效率。需要時也可在調用BeginTrans()方法之前,先設置Connection對象的IsolationLevel屬性值,詳細內容參見MSDN中有關ADO的技術資料。
2.4 斷開連接
下面的代碼將斷開與數據庫的連接,并銷毀_ConnectionPtr實例
if(m_pConnection) {//關閉ADO連接 if(m_pConnection->State) { m_pConnection->Close(); } //下面的語句可有可無。因為m_pConnection是智能指針,析構時會自動Release m_pConnection.Release(); } |
?
第3章 _RecordsetPtr
_RecordsetPtr表示記錄集。
3.1 打開記錄集
使用Open方法打開記錄集,其聲明如下:
HRESULT Recordset15::Open(const _variant_t& Source
????????????????????????????,const _variant_t & ActiveConnection
????????????????????????????,enum CursorTypeEnum CursorType
????????????????????????????,enum LockTypeEnum LockType
????????????????????????????,long Options);
Source????????????????數據查詢字符串
ActiveConnection????是_Connection*或連接數據庫的字符串
CursorType????????????光標類型
LockType????????????鎖定類型
Options????????????????可以取如下值之一:
????????????????????adCmdText????????????表明CommandText是文本命令
????????????????????adCmdTable????????????表明CommandText是表名
????????????????????adCmdProc????????????表明CommandText是存儲過程
????????????????????adCmdUnknown????????未知
下面的代碼首先實例化一個_RecordsetPtr,然后在_ConnectionPtr的基礎上,打開一個記錄集。注意Open函數的第二個參數,表示數據庫連接。
_RecordsetPtr m_pRecordset; try { //創建 COM 對象,__uuidof(Recordset) 可以替換為 "ADODB.Recordset" hr = m_pRecordset.CreateInstance(__uuidof(Recordset)); if(SUCCEEDED(hr)) { m_pRecordset->CursorLocation = adUseClient; m_pRecordset->Open(_T("SELECT * FROM DemoTable") ,m_pConnection.GetInterfacePtr() ,adOpenDynamic ,adLockOptimistic //樂觀鎖 ,adCmdText); } } catch(_com_error& e) { AfxMessageBox(e.Description()); } |
可以將Open函數的第二個參數更換為連接字符串,如:"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=Demo.mdb"。在此情況下,Open函數首先連接數據庫,然后再打開一個記錄集。
3.1.1 CursorLocation
CursorLocation表示游標的位置,有兩個選項:
adUseServer????=????2????//默認,表示游標在服務端
adUseClient????????=????3????//表示游標在客戶端
游標在服務端,則記錄集對數據變化是敏感的。亦即當其它用戶修改了數據庫內的數據后,有可能會影響到客戶端已經打開的記錄集。游標在客戶端,就不會有數據敏感性了。
游標在服務端,則客戶端打開記錄集時數據將保留在服務端,不會通過網絡傳給客戶端。但是訪問記錄集里的數據時,就會頻繁的網絡通訊。游標在客戶端,則客戶端打開記錄集時數據將通過網絡緩存到客戶端。訪問記錄集里的數據時,將不再需要網絡通訊。因此,如果頻繁的訪問數據則客戶端游標的性能較高。
此外,服務端游標不支持書簽而客戶端游標支持書簽。這將影響到某些函數的使用,如:GetRecordCount函數有可能返回-1,而不是實際的記錄個數。
3.1.2 CursorType
游標類型有四種:
adOpenForwardOnly????????=????0,????//向前游標
adOpenKeyset????????????=????1,????//鍵集游標
adOpenDynamic????????=????2,????//動態游標
adOpenStatic????????????=????3????//靜態游標
adOpenForwardOnly、adOpenStatic 表示記錄集是靜態的:打開記錄集后,它就不會再發生變化,即使其他用戶修改了數據庫里的數據。兩者的區別在于adOpenForwardOnly只能向前移動游標,而adOpenStatic可以任意移動游標。
adOpenKeyset、adOpenDynamic 表示記錄集是動態的:打開記錄集后,能夠反映其他用戶所做的修改。
adOpenKeyset 的實現原理:打開的記錄集僅僅保存記錄的關鍵字(Key),訪問某條記錄時是根據 Key 到數據庫訪問的。所以,其他用戶修改了某條記錄后,再訪問這條記錄,是能夠發現記錄改變了。但是,其他用戶如果增加、刪除記錄,并不會更改關鍵字記錄集,因此通過關鍵字記錄集,無法及時獲知增加、刪除的記錄。
adOpenDynamic 的實現原理:打開的記錄集會根據數據庫內容的變化及時予以更新。但是需要注意CursorLocation 必須等于 adUseServer,即游標必須在服務端才能實現此功能。
3.1.3 LockType
adLockReadOnly????????=????1,????????//只讀
adLockPessimistic????????=????2, ????????//悲觀鎖
adLockOptimistic????????=????3, ????????//樂觀鎖
adLockBatchOptimistic????=????4 ????????//批量樂觀鎖
悲觀鎖要求CursorLocation = adUseServer。它表示:編輯字段時就鎖定記錄,Update之后停止鎖定。這種方法最保險,但是效率最低;
樂觀鎖只有在 Update 時才鎖定記錄,更新完畢后停止鎖定。也就是說,每調用一次Update,都會鎖定、解鎖一次;
批量樂觀鎖表示調用UpdateBatch時鎖定記錄,批量更新完畢后停止鎖定。也就是說,每調用一次UpdateBatch,都會鎖定、解鎖一次。
假定使用悲觀鎖,多用戶執行下面三行代碼,會發生什么情況呢?
m_pRecordset->MoveFirst(); m_pRecordset->PutCollect("name","Test"); m_pRecordset->Update(); |
用戶A執行到m_pRecordset->PutCollect,將鎖定記錄集的第一行。其它用戶運行到m_pRecordset->PutCollect時,也想鎖定第一行,但因為已經被A鎖定,所以其它用戶執行到此行時會等待若干時間后拋出異常。
用戶A執行完m_pRecordset->Update后,將解鎖記錄集的第一行。注意:具體何時解鎖,每種數據庫好像不盡相同。經筆者測試,發現Access2000是Update后即解鎖,而SQL Server2008在記錄集關閉后才解鎖。
3.2 遍歷記錄集
如下代碼將遍歷記錄集。
_variant_t var; CString sName,sAge; try { //如果 BOF 和 EOF 均為真,則無記錄 if(!m_pRecordset->BOF || !m_pRecordset->adoEOF) { m_pRecordset->MoveFirst(); while(!m_pRecordset->adoEOF) { var = m_pRecordset->GetCollect(_T("Name")); if(var.vt != VT_NULL) {//姓名 sName = (LPCTSTR)_bstr_t(var); } else { sName.Empty(); } var = m_pRecordset->GetCollect(_T("Age")); if(var.vt != VT_NULL) {//年齡 sAge = (LPCTSTR)_bstr_t(var); } else { sAge.Empty(); } m_pRecordset->MoveNext(); } } } catch(_com_error& e) { AfxMessageBox(e.Description()); } |
var = m_pRecordset->GetCollect(_T("Name"));的等價代碼如下:
FieldsPtr????pFields????=????m_pRecordset->Fields; FieldPtr????????pField????=????pFields->Item[_T("Name")]; var????????????????????=????pField->Value; |
3.3 關閉記錄集
下面的代碼將關閉記錄集,并銷毀_RecordsetPtr實例。
if(m_pRecordset) {//關閉記錄集 if(m_pRecordset->State) { m_pRecordset->Close(); } //下面的語句可有可無。因為m_pRecordset是智能指針,析構時會自動Release m_pRecordset.Release(); } |
3.4 訪問BLOB
在Microsoft SQL中,text、image……被當做二進制數據進行處理。
可以用Field對象的GetChunk和AppendChunk方法來訪問。每次可以讀出或寫入全部數據的一部分,它會記住上次訪問的位置。但是如果中間訪問了別的字段后,就又得從頭來了。
3.4.1 寫入
可以使用m_pRecordset->PutCollect("data",varBLOB)將二進制數據一次性寫入,也可以使用AppendChunk分多次寫入二進制數據。
void CDlgMain::blobFileToDB(LPCTSTR szFile) { CFile f; if(f.Open(szFile,CFile::modeRead | CFile::shareDenyWrite)) { FieldPtr fd = m_pRecordset->Fields->Item["data"]; DWORD dwSize = f.GetLength(); if(dwSize > 0) { const int????????????nBlock????????= 1024; DWORD????dwWrite????????= 0; //已經寫入數據庫的字節數 DWORD????????????dwAppend????= 0; //單次寫入的字節數 UINT????????????????uRead????????= 0; SAFEARRAY*????psa????????= SafeArrayCreateVector(VT_UI1,0,nBlock); _variant_t????????????vBLOB; vBLOB.vt????????????=????VT_ARRAY | VT_UI1; vBLOB.parray????????=????psa; ? for(;;) { dwAppend = dwSize - dwWrite; if(!dwAppend) { break; } if(dwAppend > nBlock) { dwAppend = nBlock; } SafeArrayLock(psa); uRead = f.Read(psa->pvData,dwAppend); SafeArrayUnlock(psa); if(uRead != dwAppend) { break; } psa->rgsabound[0].cElements = dwAppend; fd->AppendChunk(vBLOB); dwWrite += dwAppend; } //SafeArrayDestroy(psa); //vBLOB析構時會自動釋放數組 psa } else { _variant_t vNull; vNull.vt = VT_NULL; fd->PutValue(vNull); } f.Close(); } } |
代碼fd->PutValue(vNull);相當于m_pRecordset->PutCollect("data",vNull)用來設置BLOB為NULL,也就是清空。也可以使用SQL語句清空某個BLOB字段,如:
UPDATE 表名 SET BLOB字段名 = NULL WHERE ID=1
3.4.2 讀取
可以使用m_pRecordset-> GetCollect("data")將二進制數據一次性讀取出來,也可以使用GetChunk分多次讀取二進制數據。
void CDlgMain::blobDBtoFile(LPCTSTR szFile) { CFile f; if(f.Open(szFile,CFile::modeWrite | CFile::modeCreate)) { FieldPtr????fd????????????=????m_pRecordset->Fields->Item["data"]; long????????nSizeActual????=????fd->ActualSize; if(nSizeActual > 0) { const int????????????nBlock????=????1024; long????????????????nRead????=????0; ????//已經讀取的字節數 long????????????????nGet????=????0; ????//單次讀取的字節數 _variant_t????????????vBLOB; ULONG????????????uWrite????=????0; ????//單次寫入文件的字節數 SAFEARRAY*????psa; ? for(;;) { nGet = nSizeActual - nRead; if(!nGet) { break; } if(nGet > nBlock) { nGet = nBlock; } vBLOB = fd->GetChunk(nGet); nRead += nGet; if(vBLOB.vt == (VT_ARRAY | VT_UI1) && (psa = vBLOB.parray) && psa->cDims == 1) { uWrite = psa->rgsabound[0].cElements; if(uWrite) { SafeArrayLock(psa); f.Write(psa->pvData,uWrite); SafeArrayUnlock(psa); } } } } f.Close(); } } |
3.4.3 更新
通過_CommandPtr,執行帶參數的SQL語句,可實現BLOB數據的修改。請參考_CommandPtr這一章。
3.5 書簽
書簽(bookmark)可以唯一標識記錄集中的一個記錄,用于快速地將當前記錄移回到已訪問過的記錄,以及進行過濾等等。Provider會自動為記錄集中的每一條記錄產生一個書簽,我們只需要使用它就行了。我們不能試圖顯示、修改或比較書簽。ADO用記錄集的Bookmark屬性表示當前記錄的書簽。
用法步驟:
rst->Supports(adBookmark);????????????//判斷是否支持書簽
_variant_t VarBookmark;????????????//建立書簽變量
VarBookmark = rst->Bookmark;????????//獲得書簽值
... ... ...????????????????????????//可以移動記錄
if(VarBookmark.vt != VT_EMPTY)
{//將記錄位置恢復到書簽位置
rst->Bookmark = VarBookmark;
}
3.6 過濾
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屬性。
3.7 Find
以下代碼用于查找記錄
pRst->Find("姓名 = '趙薇'",1,adSearchForward);
一般情況下,這種查找是順序查找,效率較低。可針對某個字段進行排序,其方法如下:
//將該字段的Optimize屬性設置為True
pRst->Fields->GetItem("姓名")->Properties->
GetItem("Optimize")->PutValue("True");
... ... ...
pRst->Find("姓名 = '趙薇'",1,adSearchForward);
... ... ...
//將該字段的Optimize屬性設置為False
pRst->Fields->GetItem("姓名")->Properties->
GetItem("Optimize")->PutValue("False");
3.8 Sort
要排序也很簡單,只要把要排序的關鍵字列表設置到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屬性值在需要時隨時可以修改。
3.9 Index
pRst->Index="";????????????//首先設置索引(數據庫里建立的索引)
pRst->Seek(...,...);????????//有序查找
3.10 綁定數據
定義一個綁定類,將其成員變量綁定到一個指定的記錄集,以方便于訪問記錄集的字段值。
3.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;}
3.10.2 綁定
_RecordsetPtr Rs1;
IADORecordBinding *picRs=NULL;
CCustomRs rs;
......
Rs1->QueryInterface(__uuidof(IADORecordBinding),(LPVOID*) picRs);
picRs->BindToRecordset(&rs);
派生出的類必須通過IADORecordBinding接口才能綁定,調用它的BindToRecordset方法就行了。
3.10.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( // 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();)。
3.10.4 添加新記錄
此時還可以用IADORecordBinding接口添加新記錄
if(FAILED(picRs->AddNew(&rs)))
......
?
第4章 _CommandPtr
_CommandPtr用來執行SQL語句或調用存儲過程。
4.1 執行SQL語句
下面的代碼首先實例化一個_CommandPtr,然后執行一條SQL語句,執行返回的結果就是一個記錄集:
_CommandPtr cmd; cmd.CreateInstance(__uuidof(Command)); cmd->ActiveConnection????=????theApp.m_pConnection; cmd->CommandText????????=????"select * from file where name like '%.jpg'"; _RecordsetPtr????rs????????=????cmd->Execute(NULL,NULL,adCmdText); |
_CommandPtr不僅僅能執行SQL語句,它還可以給SQL語句傳遞參數。
4.1.1 無名參數
下面的代碼中,SQL語句中的?號就是一個無名參數。執行SQL語句時,從左至右第一個?號將被cmd->Parameters->Item[0]->Value代替;第二個?號將被cmd->Parameters->Item[1]->Value代替……
所以,調用cmd->Execute執行SQL語句前,需要調用cmd->CreateParameter創建參數,并調用cmd->Parameters->Append將此參數添加至cmd->Parameters集合。
_CommandPtr cmd; cmd.CreateInstance(__uuidof(Command)); cmd->ActiveConnection????=????theApp.m_pConnection; cmd->CommandText????????=????"select * from file where name like ?"; cmd->Parameters->Append( ????????????????cmd->CreateParameter("",adChar,adParamInput,-1,"%.jpg")); _RecordsetPtr????rs????????=????cmd->Execute(NULL,NULL,adCmdText); |
4.1.2 有名參數
下面的代碼中,SQL語句中的@name就是一個有名參數。執行SQL語句時,@name將被cmd->Parameters->Item["@name"]->Value代替。
所以,調用cmd->Execute執行SQL語句前,需要調用cmd->CreateParameter創建有名參數,并調用cmd->Parameters->Append將此參數添加至cmd->Parameters集合。
_CommandPtr cmd; cmd.CreateInstance(__uuidof(Command)); cmd->ActiveConnection????=????theApp.m_pConnection; cmd->CommandText????????=????"select * from file where name like @name"; cmd->Parameters->Append( ????????????????cmd->CreateParameter("@name",adChar,adParamInput,-1,"%.jpg")); _RecordsetPtr????rs????????=????cmd->Execute(NULL,NULL,adCmdText); |
4.2 修改BLOB
通過_CommandPtr,執行帶參數的SQL語句,可實現BLOB數據的修改。
_variant_t vBLOB; vBLOB.vt = VT_ARRAY | VT_UI1; vBLOB.parray = SafeArrayCreateVector(VT_UI1,0,30); SafeArrayLock(vBLOB.parray); memset(vBLOB.parray->pvData,0,30); SafeArrayUnlock(vBLOB.parray); ? _CommandPtr cmd(__uuidof(Command)); cmd->ActiveConnection = theApp.m_pConnection; cmd->CommandText = "UPDATE 表名 SET BLOB字段名=? WHERE ID='1'"; cmd->Parameters->Append( cmd->CreateParameter("",adVarBinary,adParamInput,-1,vBLOB)); cmd->Execute(NULL,NULL,adCmdText); |
4.3 執行存儲過程
SQL2008里,執行如下SQL語句,創建一個存儲過程:
if exists (select * from sysobjects where id = object_id(N'[sp_1]') and OBJECTPROPERTY(id, N'IsProcedure')= 1) drop procedure sp_1 GO ? CREATE PROCEDURE sp_1(@pin1 int ,@pin2 CHAR(10) ,@pout1 int OUTPUT ,@pout2 CHAR(10) OUTPUT) AS BEGIN declare @retval int select @pout1 = @pin1 + 100 select @pout2 = left( ltrim(rtrim(@pin2)) + '123' , 10) select * from [file] select @retval = 1236 return @retval END GO ? exec sp_1 10,'Test',20,'789' GO |
使用_CommandPtr執行這個存儲過程,需要傳遞、接收的參數如下:
@RETURN_VALUE(int,返回值)????????????//第0個參數,返回值
@pin1(int,輸入)????????????????????????????//第1個參數
@pin2(char(10),輸入)????????????????????//第2個參數
@pout1(int ,輸入/輸出)????????????????????//第3個參數
@pout2(char(10),輸入/輸出)????????????????//第4個參數
4.3.1 無名參數
不使用Refresh方法,執行存儲過程的代碼如下:
_CommandPtr cmd; cmd.CreateInstance(__uuidof(Command)); cmd->ActiveConnection????=????theApp.m_pConnection; cmd->CommandText????????=????"sp_1"; //存儲過程名稱 //添加參數——返回值 cmd->Parameters->Append( ????????????cmd->CreateParameter("",adInteger,adParamReturnValue,sizeof(int))); //添加參數——@pin1 cmd->Parameters->Append( ????????????cmd->CreateParameter("",adInteger,adParamInput,sizeof(int),3L)); //添加參數——@pin2 cmd->Parameters->Append( ????????????cmd->CreateParameter("",adChar,adParamInput,10,_variant_t(_T("DD1")))); //添加參數——@pout1 cmd->Parameters->Append( ????????????cmd->CreateParameter("",adInteger,adParamOutput,sizeof(int))); //添加參數——@pout2 cmd->Parameters->Append( ????????????cmd->CreateParameter("",adChar,adParamOutput,10)); //執行存儲過程 theApp.m_pConnection->CursorLocation = adUseClient; _RecordsetPtr????rs????=????cmd->Execute(NULL,NULL,adCmdStoredProc); //獲取執行結果 _variant_t vRet????????=????cmd->Parameters->Item[0L]->Value; //獲得返回值 _variant_t vin1????????=????cmd->Parameters->Item[1L]->Value; //獲得@pin1 _variant_t vin2????????=????cmd->Parameters->Item[2L]->Value; //獲得@pin2 _variant_t vout1????=????cmd->Parameters->Item[3L]->Value; //獲得@pout1 _variant_t vout2????=????cmd->Parameters->Item[4L]->Value; //獲得@pout2 |
總結:不使用Refresh方法,則調用pCmd->Parameters->Append增加參數時,必須要注意參數的順序。
4.3.2 有名參數
如果創建參數時指定參數名稱,就可以根據名稱獲取執行結果了。
//添加參數——返回值 cmd->Parameters->Append( ????cmd->CreateParameter("@RETURN_VALUE",adInteger,adParamReturnValue,sizeof(int))); //添加參數——@pin1 cmd->Parameters->Append( ????cmd->CreateParameter("@pin1",adInteger,adParamInput,sizeof(int),3L)); //添加參數——@pin2 cmd->Parameters->Append( ????cmd->CreateParameter("@pin2",adChar,adParamInput,10,_variant_t(_T("DD1")))); //添加參數——@pout1 cmd->Parameters->Append( ????cmd->CreateParameter("@pout1",adInteger,adParamOutput,sizeof(int))); //添加參數——@pout2 cmd->Parameters->Append( ????cmd->CreateParameter("@pout2",adChar,adParamOutput,10)); //執行存儲過程 theApp.m_pConnection->CursorLocation = adUseClient; _RecordsetPtr????rs????=????cmd->Execute(NULL,NULL,adCmdStoredProc); //獲取執行結果 _variant_t vRet????????=????cmd->Parameters->Item["@RETURN_VALUE"]->Value; _variant_t vin1????????=????cmd->Parameters->Item["@pin1"]->Value; _variant_t vin2????????=????cmd->Parameters->Item["@pin2"]->Value; _variant_t vout1????=????cmd->Parameters->Item["@pout1"]->Value; _variant_t vout2????=????cmd->Parameters->Item["@pout2"]->Value; |
注意:即便參數有了名稱,添加參數時的順序也不能改動。
4.3.3 Refresh
執行cmd->Parameters->Refresh();會做哪些工作呢?
1、設置cmd->Parameters->Item[0L]
設置cmd->Parameters->Item[0L]->Name 為 "@RETURN_VALUE"
設置cmd->Parameters->Item[0L]->Value 為 VT_EMPTY
2、設置adParamInput參數的Value為 VT_EMPTY
3、設置adParamOutput參數的Value為 VT_NULL
當第2、3、……次執行cmd->Execute前,可以這么做:
cmd->Parameters->Refresh(); cmd->Parameters->Item["@pin1"]->Value????=????3L; cmd->Parameters->Item["@pin2"]->Value????=????"C"; cmd->Execute(NULL,NULL,adCmdStoredProc);????????//第2次執行存儲過程 cmd->Parameters->Refresh(); cmd->Parameters->Item["@pin1"]->Value????=????4L; cmd->Parameters->Item["@pin2"]->Value????=????"D"; cmd->Execute(NULL,NULL,adCmdStoredProc); ????????//第3次執行存儲過程 |
4.3.4 游標位置
如果游標位置不為adUseClient,那么取return和output參數之前,必須把返回的記錄集關閉掉。
下面的代碼能夠正常工作。因為這行代碼執行完畢后,返回的記錄集會被銷毀,銷毀前會關閉記錄集。
pCmd->Execute(NULL,NULL,adCmdStoredProc);
下面的代碼就得注意了。在 rs 銷毀前,能否取得return和output參數,取決于游標位置是否為adUseClient。如果是adUseClient就能正常取值,否則必須關閉rs記錄集后,才能正常取值。
_RecordsetPtr rs = pCmd->Execute(NULL,NULL,adCmdStoredProc);
theApp.m_pConnection->CursorLocation的取值會影響到pCmd->Execute返回記錄集的游標位置。如:pCmd->Execute執行前,執行theApp.m_pConnection->CursorLocation = adUseClient,則返回記錄集的游標位置也是adUseClient。
4.4 重復使用命令對象
一個命令對象如果要重復使用多次(尤其是帶參數的命令),則在第一次執行之前,應將它的Prepared屬性設置為TRUE。這樣會使第一次執行減慢,但卻可以使以后的執行全部加快。
?
?
第5章 ADOX
5.1 引入庫文件
#import "C:/program Files/Common Files/system/ado/msadox.dll"
5.1.1 一個BUG
打開文件msadox.tlh,可以看到以下內容。其中,enum DataTypeEnum是先使用,后聲明的。
struct __declspec(uuid("0000061d-0000-0010-8000-00aa006d2ea4")) Columns : _Collection { ... ... ... HRESULT Append ( const _variant_t & Item, enum DataTypeEnum Type, long DefinedSize ); ... ... ... }; ? enum DataTypeEnum { adEmpty = 0, adTinyInt = 16, ... ... ... }; |
在此情況下,如下代碼將會出現編譯錯誤。
#import "C:/Program Files/Common Files/System/ado/msado15.dll"\ rename("EOF","adoEOF") no_namespace #import "C:/program Files/Common Files/system/ado/msadox.dll" |
原因在于:編譯msadox.tlh時Columns::Append函數的第2個參數將是msado15.tlh里的enum DataTypeEnum;編譯msadox.tli時Columns::Append函數的第2個參數卻變成了msadox.tlh里的JRO::DataTypeEnum。
解決方法一:調整import的順序
#import "C:/program Files/Common Files/system/ado/msadox.dll" #import "C:/Program Files/Common Files/System/ado/msado15.dll"\ rename("EOF","adoEOF") no_namespace |
解決方法二:都使用命名空間
#import "C:/Program Files/Common Files/System/ado/msado15.dll"\ rename("EOF","adoEOF") #import "C:/program Files/Common Files/system/ado/msadox.dll" |
5.2 創建數據庫
ADOX::_CatalogPtr pCatalog(__uuidof(ADOX::Catalog));
_bstr_t s("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=G:\\1.mdb");
pCatalog->Create(s);
5.3 創建數據表
ADODB::_ConnectionPtr conn(__uuidof(ADODB::Connection));
conn->Open(sConn,"","",ADODB::adModeUnknown);
conn->Execute("CREATE TABLE TestTable(記錄編號 INTEGER,姓名 TEXT,年齡 INTEGER)",NULL,ADODB::adCmdText);????
conn->Close();
?
第6章 JRO
JRO是Jet and Replication Objects的縮寫,它可以用來壓縮Access數據庫文件。
6.1 VB6.0
6.1.1 引用
6.1.2 代碼
Dim sSrc As String Dim sDes As String sSrc = "Provider=Microsoft.Jet.OLEDB.4.0;Jet OLEDB:Engine Type=5;Data Source=G:\dbFile.mdb" sDes = "Provider=Microsoft.Jet.OLEDB.4.0;Jet OLEDB:Engine Type=5;Data Source=G:\1.mdb" Dim oJetEngine As New JRO.JetEngine oJetEngine.CompactDatabase sSrc, sDes Set oJetEngine = Nothing |
注意:"Jet OLEDB:Database Password="可以指定密碼。
6.2 VC++
6.2.1 引入
引入JRO需要引入ADO庫,代碼如下:
#import "C:/Program Files/Common Files/System/ado/msado15.dll"\ rename("EOF","adoEOF") no_namespace #import "C:/Program Files (x86)/Common Files/System/ado/msjro.dll" |
上面的ADO庫未使用命名空間,如果使用了命名空間,則代碼如下。增加了兩條using語句,否則無法完成編譯。
#import "C:/Program Files/Common Files/System/ado/msado15.dll"\ rename("EOF","adoEOF") using ADODB::_Recordset; using ADODB::_RecordsetPtr; #import "C:/Program Files (x86)/Common Files/System/ado/msjro.dll" |
6.2.2 代碼
AfxOleInit(); _bstr_t sSrc(_T("Provider=Microsoft.Jet.OLEDB.4.0;Jet OLEDB:Engine Type=5;Data Source=G:\\dbFile.mdb")); _bstr_t sDes(_T("Provider=Microsoft.Jet.OLEDB.4.0;Jet OLEDB:Engine Type=5;Data Source=G:\\1.mdb")); JRO::IJetEnginePtr jet(__uuidof(JRO::JetEngine)); jet->CompactDatabase(sSrc,sDes); |
注意:"Jet OLEDB:Database Password="可以指定密碼。
?
?
第7章 常見問題
7.1 連接失敗的原因
Enterprise Managemer內,打開將服務器的屬性對話框,在Security選項卡中,有一個選項Authentication。
如果該選項是Windows NT only,則你的程序所用的連接字符串就一定要包含Trusted_Connection參數,并且其值必須為yes,如:
"Provider=SQLOLEDB;Server=888;Trusted_Connection=yes"
";Database=master;uid=lad;";
如果不按上述操作,程序運行時連接必然失敗。
如果Authentication選項是SQL Server and Windows NT,則你的程序所用的連接字符串可以不包含Trusted_Connection參數,如:
"Provider=SQLOLEDB;Server=888;Database=master;uid=lad;pwd=111;";
因為ADO給該參數取的默認值就是no,所以可以省略。我認為還是取默認值比較安全一些。
7.2 改變當前數據庫
使用Tansct-SQL中的USE語句即可。
7.3 判斷某個數據庫是否存在
1、可打開master數據庫中一個叫做SCHEMATA的視圖,其內容列出了該服務器上所有的數據庫名稱。
2、更簡便的方法是使用USE語句,成功了就存在;不成功,就不存在。例如:
try
{
????m_pConnect->Execute("USE INSURANCE_2002",NULL
????????????????????????,adCmdText│adExecuteNoRecords);
}
catch(_com_error&e)
{//數據庫INSURANCE_2002不存在
}
7.4 判斷某個表是否存在
1、同樣判斷一個表是否存在,也可以用是否成功地打開它來判斷,十分方便,例如:
try
{
????m_pRecordset->Open("mytable"
????????????????????????,_variant_t((IDispatch *)m_pConnection,true)
????????????????????????,adOpenKeyset,adLockOptimistic,adCmdTable);
}
catch (_com_error &e)
{//該表不存在
}
2、要不然可以采用麻煩一點的辦法,就是在MS-SQL服務器上的每個數據庫中都有一個名為sysobjects的表,查看此表的內容即知指定的表是否在該數據庫中。
3、同樣,每個數據庫中都有一個名為TABLES的視圖(View),查看此視圖的內容即知指定的表是否在該數據庫中。
7.5 ADO鎖定整張表
Dim oConn As New ADODB.Connection
Dim oRs As New ADODB.Recordset
oConn.ConnectionTimeout = 15
oConn.Open 'Provider=SQLOLEDB.1;Password=***;Persist Security Info=True;User ID=***;Initial Catalog=XSSystem;Data Source=10.108.0.1'
oConn.CommandTimeout = 15
oConn.IsolationLevel = adXactSerializable
oConn.BeginTrans
oRs.CursorLocation = adUseClient
oRs.Open 'SELECT * FROM ShangYaoGuFenBuyTable with(tablockx) where ID='123' ', oConn, adOpenKeyset, adLockPessimistic
If oRs.RecordCount > 0 Then
MsgBox '已經有一條記錄了'
Else
oRs.AddNew
oRs('id') = '123'
oRs.Update
End If
oRs.Close
oConn.CommitTrans '在此步驟之前,ShangYaoGuFenBuyTable整張表會被鎖住,其他用戶不能進行任何訪問
oConn.Close
Set oConn = Nothing
7.6 獲取記錄集行數
可以使用SQL語句:select count(*) from 表名
7.7 解決并發沖突
使用:Field 對象的 UnderlyingValue 和 OriginalValue 屬性;Recordset 的 Resync 方法和 Filter 屬性。
調用UpdateBatch后,一定要立即檢查Errors集合是否有錯誤。如果有錯誤,則應檢查錯誤是否為并發沖突:
1、設置 Recordset 的 Filter 屬性為adFilterConflictingRecords。若此時 RecordCount 屬性等于零,就說明錯誤是由沖突以外的其他原因引起的。
2、調用 Recordset 的 Resync 方法,將 AffectRecords 參數設置為adAffectGroup,將 ResyncValues 參數設置為 adResyncUnderlyingValues。Resync 方法將用來自基本數據庫中的數據刷新在當前 Recordset 對象中的數據。通過使用 adAffectGroup,可以確保只有使用當前篩選設置的情況下可見的記錄。
轉載于:https://www.cnblogs.com/hanford/p/6164335.html
總結
- 上一篇: Dom之标签属性
- 下一篇: svn 怎么直接同步指定服务器的某个文件