|  | Unser Angebot für Sie! Lesen Sie diesen Beitrag und 500 andere sofort im Onlinearchiv, und erhalten Sie alle zwei Monate brandheißes Access-Know-how auf 72 gedruckten Seiten! Plus attraktive Präsente, zum Beispiel das bald erscheinende Buch 'Access 2010 - Das Grundlagenbuch für Entwickler'! |
| |
Datenzugriff im Schichtbetrieb
Autor: Christoph Spielmann, Düsseldorf !
Was Access von anderen Entwicklungsumgebungen unterscheidet, ist insbesondere die einfache und schnelle Entwicklung kompletter Anwendungen ohne viel Programmcode. Eine einfache Datenbank ist so in vielen Fällen bereits nach wenigen Stunden in den Grundzügen fertig gestellt. Sobald die Datenbank aber wächst, werden die ersten Schwächen und Probleme deutlich. Dieser Artikel zeigt Ihnen anhand eines Beispiels für Access 2000, wie Sie diese Schwächen vermeiden.
Der Monolith
Der typische Aufbau einer Access-Anwendung sieht folgendermaßen aus: Sie haben verschiedene Tabellen, die - teils mit oder teils ohne Abfragen - an Formulare gebunden sind.
Innerhalb der Formulare werden die Daten angezeigt, verändert und neu hinzugefügt. Beim Verändern und Hinzufügen prüft ein Makro oder eine VBA-Prozedur, ob die Eingaben bestimmten Plausibilitätsregeln entsprechen.
Teilweise wird hierzu auf Werte anderer Formulare zugegriffen und teilweise findet eine Validierung auch innerhalb der Gültigkeitsregel-Eigenschaft von Tabellen oder Formularfeldern statt.
Wenn Ihre Datenbank ein zweites Formular enthält, in dem Sie die gleichen Daten evtl. tabellarisch bearbeiten, dann bringen Sie hier einen Großteil der Logik nochmals unter. Bei Änderungen passen Sie dann notwendigerweise beide Formulare an.
Diese Art der Anwendungsentwicklung funktioniert bis zu einem bestimmten Punkt sehr gut, da Ergebnisse relativ zügig erzielt werden können.
Wächst die Datenbank aber weiter, zeigen sich sehr schnell die Schwächen:
Änderungen müssen häufig an vielen Stellen parallel durchgeführt werden.
Wenn man an einer Stelle eine Kleinigkeit ändert, wirkt sich dies oft auf viele andere Stellen der Datenbank aus. Die Folge sind viele Fehler und eine wenig robuste Datenbank.
Da die Geschäftslogik über viele Makros, Module, Abfragen und Formulare verstreut ist, ist die Pflege zeitintensiv. Zudem können fremde Entwickler nur sehr schwer die Funktionsweise der Datenbank nachvollziehen.
Der Entwickler kann leicht die Übersicht über die Software verlieren, sodass sich weitere Fehler einschleichen.
Eine solche Software-Architektur wird als monolithisch bezeichnet, da sich alle Funktionen durcheinander in einem Klotz befinden.
Das Schichtmodell
Die Lösung des Problems besteht darin, die Anwendung nach dem Schichtenmodell aufzubauen. Hierbei werden häufig drei Schichten verwendet: Datenschicht, Business-Schicht und Präsentationsschicht.
In der Datenschicht sind Funktionen untergebracht, die lesend oder schreibend auf die Tabellen zugreifen. Jeglicher Datenzugriff muss über diese Schicht erfolgen.
In der Business-Schicht werden Plausibilitäten geprüft. Dazu gehört beispielsweise die Prüfung, ob der aktuelle Benutzer die betreffende Funktion ausführen darf oder ob alle Datenfelder korrekte Werte beinhalten. Die Business-Schicht greift zur Prüfung ausschließlich auf die Datenschicht zu.
An oberster Stelle liegt die Präsentationsschicht. Hier werden Daten erfasst oder angezeigt. Die Präsentationsschicht darf nur auf die Business-Schicht, nicht jedoch auf die Datenschicht zugreifen.
Die Vorteile liegen auf der Hand: Wenn Sie in der Business-Schicht eine Prüfung eines Schreibvorgangs unterbringen und alle Formulare dieses Funktion nutzen, brauchen Sie auch nur eine zentrale Funktion anpassen bzw. korrigieren.
Ein anderer Vorteil besteht darin, dass Sie beim Umstieg z. B. auf den SQL-Server lediglich die Datenschicht anpassen müssen. Die Business-Schicht, in der das eigentliche Know-how Ihrer Anwendung liegt, kann dagegen unverändert bleiben.
Das Schichtmodell in der Praxis
Das hier vorgestellte Beispiel besteht aus nur einer einzelnen Tabelle, die Adressdaten zusammen mit einem Anlage- und einem Änderungsdatum speichert.
Die Beispieldatenbank finden Sie auf der Heft-CD unter dem Dateinamen Klassen2000.mdb. (
Die Tabelle tblAdressen besteht aus den folgenden Feldern (s. Abb. 1):
AdresseID (Long Integer, Primärschlüssel der Tabelle)
Firmenname (Text)
Nachname (Text)
Vorname (Text)
Straße (Text)
PLZ (Text)
Ort (Text)
Anlagedatum (Datum/Uhrzeit)
Änderungsdatum (Datum/Uhrzeit)

Abb. 1: Die Tabelle tblAdressen in der Entwurfsansicht.
Standardwerte oder Gültigkeitsregeln sind nicht definiert, da diese Aufgaben von der Business-Schicht erledigt werden.
HinweisParallel zu der Beispieldatenbank Klassen2000.mdb finden Sie noch eine weitere Datenbank namens KlassenXP.mdb, die die Vorgehensweise beim Einsatz von XP beschreibt. Access 2000 enthält einige Fehler die bei der Bindung von Recordsets an Formulare zutage treten. Bei Access XP sind diese Probleme behoben, sodass hier einige Prozeduren einen einfacheren und eleganteren Aufbau haben. (
Aufbau der Datenschicht
Zum Zugriff auf die Tabellen ist nun zunächst einmal die Anlage der Datenschicht erforderlich.
Um eine gute Übersicht zu gewährleisten, sollte pro Tabelle ein entsprechendes Klassenmodul angelegt werden, in dem alle Funktionen untergebracht sind, die sich mit dieser Tabelle beschäftigen.
Public Function GetAdresse(AdresseID As Long) _ As Recordset
Dim SQL As String
Dim Data As New ADODB.Recordset
SQL = "SELECT * FROM tblAdressen"
SQL = SQL & " WHERE AdresseID=" & CStr(AdresseID)
With Data
.ActiveConnection = CurrentProject.Connection
.Source = SQL
.LockType = adLockReadOnly
.CursorType = adOpenStatic
.CursorLocation = adUseClient
.Open
End With
Set GetAdresse = Data
End Function
Quellcode 1

Abb. 2: Der Zugriff auf die Tabellen erfolgt ausschließlich über das Klassenmodul AdressenD.
Da in diesem Beispiel nur eine Tabelle zum Einsatz kommt, existiert auch nur ein Klassenmodul. Dies trägt den Namen AdressenD. Das D steht hierbei für Datenschicht (s. Abb. 2).
Einzelne Adresse abrufen
Die erste Prozedur dieses Klassenmoduls trägt den Namen GetAdresse. Ihre Aufgabe ist es, eine einzelne Adresse aus der Tabelle abzurufen und die Daten in Form eines Recordsets zurück zu liefern. Als Parameter erwartet die Prozedur die Primärschlüsselnummer der Adresse (s. Quell-code 1).
Innerhalb der Prozedur wird ein SQL-Text zusammengestellt mit dessen Hilfe danach ein Recordset gefüllt wird. Dieses wird dann als Ergebnis der Funktion zurückgegeben. Damit sich Änderungen am Recordset, die in einer höheren Schicht durchgeführt werden, nicht auf die Datenbank auswirken, sollte das Recordset vor der Übergabe an die höhere Schicht noch entbunden (disconnected) werden:
Set .ActiveConnection = Nothing
Schließlich sollen Schreibzugriffe kontrolliert über eine andere Datenschicht-Funktion durchgeführt werden.
Aufgrund eines Fehlers in Access 2000 lässt sich ein solches Recordset jedoch nicht an ein Formular binden, weshalb auf diese Anweisung in dieser Beispieldatenbank generell verzichtet werden muss. Beim Einsatz von Access XP tritt das Problem allerdings nicht mehr auf.
Um direkte Schreibzugriffe zu verhindern, wird die LockType-Eigenschaft auf ReadOnly gestellt.
Der LockType wird bewusst nicht auf ReadOnly gesetzt. Dies hat den folgenden Grund:
Public Function GetAdressenSuchergebnis(Firmenname As _ String, Nachname As String) As Recordset
Dim SQL As String
SQL = "SELECT AdresseID, Firmenname, Nachname, " _ SQL = SQL & "Vorname, PLZ FROM tblAdressen"
SQL = SQL + " WHERE True"
If Firmenname <> "" Then
SQL = SQL + " AND Firmenname LIKE " + _ Quote("%" + Firmenname + "%")
End If
If Nachname <> "" Then
SQL = SQL + " AND Nachname LIKE " + _ Quote("%" + Nachname + "%")
End If
Dim Data As New ADODB.Recordset
With Data
.ActiveConnection = CurrentProject.Connection
.Source = SQL
.LockType = adLockReadOnly
.CursorType = adOpenStatic
.CursorLocation = adUseClient
.Open
End With
Set GetAdressenSuchergebnis = Data
End Function
Quellcode 2
Public Function SaveAdresse(Firmenname As String, _
Nachname As String, Vorname As String, _
Strasse As String, PLZ As String, Ort As String, _
Anlagedatum As Date, Änderungsdatum As Date) As Long
Dim SQL As String
SQL = "SELECT * FROM tblAdressen WHERE False"
Dim Data As New ADODB.Recordset
With Data
.ActiveConnection = CurrentProject.Connection
.Source = SQL
.LockType = adLockOptimistic
.CursorType = adOpenDynamic
.MaxRecords = 1
.Open
.AddNew
!Firmenname = ZN(Firmenname)
!Nachname = ZN(Nachname)
!Vorname = ZN(Vorname)
!Strasse = ZN(Strasse)
Quellcode 3 (Fortsetzung nächste Seite)
Das Recordset wird später auch in der Präsentationsschicht verwendet und hier an das entsprechende Access-Formular gebunden.
Wäre hier ReadOnly eingestellt, könnte man die Daten des Formulars nicht bearbeiten (und dann letztendlich auch nicht an die entsprechende Speicher-Funktion weiterleiten).
Abfragen eines Suchergebnisses
Da die Anwendung auch eine Suchfunktion zur Verfügung stellt, liefert die Funktion GetAdressenSuchergebnis mehrere Adressen zurück, die einem Suchkriterium entsprechen (s. Quellcode 2). Auch in dieser Funktion wird wieder ein SQL-Text zusammengestellt und ein entsprechendes Recordset gefüllt. Um unbeabsichtigte Änderungen am Recordset zu vermeiden, wird der LockType auf adLockReadOnly und der CursorType auf adOpenStatic gesetzt.
Die Quote-Funktion hat die Aufgabe, evtl. vorhandene Anführungszeichen im Suchtext zu doppeln, sodass Access kein Problem bei der Auswertung der Abfrage hat. Sie finden die Prozedur im Modul modTools der Beispieldatenbank.
Adresse hinzufügen
Die beiden vorgestellten Prozeduren dienen lediglich dem Einlesen, aber nicht dem Speichern von Daten.
!PLZ = ZN(PLZ)
!Ort = ZN(Ort)
!Anlagedatum = Anlagedatum
!Änderungsdatum = Änderungsdatum
SaveAdresse = !AdresseID
.Update
.Close
End With
End Function
Quellcode 3 (Fortsetzung)
Public Sub UpdateAdresse(AdresseID As Long, Firmenname _ As String, Nachname As String, Vorname As String, _ Strasse As String, PLZ As String, Ort As String, _ Änderungsdatum As Date)
Dim SQL As String
SQL = "SELECT * FROM tblAdressen"
SQL = SQL + " WHERE AdresseID=" + CStr(AdresseID)
Dim Data As New ADODB.Recordset
With Data
.ActiveConnection = CurrentProject.Connection
.Source = SQL
.LockType = adLockOptimistic
.CursorType = adOpenDynamic
.Open
!Firmenname = ZN(Firmenname)
!Nachname = ZN(Nachname)
!Vorname = ZN(Vorname)
!Strasse = ZN(Strasse)
!PLZ = ZN(PLZ)
!Ort = ZN(Ort)
!Änderungsdatum = Änderungsdatum
.Update
.Close
End With
End Sub
Quellcode 4
Public Sub DeleteAdresse(AdresseID As Long)
Dim SQL As String
SQL = "DELETE * FROM tblAdressen"
SQL = SQL + " WHERE AdresseID=" + CStr(AdresseID)
CurrentProject.Connection.Execute SQL
End Sub
Quellcode 5
Die Prozedur SaveAdresse hat dagegen die Aufgabe, eine Adresse in die Tabelle zu schreiben (s. Quellcode 3). Die Prozedur hat für jedes Feld der Tabelle einen entsprechenden Parameter. Ausnahme ist der Primärschlüssel. Dieser wird nach Anlage als Ergebnis der Prozedur zurück geliefert.
Die ZN-Funktion wandelt einen Leerstring in den Wert Null um. Dies ist erforderlich, da als Parameter der Datentyp String und nicht Variant verwendet wird.
Vorhandene Adresse aktualisieren
Die Update-Prozedur hat einen ähnlichen Aufbau (s. Quellcode 4). Im Unterscheid zur Anlage einer neuen Adresse wird bei dieser Prozedur kein Wert zurückgegeben.
Die ID muss stattdessen als Parameter übergeben werden, sodass die Prozedur weiß, welche Adresse sie aktualisieren soll.
Adresse löschen
Zu guter Letzt fehlt noch eine Prozedur zum Löschen einer Adresse (s. Quellcode 5)
Aufbau der Business-Schicht
Die Aufgabe der Business-Schicht ist es, den Aufruf der entsprechenden Datenschicht-Prozeduren zu koordinieren und die Datenbank vor unsinnigen Speichervorgängen zu schützen.
Public Function GetAdresse(AdresseID As Long) As _ Recordset
With New AdressenD
Set GetAdresse = .GetAdresse(AdresseID)
End With
End Function
Quellcode 6
Public Function GetAdressenSuchergebnis(Firmenname As _ String, Nachname As String) As Recordset
If Firmenname = "" And Nachname = "" Then
Err.Raise vbObjectError + 2000, _ "UpdateAdresse", _ "Firmenname oder Nachname erforderlich"
End If
With New AdressenD
Set GetAdressenSuchergebnis = _ .GetAdressenSuchergebnis(Firmenname, _ Nachname)
End With
End Function
Quellcode 7
Public Function SaveAdresse(Firmenname As String, _ Nachname As String, Vorname As String, Strasse As _ String, PLZ As String, Ort As String) As Long
If Firmenname = "" And Nachname = "" Then
Err.Raise vbObjectError + 2000, _ "UpdateAdresse", "Firmenname oder Nachname erforderlich"
End If
If PLZ = "" Then
Err.Raise vbObjectError + 2000, _ "UpdateAdresse", "Postleitzahl erforderlich"
End If
If Ort = "" Then
Err.Raise vbObjectError + 2000, _ "UpdateAdresse", "Ort erforderlich"
End If
With New AdressenD
SaveAdresse = .SaveAdresse(Firmenname, _
Nachname, Vorname, Strasse, PLZ, Ort, _ Now, Now)
End With
End Function
Quellcode 8
In diesem Beispiel trägt das entsprechende Klassenmodul den Namen AdressenB. Das B steht hierbei für Business-Schicht.
Bei den öffentlichen Prozeduren der Business-Schicht bietet es sich an, wenn möglich die Namen der Datenschicht zu übernehmen. Dementsprechend trägt die erste Prozedur den Namen GetAdresse (s. Quellcode 6). Diese Prozedur macht nichts weiter, als eine neue Instanz der Datenschicht-Klasse AdressenD zu erzeugen und hier die gleichnamige Prozedur GetAdresse aufzurufen.
Die Prozedur GetAdressenSuchergebnis hat einen ähnlich einfachen Aufbau (s. Quellcode 7). Hier wird jedoch bereits geprüft, ob entweder ein Firmenname oder ein Nachname angegeben ist. Dies soll vermeiden, dass der Benutzer zu viele Datensätze abfragt und die Performance der Datenbank darunter leiden würde.
Sollten beiden Parameter Leerstrings enthalten, löst die Prozedur einen Laufzeitfehler mit einem entsprechenden Fehlertext aus. Als Fehlernummer wird hierbei der Wert 2000 zuzüglich dem Wert der Access-Konstanten vbObjectError verwendet.
Der Wert 2000 könnte innerhalb Ihrer Anwendung als genereller Fehler der Business-Schicht interpretiert werden.
Zusätzlich könnten Sie weitere Fehlernummern reservieren, um genauere Informationen über die Art des Fehlers an die höhere Schicht zu liefern.

Abb. 3: Das Suchformular in der Entwurfsansicht
Public Sub DeleteAdresse(AdresseID As Long)
With New AdressenD
.DeleteAdresse AdresseID
End With
End Sub
Quellcode 9
Private Sub Form_Open(Cancel As Integer)
Me.Section(0).Visible = False
End Sub
Quellcode 10
Private Sub btnSuchen_Click()
RefreshSuchergebnis
End Sub
Quellcode 11
Die folgende Auflistung gibt Ihnen einige Anregungen:
2001: Parameter darf kein Leerstring sein.
2002: Parameter außerhalb des erwarteten Wertebereichs.
2003: Parameter untereinander nicht schlüssig.
2004: Benutzer hat nicht die erforderlichen Rechte zur Ausführung.
2005: Datensatz konnte nicht gelöscht werden.
2006: Datensatz existiert nicht mehr.
Eine wichtige Besonderheit ist noch zu beachten: Standardmäßig unterbricht Access die Programmcode-Ausführung, wenn in einem Klassenmodul ein Fehler auftritt. Um Fehler an die höhere Schicht zu melden, darf dies jedoch nicht der Fall sein. Die entsprechende Einstellung nehmen Sie wie folgt vor:
Öffnen Sie ein beliebiges VBA-Modul (z.B. das Klassenmodul AdressenD)
Wählen Sie den Menüpunkt Extras/Optionen aus.
Wechseln Sie in das Register Allgemein.
Aktivieren Sie hier aus der Gruppe Unterbrechen bei Fehlern die Option Bei nicht verarbeiteten Fehlern aus.
Bestätigen Sie mit OK.
Prüfung bei Speichervorgängen
Eine etwas ausführlichere Prüfung wird von der Prozedur SaveAdresse vorgenommen (s. Quellcode 8).
Hier wird beispielsweise geprüft, ob entweder ein Firmenname oder ein Nachname vorhanden ist.
Sollte dies nicht der Fall sein, darf der Datensatz nicht gespeichert werden. Auch das Vorhandensein einer Postleitzahl und einer Ortsangabe wird geprüft.

Abb. 4: Da die Datenbindung erst nach Ausführung der ersten Suche erfolgt, wird der Detailbereich vorher ausgeblendet. Ansonsten wäre hier der Text #Name# sichtbar, da Access die Felder nicht an eine Datenquelle binden kann.
Private Sub RefreshSuchergebnis()
On Error GoTo Fehler
Dim Data As ADODB.Recordset
With New AdressenB
Set Data = .GetAdressenSuchergebnis _ (Nz(Me.txtFindFirmenname.Value, ""), _ Nz(Me.txtFindNachname.Value, ""))
End With
Set Me.Recordset = Data
Me.Section(0).Visible = True
Exit Sub
Fehler:
MsgBox Err.Description
Exit Sub
End Sub
Quellcode 12
Private Sub btnDetails_Click()
Dim AdresseID As Long
AdresseID = Me.Recordset.Fields("AdresseID").Value
DoCmd.OpenForm "frmAdresseBearbeiten", acNormal, , , , acDialog, AdresseID
RefreshSuchergebnis
End Sub
Quellcode 13
Die Meldung von Plausibilitätsverletzungen an die höhere Schicht wird wieder mit Hilfe von Laufzeitfehlern bewerkstelligt.
Einen fast identischen Aufbau hat die Prozedur UpdateAdresse, deren Aufbau Sie in der Beispieldatenbank finden.
Löschen einer Adresse
Die Funktion DeleteAdresse ruft die gleichnamige Prozedur der Datenschicht auf, um einen Datensatz zu löschen (s. Quellcode 9). Sollte Ihre Datenbank mit Benutzerrechten arbeiten, könnte die Prozedur noch um eine entsprechende Prüfung erweitert werden.
Die Präsentationsschicht
Die Präsentationsschicht besteht in diesem Beispiel aus einem Suchformular sowie einem Formular zur Bearbeitung und Neuanlage von Adressen.
Das Suchformular
Das Suchformular ist ein einfaches Endlosformular (s. Abb. 3).
Im Kopf befinden sich die zwei Textfelder txtFindFirmenname und txtFindNachname zur Eingabe der Suchkriterien.
Mit der Schaltfläche btnSuchen wird die Suche gestartet. Die Schaltfläche btnNeu legt eine neue Adresse an.
Im Detailbereich sind die Felder Firmenname, Nachname, Vorname und PLZ als Suchergebnis aufgelistet. Die einzelnen Steuerelemente sind über die Steuerelementinhalt-Eigenschaft an die betreffenden Felder gebunden.
Diese Bindung muss jedoch von Hand erfolgen, da unter der Datenherkunft-Eigenschaft des Formulars weder eine Abfrage noch eine Tabelle eingetragen ist und damit die automatischen Access-Funktionen nicht zum Zuge kommen. Rechts neben den Feldern ist eine Schaltfläche namens btnDetails platziert. Hiermit kann in die Bearbeitungsmaske gesprungen werden.
Funktionsweise des Suchformulars
Beim Öffnen des Formulars wird zunächst einmal der Detailbereich unsichtbar gemacht (s. Quellcode 10). Dies ist erforderlich, da hier aufgrund der fehlenden Datenbindung der Text #Name# angezeigt wird (s. Abb. 4).
Klickt der Benutzer auf die Suchen-Schaltfläche, wird der Suchvorgang gestartet (s. Quellcode 11). Die Unterprozedur RefreshSuchergebnis hat den Aufbau aus Quellcode 12.
Zunächst wird ein neues Recordset dimensioniert und mittels GetAdressenSuchergebnis-Funktion der Business-Schicht mit Daten gefüllt. Die Eingaben der beiden Suchfelder werden hierbei als Parameter übergeben, wobei die Nz-Funktion NULL-Werte in Leerstrings umwandelt.
Anschließend wird dieses Recordset der Recordset-Eigenschaft des Formulars zugewiesen, wodurch letztendlich die Datenbindung erfolgt.
|