14. ADO的運作模式

ADO是非常強大的,以致於必須分成兩章來講解。第13章涵蓋的範圍為ADO的物件模型,涵蓋其屬性、方法與事件等。在本章,將介紹實作上的問題以及一些ADO必須性的特殊功能。

先前準備:ADO是逐漸演進的技術,好消息是微軟保證會持續地改進ADO。因此,可能會隨著時間慢慢地流逝,ADO的錯誤也隨之減少。更甚,ADO的行為模式深受OLE DB提供者影響,所以許多問題其實皆看您連結哪種資料庫而定,當您安裝了較新版本的提供者後,這些問題可能就消失了。可能的話,筆者會試著撰寫不同提供者的程式碼,但筆者不可能測試所有的組合情形。所以這可能得靠您自己了。

設定Connection
 

ADO物件群組中最彈性的部分,可從執行各資料庫導向應用的首要步驟中明顯看出,也就是設定與資料庫之間的連接部分。事實上,大多數的選擇是建立一個獨立的連接物件,但它並不是唯一可運用的一種。譬如,可以建立獨立的Recordset物件並且指派ConnectionString給Open Method;或者,可建立獨立的Recordest物件,指派ConnectionString給ActiveConnection屬性。假設使用ADO資料控制項或DataEnvironment設計師,你甚至不需要建立一個ADO物件來建立連線。

設定連線字串(ConnectionString)
 

不論用什麼物件來連結到ADO資料來源,都必須建立一個連線字串。將此字串分派給Connection物件的ConnectionString屬性,或者Recordset、Command物件的ActiveConnection屬性;甚至於傳遞給Connection物件的Execute方法或Recordset物件的Open方法。因此,要知道如何使用Connection,必須要瞭解如何正確地建立連線字串和運用之。

ConnectionString可包含一至多個引數,並以「引數=值」的格式表示。這些引數必須根據提供者(Provider)是否提供支援,但Provider、File Name這兩引數是大多數會提供支援的部分。第十三章 表13-1 中列出已支援的引數。

要知道哪些引數被提供者所支援是建立ConnectionString最困難的一件事。當筆者開始尋找這些未知的部分時,發現在表單中設置ADO資料控制項是最好的方法,可直接在資料控制項的屬性中建立Connection,並知道ConnectionString屬性的最終值。

以Microsoft Data Link檔案作資料庫的連接是最簡單的方法(在 第八章 The Binding Mechanism章節中說明如何建立這些檔案)。UDL檔案包含ADO物件如何連接資料庫的所有必須資訊,包括Provider的名稱及FileName引數。實際上,若只使用Provider、FileName這兩個引數,會出現錯誤。以下的例子是假設使用一個指向Biblio資料庫的UDL檔案:

Dim cn As New ADODB.Connection
cn.Open "File Name=C:\Program Files\Common Files\System\ole db" _
    & "\Data Links\Biblio.udl"

在這例子中,直接以MSDASQL初始值作為ODBC的提供者,你也可使用File Name引數指向DSN:

cn.Open "File Name=C:\Program Files\Common Files\ODBC\Data Sources\" _
    & "Pubs.dsn"

假若運用使用者或系統ODBC資料來源名稱,你只要將ODBC資料來源名稱指定給Data Source或DSN引數。以下例子假設你是使用系統或使用者DSN來指向在Microsoft SQL Server中的Pubs資料庫,並說明在不需要建立一個明顯的Connection物件下開啟Recordset。

Dim rs As New ADODB.Recordset
rs.Open "Authors", "Provider=MSDASQL.1;User ID=sa;Data Source=Pubs"
' You can omit the name of the provider because it's the default.
rs.Open "Authors", "DSN=Pubs"

假若不使用DSN或UDL檔案,則必須要自行建立ConnectionString。這也是等同於DSN-less的ADO連線:這表示其簡化了應用程式的安裝(因為不需要設定DSN或UDL檔案在顧客的機器中);不過另一方面,會使程式設計師的工作稍微困難。當連接到Microsoft Jet資料庫,只需要提供者的名稱及MDB資料庫的路徑。

cn.Open "Provider=Microsoft.Jet.OLEDB.3.51;" _
    & "Data Source=E:\Microsoft Visual Studio\VB98\Biblio.mdb>

不論透過OLE DB提供者或MSDASQL提供者,要連接到SQL Server資料庫中是比較複雜的一件工作。以下例子是透過Windows NT整合安全性,將ConnectionString連接到位於MyServer終端機上的SQL Server之Pubs資料庫。

cn.Open "Provider=SQLOLEDB.1;Integrated Security=SSPI;" _
    & " Data Source=MyServer;Initial Catalog=Pubs;"

在這例子中,Data Source是Server的名稱,並且可藉著設定Integrated Security引數給SSPI達到整合性的保護。以下例子直接利用使用者ID及密碼,開啟一個到相同SQL Server資料庫的連接。

cn.Open "Provider=SQLOLEDB.1;Data Source=MyServer;User ID=sa;" _
    & "Password=mypwd;Initial Catalog=Pubs"

Connection Timeout是另一個便利的連接字串引數。當你利用Connection物件來開啟連接時,通常並不會需要它。因為Connection物件使用ConnectionTimeout屬性是為了讓你在開啟一個connection時,設定一個離線時間。然而,在Recordset物件的Open方法或命令的 Execute方法中建立一個直接的Connection物件時,就需要ConnectionTimeout。

rs.Open "Authors", "Provider=SQLOLEDB.1;Data Source=MyServer;User ID=sa;" _
    & "Connection Timeout=10;Initial Catalog=Pubs"

在SQL Server環境下工作時,可以使用許多額外的引數適當地調整connection。譬如,PacketSize引數在設定在網路中傳輸資料的大小(預設值是4096 bytes);Use Procedure for Prepare引數指定是否忽略ADO建立的預存程序。當你中斷連接時,可能的值有0-No、1-Yes(預設值);或者,中斷連接再加上2-Yes。當用其他named pipes方法連接SQL Server時,Network Address和Network Library引數應該要被指定;Workstation ID引數定義連接的機器。如果你以MSDASQL提供者作為連接,則必須指定數個額外的引數,而最重要的引數是要使用哪一個ODBC驅動程式。

cn.ConnectionString = "Provider=MSDASQL.1;User ID=sa;" _ 
    &"ExtendedProperties=""DRIVER= SQL Server;SERVER=ServerNT;"_
    &"MODE=Read;WSID=P2;DATABASE=pubs"""

如你所看到的,以OLE DB提供者作為設定ODBC的方法,在RDO中藉著以雙重引號並指派給Extended Properties引數,可以新增一個相同的ODBC引數字串。此字串不需要經ADO嘗試去編譯即可通過提供者。當你在Visual Basic句子中使用這一引數,如前面所說,你必須使用兩個引號。然而,必須要先提醒的是,你也可以在使用舊版的ODBC句子構造時忽略這些引數,而ADO會正確地編譯這些引數。

' You can omit the Provider argument because you're using MSDASQL.
cn.ConnectionString = "DRIVER={SQL Server};SERVER= 
     MyServer;"_&"UID=sa;DATABASE=pubs"

不論在使用ADO(User ID和Password引數)或ODBC ( UID和PWD引數),你可以忽略使用者的名字和密碼。如果你兩者都省略,那以ADO為優先。

在Microsoft Jet OLE DB Provider環境中工作,不論connection string或Connection物件的動態屬性,你可以忽略額外的登入資訊。Jet OLEDB:System Database提供群組路徑和名稱的資訊;Jet OLEDB:Registry Path握有Jet引擎的註冊帳號;Jet OLEDB:Database Password是資料庫的密碼。

cn.Properties("Jet OLEDB:Database Password") = "mypwd"

開啟Connection
 

建立正確的ConnectionString之後,接下來關於實際開啟連線的運作就端看您使用何種物件了。

明顯的Connection物件
 

在應用程式的生命週期中,通常使用對於資料來源的查詢及命令可提供再利用的單一Connection物件來建立連接。您也可以分派合理的值給Connection物件的Mode、ConnectionTimeout屬性、及Provider屬性(除非連接字串包含Provider引數或File Name引數)。

' Prepare to open a read-only connection.
Dim cn As New ADODB.Connection
cn.ConnectionTimeout = 30    ' Default for this property is 15 seconds.
cn.Mode = adModeRead         ' Default for this property is adModeUnknown.

根據這點,您可以選擇以不同的方法來開啟連接。您可以指派連接字串給ConnectionString屬性,然後引用Open方法。

cn.ConnectionString = "Provider=SQLOLEDB.1;Data Source=MyServer;" _
    & "Initial Catalog=Pubs"
' The second and third arguments are the user name and the password.
cn.Open , "sa", "mypwd"

相對地,也可把連線字串當成Open方法的第一個參數。在以下例子,使用者名稱與密碼皆包含在連線字串,因此,Open方法的參數不需要再指定其值(如果這樣做,會出現無法預期的結果)。

cn.Open "Provider=SQLOLEDB.1;Data Source=MyServer;User ID=sa;" _
    & "Password=mypwd;Initial Catalog=Pubs"

您應該對於ConnectionString屬性多作認識。假若指定一個值給ConnectionString屬性然後開啟連接,讀取ConnectionString屬性的值後也許會回應一串不同的字串。其中包含提供者所產生的許多且額外的值。這是很正常的現象。而關閉Connection物件時,會將原先指定的值再回存給ConnectionString屬性。

隱含的Connection物件
 

有時您不喜歡建立一個單一的Connection物件而較傾向於直接使用Recordset或Command物件。然而,這是大多數程式撰寫的型態。因為即使並沒有直接建立Connection物件,ADO也會幫您做。因此不論在Server端或Client端,皆沒有節省任何資源。唯一減少的是所寫的程式碼。譬如,只需要兩行程式碼便可執行任何資料庫的SQL查詢。

Dim rs As New ADODB.Recordset
rs.Open "Authors", "Provider=SQLOLEDB.1;Data Source=MyServer;User ID=sa;" _
    & "Password=mypwd;Initial Catalog=Pubs"

可以使用相似的技巧開啟以隱性連接為主的Command物件。而這一個方法就需要寫更多的程式碼。因在開啟一個連接和實行Execute方法前必須要設定ActiveConnection屬性和CommandText屬性。下面是一段例子。

Dim cmd As New ADODB.Command
cmd.ActiveConnection = "Provider=SQLOLEDB.1;Data Source= 
     MyServer;" _&"userID=sa;Password=mypwd;Initial Catalog=Pubs"
cmd.CommandText = "DELETE FROM Authors WHERE State = 'WA'"
cmd.Execute

以前面所說的用Recordset物件或Command物件方法開啟連接,ADO物件藉著Recordset物件或Command物件的ActiveConnection屬性,建立間接的Connection物件。如以下所述:

' Display the errors in the connection created by the previous example.
Dim er As ADODB.Error
For Each er In cmd.ActiveConnection.Errors
    Debug.Print er.Number, er.Description
Next

以Recordset物件或Command物件開啟間接Connection物件,則此Connection物件的屬性值皆會是預設值。這樣的限制是鼓勵使用直接Connection物件的一個好理由。預設情況下,ADO的ConnectionTimeout屬性值為15秒,且為唯讀、server端、forward-only的資料指標,其CacheSize等於1(也常稱為無指標)。

Properties集合
 

尚未討論到Connection物件其中一項重要觀點,就是建立Connection時連接字串提供的訊息不足夠會發生什麼狀況?假若是在標準的OLE DB工作環境下,這問題會被Connection屬性群中的動態Prompt屬性來控制。此屬性有以下的值:1-adPromptAlways(永遠出現登入對話方塊)、2-adPromptComplete(遺失某一部份的訊息時才會出現登入對話方塊)、3-adPromptCompleteRequired(和adPromptComplete相似,不同的是使用者不用鍵入任何值)和4-adPromptNever(從不出現登入對話方塊)。預設值是4-adPromptNever:ConnectionString的資訊不足而無法實現這些運算值,無法登入的對話方塊會出現並且應用程式會收到錯誤。根據以下的程式碼來修改預設值。

' Display the login dialog box if necessary.
Dim cn As New ADODB.Connection
cn.Properties("Prompt") = adPromptComplete
cn.Open "Provider=MSDASQL.1;Data Source=Pubs"

Prompt動態屬性只在MSDASQL的環境下運作。屬性群包含許多其他有影響性的資訊。譬如,應用程式藉由DBMS Name和DBMS Version動態屬性可以決定資料庫的名稱及版本、Data Source Name屬性可以決定伺服器的名稱。另一群回應提供者訊息的屬性有:Provider Name屬性回應DLL檔的名字、 Provider Friendly Name屬性顯示從安裝在機器上的OLE DB清單中你選擇的提供者、 Provider Version屬性告知提供者的版本。如果在顧客的機器上無法正常的運作,您可以列印這些資訊。

非同步化的Connection
 

在這些connection的例子中,可以發現一個共通性:同步化。這狀況表示了Connection確實被建立、連接逾時、錯誤發生後,Visual Basic應用程式才能依據Open方法來執行。於此際,應用程式無法回應使用者的動作,此種狀況必須要避免,尤其是將ConnectionTimeout屬性設的太高時。

幸運地,ADO以簡易的方式來解決這問題。實際上,您可以藉由給予Open方法Option引數adAsyncConnect值,預防Visual Basic應用程式等待。以非同步開啟一個connection,必須要確定Connection是否已經預備好(或已錯誤發生)。有兩個方式可以達到:藉由查詢Connection的State屬性或者使用事件。查詢State屬性是最簡單的方案,但需要以複雜的運算值來確認connection使否已預備好,這通常是不適當的。

Dim cn As New ADODB.Connection
On Error Resume Next
cn.Open "Provider=sqloledb;Data Source=MyServer;Initial Catalog=pubs;" _
    &"UserID=sa;Password=;", , , adAsyncConnect
' State is a bit-field value, so you need the And operator to test one bit.
Do While (cn.State And adStateConnecting)
    ' Perform your operations here.
    ...
    ' Let the user interact with the program's user interface.
    DoEvents
