Der große ADO-Kurs - Seite 8
von Florian Reischl
Sonstige Methoden
Es sind nun eigentlich alle Objekte von ADO beschrieben. Es bleiben jedoch noch zwei komplett neue Methoden offen welche in den vorherigen Beschreibungen ausgelassen wurden. Gründe hierfür sind die teilweise Komplexität der Themen und die Tatsache, dass hierfür alle ADO-Objekte bekannt sein sollten.
7.1 UDL-Dateien
UDL-Dateien („Universal Data Links“) sind eine Andere Möglichkeit eine Connection zu erstellen und können den ConnectionString ersetzen. Ihr großer Vorteil liegt darin, daß bei Änderungen in der Serverlanschaft, der DB oder sonstiger grundlegender Eigenschaften der Datenbasis nicht ein komplett neues Kompilat (EXE) der Anwendung erstellt und herausgegeben werden muß sondern nur die extern liegende Datei ersetzt oder geändert werden muß was leicht durch eine kleine Batch-Datei gemacht werden kann.
Erstellen Sie hierfür im Explorer einfach eine neue Datei mit der Endung *.udl und öfnen diese mit einem Doppelklick. Dann können alle Eigenschaften für die Verbindung eingestellt werden und auch getestet werden. Geben Sie jedoch auf keinen Fall (!!) das Passwort mit ein und lassen dieses speichern, eine UDL ist zwar keine Textdatei kann jedoch ohne weiteres mit jedem Editor geöffnet werden! Das ist auch der eigentlich eigentliche Nachteil der Dateien, da der User diese Dateien leicht ändern kann, aber seien wir mal ehrlich, welcher User schaut sich alle Dateien an die bei einem Programm im Programmordner sind? Und außerdem, na wenn schon, er kann mit den Daten eigentlich nicht viel damit anfangen. (Bei Access sicherstellen, daß durch Änderungen des Zugriffsmodus keine zu großen Zugriffsberechtigungen entstehen können, hierfür die Userberechtigungen der System-MDW anpassen).
Wenn Sie sich eine UDL-Datei selbst mal mit einem Editor anschauen werden Sie sehen, daß die Daten darin eigentlich genau dem ConnectionString entsprechen. So können Änderungen auch sehr leicht über eine Batch-Datei durchgeführt werden.
Aus einem mir nicht verständlichen Grund scheint der Einsatz von UDL-Dateien leider nicht mit der Prompt-Eigenschaft der Connection vereinbar zu sein, also entweder nutzt man den Luxus des Microsoft-Anmeldefensters oder den einer UDL-Datei. Außer man verlässt sich auf die NT-Sicherheit. Einzige Möglichkeit dieses Manko zu umgehen ist eine kleine Fehlerroutine.
Hier ein kleines Beispiel wie diese Datei an eine Connection übergeben wird wenn NT-Authentifikation eingestellt ist:
Private Sub Form_Load() CnT.Open "FILE NAME=" & App.Path & "\DBLnk.udl" End Sub Ein Beispiel wie man das Prompt trotz UDL nutzen kann: Private Sub Command1_Click() Set Cn = New ADODB.Connection On Error Goto ErrCn Cn.Open "FILE NAME=E:\Temp\aaa.udl" Exit Sub ErrCn: ' Logindaten nachtragen If Err.Number = -2147217843 Then ' Loginfehler Cn.Properties("Prompt") = adPromptCompleteRequired Cn.Open Resume Next Else MsgBox Err.Description End If End Sub
7.1.1 DataLinks
Dieses Objekt halte ich persönlich für Unsinn, aber der Vollständigkeit wegen soll es erwähnt werden. Es kommt aus einer anderen Bibliothek und einen neuen Verweis im Projekt auf die MSDAC („Microsoft OLE DB Service Component 1.0 Type Library“) setzen. Diese Library stellt eigentlich nichts anderes als eine Möglichkeit für das Erstellen einer Verbindung über den UDL-Dialog im Programm zur Verfügung. Auf Grund der Menge der Möglichkeiten welche eingestellt werden können sehe ich als einzige Möglichkeit der Verwendung einen SQL-Editor oder ähnliches. Wenn sie jedoch mehr Sinn in diesen Objekte sehen, bitte.
Wir sehen uns hier nur ein Objekt dieser Bibliothek an, das DataLinks-Objekt. Es gibt zwei Möglichkeiten den Dialog zu öffnen, entweder mit PromptEdit oder PromptNew, wobei sich diese im sinnvollen Einsatz wiederum eigentlich nur durch die Schreibweise unterscheiden. Bei PromptEdit wird eine bestehende Connection mit eventuellen vorgegebenen Einstellungen (z.B. über GetSettings) übergeben wird.
Beispiel für PromptEdit:
Private Sub Form_Load() Dim dlk As New MSDASC.DataLinks CnT.Provider = "SQLOLEDB.1" CnT.Properties("Data Source") = "FLO_MAIN" CnT.Properties("Initial Catalog") = "db_Test" dlk.PromptEdit CnT CnT.Open End Sub
Beispiel für PromptNew:
Private Sub Form_Load() Dim dlk As New MSDASC.DataLinks Set CnT = dlk.PromptNew CnT.Open End Sub
7.2 Events
Seit ADO haben die Connection und das Recordset zusätzlich zu ihren Eigenschaften, Methoden und Funktionen auch Events. Diese Neuheit bietet die Möglichkeit Sicherheitsmechanismen, Fehlerbehandlungen und andere Akitonen zentral zu verwalten, sowie viele andere Möglichkeiten die bei DAO einfach nicht möglich waren. Auch im Bezug auf Arbeiten mit verknüpften Daten und vielem mehr sind diese Events sehr stark einzusetzen und sparen viel Code.
Zu beachten ist hierbei, daß die Objekte mit WithEvents im allgemeinen Bereich deklariert werden müssen und nicht von vorn herein mit New klassifiziert werden können sondern erst dynamisch (z.B. im Form_Load) mit Set neu instanziiert werden können:
Dim WithEvents Rs As ADODB.Recordset ' ... Set Rs = New ADODB.Recordset ' ...
Die große Stärke der Events wird jedoch erst im Umgang mit asynchronen Abläufen ersichtlich. Hierzu folgen noch Beschreibungen weiter unten im Text.
7.2.1 Connection
Bei der Connection können Events beispielsweise genutzt werden um Fehlerroutinen global zu behandeln oder Stati von der Verbindung zur Datenbasis anzugeben. Man kann hier allgemeine Meldungen an den User zurückliefern oder auch aus Sicherheitsgründen Datenänderungen wieder zurücksetzen.
Ein kleines Beispiel für eine zentrale Behandlung um eventuelle Fehler in der DB zu verhindern:
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 = adStatusErrorsOccurred Then MsgBox "Es ist ein Fehler aufgetreten." Cn.RollbackTrans Else Cn.CommitTrans End If End Sub 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) Cn.BeginTrans End Sub Private Sub Form_Load() Dim i As Integer Dim Cmd As ADODB.Command Set Cn = New ADODB.Connection With Cn .CursorLocation = adUseClient .Provider = "SQLOLEDB.1" .Properties("Data Source") = "FLO_MAIN" .Properties("Integrated Security") = "SSPI" .Properties("Initial Catalog") = "db_Test" .Open End With Set Cmd = New ADODB.Command With Cmd .ActiveConnection = Cn .CommandType = adCmdStoredProc .CommandText = "sp_Test" .Parameters.Refresh .Parameters("@ReportId") = 6 .Execute End With End Sub
Beim Ausführen einer StoredProcedure können auf dem Server durchaus erst Fehler auftreten wenn die ersten Änderungen bereits durchgeführt wurden. Über das Abfragen und eventuelle zurücksetzen der Änderungen kann man hier Fehleinträge auf der Datenbank vermeiden.
7.2.2 Recordset
Bei Recordsets können durch die verschiedenen Events gestimmte Aktionen in der Benutzeroberfläche ausgeführt werden und so Fehler vermieden werden. So kann beispielsweise beim Klicken einer Schaltfläche „Neuer Datensatz“ und das Ausführen eines Rs.AddNew im MoveComplete die Schaltfläche aufgrund des EditModes disabled werden bis der Status nicht mehr adEditAdd ist. Dies verhindert Fehler durch nochmaliges klicken der Schaltfläche.
Oft bestehen die Daten einer Benutzeroberfläche nicht nur aus einem Recordset sondern zusätzlichen Detaildaten die beispielsweise in einem Grid angezeigt werden. Ein häufiger Einsatz ist hier ein Bestellformular wobei die Hauptdaten aus dem Lieferanten, Bestelldatum, Sachbearbeiter, usw. bestehen und die Detaildaten aus den verschiedenen Artikeln, Bestellmenge, Preisen,...
Hier ein Beispiel für den Einsatz mit Haupt- und Detaildaten. Die Hauptdaten sind durch den Recordset RsMain dargestellt, die Detaildaten durch RsDetail. Die Verbindung der Daten durch das in beiden Recordsets vorhandene Feld „A_Id“ (Artikelnummer) gesichert. Jetzt wird beim Neuanlegen von Datensätzen die ID an die Detaildaten automatisch übergeben:
Option Explicit Dim Cn As ADODB.Connection Dim RsMain As ADODB.Recordset Dim WithEvents RsDetail As ADODB.Recordset Dim bChanged As Boolean Private Sub Form_Load() Set Cn = New ADODB.Connection With Cn .CursorLocation = adUseClient .Provider = "SQLOLEDB.1" .Properties("Data Source") = "FLO_MAIN" .Properties("Integrated Security") = "SSPI" .Properties("Initial Catalog") = "db_Test" .Open Set RsMain = .Execute("SELECT * FROM tbl_Artikel") End With Set RsDetail = New ADODB.Recordset With RsDetail .ActiveConnection = Cn .CursorLocation = adUseClient .CursorType = adOpenKeyset .LockType = adLockOptimistic .Source = "SELECT * FROM tbl_Bestellungen" .Open End With Set Me.DataGrid1.DataSource = RsDetail End Sub Private Sub RsDetail_FieldChangeComplete( _ ByVal cFields As Long, _ ByVal Fields As Variant, _ ByVal pError As ADODB.Error, _ adStatus As ADODB.EventStatusEnum, _ ByVal pRecordset As ADODB.Recordset) bChanged = False End Sub Private Sub RsDetail_WillChangeField( _ ByVal cFields As Long, _ ByVal Fields As Variant, _ adStatus As ADODB.EventStatusEnum, _ ByVal pRecordset As ADODB.Recordset) With RsDetail If .EditMode = adEditAdd Then If Not bChanged Then bChanged = True .Fields("A_Id") = RsMain.Fields("A_Id") End If End If End With End Sub
7.3 Hierarchische Recordsets
Hierarchische Recordsets sind eine Neuerung von ADO die es vorher einfach nicht gab. Hierbei handelt es sich um Recordsets welche in einer Baumstruktur an den Client zurückgeliefert werden. Früher konnte man von einander Abhängige Recordsets nur über zwei oder mehr Recordsets realisieren.
Man hat beispielsweise ein Recordset RsArtikelart und ein Recordset RsArtikelDetail. Beide werden in jeweils einem DataGrid (für DAO-ler: Nachfolger des DBGrid) angezeigt. Nun kann beispielsweise im Event RowColChange des DataGrids oder im Event MoveComplette des RsArtikelart-Recordsets die Daten für das Recordset RsArtikelDetail neu mit der Id der Artikelart abgefragt werden, so erhält man immer die Detaildaten zu einem Hauptdatensatz.
Eine andere Möglichkeit wäre über ein normales JOIN die Daten mit einander zu verknüpfen, jedoch ist hier eine schöne Navigation durch die Hauptwerte nur sehr unschön möglich.
Die hier verwendeten SQL-Statements werden nicht in VB-Code eingebunden gezeigt. Da es bei Statements für hierarchische Recordsets meist um größere Strings handelt überlasse ich diese Arbeit gerne jedem selbst. Ein kleiner Tipp, für Tests ist ein Formular mit einer TextBox für den SQL-String, einem „Microsoft Hirarchical FlexGrid“ und einem Button um das Recordset zu öffnen sehr hilfreich.
7.3.1 MSDataShape
Der Aufbau eines Hierarchischen Recordsets erfordert eine gewisse Vorarbeit. Die normalen DB-Treiber von Access und SQL-Server (ODBC und auch OLEDB) schaffen diese Funktionalität nicht. Dafür gibt es von Microsoft den ADO-Treiber „MSDataShape“, welcher diese Funktionalität unterstützt, er sollte im Lieferumfang der MDAC_TYP vorhanden sein. Als erstes muß die Connection geändert werden. Der neue Treiber muß gesetzt werden und der eigentliche Datenbanktreiber nimmt die untergeordnete Rolle des „Data Provider“ ein.
Für Access:
With CnT .CursorLocation = adUseClient .Provider = "MSDataShape" .Properties("Data Provider") = "Microsoft.Jet.OLEDB.4.0" .Properties("Data Source") = "E:\Temp\db3.mdb" .Open End With
Für SQL-Server:
With CnT .CursorLocation = adUseClient .Provider = "MSDataShape" .Properties("Data Provider") = "SQLOLEDB.1" .Properties("Data Source") = "FLO_MAIN" .Properties("Initial Catalog") = "db_Test" .Properties("User Id") = "florianr" .Properties("Password") = "flo" .Open End With
Providerspezifische Eigenschaften wie z.B. „Jet OLEDB:Database Password“ werden in Form eines kleinen ConnectionStrings in die Eigenschaft „Extendet Properties“ übergeben.
7.3.2 Microsoft Hierarchical FlexGrid
Wer bereits mit ADO und den dazugehörigen Steuerelementen zu tun hatte ist sicher schon auf das „Microsoft Hierarchical FlexGrid“ (MSHFlexGrid) gestoßen. Abgesehen vom unaussprechbarem Namen ist das MSHFlexGrid unter anderem der Nachfolger des altbekannten FlexGrids. Das MSHFlexGrid ist aber auch das einzige Control das hierarchische Datensätze anzeigen kann. Wenn hier ein hierarchisches Recordset über die gewohnte Methode mit
Set MSHFlexGrid.DataSource = Rs
angebunden wir erscheinen am Anfang der Zeilen kleine „+“ oder „-„ Zeichen welche die Detaildaten ein- oder ausblenden können.
7.3.3 Grundstruktur
Der Aufbau eines hierarchischen Recordsets geschieht über ein SQL-Statement welches einige neue Begriffe und eine neue Syntax mit sich bringt. Den Anfang bildet ein SHAPE welches die obere Hierarchie des Recordsets angibt und einen Namen erhält (hier ArtikelHierarch). Dann wird über die APPEND-Methode eine Unterebene für die erste Hierarchieebene gebildet welches mit RELATE über den Namen der zu verbindenden Spalten angebunden wird.
SHAPE {Select * FROM tbl_Artikel ORDER BY A_Name} As ArtikelHierarch Append ({Select * FROM tbl_Bestellungen} As BestellSub RELATE 'A_ID' TO 'A_ID') AS BestellSub
Wenn man nun eine Tabelle tbl_Artikel hat mit den Daten über alle Artikel und eine Tabelle tbl_Bestellungen mit allen Bestellungen die für die Artikel und die Artikel-ID A_ID kann man nun alle Artikel und die jeweils dazu bestehenden Bestellungen anzeigen:
Zu beachten ist bei dieser Syntax die vielen neuen Schreibweisen. Die einzelnen Recordsets werden hier mit geschweiften Klammern „{…}“ eingeschlossen und erhalten eine Bezeichnung über die bekannte AS-Klausel. Die APPEND-Methode schließt das RELATE ein wird jedoch mit normalen Klammern „(…)“ begrenzt. Die Verbindung der Spalten des Recordsets der ersten Ebene und der zweiten Ebene werden nicht wie normalerweise gewohnt mit „=“ verbunden sondern mit dem Schlüsselwort „TO“ verbunden und die Namen der Spalten werden als Strings über Hochkommas geschrieben.
7.3.4 Hierarchien in einer einzelnen Tabelle
Jeder der schon öfter mit SQL gearbeitet hat kennt das leidige Problem wenn man versucht in einer Tabelle eine Aggregatfunktion (COUNT, MAX, MIN, …) auszuführen jedoch eigentlich auch die Detaildaten mit anzeigen möchte. Hier entstanden bislang riesige JOINs oder IN-Statements. Auch dies lässt sich über hierarchische Recordsets sehr schön lösen in dem man noch zwei neue Begriffe, nämlich CUMPUTE und BY, in die Syntax mit aufnimmt.
Zum Beispiel möchte man die Daten der Bestelltabelle sehen und dazu die Menge aller verkauften Artikel gruppiert nach Artikelnummern:
SHAPE {Select * FROM tbl_Bestellungen} As Grund COMPUTE Grund, SUM(Grund.Menge) As Gesamtmenge BY Artikelnummer
Das Ergebnis kann dann so aussehen:
7.3.5 Über mehrere Tabellen mit Aggregatfunktionen
Jetzt wird es noch mal etwas komplexer. Bei Relationalen Datenbanken wird in einer Detailtabelle schließlich nur die ID eines Hauptwertes gespeichert (siehe oben die Artikelnummer) und mit dieser können User nur selten wirklich etwas anfangen. Somit müssen wir erst die Daten über ein JOIN miteinander verbinden und darauf dann unser COMPUTE setzen.
Wir lassen uns nun die Daten aus der Artikeltabelle mit denen aus der Bestelltabelle zusammen anzeigen und zusätzlich über die COUNT-Funktion die Summe aller Bestellungen für die einzelnen Artikel ermitteln:
SHAPE { Select A_Name As Artikel, A_Preis As Preis, B_Id As Bestellnummer, B_Menge As Menge FROM tbl_Artikel INNER JOIN tbl_Bestellungen On tbl_Artikel.A_Id = tbl_Bestellungen.A_Id } As AtikelCompute COMPUTE AtikelCompute, COUNT(AtikelCompute.'Artikel') AS Anzahl BY 'Artikel'
Das Ergebnis könnte dann so aussehen:
7.3.6 Arbeiten mit hierarchischen Recordsets
Nun haben wir gesehen wie man hierarchische Recordsets aufbaut, noch nicht gesehen wie man diese denn im Code behandelt. Zu Grunde legen wir den nachfolgenden Beispielen das SQL-Statement aus dem Grundbeispiel:
SHAPE {Select * FROM tbl_Artikel ORDER BY A_Name} As ArtikelHierarch Append ({Select * FROM tbl_Bestellungen} As BestellSub RELATE 'A_ID' TO 'A_ID') AS BestellSub
Wenn man bei einem solchen Recordset die Felder anzeigen lässt wird man seltsamer Weise nur die Felder das Hauptrecordsets sehen und den Namen des Unterrecordsets („BestellSub“), darum wird nun dem DataTypeEnum aus den Feld-Typen des Recordsets ein neuer Wert hinzugefügt. Der adChapter, er liegt vor wenn wir einen Unterdatensatz in einem SHAPE haben. Hierbei handelt es sich eigentlich um ein eigenständiges Recordset. Dieses kann an ein neues Recordset-Objekt übergeben werden und genau wie das erste auch gehandelt werden.
Als Beispiel lassen wir die Detaildaten der Bestellungen für den ersten Artikel in einem DataGrid anzeigen:
Option Explicit Dim CnT As ADODB.Connection Dim RsT As ADODB.Recordset Dim RsDetail As ADODB.Recordset Private Sub Form_Load() ' SQL-Statement für hierarchisches Recordset Const sSQL As String = _ "SHAPE {SELECT * FROM tbl_Artikel ORDER BY A_Name} " & _ "AS ArtikelHierarch " & _ "APPEND ({SELECT * FROM tbl_Bestellungen} " & _ "AS BestellSub " & _ "RELATE 'A_ID' TO 'A_ID') AS BestellSub" Dim i As Integer Set CnT = New ADODB.Connection With CnT .CursorLocation = adUseClient .Provider = "MSDataShape" .Properties("Data Provider") = "SQLOLEDB.1" .Properties("Data Source") = "FLO_MAIN" .Properties("Initial Catalog") = "db_Test" .Properties("User Id") = "florianr" .Properties("Password") = "flo" .Open End With Set RsT = New ADODB.Recordset With RsT .ActiveConnection = CnT .CursorLocation = adUseClient .CursorType = adOpenKeyset .LockType = adLockOptimistic .Source = sSQL .Open End With ' Hierarchisches Recordset an MSHFlexGrid Set Me.MSHFlexGrid1.DataSource = RsT For i = 0 To RsT.Fields.Count - 1 If RsT.Fields(i).Type = adChapter Then Set RsDetail = RsT.Fields(i).Value Exit For End If Next i If Not RsDetail Is Nothing Then Set Me.DataGrid1.DataSource = RsDetail End If End Sub
Nun ist das Recordset RsDetail fest (!) an die Detaildaten angebunden. Wenn man nun durch das Hauptrecordset navigiert werden automatisch die Daten des Unterrecordsets geupdated und an das Detaildatenrecordset übergeben.
7.4 Asynchrone Aktionen
Was hat asynchron wieder mit ADO zu tun? Das bedeutet hier, daß viele Aktionen auf einer Datenbank ausgeführt werden müssen laufen können ohne, daß die Anwendung im Vordergrund darauf warten muß. Hierbei wird eine Aktion an den Server weitergegeben und das Programm läuft sofort weiter. Das kann sowohl dafür nützlich sein um die Anwendung bereits für weitere Arbeiten welche nicht direkt mit Arbeiten auf dem Server zu tun haben frei zu geben als auch um beispielsweise über eine Statusbar eine Prozessbar oder sonstiges die Arbeit dem Anwender zu verdeutlichen.
Bei dieser Methode muß jedoch einiges beachtet werden. Um eine asynchrone Aktion abzusetzen muß beim Ausführen der Aktion der jeweilige Parameter für die asynchrone Aktion als Option übergeben werden.
7.4.1 Asynchroner Verbindungsaufbau
Bei vielen Datenbankprogrammen ist das leidige Thema bekannt, daß man sich anmeldet und dann erst einige Zeit warten muß bis man auf der Startmaske des Programms landet auf welcher meist noch gar keine Daten angezeigt werden. Mit ADO ist es möglich die Verbindung herzustellen und das Programm bereits weiterlaufen zu lassen. So können z.B. alle anderen rechen- und zeitintensiven Arbeiten des Programms, die ebenfalls am Anfang ausgeführt werden müssen bereits ausgeführt werden. Hierbei muß jedoch beachtet werden, daß Daten erst abgefragt werden können wenn die Verbindung komplett hergestellt worden ist. Die Option die hier übergeben werden muß ist adAsyncConnect.
Hier ein kleines Beispiel das zeigt, daß die Connection begonnen wird, dann der Code weiterläuft und erst wenn die Verbindung steht dies signalisiert wird. Zu diesem Zweck wird einfach vor dem Anfang des Verbindungsaufbaus, nach dem Starten und beim Vollenden des Verbindungsaufbaus eine kleine Meldung auf das Form geschrieben
:Option Explicit Dim WithEvents CnT As ADODB.Connection Private Sub CnT_ConnectComplete( _ ByVal pError As ADODB.Error, _ adStatus As ADODB.EventStatusEnum, _ ByVal pConnection As ADODB.Connection) If adStatus = adStatusOK Then Me.Print "Connection steht" End If End Sub Private Sub Form_Load() Me.AutoRedraw = True Me.Print "Anfang connecten" Set CnT = New ADODB.Connection With CnT .CursorLocation = adUseClient .Provider = "MSDataShape" .Properties("Data Provider") = "SQLOLEDB.1" .Properties("Data Source") = "FLO_MAIN" ' NT-Authentifikation .Properties("Initial Catalog") = "db_Test" .Properties("Integrated Security") = "SSPI" .Open Options:=adAsyncConnect End With Me.Print "Am connecten..." End Sub
Nun ist das Recordset RsDetail fest (!) an die Detaildaten angebunden. Wenn man nun durch das Hauptrecordset navigiert werden automatisch die Daten des Unterrecordsets geupdated und an das Detaildatenrecordset übergeben.
7.4.2 Asynchrone Aktionen
Was hat asynchron wieder mit ADO zu tun? Das bedeutet hier, daß viele Aktionen auf einer Datenbank ausgeführt werden müssen laufen können ohne, daß die Anwendung im Vordergrund darauf warten muß. Hierbei wird eine Aktion an den Server weitergegeben und das Programm läuft sofort weiter. Das kann sowohl dafür nützlich sein um die Anwendung bereits für weitere Arbeiten welche nicht direkt mit Arbeiten auf dem Server zu tun haben frei zu geben als auch um beispielsweise über eine Statusbar eine Prozessbar oder sonstiges die Arbeit dem Anwender zu verdeutlichen.
Bei dieser Methode muß jedoch einiges beachtet werden. Um eine asynchrone Aktion abzusetzen muß beim Ausführen der Aktion der jeweilige Parameter für die asynchrone Aktion als Option übergeben werden.
Asynchroner Verbindungsaufbau
Bei vielen Datenbankprogrammen ist das leidige Thema bekannt, daß man sich anmeldet und dann erst einige Zeit warten muß bis man auf der Startmaske des Programms landet auf welcher meist noch gar keine Daten angezeigt werden. Mit ADO ist es möglich die Verbindung herzustellen und das Programm bereits weiterlaufen zu lassen. So können z.B. alle anderen rechen- und zeitintensiven Arbeiten des Programms, die ebenfalls am Anfang ausgeführt werden müssen bereits ausgeführt werden. Hierbei muß jedoch beachtet werden, daß Daten erst abgefragt werden können wenn die Verbindung komplett hergestellt worden ist. Die Option die hier übergeben werden muß ist adAsyncConnect.
Hier ein kleines Beispiel das zeigt, daß die Connection begonnen wird, dann der Code weiterläuft und erst wenn die Verbindung steht dies signalisiert wird. Zu diesem Zweck wird einfach vor dem Anfang des Verbindungsaufbaus, nach dem Starten und beim Vollenden des Verbindungsaufbaus eine kleine Meldung auf das Form geschrieben:
Option Explicit Dim CnT As ADODB.Connection Dim WithEvents RsT As ADODB.Recordset Private Sub Form_Load() Me.AutoRedraw = True Me.Print "Anfang connecten" Set CnT = New ADODB.Connection With CnT .CursorLocation = adUseClient .Provider = "MSDataShape" .Properties("Data Provider") = "SQLOLEDB.1" .Properties("Data Source") = "FLO_MAIN" .Properties("Initial Catalog") = "db_Test" .Properties("Integrated Security") = "SSPI" .Open End With Set RsT = New ADODB.Recordset With RsT .ActiveConnection = CnT .CursorLocation = adUseClient .CursorType = adOpenKeyset .LockType = adLockOptimistic .Source = "SELECT * FROM tblMany" .Open Options:=adAsyncFetch End With RsT.MoveNext Me.Print "Am Datan auslesen..." Set Me.DataGrid1.DataSource = RsT End Sub Private Sub RsT_FetchComplete( _ ByVal pError As ADODB.Error, _ adStatus As ADODB.EventStatusEnum, _ ByVal pRecordset As ADODB.Recordset) Me.Print "Recordset geladen" End Sub
Auch hier wird die Aktion das die Daten vollständig sind durch eine Methode des Recordsets angezeigt und die verschiedenen Stati der Einfachheit halber auf die Form geschrieben.
7.4.3 Asyncrone Commands
Gerade hier kann man mit asynchronen Arbeiten eine Anwendung wesentlich optimieren. StoredProcedures können manchmal einige Zeit in Anspruch nehmen und das kann dann wie beim Recordset verbessert werden. Hierfür ist die zu übergebende Option adAsyncExecute. Dieser Parameter kann auch für eine Connection verwendet werden um ein Execute zu optimieren.
Wenn von der SQL-Prozedur keine Daten zurückgeliefert werden (z.B. bei Löschungen) kann dies ebenfalls als Option mit adExecuteNoRecords angegeben werden welche die Anwendung noch mal optimiert, so kann eine StoredProcedure ohne weiteres 30 Sekunden auf dem Server dauern ohne das dies die Anwendung stört.
Hier ein Beispiel für optimiertes Ausführen einer StoredProcedure ohne Datenrückgabe:
Option Explicit Dim CnT As ADODB.Connection Private Sub Form_Load() Dim Cmd As ADODB.Command Set CnT = New ADODB.Connection With CnT .CursorLocation = adUseClient .Provider = "MSDataShape" .Properties("Data Provider") = "SQLOLEDB.1" .Properties("Data Source") = "FLO_MAIN" .Properties("Initial Catalog") = "db_Test" .Properties("Integrated Security") = "SSPI" .Open End With Set Cmd = New ADODB.Command With Cmd .ActiveConnection = CnT .CommandType = adCmdStoredProc .CommandText = "sp_Delete_Bestell" .Parameters.Refresh .Parameters("@BestellId") = 123 .Execute Options:=adAsyncExecute + adExecuteNoRecords End With End Sub
7.4.4 Sicherheit bei asynchronen Aktionen
Eines muß bei asynchronen Aktionen beachtet werden, eine Connection kann zwar mehrere Commands und Recordsets gleichzeitig verwalten, jedoch nicht mehrere Aktionen gleichzeitig ausführen. Hierzu können Sie sich aber sehr leicht über eine kleine Funktion helfen welche den Status der Connection abfragt und solange wartet. Dies lässt sich am besten mit der Sleep-API oder über ein DoEvents (hier CheckForExec) lösen:
Option Explicit Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long) Private Function CheckForExec( _ Conn As ADODB.Connection, _ Optional TimeOut As Long = 15 _ ) As Boolean Dim dTemp As Date If Conn.State = adStateClosed Then ' Wenn Verbindung geschlossen, Conn.Open , , , adOpenAsync ' dann öffnen End If ' Wenn Status nicht OK, dann Schleife beginnen If Conn.State = adStateExecuting Or Conn.State = adStateFetching Then dTemp = Now Do Sleep 100 ' 100 Millisekunden warten ' Nach vorgegebener Max-Zeit beenden If DateDiff("s", dTemp, Now) >= TimeOut Then MsgBox "Die Verbinung zum Server ist gerade ausgelastet. " & _ "Bitte warten Sie etwas und führen Sie dann nochmal aus." CheckForExec = False Exit Function End If Loop Until Conn.State = adStateOpen End If CheckForExec = True End Function Private Sub Command1_Click() Set RsT = New ADODB.Recordset With RsT .ActiveConnection = CnT .LockType = adLockOptimistic .CursorLocation = adUseClient .CursorType = adOpenKeyset .Source = "SELECT * FROM Grunddaten" If Not CheckForExec(CnT, 15) Then Exit Sub End If .Open End With End Sub