Die Community zu .NET und Classic VB.
Menü

Der große ADO-Kurs - Seite 8

 von 

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

Listing 27

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

Listing 28

Beispiel für PromptNew:

Private Sub Form_Load()
Dim dlk As New MSDASC.DataLinks

Set CnT = dlk.PromptNew

CnT.Open

End Sub

Listing 29

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

Listing 30

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

Listing 31

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

Listing 32

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

Listing 33

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:


Abbildung 1: Bild 1

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:


Abbildung 2: Bild 2

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:


Abbildung 3: Bild 3

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

Listing 34

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

Listing 35

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

Listing 36

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

Listing 37

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

Listing 38