Loop
' Check whether the connection has been successful.
If cn.State And adStateOpen Then MsgBox "The connection is open."

比較好的方式是使用ConnectionComplete事件。當您要開啟一個Connection時,使用WithEnents關鍵字宣告Connection物件變數,並當要開啟連線時,建立其新的實體。範例如下。

Dim WithEvents cn As ADODB.Connection
Private Sub cmdConnect_Click()
    Set cn = New ADODB.Connection
    cn.ConnectionTimeout = 20
    cn.Open "Provider=sqloledb;Data Source=MyServer;" _
        & "Initial Catalog=pubs;", "sa", , adAsyncConnect
End Sub
Private Sub cn_ConnectComplete(ByVal pError As ADODB.Error, adStatus As _
    ADODB.EventStatusEnum, ByVal pConnection As ADODB.Connection)
    If adStatus = adStatusOK Then
        MsgBox "The connection is open"
    ElseIf adStatus = adStatusErrorsOccurred Then
        MsgBox "Unable to open the connection" & vbCr & Err.Description
    End If
End Sub

雖然WillConnect事件效益有限,Connection物件也可以使用。譬如,可以使用WillConnect修正ConnectionString以便指定要作為連結的Provider(代替修改在應用程式程式碼多處的字串),或者可以給使用者選擇伺服器、鍵入他們的密碼等等的能力。

Private Sub cn_WillConnect(ConnectionString As String, UserID As String, _
    Password As String, Options As Long, adStatus As _
    ADODB.EventStatusEnum, ByVal pConnection As ADODB.Connection)
    If UserID <> "" And Password = "" Then
        ' Ask for user's password.
        Password = InputBox("Please enter your password")
        If Password = "" Then 
            ' If not provided, cancel the command if possible.
            If adStatus <> adStatusCantDeny Then adStatus = adStatusCancel
        End If
    End If
End Sub

當使用WillConnect事件時,必須要注意的是其參數值會完全傳遞自Open方法中相對的引數。例如,假若在connection String中使用者名稱和密碼被省略,則UserID和Password都會收到空白值。假設設定adStatus參數為adStatusCancel,則Open方法會收到錯誤訊息且Connection無法建立。

非同步的模式Connection無法開啟時,WillConnect事件和ConnectComplete事件會觸發,且假設在WillConnect事件中取消動作,ConnectComplete事件仍會被觸發。這一例子是,ConnectComplete事件的adStatus參數會被設定成adStatusErrorsOccurred,且pError.Number為錯誤3712,「已被使用者取消運作。」

處理資料
 

成功開啟connection之後,下一步將是從資料來源中讀取某些記錄。您可以各種方式來讀取資料,不過這些方法中都牽涉到Recordset物件的開啟。

開啟Recordset物件
 

開啟Recordset前,必須決定要檢索哪些記錄(Record)、要建立何種型態的指標(Cursor)、指標位置等等。

來源字串
 

Source屬性是Recordset物件最重要的屬性,它指出哪些record必須要被檢索。此一屬性代表著資料庫、檢視、預存程式的名稱或是一段SELECT命令的文字。當以file-based Recordsets為運作環境,Source屬性也可以指出檔案的名稱和路徑(file-based Recordsets後面章節中會談到)。下面有一些例子:

' Select a different source, based on an array of option buttons.
Dim rs As New ADODB.Recordset
If optSource(0).Value Then          ' Database table
    rs.Source = "Authors"
ElseIf optSource(1).Value Then      ' Stored procedure
    rs.Source = "reptq1" 
ElseIf optSource(2) Then            ' SQL query
    rs.Source = "SELECT * FROM Authors" WHERE au_lname LIKE 'A*'"
End If

當開啟一個Recordset時,必須指定您要使用的Connection。必須要做下面四項方法的其中一個:

  • 建立一個單一的Connection物件,並設定所需的屬性(如:connection timeout、user name和password等等屬性值),開啟connection。在開啟Recordset之前,指定connection給Recordset物件的ActiveConnection屬性。
     
  • 如前面所提建立一個單一的Connection物件,以Recordset物件的Open方法作為第二個引數。這一方法是相同的效果,但節省一行敘述。
     
  • 讓連線字串作為Recordset物件Open方法的第二個引數。ADO建立一個間接的Connection物件,稍後可透過Recordset物件的ActiveConnection屬性來擷取。這一方法是這四種中最簡潔的一個,只需要一行執行敘述。
     
  • 以前兩個方法所提的建立單一的Connection物件。將來源字串作為Execute方法中第一個引數,並指派回應值給Recordset變數。這種方法較無彈性,因為傳回的Recordset限制較多。
     

在本章節後面有一段 〈使用Command物件〉 會談到以Command物件開啟Recordset的方法。以下有一些例子,開啟在名為P2的SQL Server中Pubs資料庫的Authors資料表。

' Method 1: explicit Connection assigned to the ActiveConnection property.
Dim cn As New ADODB.Connection, rs As New ADODB.Recordset
cn.ConnectionTimeout = 5
cn.Open "Provider=sqloledb;Data Source=P2;Initial Catalog=pubs;", "sa"
Set rs.ActiveConnection = cn
rs.Open "Authors"

' Method 2: explicit Connection passed to the Open method.
Dim cn As New ADODB.Connection, rs As New ADODB.Recordset
cn.ConnectionTimeout = 5
cn.Open "Provider=sqloledb;Data Source=P2;Initial Catalog=pubs;", "sa"
rs.Open "Authors", cn
' Method 3: implicit Connection created in the Recordset's Open method.
' Note  that you need to embed additional connection attributes (such as
' connection timeout and user ID) in the connection string.
Dim rs As New ADODB.Recordset
rs.Open "Authors", "Provider=sqloledb;Data Source=P2;" _
    & "Initial Catalog=pubs;User ID=sa;Connection Timeout=10"
' Method 4: the Execute method of the Connection object. By default, it 
' opens a server-side forward-only, read-only Recordset with CacheSize=1.
Dim cn As New ADODB.Connection, rs As New ADODB.Recordset
Cn.Open "Provider=sqloledb;Data Source=P2;Initial Catalog=pubs;", "sa"
Set rs = cn.Execute("Authors")

要注意到這些方法中有一些不同之處:第一、第二及第四種方法中在不同的Recordset中共享相同的Connection;另一方面,以第三種方式開啟不同的Recordset,即使以相同的連線字串傳遞給Recordset,依然是不同的Connection。


小秘訣

當您要使用ConnectionString開啟Recordset並且想再利用相同的間接Connection物件來開啟另一個Recordset時,可運用第一個Recordset的ActiveConnection屬性,如下:

' Open a new Recordset on the same connection as "rs".
Dim rs2 As New ADODB.Recordset
rs2.Open "Publishers", rs.ActiveConnection

您可以設定給Open方法或Source屬性許多類型的字串,讓ADO決定要以哪一種呈現。然而,這樣會付出代價,因為會促使ADO進行多次資料庫查詢以便得知來源字串是所指定的資料表、View、預存程序或SQL命令文字。藉由分配一個正確的值給Open方法的最後一個引數,您可以對Server避免掉額外的程序。例子如下:

' Select a different source, based on an array of option buttons.
If optSource(0).Value Then          ' Database table
    rs.Open "Publishers", , , , adCmdTable
Else optSource(1).Value Then        ' Stored procedure
    rs.Open "reptq1", , , , adCmdStoredProc
ElseIf optSource(2) Then            ' SQL query
    rs.Open "SELECT * FROM Authors", , , , adCmdText
End If

資料指標和一致性
 

Recordset在功能和效能上有極大差異。舉例來說,Recordset的型態可以是可更新或唯讀的;可支援MoveNext命令或向前(後)捲動。另一個不同點是,Recordset可以是包含實際的資料,或者是因為需要而將資料作整合後呈現出來的資料。當需要檢索新資料時,以資料錄中的書籤為主的用戶端Recordset雖然使用少數資源但是卻會造成網路壅塞。順便一提地,要對不同資料比較效能是不太可能的,因為有太多因素需要考慮。

Recordset所支援的各種運作中是依據何種資料指標而有作用。資料指標是記錄的集合體,在伺服器中的資料庫或用戶端應用程式中儲存及維護。從 第13章 您可以了解到ADO支援四種資料引擎指標的型態:順向唯讀資料指標、靜態資料指標、索引鍵集資料指標和動態資料指標。

由於需要多量的資源及CPU時間,使得資料指標在專業的程式員中並不討好。再來就是,資料指標常鎖定資料庫,使資料庫的再一次降低它的延展性。通常在高負載量的用戶端或伺服端的應用程式中較少使用資料指標的Recordset作為檢索,而使用SQL命令,甚至於預存程序作為更新和新增資料錄的方法。

那資料指標到底有什麼優點?其中之一是,要檢索少量的資料─譬如,數百筆的資料─,資料指標是一項合理的選擇。再來,當您需要讓使用者瀏覽資料和向前向後捲動資料時,或者使用介面是以控制項為主時,資料指標就可以派上用場。在某些狀況下,多多少少都會用到資料指標(特別是,用戶端的資料指標),因為少數的ADO特質必須用資料指標才有用。舉例來說, File-Base的Recordset和階層式Recordset只能使用用戶端靜態資料指標,且只有此類Recordset才可使用Sort方法。

若要以資料指標達到需求,至少必須藉由採用簡單的但有效果的技術來做到降低間接成本。首先,減少在資料指標中的記錄數量改以合適的Where子句取代,考慮使用MaxRecords屬性避免過大的資料指標。第二,為了釋放在伺服端上的資料頁及索引頁的鎖定,盡可能的移到Recordset最後一筆。第三,設定Recordset的CursorLocation屬性、CursorType屬性和LockType屬性以便於使資料指標不會比實際需要還要強大(因此效能變小)。

談到CursorType屬性和LockType屬性,您應該記得第13章談到Open方法中的第三及第四個引數,也可以從這邊設定。以下是一些例子:

' Open a server-side dynamic cursor. 
' (Assumes that the ActiveConnection property has been set already.)
rs.CursorType = adOpenDynamic
rs.Open "SELECT * FROM Authors", , , , adCmdText
' Open a server-side keyset cursor, with a single statement.
rs.Open "SELECT * FROM Authors", , adOpenKyset, adLockOptimistic, adCmdText

開啟Recordset前,將CursorLocation屬性設為adUserClient,可以建立一個用戶端的靜態資料指標。事實上,這個屬性似乎比CursorType屬性有更高的權限:不論在CursorType屬性中或者在Open方法的引數中設定資料指標的型態,ADO依舊會在用戶端建立靜態指標Recordset。

' Open a client-side static cursor.
rs.CursorLocation = adUseClient
rs.CursorType = adOpenStatic        ' This statement is optional.
rs.Open "SELECT * FROM Authors", , , , adCmdText

用戶端靜態指標提供不錯的優點,因為使用用戶端的資源而非伺服端的。唯一使用伺服端的資源是開啟Connection。在隨後的章節中會談到如何在此議題上使用分離的Recordset及批次更新。

伺服端的資料指標也有優點。可以使用功能性低的終端機並且對於資料指標的型態及鎖定選擇提供多樣的選擇。例如:索引鍵集或動態的資料指標只在伺服端運作;伺服端的靜態資料指標可以讀/寫;用戶端的資料指標只能唯讀或使用悲觀性批次更新。假設只使用伺服端的資料指標,這些資料指標可以讓您對SQL伺服器做多樣的變化。另一方面,伺服端資料指標消耗伺服端的資源,因此延展性也常是一個議題。在TempDB資料庫中建立的每一個資料指標,您必須要確定TempDB資料庫能夠供應用戶端應用程式要求的所有資料指標。最後,伺服端資料指標通常需要寬暢的網路交通,因為每次用戶端需要不同的Recordset時,便需要自伺服器擷取資料。


小秘訣

在Visual Basic文件中有錯誤之處,指出以Connection物件的Execute方法產生的Recordset是伺服端無指標的Recordset。事實是,在建立一個Recordset前將Connection物件的CursorLocation屬性設為adUseClient,您也可以產生用戶端的靜態資料指標。

' This code creates a client-side static cursor.
Dim cn As New ADODB.Connection, rs As New ADODB.Recordset
cn.Open "Provider=sqloledb;Data Source=P2;" & _
    "Initial Catalog=pubs;", "sa"
cn.CursorLocation = adUseClient
Set rs = cn.Execute("Authors")

然而,無法發現以Execute方法產生伺服端資料指標的方式。


單一Recordset物件
 

ADO Recordset物件比DAOs和RDOs更有彈性,您甚至於不需要開啟Connection來建立Recordset。實際上,ADO提供者支援兩種不同型態的Recordset:依據需要的資料為主的stand-alone Recordset和file-based Recordset。

Stand-alone Recordset在觀念上很簡單。建立一個新的Recordset物件,加入一至多個欄位到Fields集合中,最後開啟它。你所得到的是用戶端靜態資料指標Recordset且為批次更新的悲觀鎖定。

' Creates a dissociated Recordset with three fields
Dim rs As New ADODB.Recordset
rs.Fields.Append "FirstName", adChar, 40, adFldIsNullable
rs.Fields.Append "LastName", adChar, 40, adFldIsNullable
rs.Fields.Append "BirthDate", adDate
rs.Open

在開啟Recordset之後,您可以新增記錄,更甚者可以將此Recordset傳遞給ADO資料控制項的Recordset屬性或者其他資料連結控制項的DataSuurce屬性。讓您可以連接任何型態的資料,即使資料並沒有儲存在資料庫中。譬如;如圖14-1藉由以下的程式碼,可以在DataGrid控制項中顯示多維的資料內容。(隨書光碟有完整的收錄)。

Dim rs As New ADODB.Recordset
Dim lines() As String, fields() As String
Dim i As Long, j As Long
    
