CDatabase類
要建立與數據源的連接,首先應構造一個CDatabase對象,然後再調用CDatabase的Open成員函數.Open函數負責建立連接,其聲明為
virtual BOOL Open( LPCTSTR lpszDSN, BOOL bExclusive = FALSE, BOOL bReadOnly = FALSE, LPCTSTR lpszConnect = “ODBC;”, BOOL bUseCursorLib = TRUE ); throw( CDBException, CMemoryException );
參數lpszDSN指定了數據源名(構造數據源的方法將在後面介紹),在lpszConnect參數中也可包括數據源名,此時lpszDSN必需為NULL,若在函數中未提供數據源名且使lpszDSN為NULL,則會顯示一個數據源對話框,用戶可以在該對話框中選擇一個數據源.參數bExclusive說明是否獨占數據源,由於目前版本的類庫還不支持獨占方式,故該參數的值應該是FALSE,這說明數據源是被共享的.參數bReadOnly若為TRUE則對數據源的連接是只讀的.參數lpszConnect指定了一個連接字符串,連接字符串中可以包括數據源名、用戶帳號(ID)和口令等信息,字符串中的"ODBC"表示要連接到一個ODBC數據源上.參數bUseCursorLib若為TRUE,則會裝載光標庫,否則不裝載,快照需要光標庫,動態集不需要光標庫. 若連接成功,函數返回TRUE,若返回FALSE,則說明用戶在數據源對話框中按了Cancel按鈕。若函數內部出現錯誤,則框架會產生一個異常。
下面是一些調用Open函數的例子。
CDatabase m_db; //在文檔類中嵌入一個CDatabase對象
//連接到一個名為"Student Registration"的數據源
m_db.Open("Student Registration");
//在連接數據源的同時指定了用戶帳號和口令
m_db.Open(NULL,FALSE,FALSE,"ODBC;DSN=Student Registration;UID=ZYF;PWD=1234");
m_db.Open(NULL); //將彈出一個數據源對話框
要從一個數據源中脫離,可調用函數Close。在脫離後,可以再次調用Open函數來建立一個新的連接.調用IsOpen可判斷當前是否有一個連接,調用GetConnect可返回當前的連接字符串。函數的聲明為
virtual void Close( );
BOOL IsOpen( ) const; //返回TRUE則表明當前有一個連接
const CString& GetConnect( ) const;
CDatabase的析構函數會調用Close,所以只要刪除了CDatabase對象就可以與數據源脫離。
CRecordset類
CRecordset類代表一個記錄集.該類是MFC的ODBC類中最重要、功能最強大的類。
10.5.1 動態集、快照、光標和光標庫
在多任務操作系統或網絡環境中,多個用戶可以共享同一個數據源。共享數據的一個主要問題是如何協調各個用戶對數據源的修改。例如,當某一個應用改變了數據源中的記錄時,別的連接至該數據源的應用應該如何處理。對於這個問題,基於MFC的ODBC應用程序可以采取幾種不同的處理辦法,這將由程序采用哪種記錄集決定。
記錄集主要分為快照(Snapshot) 和動態集(Dynaset)兩種,CRecordset類對這兩者都支持。這兩種記錄集的不同表現在它們對別的應用改變數據源記錄采取了不同的處理方法。
快照型記錄集提供了對數據的靜態視.快照是個很形象的術語,就好象對數據源的某些記錄照了一張照片一樣.當別的用戶改變了記錄時(包括修改、添加和刪除),快照中的記錄不受影響,也就是說,快照不反映別的用戶對數據源記錄的改變.直到調用了CRecordset::Requery重新查詢後,快照才會反映變化.對於象產生報告或執行計算這樣的不希望中途變動的工作,快照是很有用的。需要指出的是,快照的這種靜態特性是相對於別的用戶而言的,它會正確反映由本身用戶對記錄的修改和刪除,但對於新添加的記錄直到調用Requery後才能反映到快照中.
動態集提供了數據的動態視.當別的用戶修改或刪除了記錄集中的記錄時,會在動態集中反映出來:當滾動到修改過的記錄時對其所作的修改會立即反映到動態集中,當記錄被刪除時,MFC代碼會跳過記錄集中的刪除部分.對於其它用戶添加的記錄,直到調用Requery時,才會在動態集中反映出來。本身應用程序對記錄的修改、添加和刪除會反映在動態集中。當數據必須是動態的時侯,使用動態集是最適合的。例如,在一個火車票聯網售票系統中,顯然應該用動態集隨時反映出共享數據的變化。
在記錄集中滾動,需要有一個標志來指明滾動後的位置(當前位置)。ODBC驅動程序會維護一個光標,用來跟蹤記錄集的當前記錄,可以把光標理解成跟蹤記錄集位置的一種機制。
光標庫(Cursor Library)是處於ODBC驅動程序管理器和驅動程序之間的動態鏈接庫(ODBCCR32.DLL).光標庫的主要功能是支持快照以及為底層驅動程序提供雙向滾動能力,高層次的驅動程序不需要光標庫,因為它們是可滾動的.光標庫管理快照記錄的緩沖區,該緩沖區反映本程序對記錄的修改和刪除,但不反映其它用戶對記錄的改變,由此可見,快照實際上相當於當前的光標庫緩沖區.
應注意的是,快照是一種靜態光標(Static Cursor).靜態光標直到滾動到某個記錄才能取得該記錄的數據.因此,要保證所有的記錄都被快照,可以先滾動到記錄集的末尾,然後再滾動到感興趣的第一個記錄上.這樣做的缺點是滾動到末尾需要額外的開銷,會降低性能.
與快照不同,動態集不用光標庫維持的緩沖區來存放記錄.實際上,動態集是不使用光標庫的,因為光標庫會屏蔽掉一些支持動態集的底層驅動程序功能.動態集是一種鍵集驅動光標(Keyset-Driven Cursor),當打開一個動態集時,驅動程序保存記錄集中每個記錄的鍵.只要光標在動態集中滾動,驅動程序就會通過鍵來從數據源中檢取當前記錄,從而保證選取的記錄與數據源同步.
從上面的分析中可以看出,快照和動態集有一個共同的特點,那就是在建立記錄集後,記錄集中的成員就已經確定了.這就是為什麼兩種記錄集都不能反映別的用戶添加記錄的原因.
10.5.2 域數據成員與數據交換
CRecordset類代表一個記錄集.用戶一般需要用ClassWizard創建一個CRecordset的派生類.ClassWizard可以為派生的記錄集類創建一批數據成員,這些數據成員與記錄的各字段相對應,被稱為字段數據成員或域數據成員.例如,對於表10.2所示的將在後面例子中使用的數據庫表,ClassWizard會在派生類中加入6個域數據成員,如清單10.1所示.可以看出域數據成員與表中的字段名字類似,且類型匹配.
表10.2 stdreg32.mdb中的Section表
CourseID
(Text)
SectionNo
(Text)
InstructorID
(Text)
RoomNo
(Text)
Schedule
(Text)
Capacity
(int)
MATH101
1
KLAUSENJ
KEN-12
MWF10-11
40
MATH101
2
ROGERSN
WIL-1088
TTH3:30-5
15
MATH201
1
ROGERSN
WIL-1034
MWF2-3
20
MATH201
2
SMITHJ
WIL-1054
MWF3-4
25
MATH202
1
KLA
WIL-1054
MWF9-10
20
MATH202
2
ROGERSN
KEN-12
TTH9:30-11
15
MATH202
3
KLAUSENJ
WIL-2033
TTH3-4:30
15
清單10.1 派生類中的域數據成員
class CSectionSet : public CRecordset
{
public:
. . . . . .
//{{AFX_FIELD(CSectionSet, CRecordset)
CString m_CourseID;
CString m_SectionNo;
CString m_InstructorID;
CString m_RoomNo;
CString m_Schedule;
int m_Capacity;
//}}AFX_FIELD
. . . . . .
};
域數據成員用來保存某條記錄的各個字段,它們是程序與記錄之間的緩沖區.域數據成員代表當前記錄,當在記錄集中滾動到某一記錄時,框架自動地把記錄的各個字段拷貝到記錄集對象的域數據成員中.當用戶要修改當前記錄或增加新記錄時,程序先將各字段的新值放入域數據成員中,然後調用相應的CRecordset成員函數把域數據成員設置到數據源中.
不難看出,在記錄集與數據源之間有一個數據交換問題.CRecordset類使用"記錄域交換"(Record Field Exchange,縮寫為RFX)機制自動地在域數據成員和數據源之間交換數據.RFX機制與對話數據交換(DDX)類似.CRecordset的成員函數DoFieldExchange負責數據交換任務,在該函數中調用了一系列RFX函數.當用戶用ClassWizard加入域數據成員時,ClassWizard會自動在DoFieldExchange中建立RFX.典型DoFieldExchange如清單10.2所示:
清單10.2 典型的DoFieldExchange函數
void CSectionSet::DoFieldExchange(CFieldExchange* pFX)
{
//{{AFX_FIELD_MAP(CSectionSet)
pFX->SetFieldType(CFieldExchange::outputColumn);
RFX_Text(pFX, _T("[CourseID]"), m_CourseID);
RFX_Text(pFX, _T("[SectionNo]"), m_SectionNo);
RFX_Text(pFX, _T("[InstructorID]"), m_InstructorID);
RFX_Text(pFX, _T("[RoomNo]"), m_RoomNo);
RFX_Text(pFX, _T("[Schedule]"), m_Schedule);
RFX_Int(pFX, _T("[Capacity]"), m_Capacity);
//}}AFX_FIELD_MAP
}
10.5.3 SQL查詢
記錄集的建立實際上主要是一個查詢過程,SQL的SELECT語句用來查詢數據源.在建立記錄集時,CRecordset會根據一些參數構造一個SELECT語句來查詢數據源,並用查詢的結果創建記錄集.明白這一點對理解CRecordset至關重要.SELECT語句的句法如下:
SELECT rfx-field-list FROM table-name [WHERE m_strFilter]
[ORDER BY m_strSort]
其中table-name是表名,rfx-field-list是選擇的列(字段).WHERE和ORDER BY是兩個子句,分別用來過濾和排序。下面是SELECT語句的一些例子:
SELECT CourseID, InstructorID FROM Section
SELECT * FROM Section WHERE CourseID=‘MATH202’ AND Capacity=15
SELECT InstructorID FROM Section ORDER BY CourseID ASC
其中第一個語句從Section表中選擇CourseID和InstructorID字段.第二個語句從Section表中選擇CourseID為MATH202且Capacity等於15的記錄,在該語句中使用了象"AND"或"OR"這樣的邏輯連接符.要注意在SQL語句中引用字符串、日期或時間等類型的數據時要用單引號括起來,而數值型數據則不用.第三個語句從Section表中選擇InstructorID列並且按CourseID的升序排列,若要降序排列,可使用關鍵字DESC.
提示:如果列名或表名中包含有空格,則必需用方括號把該名稱包起來。例如,如果有一列名為“Client Name”,則應該寫成“[Client Name]”。
10.5.4 記錄集的建立和關閉
要建立記錄集,首先要構造一個CRecordset派生類對象,然後調用Open成員函數查詢數據源中的記錄並建立記錄集.在Open函數中,可能會調用GetDefaultConnect和GetDefaultSQL函數.函數的聲明為
CRecordset( CDatabase* pDatabase = NULL);
參數pDatabase指向一個CDatabase對象,用來獲取數據源.如果pDatabase為NULL,則會在Open函數中自動構建一個CDatabase對象.如果CDatabase對象還未與數據源連接,那麼在Open函數中會建立連接,連接字符串(參見10.3.1)由成員函數GetDefaultConnect提供.
virtual CString GetDefaultConnect( );
該函數返回缺省的連接字符串.Open函數在必要的時侯會調用該函數獲取連接字符串以建立與數據源的連接.一般需要在CRecordset派生類中覆蓋該函數並在新版的函數中提供連接字符串.
virtual BOOL Open( UINT nOpenType = AFX_DB_USE_DEFAULT_TYPE, LPCTSTR lpszSQL = NULL, DWORD dwOptions = none );
throw( CDBException, CMemoryException );
該函數使用指定的SQL語句查詢數據源中的記錄並按指定的類型和選項建立記錄集.參數nOpenType說明了記錄集的類型,如表10.3所示,如果要求的類型驅動程序不支持,則函數將產生一個異常.參數lpszSQL是一個SQL的SELECT語句,或是一個表名.函數用lpszSQL來進行查詢,如果該參數為NULL,則函數會調用GetDefaultSQL獲取缺省的SQL語句.參數dwOptions可以是一些選項的組合,常用的選項在表10.4中列出.若創建成功則函數返回TRUE,若函數調用了CDatabase::Open且返回FALSE,則函數返回FALSE.
表10.3 記錄集的類型
類型
含義
AFX_DB_USE_DEFAULT_TYPE
使用缺省值.
CRecordset::dynaset
可雙向滾動的動態集.
CRecordset::snapshot
可雙向滾動的快照.
CRecordset::dynamic
提供比動態集更好的動態特性,大部分ODBC驅動程序不支持這種記錄集.
CRecordset::forwardOnly
只能前向滾動的只讀記錄集.
表10.4 創建記錄集時的常用選項
選項
含義
CRecordset::none
無選項(缺省).
CRecordset::appendOnly
不允許修改和刪除記錄,但可以添加記錄.
CRecordset::readOnly
記錄集是只讀的.
CRecordset::skipDeletedRecords
有些數據庫(如FoxPro)在刪除記錄時並不真刪除,而是做個刪除標記,在滾動時將跳過這些被刪除的記錄.
virtual CString GetDefaultSQL( );
Open函數在必要時會調用該函數返回缺省的SQL語句或表名以查詢數據源中的記錄.一般需要在CRecordset派生類中覆蓋該函數並在新版的函數中提供SQL語句或表名.下面是一些返回字符串的例子.
“Section” //選擇Section表中的所有記錄到記錄集中
“Section, Course” //合並Section表和Course表的各列到記錄集中
//對Section表中的所有記錄按CourseID的升序進行排序,然後建立記錄集
“SELECT * FROM Section ORDER BY CourseID ASC”
上面的例子說明,通過合理地安排SQL語句和表名,Open函數可以十分靈活地查詢數據源中的記錄.用戶可以合並多個表的字段,也可以只選擇記錄中的某些字段,還可以對記錄進行過濾和排序.
上一小節說過,在建立記錄集時,CRecordset會構造一個SELECT語句來查詢數據源.如果在調用Open時只提供了表名,那麼SELECT語句還缺少選擇列參數rfx-field-list(參見10.5.3).框架規定,如果只提供了表名,則選擇列的信息從DoFieldExchange中的RFX語句裡提取.例如,如果在調用Open時只提供了"Section"表名,那麼將會構造如下一個SELECT語句:
SELECT CourseID,SectionNo,InstructorID,RoomNo, Schedule,Capacity FROM Section
建立記錄集後,用戶可以隨時調用Requery成員函數來重新查詢和建立記錄集.Requery有兩個重要用途:
使記錄集能反映用戶對數據源的改變(參見10.5.1).
按照新的過濾或排序方法查詢記錄並重新建立記錄集.
在調用Requery之前,可調用CanRestart來判斷記錄集是否支持Requery操作.要記住Requery只能在成功調用Open後調用,所以程序應調用IsOpen來判斷記錄集是否已建立.函數的聲明為
virtual BOOL Requery( );throw( CDBException, CMemoryException );
返回TRUE表明記錄集建立成功,否則返回FALSE.若函數內部出錯則產生異常.
BOOL CanRestart( ) const; //若支持Requery則返回TRUE
BOOL IsOpen( ) const; //若記錄集已建立則返回TRUE
CRecordset類有兩個公共數據成員m_strFilter和m_strSort用來設置對記錄的過濾和排序.在調用Open或Requery前,如果在這兩個數據成員中指定了過濾或排序,那麼Open和Requery將按這兩個數據成員指定的過濾和排序來查詢數據源.
成員m_strFilter用於指定過濾器.m_strFilter實際上包含了SQL的WHERE子句的內容,但它不含WHERE關鍵字.使用m_strFilter的一個例子為:
m_pSet->m_strFilter=“CourseID=‘MATH101’”; //只選擇CourseID為MATH101的記錄
if(m_pSet->Open(CRecordset::snapshot, “Section”))
. . . . . .
成員m_strSort用於指定排序.m_strSort實際上包含了ORDER BY子句的內容,但它不含ORDER BY關鍵字.m_strSort的一個例子為
m_pSet->m_strSort=“CourseID DESC”; //按CourseID的降序排列記錄
m_pSet->Open();
. . . . . .
事實上,Open函數在構造SELECT語句時,會把m_strFilter和m_strSort的內容放入SELECT語句的WHERE和ORDER BY子句中.如果在Open的lpszSQL參數中已包括了WHERE和ORDER BY子句,那麼m_strFilter和m_strSort必需為空.
調用無參數成員函數Close可以關閉記錄集.在調用了Close函數後,程序可以再次調用Open建立新的記錄集.CRecordset的析構函數會調用Close函數,所以當刪除CRecordset對象時記錄集也隨之關閉。
10.5.5 滾動記錄
CRecordset提供了幾個成員函數用來在記錄集中滾動,如下所示.當用這些函數滾動到一個新記錄時,框架會自動地把新記錄的內容拷貝到域數據成員中.
void MoveNext( ); //前進一個記錄
void MovePrev( ); //後退一個記錄
void MoveFirst( ); //滾動到記錄集中的第一個記錄
void MoveLast( ); //滾動到記錄集中的最後一個記錄
void SetAbsolutePosition( long nRows );
該函數用於滾動到由參數nRows指定的絕對位置處.若nRows為負數,則從後往前滾動.例如,當nRows為-1時,函數就滾動到記錄集的末尾.注意,該函數不會跳過被刪除的記錄.
virtual void Move( long nRows, WORD wFetchType = SQL_FETCH_RELATIVE );
該函數功能強大.通過將wFetchType參數指定為SQL_FETCH_NEXT、SQL_FETCH_PRIOR、SQL_FETCH_FIRST、SQL_FETCH_LAST和SQL_FETCH_ABSOLUTE,可以完成上面五個函數的功能.若wFetchType為SQL_FETCH_RELATIVE,那麼將相對當前記錄移動,若nRows為正數,則向前移動,若nRows為負數,則向後移動.
如果在建立記錄集時選擇了CRecordset::skipDeletedRecords選項,那麼除了SetAbsolutePosition外,在滾動記錄時將跳過被刪除的記錄,這一點對象FoxPro這樣的數據庫十分重要.
如果記錄集是空的,那麼調用上述函數將產生異常.另外,必須保證滾動沒有超出記錄集的邊界.調用IsEOF和IsBOF可以進行這方面的檢測.
BOOL IsEOF( ) const;
如果記錄集為空或滾動過了最後一個記錄,那麼函數返回TRUE,否則返回FALSE.
BOOL IsBOF( ) const;
如果記錄集為空或滾動過了第一個記錄,那麼函數返回TRUE,否則返回FALSE.
下面是一個使用IsEOF的例子:
while(!m_pSet->IsEOF( ))
m_pSet->MoveNext( );
調用GetRecordCound可獲得記錄集中的記錄總數,該函數的聲明為
long GetRecordCount( ) const;
要注意這個函數返回的實際上是用戶在記錄集中滾動的最遠距離.要想真正返回記錄總數,只有調用MoveNext移動到記錄集的末尾(MoveLast不行).
10.5.6 修改、添加和刪除記錄
要修改當前記錄,應該按下列步驟進行:
調用Edit成員函數.調用該函數後就進入了編輯模式,程序可以修改域數據成員.注意不要在一個空的記錄集中調用Edit,否則會產生異常.Edit函數會把當前域數據成員的內容保存在一個緩沖區中,這樣做有兩個目的,一是可以與域數據成員作比較以判斷哪些字段被改變了,二是在必要的時侯可以恢復域數據成員原來的值.若再次調用Edit,則將從緩沖區中恢復域數據成員,調用後程序仍處於編輯模式.調用Move(AFX_MOVE_REFRESH)或Move(0)可退出編輯模式(AFX_MOVE_REFRESH的值為0),同時該函數會從緩沖區中恢復域數據成員.
設置域數據成員的新值.
調用Update完成編輯.Update把變化後的記錄寫入數據源並結束編輯模式.
要向記錄集中添加新的記錄,應該按下列步驟進行:
調用AddNew成員函數.調用該函數後就進入了添加模式,該函數把所有的域數據成員都設置成NULL(注意,在數據庫術語中,NULL是指沒有值,這與C++的NULL是不同的).與Edit一樣,AddNew會把當前域數據成員的內容保存在一個緩沖區中,在必要的時侯,程序可以再次調用AddNew取消添加操作並恢復域數據成員原來的值,調用後程序仍處於添加模式.調用Move(AFX_MOVE_REFRESH)可退出添加模式,同時該函數會從緩沖區中恢復域數據成員.
設置域數據成員.
調用Update.Update把域數據成員中的內容作為新記錄寫入數據源,從而結束了添加.