' Open the Publishers.dat text file.
Open "Publishers.dat" For Input As #1
' Read the contents of the file, and process each individual line.
lines() = Split(Input(LOF(1), 1), vbCrLf)
Close #1
' Process the first line, which contains the list of fields.
fields() = Split(lines(0), ";")
For j = 0 To UBound(fields)
    rs.fields.Append fields(j), adChar, 200
Next
rs.Open
    
' Process all the remaining lines.
For i = 1 To UBound(lines)
    rs.AddNew
    fields() = Split(lines(i), ";")
    For j = 0 To Ubound(fields)
        rs(j) = fields(j)
    Next
Next
' Display the recordset in the DataGrid control.
rs.MoveFirst
Set DataGrid1.DataSource = rs

以相似的程式碼,可以顯示二維的字串陣列、使用者自行定義的資料結構,或者傳統的資料,就像來自從並排的資料或網路上下載的HTML資料。

ADO也支援將用戶端Recordset儲存到硬碟中。這項功能大大提升應用程式的運作和效能。舉例來講,可以在local端複製和更新容量小的資料表。或者將Recordset儲存在目錄中,讓另一支應用程式在下班時間以它製作報表。再來,可以讓使用者儲存應用程式目前的狀態─包括Recordset的進度─然後再儲存下一個運作。在第13章 〈實作固態的Recordset〉 章節中會詳細談到file-based Recordset。


 

圖14-1 藉由stand-alone Recordset,可以用資料感知元件連接任何型態的資料。

資料庫上的基本操作
 

連接資料庫的最終目的就是要讀取在資料庫中的資料及修改資料。在稍後您可以知道ADO提供數種方式來達到這些工作。

讀取記錄
 

建立Recordset後,使用Do...Loop迴圈方式讀取在Recordset中的資料,如下:

' Fill a list box with the names of all the authors.
Dim rs As New ADODB.Recordset
rs.Open "Authors", "Provider=sqloledb;Data Source=P2;" _
    & "Initial Catalog=pubs;User ID=sa;Connection Timeout=10"
Do Until rs.EOF
    LstAuthors.AddItem rs("au_fname") & " " & rs("au_lname")
    rs.MoveNext
Loop
rs.Close

除了先前提到的Recordset型態,所有的Recordset─包括無指標Recordset─支援MoveNext方法。可以使用較繁雜的語法來讀取目前記錄上的資料:

rs.Fields("au_fname").Value

不過在大多數的時候,可以省略Fields(Recordset物件的預設屬性)和Value(Filed物件的預設屬性),而使用簡潔的語法:

rs("au_fname")

若欄位資料是大筆的二進位資料(BLOB)時,讀取其值時可能會失敗,例如儲存在資料庫中的圖片檔或長篇的文字檔。在這種情況下,應該要用Field物件的GetChunk方法來擷取資料,在 第13章〈Field物件〉 章節中會提到。相同地,要寫入BLOB的資料可以使用AppendChuck方法。

ADO另外提供兩種方法從已開啟的Recordset中擷取資料。第一個是GetRows方法,能夠傳回二維資料的變數值。第二是GetString方法,傳回涵蓋欄位和記錄(二者以您指定的字元分隔)的長字串。一般來說,這些比使用MoveNext方法來的快速,縱使實際的速度還受限於其他的因素,包括資料指標的型態、在用戶端機器的可用系統記憶體。第13章〈檢索資料〉章節會談到這些方法。

新增、刪除、更新記錄
 

若Recordset是可更新的,則可用Recordset物件的AddNew方法新增一筆新的記錄。使用Supports方法確定是否可對Recordset新增記錄。

If rs.Supports(adAddNew) Then. . .

以下教您如何使用AddNew方法新增記錄到單獨存在的Recordset,運用相同的技巧亦可新增記錄到一般的Recordset。如果之前是使用DAO和RDO,則一開始您會對ADO物件的AddNew方法感到困擾,因為它無須確認資料的新增動作。事實上,移動記錄指標到另一筆記錄的任何動作─包括另一個AddNew方法─皆會確認新紀錄的新增。要取消新增動作,必須使用CancelUpdate方法,程式碼如下所述:

rs.AddNew
rs.Fields("Name") = "MSPress"
rs.Fields("City") = "Seattle"
rs.Fields("State") = "WA"
If MsgBox("Do you want to confirm?", vbYesNo) = vbYes Then
    rs.Update
Else
    rs.CancelUpdate
End If

當AddNew方法還沒用Update方法(不論直接或間接)或CancelUpdate方法結束動作之前,是無法關閉Recordset。

AddNew方法的另一項特質(DAO和RDO所沒有的)是允許新增資料以欄位名稱及其值的陣列方式表示。為了讓你感受到使用此特性後的速度增快,筆者改寫了在〈單一Recordset物件〉章節關於循序新增資料的程式碼:

' Build the FieldNames() variant array. (You need to do this only once.)
ReDim fieldNames(0 To fieldMax) As Variant
For j = 0 To fieldMax
    fieldNames(j) = fields(j)
Next
' Process the text lines, but use an array of values in AddNew.
For i = 1 To UBound(lines)
    fields() = Split(lines(i), ";")
    ReDim fieldValues(0 To fieldMax) As Variant
    For j = 0 To UBound(fields)
        fieldValues(j) = fields(j) ' Move values into the Variant arrays. 
    Next
    rs.AddNew fieldNames(), fieldValues()
Next

當程式碼大致相同時,將欄位名稱與其值的陣列作為AddNew方法的參數時,其速度比原來迴圈方式快了三倍。這就提醒了要注意Fields集合的狀況。

ADO物件可以在無須明白地進入編輯模式下修改目前記錄的欄位值。事實上,不像DAO物件和RDO物件,ADO Recordset物件並沒有Edit方法,當修改欄位值時便間接進入編輯模式:

' Increment unit prices of all products by 10%.
Do Until rs.EOF
    rs("UnitPrice") = rs("UnitPrice") * 1.1
    rs.MoveNext
Loop

若您不確定ADO是否已開始編輯動作,可以查詢EditMod屬性:

If rs.EditMode = adEditInProgress Then. . .

Update方法和AddNew方法極為類似,它也可以支援一串欄位名稱和其值。當相同的值要新增到多筆記錄時,此特質是很方便的。不要忘了Update方法是否被支援,取決於Recordset的型態、位置以及是否衝突等。若有疑惑可查詢Supports方法:

If rs.Supports(adUpdate) Then. . .

Delete方法的語法很單純:依據傳遞給Delete方法的參數,可刪除目前資料(預設值),或因為Filter屬性還可以刪除目前可視的所有資料。大多數的情況下,通常是使用預設值。注意在使用此方法後,目前記錄變成無效的,因此應該在刪除動作後,立即將資料指標移到有效的記錄:

rs.Delete
rs.MoveNext
If rs.EOF The rs.MoveLast

鎖定問題
 

即使Recordset是可更新的,也無法確保Update方法會成功。事實上,一筆可更新的記錄也有可能成為無法更新的(可能是暫時的)。原因可能是因為該筆資料正被另一位使用者編輯中。這種問題只有當所開啟的Recordset其鎖定條件非adLockReadOnly時才會發生。倘若開啟一個唯讀的Recordset,則完全未鎖定資料。可以很愉悅的在Recordset中來回遊走,而無須擔心鎖定的問題,

使用不同的鎖定型態可以讓不同的使用者連接到相同的資料錄中。例如,使用者A使用悲觀鎖定,而使用者B使用樂觀鎖定。這種情況下使用者A可以鎖定資料庫即使使用者B已經對它進行編輯動作,使用者B必須等到使用者A完成更新動作後才會解除鎖定狀態。若使用悲觀鎖定編輯模式,只有在一開始編輯資料錄時可捕捉錯誤。然而,使用樂觀鎖定編輯模式,則只能在間接或直接更新資料錄時捕捉錯誤。若使用樂觀批次鎖定,則必須要解決更新衝突這項問題,在本章後面會做解釋。

當您使用悲觀鎖定而編輯失敗時,會出現錯誤&H80004005-「無法更新;正被在機器〈機器名〉的使用者〈使用者名〉鎖定中」。而樂觀鎖定在對已開啟Recordset更新命令失敗後也會出現相同的錯誤。這兩種情況,有一策略可以解決這些鎖定問題:一般而言,可自動地重試或提醒使用者編輯或更新動作失敗,讓他自行決定是否再一次執行此動作。

' Update strategy for optimistic locking.
On Error Resume Next
Do
    Err.Clear
    rs.Update
    If Err = 0 Then
        Exit Do
    ElseIf MsgBox("Update command failed:" & vbCr & Err.Description, _
        vbRetryCancel + vbCritical) = vbCancel Then
        Exit Do
    End If
Loop

注意

Microsoft Jet OLE DB Provider 3.51版本有一個嚴重的問題:若使用樂觀鎖定並且間接或直接的更新失敗,會出現&H80040E21「錯誤產生」的錯誤。這並不是很有幫助。然而,樂觀鎖定更新出現這項錯誤只在於第一次嚐試做更新動作時出現。若在稍後嘗試做更新,資料錄依然被鎖住,但卻不會看到任何錯誤訊息,程式碼對於更新動作做出成功的不正確判斷。這項錯誤已在4.0版修正,正確的錯誤號碼是&H80004005。SQL Server 6.5版的OLE DB提供者還是回應錯誤的錯誤號碼,不過至少重試更新同一筆被鎖定的記錄時,依然會發生錯誤。


許多資料庫引擎─包括Microsoft Jet和SQL Server6.5版及之前的版本─並不支援資料錄層次鎖定,而是鎖定整頁,其可能包含許多筆資料。(例如,Microsoft Jet支援每頁2KB。)這表示即使使用者並沒有更新某筆資料,其一樣會被鎖住,只因使用者鎖定在相同頁次的另一個資料。Microsoft SQL Server 7版和Oracle資料庫支援資料錄層次的鎖定。鎖定也可以在索引頁上運作,因此,可以避免更新到另一位使用者在您運作的範圍內的索引頁中。

以SQL命令執行更新
 

如你所知的,最有效率的Recordset都是順向、唯讀且無指標形態的,但它也是無法更新的Recordset。即使您選擇其他型態的資料指標,為獲得更好的延展性還是建議您將Recordset開啟為唯讀模式,可以避免鎖定及傳遞運用範圍變廣。然而,對於加入、新增、刪除Recordset必須有一套策略,若這些動作需要的話。假如Recordset無法更新,只有一個選擇就是傳送SQL命令給資料庫或執行預存程序。本節將告訴您不帶參數的簡單SQL命令。此一方法的觀念也可以應用在其他的狀況下,在本章稍後部分的 〈參數命令和查詢〉 章節中會談到參數查詢。

若在唯讀的Recordset環境中,可以使用UPDATE命令來更新單獨的資料列,只要能夠唯一表示目前的資料列即可。通常在Where子句中使用主鍵值做更新。

' Ask the end user for a new price for each product that costs more
' than $40.
Dim rs As New ADODB.Recordset, cn As New ADODB.Connection
Dim newValue As String
cn.Open "Provider=Microsoft.Jet.OLEDB.4.0;" _
    & "Data Source=C:\Program Files\Microsoft Visual Studio\VB98\NWind.mdb"
rs.Open "Products", cn
Do Until rs.EOF
    If rs("UnitPrice") > 40 Then
        ' In a real-world application, you will surely use a better UI.
        newValue = InputBox("Insert a new price for product " & _
            rs("ProductName"), , rs("UnitPrice"))
        If Len(newValue) Then
            cn.Execute "UPDATE Products SET UnitPrice=" & newValue & _
                " WHERE ProductID =" & rs("ProductID")
        End If
    End If
    rs.MoveNext
Loop

使用SQL命令來刪除一筆資料列也很雷同,只是需要改以DELETE命令:

' Ask users if they want to selectively delete suppliers from Italy. 
Dim rs As New ADODB.Recordset, cn As New ADODB.Connection
cn.Open "Provider=Microsoft.Jet.OLEDB.4.0;" _
    & "Data Source=E:\Microsoft Visual Studio\VB98\NWind.mdb"
rs.Open "Suppliers", cn
Do Until rs.EOF
    If rs("Country") = "Italy" Then
        If MsgBox("Do you want to delete supplier " & rs("Company Name") _
            & "?", vbYesNo) = vbYes Then
            cn.Execute "DELETE FROM Suppliers WHERE SupplierID =" _
                & rs("SupplierID")
        End If
    End If
    rs.MoveNext
Loop

更新和刪除動作可能因為一些因素造成失敗,因此應該避免這些不可預期的錯誤。例如,如果刪除在產品資料表中對應到其他資料錄的資料錄會造成失敗,除非於此兩表格中建立鏈結的刪除關係。

新增一筆資料錄需要INSERT INTO命令:

cn.Execute "INSERT INTO Employees (LastName, FirstName, BirthDate) " _
    & "VALUES ('Smith', 'Robert', '2/12/1953')"

當要從控制項中檢索資料時,必須以程式寫出下面的SQL子句:

cn.Execute "INSERT INTO Employees (LastName, FirstName, BirthDate) " _
    & "VALUES ('" & txtLastName & "', '" & txtFirstName _
    & "', '" & txtBirthDate & "')"

可藉由定義一個程序來使得程式碼變少且更容易閱讀,方法如下:

' Replace all @n arguments with provided values.
Function ReplaceParams(ByVal text As String, ParamArray args() As Variant)
    Dim i As Integer
    For i = LBound(args) To UBound(args)
        text = Replace(text, "@" & Trim$(i + 1), args(i))
    Next
    ReplaceParams = text
End Function

可以使用ReplaceParams規則改寫先前的INSERT命令:

sql = "INSERT INTO Employees (LastName, FirstName, BirthDate) " _
    & "VALUES ('@1', '@2', '@3')
cn.Execute ReplaceParams(sql, txtLastName, txtFirstName, txtBirthDate)

Client端樂觀批次更新
 

直到現在,還沒有詳細談到樂觀批次更新的運作:因為這需要完全不同的程式邏輯,因此需要獨立一節來討論。

離線Recordset
 

簡而言之,ADO讓您在Recordset上執行各種命令─包括刪除、新增、更新─而不會立即影響在資料庫中的原始資料列。甚至可藉由設定ActiveConnection屬性為Nothing來中斷Recordset與資料庫之間的聯繫,並可任意地關閉原始的Connection物件。當最後要確認資料庫的更新時,只需要再連接Recordset並且運用UpdateBatch命令。或者可以使用CancelBatch方法取消之前的改變。底下這一段程式類似於之前章節所提到的部分,不過是使用樂觀批次更新而非UPDATE SQL命令。

Dim rs As New ADODB.Recordset, cn As New ADODB.Connection
' Open the recordset with optimistic batch locking.
cn.ConnectionString = "Provider=Microsoft.Jet.OLEDB.4.0;" _
    & "Data Source=C:\Microsoft Visual Studio\VB98\NWind.mdb"
cn.Open
rs.CursorLocation = adUseClient
rs.Open "Products", cn, adOpenStatic, adLockBatchOptimistic
' Disconnect the recordset from the data source and free the connection.
Set rs.ActiveConnection = Nothing
cn.Close

Do Until rs.EOF
    If rs("UnitPrice") > 40 Then
        ' In a real-world application, you'll surely use a better UI.
        newValue = InputBox("Insert a new price for product " & _
            rs("ProductName"), , rs("UnitPrice"))
        If Len(newValue) Then rs("UnitPrice") = newValue
    End If
    rs.MoveNext
Loop
' Ask for confirmation of all changes.
If MsgBox("Send updates to the database?", vbYesNo) = vbYes Then
    ' Reestablish the connection, and send the updates.
    cn.Open 
    Set rs.ActiveConnection = cn
    rs.UpdateBatch
Else
    rs.CancelBatch
End If

要注意的是此程式如何關閉不再被Recordset使用的連接,而當需要時又再度開啟。這項技術可能比到目前為止所學得的技術更能提升你對應用程式的運用。

解決衝突
 

上述程式碼中忽略了任何樂觀批次更新處理的一個重要部份-衝突處理。事實上,「樂觀」其意為當您在處理某Recordset時,不希望其他使用者更新到相同的資料列。然而在實作上,必須以手動方式解決這些衝突。而這些衝突發生的原因可能有,您要更新的資料已被另一位使用者所刪除,或您要更新的欄位已被另一位使用者所異動。預設情況下, ADO只有在兩名使用者修改同個欄位時會發出衝突訊號,而當修改同一筆資料的不同欄位時卻不會發出衝突訊號。為有最好的結果,必須確認要更新的資料表有主鍵值。否則可能會意外地更新超過您所想到更新的資料列數量。

設定Filter屬性為adFikterConflictingRecords,可以看出哪些資料列發生衝突,而且在Recordset中以迴圈方式測試每筆資料列的Status屬性:

' A skeletal code that resolves batch update conflicts
On Error Resume Next
rs.UpdateBatch
rs.Filter = adFilterConflictingRecords
If rs.RecordCount > 0 Then
    ' Resolve the conflicts here.
End If
' Go back to the regular recordset.
rs.Filter = adFilterNone

再來,要有方法解決所發現的衝突。首先,可以瀏覽Recordset的每一筆資料列,並且查詢他們的Status屬性。如果傳回adRecModified表示是另一位使用者正在更動目前使用者在編輯的欄位;而傳回adRecDeleted代表此筆資料列已被刪除; adRecConcurrencyViolation代表錯誤。第13章 表13-2 列出Status屬性的所有值。記住這些都是位元值,所以應該藉由And運算測試每個單獨的位元,程式碼如下:

If rs.Status And adRecModified Then. . .

如果資料已被修改過,您必須決定要怎麼辦。不幸的是,這並沒有統一的有效規則。自動處理衝突的策略總是危險的;通常最好的方法是讓使用者做決定。然而,為了讓他們作出正確的決定,您必須顯示已存在 資料庫的新值。不幸的是,單只是查詢Field物件的UnderlyingValue屬性是沒有用的,因為它會傳回與OriginalValue屬性相同的值〈這是當Recordet開啟時的原始值〉。為了讓UnderlyingValue屬性傳回正確值,必須執行Recordset物件的Resync方法。

可以傳給Resync方法兩個參數。第一個參數是決定哪些資料列需要重新同步化,它可以有以下這些值:adAffectAllChapters(預設值,影響所有的資料列)、adAffectGroup(影響目前filter所得到的資料列)、adAffectCurrent(影響目前的資料列)。對於我們的目的,adAffectGroup是最好的選擇。第二個參數決定Field物件的屬性如何被影響:在此所需要的值是adResyncUnderlyingValues,其可自資料庫讀出資料,並將之設定給UnderlyingValue屬性。若您錯誤地使用adResyncAllValues(預設值)作為第二個參數,則會覆寫掉Value屬性,因而遺失使用者所鍵入的資料。以下的程式碼將這些觀念整合在一起,且顯示所有衝突資料列的列表,包含各相關的欄位:

On Error Resume Next
rs.UpdateBatch
rs.Filter = adFilterConflictingRecords
If rs.RecordCount Then
    Dim fld As ADODB.Field
    ' Resync the Recordset to retrieve correct values for UnderlyingValue.
    rs.Resync adAffectGroup, adResyncUnderlyingValues
    ' Loop on all the conflicting records. Note that setting the Filter
    ' property implicitly performs a MoveFirst method.
    Do Until rs.EOF
        Print "Conflict on record: " & rs("ProductName")
        For Each fld In rs.Fields
            ' Display fields whose local and underlying values don't match.
            If fld.Value <> fld.UnderlyingValue Then
                Print "Field: " & fld.Name _
                    & "- Original value = " & fld.OriginalValue _
                    & "- Value now in database = " & fld.UnderlyingValue _
                    & "- Local value = " & fld.Value
            End If
        Next
        rs.MoveNext
    Loop
End If
rs.Filter = adFilterNone

即使underlying值等於用戶端的值,ADO也發發出衝突訊號。換句話說,假如兩個使用者嘗試在記錄中的同一欄位儲存相同的值,ADO會發出警告。在您或使用者擁有要做出決定所需要的全部資訊後,您可以用下面的其中之一解決此衝突:

要看樂觀批次更新的實際運作,請執行兩次在附贈的光碟中內的BatchUpdate專案,在此兩個程式中修改同一筆資料列,然後按下Update按鈕。在第一個程式中,會收到OK訊息;在另一個程式中,會得到一個錯誤訊息,並可瀏覽衝突的資料列,重新同步化Recordset,及觀察所有欄位的相對應屬性值,如圖14-2所示。這應用程式作業於Pubs SQL Server資料庫和Biblio.mdb Jet資料庫下。


 

圖14-2 樂觀批次更新範例範例

Update Criteria屬性
 

如前面所提,當不同的使用者修正相同的欄位時,預設情況下,ADO會產生衝突現象。換句話說,若這兩位使用者對相同的資料列的不同欄位做修正,將不會有錯誤產生。這樣的情況是非常危險的,會對資料庫造成不一致性。幸運地,可以藉由Recordset物件的Update Criteria動態屬性更改預設行為。這一屬性影響被ADO所使用的欄位以便用來指出被更新的資料列。您可以將此屬性設定為下面四種值的其中一個:0-adCriteriaKey〈ADO只使用主鍵值〉、1-adCriteriaAllCols〈ADO使用Recordset中所有的行〉、2-adCriteriaUpdCols〈預設值,ADO使用鍵值與更新的欄位〉、3-adCriteriaTimeStamp〈如果有的話,ADO使用TIMESTAMP行;否則使用adCriteriaAllCols〉。

因為Update Criteria是動態屬性,可以藉由Properties集合來設定,如以下所示:

rs.Properties("Update Criteria") = adCriteriaTimeStamp

通常,如果資料表包含TIMESTAMP欄位,將屬性設為adCriteriaTimeStamp可以提供最好的效能。相反地,將它設為adCriteriaAllCols是這些值中最沒有效率的(雖然是最安全的)。注意,您並不需要使用adCriteriaTimeStamp字串來檢索TIMESTAMP欄位。在Microsoft知識庫中編號Q190727有更多的資訊。

使用Command物件
 

您已經知道如何用Connection物件的Execute方法執行命令,與使用Recordset物件的Open方法來擷取一串記錄了。您也可以用這兩個簡單的技巧寫出許多有用的應用程式,不過對於更多的需求工作,還需要Command物件的力量。當您需要作參數查詢時,Command物件是最好的選擇;當您要呼叫帶參數的預存程序且傳回資料時,Command物件是唯一可用的方案。

動態命令
 

一個Command物件表示一個你打算在資料來源上執行的命令。此時,您需要一個開啟的連結以便真正地執行這命令,不過當建立一個Connection物件時卻還不需要擁有它。換句話說,您可以建立一個單獨存在的Command並設定屬性,然後透過Command物件的ActiveConnection屬性(其作用類似同名的Recordset屬性)將之關聯到一個開啟的連結。底下這個簡單的Command例子,其在Pubs SQL Server範例資料庫的Publishers資料表中執行UPDATE SQL命令:

' Prepare the Command object's properties.
Dim cmd As New ADODB.Command
cmd.CommandText = "UPDATE Publishers SET city = 'London' " _
    & "WHERE Pub_ID = '9999'"
cmd.CommandTimeout = 10
cmd.CommandType = adCmdText          ' This saves ADO some work.
' When you're ready, open the connection and fire the command.
Dim cn As New ADODB.Connection
Dim recs As Long
cn.Open "Provider=sqloledb;Data source=p2;user id=sa;initial catalog=pubs"
Set cmd.ActiveConnection = cn
cmd.Execute recs
Print "RecordsAffected = " & recs

相反地,您可以傳遞一個字串給Command物件的ActiveConnection屬性,而在這一個情況下ADO會建立一個間接的Connection物件。建議您不要使用最後一個技巧,因為它對於Connection只提供少部分的控制。對於這個技巧以一個用簡單程式為例子:

cmd.ActiveConnection = "Provider=sqloledb;Data Source=p2;User Id=sa;" _
    & "Initial Catalog=pubs"
cmd.Execute recs

傳回資料列的查詢
 

使用Command物件來執行傳回資料列的查詢有三種不同的方法。它們都是一樣的,可依據特定的工作或您的寫作風格任選其中一個。第一種技巧,只需要指定一個Recordset物件接收Command物件Execute方法的傳回值:

' This code assumes that Command's properties have been set correctly.
Dim rs As ADODB.Recordset
Cmd.CommandText = "SELECT * FROM Publishers WHERE country = 'USA'"
Set rs = cmd.Execute

' At this point, the Recordset is already open.

第二種技巧,指派Command物件給Recordset物件的Source屬性,程式如下:

Set rs.Source = cmd
Rs.Open

第三種方式是這三種中最簡單的:

Rs.Open cmd

當您傳遞Command物件給Recordset物件的Open方法時,Recordset會承襲Command物件的連接。基於這個理由,當傳遞不同的Connection物件或ConnectionString傳遞給Open方法,將產生錯誤。若傳遞未與開啟的Connection有關聯的Command物件,一樣會產生錯誤。將Recordset與Command做聯繫後,可以使用Recordset的ActiveCommand屬性得到所關聯的Command。然而,不要嘗試再將Command傳遞給這個屬性,因為它是唯讀的。

帶有參數的命令和查詢
 

據前面所看到的程式碼,Command物件並沒有比SQL命令來的好用。真正可顯現這些物件的力量在於帶有參數的Command和查詢。例如,常必須要根據所給予的國家來選擇出版商。這種查詢可使用Command物件:

Dim cmd As New ADODB.Command, rs As ADODB.Recordset
cmd.ActiveConnection = "Provider=sqloledb;Data source=p2;user id=sa;" _
    & "initial catalog=pubs"
' Use question marks as placeholders for parameters.
cmd.CommandText = "SELECT * FROM Publishers WHERE country = ?"
' You can pass CommandType as the Execute's third argument.
Set rs = cmd.Execute(, "USA", adCmdText)

當有多個參數時,必須以Variants形態的陣列方式傳入,此時可用Array函數:

cmd.CommandText = "SELECT * FROM Publishers WHERE country = ? " _
    & " AND Pub_Name LIKE ?"
' 注意事項that the LIKE operator follows the SQL Server syntax.
Set rs = cmd.Execute(, Array("USA", "N%"), adCmdText)

若以Parameters集合來指定這些參數值,可以寫出更漂亮的程式碼:

cmd.Parameters.Refresh            ' Create the collection (optional).
cmd.Parameters(0) = "USA"
cmd.Parameters(1) = "N%"
Set rs = cmd.Execute()

Parameters集合的Refresh方法是可選擇的,因為只要參考到此集合的任一屬性或方法(除了Append),ADO都會分析查詢子句並建立此Connection,雖然會有點負荷。幸運地,自行建立Connection而節省負載並不困難,只要使用Command的CreateParameter方法:

' Create the collection of parameters. (Do this only once.)
With cmd.Parameters
    .Append cmd.CreateParameter("Country", adChar, adParamInput, 20)
    .Append cmd.CreateParameter("Name", adChar, adParamInput, 20)
End With
' Assign a value to parameters.
cmd.Parameters("Country") = "USA"
cmd.Parameters("Name") = "N%"
Set rs = cmd.Execute()

Command物件的Prepared屬性在最佳化帶有參數的查詢中扮演一個重要的角色。如果這屬性是True,在您第一次使用Command物件的Execute方法,ADO會在伺服端建立一個暫存的預存程序。在第一個執行中會增加一些負擔,不過可以加速後續的呼叫。當Connection關閉,暫存的預存程序會自動刪除。有一點要注意的:藉由追蹤SQL呼叫後發覺,在SQL Server 6.3 SP3或更早的版本中,此屬性並沒有作用。

使用DataEnvironment設計師
 

在設計階段使用DataEnvironment設計師的Connection物件和Command物件可以大幅簡化程式碼。您等會將發覺,所需要的程式碼會大幅減少,因為這些物件的大部分屬性都可在設計階段時便加以設定,其操作的方式跟一般在表單及控制項中的概念沒什麼不同。

連線(Connection)和命令(Command)
 

可以使用Connection物件開啟資料庫、開始一個交易等等。然而,多數的情況中,您不需要明顯地開啟一個Connection,因為在執行階段只要參考到屬於某Connection的Command物件時,DataEnvironment就會自動地開啟連線。實際上,只有需要設定Connection物件的某些屬性(例如,使用者名稱和密碼)才會參考到Connection:

' This code assumes that Data Environment1 has a Connection object named "Pubs"
' and a Command object named ResetSalesReport under it.
Dim de As New DataEnvironment1
de.Pubs.Open userid:="sa", Password:="mypwd"
de.ResetSalesReport

藉由設定RunPromptBehavior屬性可決定登入對話方塊是否出現。在設計階段中所定義的Command物件會變成DataEnviromment的方法。以下的例子就是在沒有直接開啟Connection下執行Command物件,因為所有的登入資訊在設計階段中已經定義:

' You can hardly write code more concise than this!
DataEnvironment1.ResetSalesReport

先前提到的兩個程式碼在這一方面有些不同:前者,建立一個名為de的DataEnvironment1設計師實體,而後者則使用全域名稱。這意味Visual Basic管理DataEnvironmen設計師如同表單設計師,可以把類別名稱當成變數(第9章中有詳細的說明)。將這記在心頭,因為您也許會不經意地建立許多設計者的實體,而不知道已經浪費了記憶體和資源。

在執行階段,DataEnvironment設計者有三種集合:Connections、Commands和Recordsets。可以用這些集合自資料庫查詢使用者要的資料:

' Fill a list box with the names of all supported Commands.
' BEWARE: just referencing the Commands collection opens the connection.
Dim cmd As ADODB.Command
For Each cmd In DataEnvironment1.Commands
    List1.AddItem cmd.Name
Next

資料集(Recordset)
 

一個設計師實體延伸出Connection和Command集合,且也延伸出一個Recordset物件(每個Command物件所傳回的結果集)。此Recordset的命名規則為rs-CommandName,其中CommandName為建立此Recordset的Command。

例如,若您已定義一個名為Authors的Command物件作為查詢,則DataEnvironment物件將會延伸出一個屬性叫rsAuthors,其形態為Recordset。Recordset的預設情況是關閉的,所以在使用之前必須要執行相關的Command:

' Fill a list box with authors' names.
Dim de As New DataEnvironment1
de.Authors                      ' Run the query.
Do Until de.rsAuthors.EOF
    List1.AddItem de.rsAuthors("au_fname") & " " & de.rsAuthors("au_lname")
    de.rsAuthors.MoveNext
Loop
de.rsAuthors.Close

另一種方法,您也可以直接開啟一個Recordset物件。後面這項技術比較彈性,因為在開啟Recordset之前可以先設Recordset的屬性。

Dim rs As ADODB.Recordset
' Get a reference to the Recordset, and open it with an optimistic lock.
Set rs = DataEnvironment1.rsAuthors
rs.Open LockType:=adLockOptimistic
Do Until rs.EOF
    List1.AddItem rs("au_fname") & " " & rs("au_lname")
    rs.MoveNext
Loop
rs.Close

當然您可以用WithEvent關鍵字來宣告rs變數,以便可捕捉被Recordset物件所引發的事件。

帶參數的查詢
 

如果Command物件帶有一至多個參數,可以在Command名稱後傳遞這些參數。為了測試這一特質,建立名為AuthorsByState的Command物件,其架構在連接到Pubs SQL Server資料庫的Connection物件,程式碼如下:

SELECT au_lname, au_fname, address, city, zip, state FROM authors 
    WHERE (state =?)

執行這段程式碼的範例:

DataEnvironment1.AuthorsByState "CA"
' Show the results in a DataGrid control.
Set DataGrid1.DataSource = DataEnvironment1.rsAuthorsByState

執行帶有參數的預存程序是一件麻煩的事,因為ADO無法判定參數的正確型態,您必須調整DataEnvironment設計師顯示在Command物件屬性頁中的參數欄。再來,如果使用SQL Server 6.5,請安裝Server Pack 4(可以在Visual Studio光碟中找到),會修復這一部份的錯誤。呼叫SQL Server的SampleStoredProc的預存程序,需要一個輸入參數值、一個輸出參數值,然後會有傳回值。以下Visual Basic文件的範例:

Dim outParam As Long, retValue As Long
retValue = DataEnvironment1.SampleStoredProc(100, outParam)
Set DataGrid1.DataSource = DataEnvironment1.rsSampleStoredProc 
Print "Output parameter = " & outParam
Print "Return value = " & retValue

筆者發現這個語法有許多的問題。最糟的事,當要省略幾個參數時,不能使用這個方法。為了避免這些問題,可以藉助ADOCommand物件的Parameters集合。為了對應這一個物件,您必須查詢DataEnvironment的Commands屬性,如下所示:

With DataEnvironment1.Commands("SampleStoredProc")
    ' This is the "royalty" parameter.
    .Parameters(1) = 100                 
    Set DataGrid1.DataSource = .Execute
    ' Retrieve the output parameter.
    Print "Output parameter = " & .Parameters(2)
    ' The return value is always in Parameters(0).
    Print "Return value = " & .Parameters(0)
End With

最重要的一點:當使用Command集合檢索ADO的Command物件,就某種意義上來講,是略過DataEnvironment設計師的傳回Recordset機制。基於上述理由,只能使用Execute方法傳回Recordset,而不是使用設計師的rsSampleStoredProc屬性。最後,可以直接傳遞參數給Execute方法,與並透過Parameters集合得到輸出值與傳回值:

Dim recordsAffected As Long
With DataEnvironment1.Commands("SampleStoredProc")
    ' The array of parameters passed to the Execute method must account for
    ' the return value, which is always the first parameter.
    Set DataGrid1.DataSource = .Execute(recordsAffected, Array(0, 100))
    Print "Output parameter = " & .Parameters(2)
    Print "Return value = " & .Parameters(0)
End With

可再使用的模組
 

說到這個,筆者將以DataEnvironment設計師來建立可於程式中使用的Connection物件、Command物件、Recordset物件,但不需要在執行階段中用程式碼定義。然而,不要忘了在設計師內也可以寫程式。這一程式碼會回應由DataEnvironment物件所建立的Connection物件和Recordset物件所產生的事件。此外,您可以用任何形式的模組增加公開的屬性、方法和事件。模組的能力可以讓您在DataEnvironment模組中簡化複雜的邏輯程式碼,且可以在任何應用程式中再使用。一種對這些公開的屬性可能的再使用是提供有意義的名字給要傳遞給Command的Paramenters集合的參數,如下所述:

' Inside the DataEnvironment module
Public Property Get StateWanted() As String
    StateWanted = Commands("AuthorsByState").Parameters("State")
End Property
Public Property Let StateWanted(ByVal newValue As String)
    Commands("AuthorsByState").Parameters("State") = newValue
End Property

以下範例是以InfoText屬性為例,它能從Connection的Infomessage事件中收集所有的輸出值:

Private m_InfoText As String
Public Property Get InfoText() As String
    InfoText = m_InfoText
End Property
Public Property Let InfoText(ByVal newValue As String)
    m_InfoText = newValue
End Property
' Add a new text line to the InfoText property.
Private Sub Connection1_InfoMessage(ByVal pError As ADODB.Error, _
    adStatus As EventStatusEnum, ByVal pConnection As ADODB.Connection)
    m_InfoText = m_InfoText & "pError = " & pError.Number & " - " & _
        pError.Description & vbCrLf
End Sub

DataEnvironment物件的缺點
 

第一次看到DataEnvironment物件的能力時,受到震撼。筆者認為在這些章節中已表達筆者的觀點。但若筆者不說明DataEnvironment設計師仍然有一些嚴重的問題,那對讀者來說是不太公平的。這些問題有時會阻礙其在屬於銷售產品類的應用程式中使用。以下是筆者的發現:

DataEnvironment在處理帶有參數的預存程序時並沒有很好,尤其是有輸出數和傳回值的預存程序。特別地,有時候不能用DataEnvironment物件的Command方法來呼叫這些函數—例如之前DataEnvironment1.SampleStoredProc的例子。您必須透過Command的Parameters集合傳遞這些參數。

  • 使用DataEnvironment設計師作為資料來源,與數個控制項進行資料繫結時,不能信賴DataEnvironment的自動連結機制。事實上,如果連接失敗,並不會有錯誤傳給程式:只會發現不會有資料在資料感知控制項中,而沒有任何錯誤發生的提示。如果Connection物件的RunPromptBehavior屬性設為adPromptNever(這是大多數的設法),使用者無法辦法解決問題。基於此,必須在Form_Load事件中測試連結是否開啟做測試,如下所示:

     
    Private Sub Form_Load()
        If (DataEnv1.Connection1.State And adStateOpen) = 0 Then
            ' In a real application you'll do something smarter
            ' than just showing a message box.
            MsgBox "Unable to open the connection", vbCritical
        End If
    End Sub
  • 一般來講,並不能確定資料來源的路徑與使用者的目錄結構一致。因此,您必須提供從註冊區或INI檔中提供正確的應用程式路徑給他。舉例來說,在從資料庫中抓取資料來顯示前,必須要做此動作,範例如下:

     
    Private Sub Form_Load()
        If (DataEnv1.Connection1.State And adStateOpen) = 0 Then
            Dim conn As String
            conn = "Provider=Microsoft.Jet.OLEDB.3.51;"_
                & "Data Source=???"
            ' ReadDataBasePathFromINI is defined elsewhere.
            conn = Replace(conn, "???", ReadDataBasePathFromINI())
            DataEnv1.Connection1.ConnectionString = conn
            DataEnv1.Connection1.Open
        End If
    End Sub
  • 在某些情況下,DataEnvironment物件開啟過多的connection。舉例來說,您有兩個以上相同的DataEnvironment設計師實作,每個實作會開啟不同的connection。如果沒有注意到這種行為,可能容易對可用的connection造成困惑,特別是在SQL Server Developer版本 (其只允許比真正的產品少的connection)。
     
  • 在大部分情況下,DataEnvironment設計師如同類別模組,它的實行可能產生一些危險。例如,若以全域的DataEnvironment間接開啟Connection,當程式在執行時,這一個Connection決不會關閉。更正確地說,SQL Server會確認Connection真的關閉了,但其似乎在程式已經結束其執行後方發生。這表示不能藉由Connection的Disconnect事件來執行清除的程式。更多無法說明的部分是:全域DataEnviroment實作甚至無法接收Terminate事件(當程式結束時,所有物件都會產生的事件),所以您不能依靠這一事件以正規方式關閉connection。在IDE和已編譯過的程式中,這錯誤是很明顯地發生。
     

最後,不要對盲目地猜測DataEnvironment設計師會如預期般的運作,並且在極端的狀況下對它作測試,就像當connection無法被保證或不足時。

進階技術
 

對於ADO檢索資料的基本技術應該熟悉。底下是關於進階的主題,就像非同步運作和階層式Recordset。

Recordset事件
 

在RDO,你可能認為只有當執行非同步動作時才需要處理事件。事實是雖然事件在非同步動作中扮演一個重要角色,但在其他不同情況下仍是有用的。事實上,不論動作是否同步,ADO會引發事件。在下一節中會提到非同步查詢和擷取動作,不過現在要介紹一些Recordset事件,當在進行同步動作時是有用的。

Recordset物件有11個事件,包含4組Will/Complete事件加上FetchProgress、FetchComplete和EndOfRecordset事件。Visual Basic文件並沒有什麼幫助,只有藉由範例程式來了解事件的運作或練習在 第13章 所介紹的ADO程式。筆者發現在文件上所沒有(或不夠)的事件資訊。例如,筆者發現到ADO有時候會引發沒有預期的事件,這些在稍後會解說。首先,先從基礎開始。

ADO會引發以下成組屬於Recordset的Will/Complete事件:

而這些事件雖然不一樣,他們還是有許多相似處。如所有的事件都可接收adStatus參數。在Willxxxx事件,adStatus參數值可以為adStatusOK或adStatusCantDeny:在一正規的例子中,如果您要取消這一動作引起的事件,可設此屬性為adStatusCancel。所有xxxxComplete事件可接收adStatus參數和包含錯誤發生訊息的pError參數。

欄位驗證
 

當要驗證欄位值時,可取消引起事件的動作的能力特別有用:不須在所有程式中撰寫驗證程式碼,只需要撰寫在WillChangeField事件中即可。事件接收cFields參數(欄位的數量)和Fields參數(Field物件陣列)。以下範例顯示如何使用此事件來驗證存放在欄位內的值:

Private Sub rs_WillChangeField(ByVal cFields As Long, 
    ByVal Fields As Variant, adStatus As ADODB.EventStatusEnum, 
    ByVal pRecordset As ADODB.Recordset)
    Dim fld As ADODB.Field, i As Integer
    ' If we can't cancel this event, there's no point in
    ' validating fields.
    If adStatus = adStatusCantCancel Then Exit Sub
    ' Note  that we can't use a For Each loop.
    For i = 0 To UBound(Fields)
        Set fld = Fields(i)
        Select Case fld.Name
            Case "FirstName", "LastName"
                ' These fields can't be empty strings or Null.
                If (fld.Value & "") = "" Then adStatus = adStatusCancel
            Case "GrandTotal"
                ' This field must be positive.
                If fld.Value < 0 Then adStatus = adStatusCancel
            ' Add Case blocks for other fields here.
        End Select
    Next
End Sub

若指定與欄位值相同的值時,WillChangeField事件也會引發。若發現這種例子且取消動作時,在某些時候可能會節省ADO,特別在壅塞的網路頻寬上。要記住主程式應該要準備處理&H80040E4E的錯誤:「經確認後,改變已被取消;沒有欄位被改變。」

若能偵測錯誤的值並在WillChangeField事件程序中修復他們是很重要的。不幸地,在事件中修改欄位值似乎不可能:您只能接受或拒絕由主程式所設定的值。當主程式呼叫帶有一串欄位名稱和值的Update方法時,事件會接收到許多欄位。其實cFields參數是不需要的,因為可以用UBound(Fields)+1來代替。

FieldChangeComplete有使用上的限制,至少對欄位驗證就有點限制。若未使用資料感知控制項,則可用此事件更新螢幕上的資料。若使用資料感知控制項,可用此事件來更新包含計算值的非資料感知控制項。然而,對此事件而言-包含所有xxxxComplete事件—如果相對的動作被程式取消或因ADO所引發錯誤時,此事件仍會引發。基於此,首先總要檢查adStatus參數:

Private Sub rs_FieldChangeComplete(ByVal cFields As Long, _
    ByVal Fields As Variant, ByVal pError As ADODB.Error, 
    adStatus As ADODB.EventStatusEnum, ByVal pRecordset As ADODB.Recordset)
    If adStatus <> adStatusErrorsOccurred Then
        ' Update your unbound controls here.
    End If
End Sub

假如您絕對不使用這一事件,可藉由要求ADO不要再次執行它以改善執行速度:

Private Sub rs_FieldChangeComplete(& )
    ' This event will be invoked only once.
    adStatus = adStatusUnwantedEvent
End Sub

資料行驗證
 

通常,獨自的欄位驗證對確認資料庫包含有效資料是不夠的。因此,在資料寫入資料庫前,需要驗證所有欄位值。這是WillChangeRecord事件的主要工作。

對此事件而言,adReason值指出紀錄為何被改變,cRecords指出已影響的紀錄筆數。(adReason可以接受的值在第13章 表13-4 中列出。)在目前記錄中第一次更新欄位,ADO會引發WillChangeRecord事件(和相對的RecordChangeComplete事件),並將adReason設為adRsnFirstChange,好給您機會準備記錄的更新(或拒絕它)。當記錄已準備寫入資料庫中,ADO會引發另一此的WillChangeRecord/RecordChangeComplete事件,這一次有另一個特定值設給adReason。在表13-4中可以看到這些值。然而,提醒您即使記錄因MoveNext方法而更新,WillChangeRecord事件接收到的adReason值仍是adRsnUpdate。這是因為當改變一個以上的欄位後並移動到另一個記錄時,對ADO而言這是隱含的Update方法。

在WillChangeRecord事件,不能修改Recordset物件的欄位值,因此,您不能使用這一事件來提供預設值給欄位、自動修復無效的值、強迫大寫或小寫等。若發現某些值不正確或不完全時,只能測試欄位值和拒絕更新動作。由於當第一個欄位被更新時,此事件也會引發,因此必須測試adReason參數的值:

Private Sub rs_WillChangeRecord(ByVal adReason As ADODB.EventReasonEnum, _
    ByVal cRecords As Long, adStatus As ADODB.EventStatusEnum, _
    ByVal pRecordset As ADODB.Recordset)
    If adReason <> adRsnFirstChange Then
        ' These two fields can't both be empty strings.
        If rs("CustAddress") = "" And rs("ShipAddress") = "" Then
            AdStatus = adStatusCancel
        End If
    End If
End Sub

以非資料感知控制項顯示資料
 

若應用程式不用資料感知控制項來顯示資料,則必須撰寫將資料自Recordset中讀出並顯示在螢幕上的程式碼,以及撰寫從將資料自控制項移到資料庫的程式碼。一般來說,會使用WillMove事件來移動資料從控制項到資料庫,且使用MoveComplete事件把資料從資料庫移到控制項。先來探討後面那個事件,範例如下:

' Assumes that the form contains two TextBox controls.
Private Sub rs_MoveComplete(ByVal adReason As ADODB.EventReasonEnum, _
    ByVal pError As ADODB.Error, adStatus As ADODB.EventStatusEnum, _
    ByVal pRecordset As ADODB.Recordset)
    ' Move data from the Recordset to on-screen controls.
    txtFirstName.Text = rs("FirstName")
    txtLastName.Text = rs("LastName")
    ' Clear the controls' "modified" flag.
    txtFirstName.DataChanged = False
    txtLastName.DataChanged = False
End Sub

如上述看到的程式碼,可在非資料感知控制項內使用DataChanged屬性。事實上,對於Visual Basic所提供的正規控制項而言,此屬性運作相當正常,當控制項內容改變時,此屬性會自動設為True。唯一真正的不同是DataChanged屬性如何被重設:當用資料感知控制項時,Visual Basic會自動重設屬性值為False,不過若使用非資料感知控制項時,你必須自行重設。然後可在WillMove事件中測試DataChanged屬性值,便可知道是否真的需要移動值自螢幕上的控制項到資料庫:

Private Sub rs_WillMove(ByVal adReason As ADODB.EventReasonEnum, _
    adStatus As ADODB.EventStatusEnum, ByVal pRecordset As ADODB.Recordset)
    ' Move data to Recordset only if user modified the controls' contents.
    If txtFirstName.DataChanged Then rs("FirstName") = txtFirstName.Text
    If txtLastName.DataChanged Then rs("LastName") = txtLastName.Text
End Sub

對此觀念叫完善的實作應該是,測試adReason引數與做出相對的反應。例如,可以決定當Recordset物件已經關閉時是否儲存資料到資料庫中。或者,由於AddNew方法而引發MoveComplete事件時,要給予控制項預設值。不像WillChangeField和WillChangeRecord事件,WillMove事件允許您指定值給Recordset的欄位,因此可以使用這一事件來提供預設值或計算欄位:

' In the WillMove event
If txtCountry.Text = "" Then rs("country") = "USA"

處理多重事件
 

Recordset中的單一運作會引發許多(甚至是巢狀的)事件。如表所示,每個方法會引發哪些事件:

方法 事件
rs.Open WillExecute

WillMove (adReason = adRsnMove)

MoveComplete(adReason = adRsnMove)

ExecuteComplete

rs("FirstName") = "John" WillChangeRecordset (adReason = adRsnMove)

RecordsetChangeComplete (adReason = adRsnMove)

WillMove (adReason = adRsnMove)

MoveComplete (adReason = adRsnMove

WillChangeRecord (adReason = adRsnFirstChange)

WillChangeField

FieldChangeComplete RecordChangeComplete (adReason = adRsnFirstChange)

rs("LastName") = "Smith" WillChangeField

ChangeFieldComplete

rs.MoveNext WillMove (adReason = adRsnMoveNext)

WillChangeRecord (adReason = adRsnUpdate)

RecordChangeComplete (adReason = adRsnUpdate)

WillChangeRecordset (adReason = adRsnMove)

RecordsetChangeComplete (adReason = adRsnMove)

MoveComplete (adReason = adRsnMoveNext)

在大多數時候,上述的順序是明確且合理的。然而,有時候又很特別。例如,MoveNext事件會引發成對的WillChangeRecordset/RecordsetChangeComplete事件。根據Visual Basic文件,這不應該發生。這有一些證據證明當ADO填滿值入區域端Cache時會引發這類事件。事實上,如果將CacheSize設成大於1(設成4好了),當每執行四次MoveNext動作時會引發此事件一次。換句話說,每次ADO重新填值入區域端的Cache時,會重新建立Recordset物件。記得這些資訊,它也許會用到。

其他事件不容易解釋。例如,為什麼一個設定值到FirstName欄位會引發WillMove/MoveComplete事件?畢竟,第一個記錄是目前的紀錄,不是嗎?實際上,這問題筆者無法回答。只能注意寫在WillMove和MoveComplete事件中的程式碼,因為此些事件可能會引發超過您所預期的。

若取消一個事件,這上述的順序會發生什麼事!例如,在因為MoveNext方法而引發的WillMove事件中設定adStatus屬性為adStatusCancel,其他事件被禁止,而ADO只引發相對的MoveComplete事件。換句話說,如果在WillChangeRecord事件中取消命令,ADO只取消所有事件除了WillChangeRecordset/ RecordsetChangeComplete事件外。通常,在將adStatus設為adStatusCancel後,這值就不能透過之後的事件來修改,直到錯誤傳回主程式傳回為止。

非同步運作
 

ADO提供多種非同步運作的樣式,都可以幫助應用程式更多回應給使用者。之前已解釋如何設定非同步的連接,現在要告知如何執行長的命令而不用等待ADO完成程式碼。

非同步命令
 

非同步運作的一個簡單形式是透過Connection物件而執行的命令。於這個例子,所要做的事是傳遞adAsyncExecute值給Connection物件的Execute方法的Optiosn參數,程式如下:

Dim cn As New ADODB.Connection, recs As Long
cn.Open "Provider=Microsoft.Jet.OLEDB.4.0;" _
    & "Data Source=E:\Microsoft Visual Studio\VB98\Biblio.mdb"
cn.Execute "DELETE FROM Publishers WHERE State = 'WA'", _
    recs, adAsyncExecute
Debug.Print recs & " records affected"        ' Displays _1.

當以這一種形式執行命令時,ADO會在執行Execute敘述後的敘述前引發WillExecute事件。因為命令尚未完成,因此recs變數接收到特定值-1。底下為WillExecute事件的語法範例:

Private Sub cn_WillExecute(Source As String, _
    CursorType As ADODB.CursorTypeEnum, LockType As ADODB.LockTypeEnum, _
    Options As Long, adStatus As ADODB.EventStatusEnum, _
    ByVal pCommand As ADODB.Command, ByVal pRecordset As ADODB.Recordset, _
    ByVal pConnection As ADODB.Connection)
    MsgBox "About to execute command " & Source
End Sub

因為所有參數都是傳址呼叫,如果必要的話,可以修正它們。藉設定adStatus引數為adStatusCancel也可以取消命令,除非它已被設為adStatusCantDeny。

' Put this code inside the WillExecute event.
If adStatus <> adStatusCantDeny Then
    If MsgBox("About to execute statement " & Source & vbCr & "Confirm?", _
        VbYesNo + vbInformation) = vbNo Then
        AdStatus = adStatusCancel
    End If
End If

當ADO完成這個命令時,會引發ExecuteComplete事件,而第一個參數表示實際影響的記錄數。

Private Sub cn_ExecuteComplete(ByVal RecordsAffected As Long, _
    ByVal pError As ADODB.Error, adStatus As ADODB.EventStatusEnum, _
    ByVal pCommand As ADODB.Command, ByVal pRecordset As ADODB.Recordset, _
    ByVal pConnection As ADODB.Connection)
    If adStatus = adStatusOK Then
        MsgBox "Execution of the command has been completed" & vbCr _
            & RecordsAffected & " record(s) were affected", vbInformation
    ElseIf adStatus = adStatusErrorsOccurred Then
        MsgBox "Execution error: " & pError.Description, vbCritical
    End If
End Sub

在WillExecute事件,您可以確定是否執行的命令具有傳回資料列的行為,只要檢查CursorType或LockType的值:如果有一者值為-1,表這是屬於動作類的命令。當ExecuteComplete的引發是因為Recordset的Open敘述時,則pRecordset為Recordset物件的參考值,但這沒有用處,因為你已有被開啟的Recordset的參考值了。當完成Connection物件的傳回資料列Execute命令時,pRecordset參數是較有用的,因為它包含查詢的結果。因此,可以指派他給ADO資料控制項或以喜歡的方式處理之。

如您期待的,在WillExecute事件的pCommand參數對應到Command物件,若事件的引發是因為Command物件的Execute方法時;或者此參數值為Nothing。有趣地,即時您不使用Command物件,ADO仍會產生一個暫存的Command物件來執行查詢,且將此物件傳遞給ExecuteComplete事件的pCommand參數。這一暫存物件讓您檢索資訊,就像來源字串(其在查詢完成後就不存在了)般:

' In the ExecuteComplete event
' The next statement works with *any* type of command or query.
Debug.Print "Statement " & pCommand.CommandText & " has been completed"

對這一能力進一步的是使用是重複的命令和失敗的查詢,例如因為逾時。像這一種情況,要簡單地執行Command物件的Execute方法且注意重複執行的問題。


小秘訣

當資料庫執行命令時,應用程式可如往常般繼續執行。如果需要知道運作是否完成,可以在ExecuteComplete事件設定一個全域旗標或更簡單地測試Connection物件的State屬性。因為這一屬性是一個位元欄位,所以應該運用如下AND運算子:

If cn.State And adStateExecuting Then. . .

當用SQL Server,您該知道一般會執行多個非同步命令,這些情形包含:沒有懸置的交易作業、現行命令屬於動作查詢、或為客戶端指標的資料傳回查詢。若遇到這些情況,SQL Server會悄悄地建立新的Connection來執行新的命令,或則的話會產生錯誤。


非同步擷取
 

ADO對非同步查詢以adAsyncFetch值做一個額外的控制。將它傳遞給Connection物件的Execute方法和Recordset物件的Open或Requery方法。當adAsyncExecute告知ADO這一查詢應該是要非同步的時,adAsyncFetch值告訴ADO應該要以非同步方式從資料來源擷取資料給Recordset。因此,ADO執行這一查詢,並立即將第一個結果填入區域端的Cache裡面,然後,逐漸擷取所有剩餘的紀錄。

若擷取動作花費較多時間,ADO會引發FetchProgress事件,可藉此來顯示進度列給使用者。當擷取完成,則引發FetchComplete事件。有關FetchComplete和adAsychFetchNonBlocking運作的更多資訊,請參閱第13章Command物件中的 Execute方法 。

預存程序
 

架構SQL Server或Oracle基礎上的Client/Server應用程式可使用預存程序來增進其功能。預存程序是一個寫在資料庫中的SQL語法,且已被編譯過能改善執行速度。預存程序讓開發者除了增進效率外,還可強化安全性,這只是許多優點之一。待會您可以知道,ADO和Visual Basic 6.0企業版在預存程序上提供許多能力。

SQL編輯器和T-SQL偵錯程式
 

若打開DataView視窗並選擇一個連接到SQL Server或Oracle資料庫的資料連結,會發現一個名為預存程序的子資料夾,裡面列出資料庫中可應用的預存程序。當您開啟相對的預存程序,可看到其傳回值和需要的參數,按兩下看參數的屬性。參數的屬性視窗顯示其ADO資料型態,當您必須要建立Command物件的Parameters集合時,這是必要的資料。

在預存程序的名字上連按兩下會進入SQL編輯器,其可讓您編輯預存程序而不需要離開Visual Basic整合環境界面。也可以用此編輯器建立觸發器。奇怪的是,這裡有個小錯誤:當顯示SQL編輯器時,DataView視窗的字型會改變成編輯器的字型,如圖14-3所示。這是無害的錯誤,且筆者發現一個關於它的用處:當我在教課時,有人抱怨DataView視窗實在不好觀看時,筆者便開啟預存程序編輯器並立即關閉它,於是DataView便轉成較大的字形。

若作業平台為SQL Server(不是Oracle)時,可以在Visual Basic環境中對預存程序偵錯。即使是遠端的伺服器和使用OLE Remote Automation來實際連接資料庫亦可。您也可以使用T-SQL Debugger增益集來執行系統或批次預存程序。The T-SQL Debugger可讓您在函數上設中斷點、單步執行於巢狀函數內或外、看全域和區域變數、顯示堆疊等。當開發一個複雜的應用程式時,這一特性可以節省時間。

設定T-SQL Debugger無法靠直覺,這裡有一些有用的技巧。第一,Debugger只能運作在SQL Server 6.5 Service Pack 3之後的版本上 (Visual Basic 6內附的是SQL Server 6.5 Service Pack 4。)第二,再安裝Visual Basic 6企業版後要立即安裝BackOffice,且必須勾選SQL Server Debugging選項。第三,SQL Server服務必須被設定成以足夠權限的使用者登入; Windows NT System Account是無法運作的。最後,確定OLE Remote Automation在您的機器上可正常運作且設定正確。

可以從SQL編輯器或從增益集選單中匯入這個編輯器,若您已經安裝並啟動T-SQL Debugger集的話。在後面的例子中,您必須指定特定的DSN與要登入的資料庫,如圖14-4所示,不過也可以偵錯批次預存程序。若當預存程序與觸發器涵蓋在程式碼內,而要偵錯他們時,在工具列中選擇T-SQL Debugging,然後勾選自動地透過RDO和ADO連線,逐步追蹤至預存程序中選項。


 

圖14-3 Visual Basic 6.0企業版讓您編輯SQL Server的預存程序並偵錯之。


 

圖14-4 T_SQL Debugger增益集。


 

圖14-5 T-SQL選項對話方塊。注意,等待時間值的單位是milliseconds。

帶有參數的預存程序
 

之前已解說如何以ADO的Command物件來執行帶有參數的SQL查詢,如何建立它們的Parameters集合或讓ADO為您建立。使用帶有參數的預存程序沒什麼差別,在此只告訴您須注意的事。

可讓ADO自動建立Command的Parameters集合。只需要在程式中引用到他們或明白地執行Parameters.Refresh命令即可。這方法有許多好處,包括較少的錯誤,因為ADO正確地擷取參數的名字與形態且自動地算出傳回值藉由建立名為RETURN_VALUE的Parameter物件。此方法另一項優點是若稍後修改某參數的形態,並不需要改變Visual Basic程式。不幸的是,ADO需要去一趟伺服器端以便擷取關於預存程序的資訊。然而,這額外命令只在第一次引用到Parameters集合時才會執行。所以只要在整個區域內保持Command物件皆存活著的話,這負荷無關緊要。有個問題是ADO可能會被輸出參數所困惑,且錯誤地認為他們是輸出/輸入參數。若這問題發生的話,只要設定參數的Direction屬性為正確值即可。很方便的,甚至在參數已被加到集合後,此屬性仍是可讀寫的。

若要節省ADO到Server的那趟過程,可以自行建立Parameters集合。下列程式範例以在Pubs資料庫的byroyalty預存程序為例:

Dim cn As New ADODB.Connection, cmd As New ADODB.Command
Dim rs As ADODB.Recordset
' Establish the connection.
cn.Open "Provider=sqloledb;Data source=p2;user id=sa;initial catalog=pubs"
Set cmd.ActiveConnection = cn
' Define the stored procedure.
cmd.CommandText = "byroyalty"
cmd.CommandType = adCmdStoredProc
' Save ADO some work by creating the parameter yourself.
cmd.Parameters.Append cmd.CreateParameter("@percentage", adInteger, _
    adParamInput)
' Set a value to this parameter, and execute the query.
cmd.Parameters("@percentage") = 100
Set rs = cmd.Execute()

當手動建立Parameters急何時,您必須注意重要的細項:如果預存程序傳回一個值,它一定是第一個參數。要看如何運作傳回值和輸出值,在DataView視窗中連按兩下byroyalty預存程序,並進入SQL編輯器,如下修改函數。(粗體字為所增加或修改的程式碼)

CREATE PROCEDURE byroyalty2 @percentage int, @totalrecs Int Output
AS
select @totalrecs= count(*) from titleauthor 
select au_id from titleauthor
where titleauthor.royaltyper = @percentage
return (@@rowcount)

底下是準備Parameters集合,執行查詢與列印結果的Visual Basic程式碼:

cmd.CommandText = "byroyalty2"
cmd.CommandType = adCmdStoredProc
' Create the Parameters collection
With cmd.Parameters
    .Append cmd.CreateParameter("RETVAL", adInteger, adParamReturnValue)
    .Append cmd.CreateParameter("@percentage", adInteger, adParamInput)
    .Append cmd.CreateParameter("@totalrecs", adInteger, adParamOutput)
End With
' Set a value for input parameters, and run the stored procedure.
cmd.Parameters("@percentage") = 100
Set rs = cmd.Execute()
' Dump the contents of the recordset.
Do Until rs.EOF
    Print "Au_ID=" & rs("au_id")
    rs.MoveNext
Loop
rs.Close
' Print the values of the output parameter and the return value.
Print "Records in titleauthor = " & cmd.Parameters("@totalrecs")
Print "Records returned by the query = " & cmd.Parameters("RETVAL")

這有幾個地方值得注意。首先,您可以使用任何名字當作傳回參數,只要它是集合的第一個元素。第二,也是最重要的,在存取傳回值和輸出變數前,必須關閉Recordset(或將它設為Nothing以便讓ADO關閉它)。對於自SQL Server傳回的順序、唯讀的Recordset而言是真的,也可以應用到其他資料指標型態和Provider。根據正式文件,ADO只從Provider讀取輸出參數與傳回值,因此如果嘗試在他們是可用狀態前讀取它們,是不可能的。

多重結果集
 

另一個ADO偉大的特性是有能力處理多重結果集。在 第13章 ,已解釋如何使用NextRecordset方法,但在此要顯示實際的例子。底下是用Visual Basic程式碼來瀏覽多重結果集:

' This code assumes that all properties have been correctly initialized.
Set rs = cmd.Execute()
Do Until rs Is Nothing
    If rs.State = adStateClosed Then
        Print "---- Closed Recordset
    Else
        Do Until rs.EOF
            For Each fld In rs.Fields
                Print fld.Name & "="; fld & ", ";
            Next
            Print
            rs.MoveNext
        Loop
        Print "---- End of Recordset"
    End If
    Set rs = rs.NextRecordset
Loop

看SQL Server和ADO如何處理預存程序,在DataView視窗的預存程序夾按下右鍵,選擇新的預存程序選單命令,然後在SQL編輯器中鍵入以下的程式碼:

Create Procedure PubsByCountry As
Select pub_name From Publishers where country='USA'    
Select pub_name From Publishers where country='France'    
Select pub_name From Publishers where country='Germany'    
Select pub_name From Publishers where country='Italy'

當您使用之前提到的Visual Basic程式碼來執行PubsByCountry預存程序時,可以看到如下的結果。

Pub_name=New Moon Books
Pub_name=Binnet & Hardley   
Pub_name=Algodata Infosystems             
Pub_name=Five Lakes Publishing            
Pub_name=Ramona Publishers  
Pub_name=Scootney Books     
---- End of Recordset
pub_name=Lucerne Publishing 
---- End of Recordset
pub_name=GGG&G              
---- End of Recordset
---- End of Recordset

最後SELECT子句傳回一個沒有包含任何記錄的Recordset。如果再一次執行NextRecordset方法,您會得到Nothing且結束迴圈。再看另一個傳回多重Recordset的查詢例子。這是個在Pub資料庫的reptql預存程序。

CREATE PROCEDURE reptq1 AS
Select pub_id, title_id, price, pubdate from titles
where price is NOT NULL order by pub_id
COMPUTE avg(price) BY pub_id
COMPUTE avg(price)

當執行reptql預存程序,先前程式產生的輸出如下。如您所視,第一個COMPUTE敘述對每一個出版商產生各自的Recordset;而第二個COMPUTE敘述產生最後的Recordset,包含所有出版商的平均價出版商:

Pub_id=0736, title_id=BU2075, price=2.99, pubdate=6/30/91, 
Pub_id=0736, title_id=PS2091, price=10.95, pubdate=6/15/91, 
Pub_id=0736, title_id=PS2106, price=7, pubdate=10/5/91, 
Pub_id=0736, title_id=PS3333, price=19.99, pubdate=6/12/91, 
pub_id=0736, title_id=PS7777, price=7.99, pubdate=6/12/91, 
---- End of Recordset
avg=9.784, 
---- End of Recordset
pub_id=0877, title_id=MC2222, price=19.99, pubdate=6/9/91, 
pub_id=0877, title_id=MC3021, price=2.99, pubdate=6/18/91, 
pub_id=0877, title_id=PS1372, price=21.59, pubdate=10/21/91, 
pub_id=0877, title_id=TC3218, price=20.95, pubdate=10/21/91, 
pub_id=0877, title_id=TC4203, price=11.95, pubdate=6/12/91, 
pub_id=0877, title_id=TC7777, price=14.99, pubdate=6/12/91, 
---- End of Recordset
avg=15.41, 
---- End of Recordset
pub_id=1389, title_id=BU1032, price=19.99, pubdate=6/12/91, 
pub_id=1389, title_id=BU1111, price=11.95, pubdate=6/9/91, 
pub_id=1389, title_id=BU7832, price=19.99, pubdate=6/22/91, 
pub_id=1389, title_id=PC1035, price=22.95, pubdate=6/30/91, 
pub_id=1389, title_id=PC8888, price=20, pubdate=6/12/94, 
---- End of Recordset
avg=18.976, 
---- End of Recordset
avg=14.7662, 
---- End of Recordset

理論上,您可以檢索多重結果集並且分派它們給不同的Recordset變數,或至少NextRecordset方法看起來似乎可行。不幸地,OLE DB提供者無法支援這一能力,所以同時只能擷取與處理一個Recordset。或可使用Clone方法(若Recordset能被複製的話)來擷取所有Recordset,將它們指定給陣列的每個項目,稍後再處理這些資料:

Dim cn As New ADODB.Connection, rs As ADODB.Recordset
' We can reasonably assume that 100 Recordset items will suffice.
Dim recs(100) As ADODB.Recordset, recCount As Integer
' Open the connection, and retrieve the first Recordset.
cn.Open "Provider=sqloledb;Data source=p2;user id=sa;" _
    & "initial catalog=pubs"
Set rs = New ADODB.Recordset
rs.Open "PubsByCountry", cn
' Retrieve all Recordsets, and clone them.
Do
    recCount = recCount + 1
    Set recs(recCount) = rs.Clone
    Set rs = rs.NextRecordset
Loop Until rs Is Nothing
' Now the recs() array contains one clone for each Recordset.

不幸地,也不能使用這項技術來更新在資料庫的欄位,看起來似乎是不可能:意圖透過存在recs( ) 陣列中的Recordset傳送資料回後端的SQL Server會出現錯誤&H80004005,「沒有足夠的基本表資訊以便更新資料。」。甚至不能中斷Recordset和關閉Connection,因為存在陣列的所有Recordset會立即關閉,即使原始的Recordset被設定為樂觀批次更新。簡而言之,您可以儲存複製後Recordset於陣列,不過實際上只有在同時間處理它們的內容時才有用(例如,當您要比較在Recordset中的記錄時)。這邊有些關於多重結果集的提示:

階層式的Recordset
 

若要筆者說ADO哪個特性最令我印象深刻的話,毋庸置疑地會選擇其可建立階層式Recordset物件的能力。階層式Recordset可以包含子Recordset物件,就像資料夾可包含其他子資料夾般。例如,可以從Publishers資料表建立Recordset,每一個記錄包含單一的出版商加上一個子Recordset包括由此公司所出版的書名列表。每一個在子Recordset的記錄可以包括每一標題的資訊再加上另一個子Recirdset包含了書籍作者的資訊等等。可以建立許多階層式Recordset而磨有任何限制(理論上而言)。建立階層式Recordset也常稱為Data Shaping。

您可以用兩個不同的方法建立階層式Recordset。最簡單的方法是在設計階段使用DataEnvironment物件互動地建立Command物件,如第八章所說;比較困難的技巧也是比較有彈性的技巧,則是在執行階段透過程式碼建立階層式Recordset。

MSDataShape提供者
 

要建立階層式Recordset的第一件事是要去選擇一個正確、適當的提供者。必須要有特別的提供者來設計Data Shaping。此提供者接著會連接到OLE DB提供者(實際連接到資料來源)。目前,只有MSDataShape提供者提供Data Shaping能力。不過理論上廠商在未來會提供其他此一型態的提供者。當在MSDataShape提供者環境中,可以使用ConnectionString中的Data Provider引數來指定實際的資料來源:

Dim cn As New ADODB.Connection
cn.Open "Provider=MSDataShape.1;Data Provider=Microsoft.Jet.OLEDB.4.0;" _
    & "Data Source= " & DBPATH

SHAPE APPEND命令
 

MSDataShape提供者支援兩種命令,一個是SHAPE APPEND關鍵字 ,另一個是SHAPE COMPUTE關鍵字。SHAPE APPEND關鍵字可在兩個傳回資料的SQL命令中建立關聯性。範例如下:

SHAPE {parent_command} [[AS] table-alias]
APPEND {child_command} [[AS] table-alias]
RELATE(parent_column TO child_column) [[AS] table-alias]

其中Parent_command SQL命令傳回主要Recordset而child_command SQL命令則傳回子Recordset。這兩個命令必須要有一個相同的欄位(雖然在各自的資料表中可能有不同的名稱),然後在RELATE子句中指定這些名稱。以下是一個簡單的SHAPE APPEND命令,其傳回階層式Recordset,包含所有的出版商與一個列出每一個出版商出版書名的子Recordset:

Dim cn As New ADODB.Connection, rs As New ADODB.Recordset
cn.Open "Provider=MSDataShape.1;Data Provider=Microsoft.Jet.OLEDB.4.0;" _
    & "Data Source=C:\Microsoft Visual Studio\vb98\biblio.mdb"
Set rs.ActiveConnection = cn
rs.Open "SHAPE {SELECT * FROM Publishers} " _
    & "APPEND ({SELECT * FROM Titles} " _
    & "RELATE PubID TO PubID) AS Titles"

在RELATE子句後的AS子句所使用的名稱變成子Recordset的名稱。要顯示階層式Recordset的內容,可以將它指定給FlexGrid控制項的DataSource屬性,範例如下:

Set MSHFlexGrid1.DataSource = rs

可以使用巢狀多重的SHAPE APPEND命令以便建立多重命令間的關聯性。例如,以下的程式碼是建立包含在Biblio.mdb所有的作者的三層的階層式Recordset。每一個記錄包含一個名為Title_Author的欄位,對應到包含每位作者所寫的書的子Recordset。然後,這個Recordset又有另一個子Recordset:其中每筆記錄皆為特定書目的資料。筆者盡可能縮排SHAPE APPEND命令以便讓他們的關係較為清楚:

SHAPE {SELECT * FROM Authors} AS [Authors With Titles]
APPEND 
    (( SHAPE {SELECT * FROM [Title Author]} 
    APPEND ({SELECT * FROM Titles} 
    RELATE ISBN TO ISBN) AS Titles1) 
RELATE Au_ID TO Au_ID) AS Title_Author

在第一個AS子句之後的名稱─Authors With Titles─是所建立的階層式命令的名稱,通常當傳遞字串給Recordset物件的Open方法或Command物件的CommandText屬性時可省略此名稱。在RELATE子句所列的欄位可以有不同的名字,只要他們為相同的資訊即可。若在關閉RELATE子句後沒有提供名稱,就會使用預設值chapter。

階層式Recordset可以有多個子Recordset。例如,底下的SHAPE APPEND命令和之前的命令相似,不過增加一個子Recordset包含在同年出生的作者。注意,APPEND關鍵字不可以重複且要將在同一層之後的子Recordset以逗號區隔開:

SHAPE {SELECT * FROM Authors} 
APPEND (( SHAPE {SELECT * FROM [Title Author]} 
    APPEND ({SELECT * FROM Titles} 
    RELATE ISBN TO ISBN) AS Titles1) AS Title_Author 
RELATE Au_ID TO Au_ID) AS Title_Author,
({SELECT * FROM Authors} 
RELATE [Year Born] TO [Year Born]) AS AuthorsBornSameYear

SHAPE的COMPUTE命令
 

SHAPE APPEND命令可從主Recordset中建立子Recordset,而SHAPE COMPUTE命令的功用則為:對Recordset的資料列進行算術運算,從而建立主Recordset。例如,可以以Title資料表的記錄為主做為一個Recordset,然後建立一個以出版年份為區隔的主Recordset。譬如,主Recordset有這兩個欄位:第一個是Year Published欄位,第二個是個包含該年所出版的書目的Recordset。SHAPE COMPUTE命令的語法如下:

SHAPE {child_command} [[AS] table_alias]
COMPUTE aggregate_command_field_list
[BY grp_field_list]

其中child_command是開始的Recordset,且一般來說是一個傳回一群記錄的SELECT子句。table_alias是主Recordset的欄位名稱,其包含了子Recordset;aggregate_command_field_list是要運算的欄位列表;grp_field_list是由子Recordset所群組化的欄位列表。

最簡單的情況下,根據在子Recordset某一欄位的值做分類。例如,可以用出版年這一欄位來分類,如下:

' You can enclose field and table names within
' single quotes or square brackets.
Rs.Open "SHAPE {SELECT * FROM Titles} AS Titles " _
    & "COMPUTE Titles BY 'Year Published'"

在COMMPUTE關鍵字之後的名稱必須要與指派給子Recordset的別名相符合。在BY子句後,可以使用逗號作為區隔以便依據多重欄位來分群:

' Group titles by publishers and year of publication.
rs.Open "SHAPE {SELECT * FROM Titles} AS Titles " _
    & "COMPUTE Titles BY PubID, 'Year Published'"

在表14-1列出COMPUTE命令有關的欄位和函數。一般來說會加入AS子句來表示在主Recordset中被運算後欄位的名稱:

' Group titles by publishers, and add a field named TitlesCount that
' holds the number of titles by each publisher.
rs.Open "SHAPE {SELECT * FROM Titles} AS Titles " _
    & "COMPUTE Titles, COUNT(Titles.Title) AS TitlesCount BY PubID"
函數語法 動作/傳回值
COUNT(alias[.fieldname]) The number of rows in the child Recordset
SUM(alias.fieldname) The sum of all values in the specified field
MIN(alias.fieldname) The minimum value in the specified field
MAX(alias.fieldname) The maximum value in the specified field
AVG(alias.fieldname) The average of all values in the specified field
STDEV(alias.fieldname) The standard deviation of all the values in the specified field
ANY(alias.fieldname) The value of a column (where the value of the column is the
  same for all rows in the child Recordset)
CALC(expression) The result of an expression that uses values from the current row only
NEW(fieldtype, [width | scale Recordset [, precision]]) Adds an empty column of the specified type to the Recordset [, precision])]
表14-1 SHAPE命令所支援的函數。Alias是出現在命令中子Recordset的名稱。

使用CALC函數來計算任意的運算式,其內容包含主Recordset目前資料行的欄位。例如,可依據出版商來對書目作分群,然後增加三個欄位,分別為出版商開始出版的年份、最近發行書本的年份與他們的差距。

rs.Open " SHAPE {SELECT * FROM Titles} AS Titles2 " _
    & "COMPUTE Titles2, MIN(Titles2.[Year Published]) AS YearMin, " _
    & "MAX(Titles2.[Year Published]) AS YearMax, " _
    & "CALC(YearMax - YearMin) AS YearDiff BY PubID"

使用階層式Recordset物件
 

階層式Recordset可以處理一般Recordset的相同方法做瀏覽。唯一不同之處在於處理包含子Recordset的Field物件的方式。要從這些Recordset中檢索資料,首先您必須要指派Filed的Value屬性給Recordset變數,如下所述:

Dim cn As New ADODB.Connection, rs As New ADODB.Recordset
Dim rsTitles As ADODB.Recordset
cn.Open "Provider=MSDataShape.1;Data Provider=Microsoft.Jet.OLEDB.4.0;" _
    & "Data Source=" & DBPATH
Set rs.ActiveConnection = cn
rs.Open "SHAPE {SELECT * FROM Titles} AS Titles " _
    & "COMPUTE Titles, COUNT(Titles.Title) AS TitlesCount BY PubID"
' Have the rsTitles variable always point to the child Recordset.
' (The StayInSync property's default value is True.)
Set rsTitles = rs("Titles").Value

' Browse the parent Recordset.
Do Until rs.EOF
    ' Show information in summary fields.
    Debug.Print "PubID=" & rs("PubID")
    Debug.Print "TitlesCount=" & rs("TitlesCount")
    ' For each row in the parent, browse the child recordset.
    Do Until rsTitles.EOF
        Debug.Print "  " & rsTitles("Title")
        rsTitles.MoveNext
    Loop
    rs.MoveNext
Loop

若主Recordse更新了,也可以使用標準的ADO命令來更新子Recordset的值。可以將包含子Recordset的Field與一般的Field分開,因為其Type屬性會傳回值136- adChapter。

主要Recordset的StayInSync屬性會影響物件變數如何指到子Recordset,與當主要Recordset的記錄指標移到另一個記錄時如何更新。這一屬性的預設值是True,表示只要指派Field物件給Recordset變數(之前的例子中是rsTitle),即使當主Recordset移到另一列時,此變數也指向子記錄。這一設定簡化階層式Recordset的導覽,也使執行速度變快,因為不用在每一個Movexxx命令後重新執行Set命令。在某些的狀況下,可能得將StayInSync設為False,其從主Recordset中分離物件變數。

當建立一個複雜的SHAPE命令時,和在設計階段使用DataEnvironment的Command物件不同的是,在程式碼中建立階層式的Recordset有極大彈性的優點。如下所述,可以在巢狀SELECT子句中加上WHERE條件:

Dim cn As New ADODB.Connection, rs As New ADODB.Recordset, 
Dim cmd As New ADODB.Command, source As String
cn.Open "Provider=MSDataShape.1;Data Provider=Microsoft.Jet.OLEDB.4.0;" _
    & "Data Source=C:\Microsoft Visual Studio\Vb98\biblio.mdb"
source = "SHAPE {SELECT * FROM Titles WHERE [Year Published] = 1990} " _
    & "AS Titles COMPUTE Titles BY PubID"
Set cmd.ActiveConnection = cn
cmd.CommandText = source
Set rs = cmd.Execute()

由於無法管理Command物件對於在SHAPE命令內的?參數如何運作,所以當使用MSDataShape提供者時,必須要放棄參數查詢。一開始聽起來好像不算壞,因為階層式Recordset本來就是用戶端,而非在伺服端編譯。可以使用空格建立虛擬的SHAPE命令和之前所提的ReplaceParams函式:

Source = "SHAPE {SELECT * FROM Titles WHERE [Year Published] = @1} " _
    & "AS Titles COMPUTE Titles BY PubID"
cmd.CommandText = ReplaceParams(source, "1990")
Set rs = cmd.Execute()

在執行階段,可以決定WHERE條件的表示式與chapter欄位的名稱,這在設計階段中使用DataEnvironment物件則是不可能的。

帶有參數的命令
 

當使用階層式Recordset時,ADO會從主資料表和子資料表中下載所有的資料,然後在用戶端建立關連性。不用說,使用比較大的資料表─實際應用程式所做的─會增加網路和用戶端資源的負擔。您可以藉由使用Source屬性的特殊參數化語法或階層式Recordset的參數來降低負荷。

Dim cn As New ADODB.Connection, rs As New ADODB.Recordset
cn.Open "Provider=MSDataShape.1;Data Provider=sqloledb.1;" _
    & "Data Source=p2;user id=sa;initial catalog=pubs"
Set rs.ActiveConnection = cn
rs.Open "SHAPE {SELECT * FROM Publishers} " _
    & "APPEND ({SELECT * FROM Titles WHERE pub_id = ?} " _
    & "RELATE pub_id TO PARAMETER 0) AS Titles"

當使用這種語法時,ADO就不會下載完全的Title資料表。相對地,只會下載Publisher資料表(當然,除非在第一個SELECT子句中增加WHERE條件)。然後會依據Pub_ID鍵值在Tilte資料表擷取符合的資料。當在Publisher資料表中移動時,ADO會根據另一個SELECT子句抓取資料,所以同時只有部份資料會下載。

這是一個完全有效率的技巧,因為ADO自動地在伺服端建立一個暫存的預存程序來檢索子資料表。不過,完全執行時間是大於標準技巧的時間,這是因為有多個查詢的緣故,所以當您要指定最終的Recordset給階層式FlexGrid控制項時是沒有用處的。帶有參數的Command可能較方便,因為ADO充分利用連接的子Recordset,僅在應用程式實際地引用到其中一個屬性或事件時才檢索它的資料,,如以下範例:

' Continuing the previous code example...
' Print the number of titles for US publishers. (This is just an example:
' in a real program you should add a WHERE clause to the Open method.)
Dim rs2 As ADODB.Recordset
Set rs2 = rs("titles").Value      ' Make the assignment just once.
Do Until rs.EOF
    If rs("country") = "USA" Then
        ' The next statement actually retrieves the records from Titles.
        Print rs("pub_name"), rs2.RecordCount 
    End If
rs.MoveNext
Loop

只有當在程式中被用來擷取的欄位稱為延時欄位(Deferred Field)。即使要處理在子資料表中的所有記錄,且客戶端機器的系統記憶體較少時,使用帶有參數的Command是有幫助的。

DataEnvironment命令物件
 

若在設計階段在DataEnvironment設計師中定義階層式的Command物件,在程式碼中還是直接使用:只要使用Command集合來擷取之,並指派它給一般的ADODB.Command物件變數,範例如下:

Dim cmd As ADODB.Command, rs As ADODB.Recordset
Set cmd = DataEnvironment1.Commands("Authors")
Set rs = cmd.Execute
Set MSHFlexGrid1.DataSource = rs

或者可以執行查詢,然後使用DataEnvironment物件一般的方法檢索最後結果的Recordset:

DataEnvironment1.Authors
Set MSHFlexGrid1.DataSource = DataEnvironment1.rsAuthors

即使您不打算在程式碼中使用DataEnvironment物件,DataEnvironment設計師對建立SHAPE命令還是有用的。事實上,您可以互動地設計一個階層式的Command物件,然後點選它並按右鍵,選擇層次資料選項,如圖14-6所示:


 

圖14-6 DataEnvironment設計師建立複雜的SHAPE命令。

本章和13章深入討論ADO如何運作,以及其少數的限制和轉變等。然而,到目前為止的程式碼都著重在資料的擷取方面。在下一章節中,要討論如何使用資料感知控制項及DataReport設計師來顯示查詢後的資料。