Hinweis:

Dieses Kapitel stammt 1:1 aus dem Visual Basic 5.0-Kompendium und wurde zur Verfügung gestellt, damit Leser und Leserinnen, die mit den DAO-Objekten arbeiten möchten, wenigstens einen Überblick erhalten. Die Abbildungen sind nicht enthalten. Wer diese unbedingt benötigt, kann sie von mir per eMail (peterm@activetraining.de) anfordern.

Die Datenzugriffsobjekte (DAOs)

Bei der Jet-Engine handelt es sich nicht um ein eigenständiges Programm, sondern vielmehr um eine Reihe (teilweise recht umfangreicher) DLLs, die sowohl von Visual Basic als auch von Office 97 und anderen Microsoft-Anwendungen genutzt werden. Die Jet-Engine besitzt eine sehr komfortable »Programmierschnittstelle«. Es sind die Data Access Objects (DAO), kurz Datenzugriffsobjekte oder noch kürzer DAOs. Die DAOs bilden ein relativ umfangreiches, aber dennoch überschaubares Objektmodell, das die Funktionalität der Jet-Engine sowie den Aufbau einer Datenbank abbildet, und das aufgrund seiner Größe in Bild 18.1 nicht ganz vollständig wiedergegeben ist1 . Einen kompletten Überblick über die Objekthierarchie erhalten Sie z. B. in der Visual-Basic-Online-Dokumentation. Dort können Sie auf jedes einzelne Objekt klicken, um eine Beschreibung zu erhalten. Und wozu sind die Objekte da? Ganz einfach, sie stellen Ihnen nahezu die gesamte Funktionalität der Jet-Engine in einer Art und Weise zur Verfügung, wie Sie es von Visual Basic her gewohnt sind. Mit Hilfe der DAOs können Sie:

Mit anderen Worten, alle elementaren Datenbankoperationen können mit den DAOs erledigt werden. Moment, wozu brauche ich die DAOs, ich habe doch mein Datensteuerelement? Gute Frage, an diesem Einwand ist natürlich etwas dran. Zwar stellt das Datensteuerelement (schätzungsweise) 90% der Funktionalität der DAOs zur Verfügung, doch der Programmierer zahlt dafür einen Preis, und der heißt Performance. Allerdings gilt dies nicht ohne Einschränkungen. Wie sich durch einfache Benchmarks, die z. B. die gleiche Datenbankoperation (etwa die Suche in Biblio.MDB) einmal mit dem Datensteuerelement und einmal mit den Datenzugriffsobjekten durchführen, zeigen läßt, ist der Unterschied zwischen dem Datensteuerelement und den DAOs bei kleinen Datenbanken kaum spürbar (auch das Datensteuerelement wurde mit jeder Visual-Basic-Version optimiert). Anders sieht es aus, wenn umfangreiche Datenbankabfragen in großen Datenbanken durchgeführt werden. Auch bezüglich des Programmieraufwands gibt es ab einer bestimmten Programmgröße keine großen Unterschiede mehr. Mit anderen Worten, wenn man schon dabei ist, kann man die »paar« Routinen zur Eingabeübernahme aus den Steuerelementen auch noch implementieren. Für Anwendungen, bei denen die Performance keine allzu große Rolle spielt, bei kleinen Datenmengen (< 10000 Datensätze als ungefähre Hausnummer) oder wenn nur ein Prototyp erstellt werden soll, ist das Datensteuerelement zu empfehlen, ansonsten die DAOs.

Hinweis:

Eine gewisse Ausnahme liegt vor, wenn Sie in der Datenbank Bilder und andere OLE-Objekte speichern und in dem Visual-Basic-Programm anzeigen möchten. Hier sieht es so aus, als böte das Datensteuerelement den unkomplizierten Zugriff.

*** Bild fehlt!!!

Bild 18.1:
Das Objektmodell der Jet-Engine in der Übersicht

Das Einbinden der DAOs in ein Projekt

Bei der Jet-Engine handelt es sich um eine Reihe von DLLs, von denen die Dateien Msjet35.dll (1.1 Mbyte) und DAO3550.DLL (569 Kbyte) die wichtigsten sind. Letztere enthält das Objektmodell und seine Komponenten. Damit Sie die Datenzugriffsobjekte von Visual Basic ansprechen können, muß die Referenz »Microsoft DAO 3.5 Object Library« über den Menübefehl Projekt/Verweise eingebunden sein.

*** Bild fehlt!!!

Bild 18.2: Um die Datenzugriffsobjekte in einem Projekt nutzen zu können, muß der entsprechende Verweis auf die Typenbibliothek eingefügt werden.

      1. Schnellkurs für Umsteiger

Falls Sie bereits unter Visual Basic 3.0 oder 4.0 die Jet-Engine programmiert haben, dürfte es Sie sicherlich interessieren, was es an Neuigkeiten gibt. Um es kurz zu fassen, falls Sie bereits die Jet-Engine in den Versionen 2-3 kennen (Visual Basic 4.0 wurde mit den 16-Bit-Versionen 2.0 und 2.5, der 32-Bit-Version 3.0 und einem 2.5/3.0-Kompatibilitätslayer ausgeliefert), hat sich (was das Datenzugriffsobjektmodell angeht) kaum etwas geändert2 . Falls Sie dagegen nur die Jet-Engine 1.1 aus Visual Basic 3.0 kennen, dann hat sich eine Menge getan. Hier das Wichtigste in aller Kürze. Das »neue« Objektmodell bringt für Visual-Basic-3.0-Programmierer zwei wichtige Umstellungen:

Um eine Datenbank über ein Database-Objekt zu öffnen, war bei Visual Basic 3.0 folgende Anweisung erforderlich:

Set Db = OpenDatabase("Biblio.mbd")

Ab Visual Basic 4.0 muß diese Anweisung, gemäß des sehr viel umfangreicheren Objektmodells, entsprechend erweitert werden:

Set Db = DbEngine.Workspaces(0).OpenDatabase("Biblio.mdb")

Die größte Umstellung betrifft die alten Dynaset-, Snapshot- und Table-Objekte. Offiziell wurden diese Objekttypen bereits mit Visual Basic 4.0 (genauer gesagt, der Jet-Engine 2.0) abgeschafft und durch das Recordset-Objekt ersetzt. Ein Recordset-Objekt steht für eine Datensatzgruppe, die wahlweise vom Typ Dynaset, Snapshot oder Table (in diesem Buch Tabelle genannt) sein kann. Wurde ein Dynaset unter Visual Basic 3.0 wie folgt erstellt:

Dim Dy As Dynaset
Set Dy = Db.CreateDynaset("Titles")

muß dafür seit Visual Basic 4.0 statt dessen die OpenRecordset-Methode verwendet werden:

Dim Dy As Recordset
Set Dy = Db.OpenRecordset("Titles", dbOpenDynaset)

Der Parameter dbOpenDynaset gibt an, daß ein Recordset-Objekt vom Typ Dynaset erstellt werden soll. Das Ergebnis ist bei beiden Varianten eine Objektvariable, die eine Referenz auf eine Datensatzgruppe vom Typ Dynaset enthält.

Neu ist auch, daß es innerhalb einer Datensatzgruppe möglich ist, die relative Position des Datensatzzeigers zu bestimmen. Dafür stehen die Eigenschaften AbsolutePosition und PercentPosition des Recordset-Objekts zur Verfügung (allerdings nicht bei Recordset-Objekten vom Typ Tabelle). Voraussetzung bei Dynaset-Datensatzgruppen ist aber nach wie vor, daß der Datensatzzeiger über die MoveLast-Methode einmal an das Ende der Datensatzgruppe bewegt wurde.

Auch das gute alte Datensteuerelement wurde bereits mit Visual Basic 4.0 deutlich verbessert. Zum einen unterstützt es über seine RecordsetType-Eigenschaft endlich auch Table-Objekte, d. h. Recordset-Objekte vom Typ Table. Damit ist es u. a. möglich, die schnelle Seek-Methode auch auf Datensatzgruppen anzuwenden, die durch das Datensteuerelement verwaltet werden. Zum anderen arbeitet das Datensteuerelement asynchron und ist z. B. in der Lage, eine Datensatzgruppe bereits im Datensätzen aufzufüllen, während das Programm die nächsten Anweisungen ausführt (standardmäßig speichert es 50 Datensätze zwischen, über die CacheSize-Eigenschaft kann dieser Wert verändert werden). Aus diesem Grund ist es hier nicht mehr erforderlich, die MoveLast-Methode auszuführen, um über die RecordCount-Eigenschaft die Anzahl an Datensätzen zu ermitteln. Sie werden vielmehr feststellen, daß dieser Wert in der RecordCount-Eigenschaft eine kurze Zeit, nachdem das Programm gestartet bzw. die Datensatzgruppe aufgebaut wurde, zur Verfügung steht.

Zum Schluß gibt es noch zwei gute Nachrichten. Verwenden Sie unbedingt das neueste Objektmodell, denn es bietet sehr viel mehr Leistung. Umstellen müssen Sie Ihre VB3-Programme allerdings nicht, denn wenn Sie anstelle der »Microsoft DAO 3.5 Object Library« die Referenz »Microsoft DAO 2.5/3.5 Compatibility Layer« einbinden, werden die alten Objekte weiterhin unterstützt.

Das Objektmodell in der Übersicht

Wie stellt man sich ein so umfangreiches Objektmodell vor, wie es die DAOs laut Bild 18.1 offenbar sind, vor? Am besten, indem man ganz am Anfang, also gang »oben« anfängt3 . An oberster Stelle steht das DBEngine-Objekt. Es steht für die gesamte Jet-Engine. Als Methoden besitzt es u. a. RegisterDatabase und RepairDatabase, Operationen also, die auf die gesamte Datenbank angewendet werden. Die nächste Ebene bilden die Workspace-Objekte. Ein Workspace-Objekt definiert eine Sitzung und legt z. B. den Kontext für Transaktionen fest. Da alle Workspace-Objekte in der Workspaces-Auflistung zusammengefaßt werden, können mit der Jet-Engine mehrere Sitzungen gleichzeitig geführt werden. Über den Workspace werden auch Benutzerrechte festgelegt. Unterhalb des Workspace-Objekts kommt das Database-Objekt. Jedes Database-Objekt steht für eine eigene geöffnete Datenbank. Innerhalb einer Sitzung können beliebig viele Datenbanken geöffnet werden (bis zu 255 bei Jet 3.5). Alle Database-Objekte werden in der Databases-Auflistung zusammengefaßt. Beim Zugriff auf eine Datenbank gelten die von Workspace-Objekt festgelegten Rahmenbedingungen, wie z. B. die Zugriffsrechte. Mit anderen Worten, ein Benutzer darf nur das, was ihm über das Workspace-Objekt erlaubt ist. Da man nur jene Arbeitsbereiche (Workspace) erstellen kann, für das man die Rechte besitzt, ergibt sich somit ein effektives Sicherheitsschema, das z. B. dazu führt, daß diese Dinge nicht beim Öffnen der Datenbank angegeben werden müssen. Das Database-Objekt ist ein sehr umfangreiches Objekt mit vielen Eigenschaften und Methoden. Über dieses Objekt steht Ihnen sowohl ein Zugriff auf die Datenbankstruktur, etwa über die TableDef-Objekte, als auch auf den Inhalt der Datenbank, d. h. seine Tabellen und Abfragen zur Verfügung. Allerdings, und das ist gerade für Datenbankeinsteiger etwas verwirrend, existieren diese Objekte nicht dadurch bereits, daß die Datenbank geöffnet wird. Sie müssen vielmehr über die OpenRecordset-Methode des Database-Objekts während der Programmausführung angelegt werden.

Übrigens besitzt auch das Datensteuerelement eine Database-Eigenschaft, die ein Database-Objekt zurückgibt. Alle Eigenschaften und Methoden des Database-Objekts stehen also auch über das Datensteuerelement zur Verfügung.

Die folgende Anweisung gibt die Anzahl der Tabellendefinitionen (d. h. TableDef-Objekte) aus, die in der Datenbank enthalten sind, die über die DatabaseName-Eigenschaft mit einem Datensteuerelement verbunden wurde:

?Data1.Database.TableDefs.Count

Allerdings gibt es beim Datensteuerelement keine Möglichkeit, etwa auf die Workspaces-Auflistung oder das DBEngine-Objekt zuzugreifen. Es arbeitet stets mit dem Standardarbeitsbereich, der durch das Admin-Benutzerkonto definiert ist.

Das Öffnen einer Datenbank geschieht über die OpenDatabase-Methode, der lediglich der Name der Datenbank übergeben werden muß:

Dim Db As Database
Set Db = DBEngine.Workspaces(0).OpenDatabase("Biblio.mdb")

Das Resultat ist eine Variable Db vom Typ Database, die eine Referenz auf die geöffnete Datenbank enthält.

Tabelle 18.1:
Die wichtigsten Datenzugriffsobjekte der Jet-Engine

Objekt

Auflistung

Bedeutung

DBEngine

Oberstes Objekt des Datenbankmodells. Es steht für die komplette Jet-Engine.

Workspace

Workspaces

Definiert den äußeren Rahmen einer Datenbanksitzung. Innerhalb eines Workspace können mehrere Datenbanken bearbeitet werden. Alle Transaktionen beziehen sich auf den Kontext des aktuellen Workspace-Objekts. Standardmäßig arbeitet man mit dem »Default«-Workspace-Objekt.

Database

Databases

Steht für eine Datenbank innerhalb eines Workspace-Objekts. Über die Databases-Auflistung können mehrere Datenbanken gleichzeitig bearbeitet werden.

TableDef

TableDefs

Enthält die Struktur einer einzelnen Tabelle.

Field

Fields

Allgemeines Objekt, das (in Abhängigkeit des »Oberobjekts«) verschiedene Bedeutungen haben kann. Als »Unterobjekt« eines TableDef-Objekts steht es für die Attribute eines einzelnes Tabellenfeldes (engl. column).

Index

Indexes

Steht für einen einzelnen Index.

Recordset

Recordsets

Datensatzgruppe vom Typ Dynaset, Snapshot oder Table, die während der Programmausführung für den Zugriff auf eine Basistabelle angelegt wird.

QueryDef

QueryDefs

Enthält eine bereits vordefinierte Datenbankabfrage.

Relation

Relations

Legt eine 1:1- oder 1:n-Beziehung zwischen einer Primär- und einer Fremdtabelle fest.

Error

Errors

Enthält für jeden Teilfehler des zuletzt aufgetretenen Fehlers ein eigenes Error-Objekt, dessen Number- und Description-Eigenschaft den Fehlertyp angibt.

Container

Containers

Enthält alle Objekte, die zu einer Datenbank gehören, und die von der Jet-Engine in einem sog. Container zusammengefaßt werden. Standardmäßig sind die Objekte Databases, Tables und Relationships dabei. Durch Hinzufügen neuer Objekte kann die Jet-Engine um benutzerdefinierte Objekte erweitert werdena .

Document

Documents

Eine Documents-Auflistung enthält alle Container-Objekte eines bestimmten Typs, z. B. Tables. Ein einzelnes Document-Objekt beschreibt ein einzelnes dieser in der Datenbank gespeicherten Objekte.

Group

Groups

Legt im Zusammenhang mit der Access-Sicherheit die Daten einer Benutzergruppe fest. Sicherheitsfeatures können von Visual Basic nur genutzt, nicht aber implementiert werden.

User

Users

Legt im Zusammenhang mit der Access-Sicherheit die Daten eines Benutzers fest. Sicherheitsfeatures können von Visual Basic nur genutzt, nicht aber implementiert werden.

 

Recordset-Objekte

Die aus der Sicht des Programmierers wichtigsten Objekte sind die Recordset-Objekte, denn über sie findet der Zugriff auf den Dateninhalt einer Datenbank statt. Es ist wichtig zu verstehen, daß es sich um nicht-dauerhafte Objekte handelt, d. h. sie werden nicht in der Datenbank gespeichert und sind daher nicht Teil der Datenbank. Sie werden vielmehr über die OpenRecordset-Methode des Database-Objekts angelegt und mit dem Schließen des Database-Objekts wieder zerstört.

Ein Recordset ist nichts anderes eine Gruppe von Datensätzen. Woher diese Datensätze kommen, kann auf verschiedene Arten festgelegt werden:

Auch das Datensteuerelement stellt über eine Recordset-Eigenschaft eine Referenz auf ein Recordset-Objekt zur Verfügung. Alle Eigenschaften, wie z. B. RecordCount, beziehen sich auf dieses Objekt und nicht auf das Datensteuerelement. Es ist daher kein Problem, ein Datensteuerelement durch eine Zuweisung an die Recordset-Eigenschaft mit einer anderen Datensatzgruppe zu verbinden.

Beispiel:

Die folgenden Anweisungen legen zunächst ein Recordset-Objekt an und weisen die dahinterstehende Datensatzgruppe anschließend einem Datensteuerelement zu, so daß der Benutzer mit den Schaltflächen des Steuerelements in den Datensätzen blättern kann.

Dim Rs As Recordset
Set Rs = Db.OpenRecordset("Titles", dbOpenDynaset)
Set Data1.Recordset = Rs

Das Recordset-Objekt ist nicht nur eines der wichtigsten Objekte, es ist gleichzeitig auch eines mit den meisten Eigenschaften und Methoden. Während Tabelle 18.1 die wichtigsten Eigenschaften aufführt, sind in Tabelle 18.2 die wichtigsten Methoden enthalten. Diese Übersicht sollen Sie natürlich nicht auswendig lernen. Sie finden sie selbstverständlich (vollständig) auch in der Visual-Basic-Hilfe. Sie soll vielmehr eine Arbeitshilfe sein, denn mehr als die in den beiden Tabellen aufgeführten Eigenschaften und Methoden werden Sie am Anfang nicht benötigen.

Bliebe noch zu erwähnen, daß alle Recordset-Objekte eines Database-Objekts in einer Recordsets-Auflistung enthalten sind. Damit besteht z. B. die Möglichkeit, alle Recordset-Objekte zu enumerieren oder einfach nur, deren Anzahl auszugeben:

?Db.Recordsets.Count

Wird ein Recordset irgendwann über die Close-Methode geschlossen, wird er aus der Recordsets-Auflistung wieder entfernt und die Count-Eigenschaft um 1 erniedrigt.

Tabelle 18.2:
Die wichtigsten Eigenschaften eines Recordset-Objekts

Eigenschaft

Bedeutung

AbsolutePosition

Enthält die Nummer des aktuellen Datensatzes für Recordset-Objekte vom Typ Dynaset oder Snapshot.

BOF

Ist True, wenn der Datensatzzeiger über den ersten Datensatz hinaus an den Anfang der Datensatzgruppe bewegt wurde.

Bookmark

Ermöglicht, die aktuelle Position des Datensatzzeigers zu speichern, so daß diese Position später wieder angesteuert werden kann.

Bookmarkable

Enthält den Wert True, wenn die aktuelle Position des Datensatzzeigers gespeichert werden kann.

CacheSize

Legt eine Anzahl an Datensätzen fest (zwischen 5 und 1200), die beim Zugriff auf eine ODBC-Datenbank im Speicher gehalten werden, so daß beim Zugriff auf einen dieser Datensätze kein Zugriff auf die ODBC-Datenquelle erforderlich ist. Der Zwischenspeicher wird über die FillCache-Methode mit Datensätzen gefüllt. Ein Wert von 0 schaltet die Zwischenspeicherung aus.

CacheStart

Enthält die Bookmark-Eigenschaft jenes Datensatzes, bei dem das Zwischenspeichern der Datensätze beginnen soll.

EditMode

Enthält den Bearbeitungsmodus des aktuellen Datensatzes. In Frage kommen folgende Einstellungen: dbEditNone – der Datensatz wird nicht bearbeitet, dbEditInProgress – die Edit-Methode wurde aufgerufen und dbEditAdd – die AddNew-Methode wurde aufgerufen, um einen neuen Datensatz anzulegen. Anhand dieser Eigenschaft kann entschieden werden, ob die Update-Methode aufgerufen werden muß.

EOF

Ist True, wenn der Datensatzzeiger über den letzten Datensatz hinaus an das Ende der Datensatzgruppe bewegt wurde.

Filter

Enthält eine Bedingung (SQL-Format), die festlegt, welche Datensätze bei der Ausführung der nächsten Refresh-Methode in die neue Datensatzgruppe übernommen werden.

LockEdits

Legt die Art der Zugriffssperre fest (LockEdits=True – vollständige Sperre, LockEdits=False, teilweise Sperre). Standardmäßig ist eine vollständige Sperre aktiv.

NoMatch

Besitzt den Wert True, wenn nach der Ausführung einer FindX- oder Seek-Methode kein Datensatz gefunden wurde.

PercentPosition

Enthält die relative Position des Datensatzzeigers relativ zum Beginn der Datensatzgruppe in Prozent für Recordset-Objekte vom Typ Dynaset oder Snapshot.

RecordCount

Enthält die Anzahl der Datensätze in der Datensatzgruppe.

Restartable

Enthält den Wert True, wenn eine Abfrage zum Anlegen der Datensatzgruppe erneut ausgeführt werden kann.

Sort

Legt fest, nach welchem Feld die Datensatzgruppe sortiert wird (entspricht dem ORDER BY-Teil eines SQL-Statements).

Type

Gibt den Typ des Recordset-Objekts an. Zur Auswahl stehen dbOpenTable, dbOpenDynaset und dbOpenSnapshot.

ValidationRule

Legt eine Regel in Form einer logischen Bedingung fest, die beim Bewegen des Datensatzzeigers für den aktuellen Datensatz angewendet wird. Beachten Sie, daß jedes einzelne Field-Objekt ebenfalls eine ValidationRule- und eine ValidationText-Eigenschaft besitzen kann.

ValidationText

Legt einen Text fest, der beim Verstoß gegen die Validierungsregel erscheint.

 

Tabelle 18.3:
Die wichtigsten Methoden eines Recordset-Objekts in der Übersicht

Methode

Bedeutung

AddNew

Fügt einen neuen Datensatz zur Datensatzgruppe hinzu.

CancelUpdate

Bricht eine Edit- oder AddNew-Operation vorzeitig ab. Der Datensatzzeiger wird nicht bewegt, der Inhalt des aktuellen Datensatzes nicht verändert.

Clone

Erstellt eine Kopie der Datensatzgruppe. Jede Kopie kann ihren eigenen aktuellen Datensatzzeiger besitzen, so daß mit mehreren Datensätzen einer Datensatzgruppe Operationen durchgeführt werden, ohne daß der Datensatzzeiger in einer Buchmarke zwischengespeichert werden muß. Unmittelbar nach dem Anlegen eines Clones muß der aktuelle Datensatzzeiger auf einen Datensatz gesetzt werden.

Close

Schließt die Datensatzgruppe. Das Recordset-Objekt wird dadurch aus der Recordsets-Auflistung entfernt.

CopyQueryDef

Gibt eine Referenz auf jenes QueryDef-Objekt zurück, mit dem die Datensatzgruppe angelegt wurde. Geschah dies ohne Mitwirkung eines QueryDef-Objekts, gibt die Methode einfach einen Nullwert zurück.

Delete

Löscht den aktuellen Datensatz. Der Datensatzzeiger wird ungültig, die AbsolutePosition-Eigenschaft erhält den Wert –1.

Edit

Schaltet den aktuellen Datensatz in den Bearbeiten-Modus.

FillCache

Füllt den internen Zwischenspeicher (Cache) mit der über die CacheSize-Eigenschaft festgelegte Anzahl an Datensätzen auf.

FindFirst

Sucht nach dem ersten Datensatz in der Datensatzgruppe, der mit einem angegebenen Suchkriterium übereinstimmt.

FindLast

Sucht nach dem letzten Datensatz in der Datensatzgruppe, der mit einem angegebenen Suchkriterium übereinstimmt.

FindNext

Sucht nach dem nächsten Datensatz in der Datensatzgruppe, der mit einem angegebenen Suchkriterium übereinstimmt.

FindPrevious

Sucht nach dem vorherigen Datensatz in der Datensatzgruppe, der mit einem angegebenen Suchkriterium übereinstimmt.

GetRows

Liest einen oder mehrere Datensätze in ein zweidimensionales Feld ein, das durch diese Methode zurückgegeben wird. Diese Methode ist immer dann sehr praktisch, wenn eingelesene Datensätze außerhalb der Datenzugriffsobjekte weiterverarbeitet werden sollen.

Move

Bewegt den Datensatzzeiger um eine bestimmte Anzahl an Datensätzen vor oder zurück bzw. relativ zu der in der Bookmark-Eigenschaft enthaltenen Position.

MoveFirst

Bewegt den Datensatzzeiger auf den ersten Datensatz in der Datensatzgruppe.

MoveLast

Bewegt den Datensatzzeiger auf den letzten Datensatz in der Datensatzgruppe.

MoveNext

Bewegt den Datensatzzeiger auf den nächsten Datensatz in der Datensatzgruppe.

MovePrevious

Bewegt den Datensatzzeiger auf den vorhergehenden Datensatz in der Datensatzgruppe.

OpenRecordset

Legt eine neue Datensatzgruppe auf der Grundlage der bereits bestehenden Datensatzgruppe an.

Requery

Führt die Abfrage, die zum Erstellen der Datensatzgruppe führte, erneut aus. Dies ist z. B. bei Datensatzgruppen notwendig, die kein Rückwärtsblättern erlauben.

Seek

Sucht unter Berücksichtigung eines Index nach einem Datensatz. Ist nur bei Recordset-Objekten vom Typ Table möglich. Ob die Suche erfolgreich war, erfährt man auch hier über die NoMatch-Eigenschaft.

Update

Schreibt den Inhalt des im Zwischenspeicher befindlichen aktuellen Datensatzes in die Datensatzgruppe zurück.

 

Dynaset, Snapshot oder Tabelle?

Zugegeben, diese Begriffe sind für den Anfänger erfahrungsgemäß recht verwirrend. Daß eine Datenbank in Tabellen organisiert ist, ist ja noch einzusehen. Doch warum spricht man nicht von Tabellen, sondern von Dynasets, Snapshots oder anderen Dingen? Nun, weil keine Tabelle eine Tabelle ist und damit in vielen Fällen etwas sehr Unhandliches. Stellen Sie sich vor, eine Tabelle besitzt keine 1000, sondern 1000000 Datensätze. Möchten Sie diese alle auf einmal in den Arbeitsspeicher laden oder gar über ein Netzwerk anfordern? Sicher nicht, zumal es mit einer einmaligen Transaktion nicht getan wäre, sondern diese in einer echten Anwendung unzählige Male am Tag stattfinden müßte. Außerdem möchte man in einem Programm in der Regel nicht die Datensätze einer Tabelle, sondern die mehrerer Tabellen bearbeiten. »Schick’ mir doch mal schnell die Liste aller noch ausstehenden Aufträge des Artikels Nr. 1235 aus dem Postleitzahlgebiet 5678 rüber«. Kein Problem, das Ergebnis ist eine Datensatzgruppe, deren Felder aus verschiedenen Tabellen stammen. Der Benutzer möchte sie, z. B. auf einer Webseite oder in einem Word-Dokument, jedoch gemeinsam sehen, sie werden daher in einer neuen Datensatzgruppe zusammengefaßt. Würden man statt dessen alle Basistabellen weitergeben, wäre dies sehr viel umständlicher. Aus diesem Grund gibt es, schon lange vor der Einführung der Jet-Engine, verschiedene Typen von Datensatzgruppen. Dazu gehören die Dynasets und die Snapshots. Auch wenn die Namen am Anfang etwas eigentümlich klingen mögen, ist die dahinterstehende Idee sehr einfach. Ein Recordset-Objekt steht allgemein für eine Datensatzgruppe, allerdings gibt es dieses bei der Jet-Engine in drei »Geschmacksrichtungen«: Dynaset, Snapshot oder Tabelle. Alle drei besitzen, wie könnte es auch anders sein, ihre Vor- und Nachteile und vor allem ihre spezifischen Einschränkungen. Für die Praxis ist es daher sehr wichtig, die Unterschiede zu kennen.

Der am häufigsten eingesetzte Recordset-Typ ist das Dynaset. Ein Dynaset ist eine temporäre Gruppe von Datensätzen, genauer gesagt, von Schlüsseln auf Datensätze, die aus einer Tabelle (in diesem Zusammenhang Basistabelle genannt), einer SQL-Abfrage, einem QueryDef-Objekt oder einem bereits existierenden Dynaset abgeleitet wird. Dynasets existieren nur im Arbeitsspeicher und werden entfernt, sobald das Programm beendet oder das Dynaset über eine Close-Methode wieder geschlossen wird. Dennoch besteht eine direkte Verbindung zu den »darunterliegenden« Basistabellen. Änderungen am Dynaset werden in die Basistabelle(n) übertragen. Sind durch andere Benutzer Änderungen an der Basistabelle vorgenommen worden, werden diese im Dynaset sichtbar, sobald der betreffende Datensatz angesteuert wird. Werden durch andere Benutzer dagegen Datensätze aus der Datensatzgruppe gelöscht oder neue Datensätze in die Datensatzgruppe aufgenommen, wird diese Veränderung in der Anzahl der Mitglieder erst dann sichtbar, wenn die Datensatzgruppe, z. B. über eine Refresh-Methode im Zusammenhang mit dem Datensteuerelement, neu aufgebaut wird.

Hinweis:

Um ein Recordset-Objekt vom Typ Dynaset oder Snapshot neu aufzubauen, muß lediglich die OpenRecordset-Methode erneut aufgerufen werden:

Set Rs = Rs.OpenRecordset(, dbOpenDynaset)

Merksatz:

Änderungen, die andere Benutzer an bestehenden Datensätzen der Datensatzgruppe eines Dynasets durchführen, werden sofort sichtbar. Datensätze, die von anderen Benutzern in die Datensatzgruppe aufgenommen werden, allerdings erst dann, wenn der Dynaset neu aufgebaut wird. Das gleiche gilt für den Fall, daß andere Benutzer Datensätze der Datensatzgruppe löschen.

Eine Besonderheit, von der noch die Rede sein wird, ist, daß die Jet-Engine zunächst lediglich die Schlüssel auf die einzelnen Datensätze, nicht aber ihren Inhalt in den Arbeitsspeicher lädt. Ein Nachteil ist, daß Indizes nicht auf Dynasets angewendet werden können. Wird ein Dynaset nach einem bestimmten Kriterium durchsucht, geht die Jet-Engine im Prinzip jeden Datensatz einzeln durch. Dynasets sind daher vergleichsweise langsam. Eine Variante des Dynasets ist der Snapshot. Wie es der Name andeutet, handelt es sich um eine »Momentaufnahme« einer Datensatzgruppe. Da die einzelnen Datensätze, die, anders als beim Dynaset, mit ihrem Inhalt in den Arbeitsspeicher geladen werden, nicht geändert werden können, handelt es sich um einen »Nur-Lese-Dynaset«. Die dritte Variante des Recordset-Objekts ist die Tabelle. Hierbei handelt es sich um ein direktes Abbild einer Basistabelle und nicht um eine Ansicht, die nur eine Untermenge der Datensätze umfaßt. Der große Vorteil von Tabellen ist, daß die auf einem Index basierende Seek-Methode zur Suche nach einem Datensatz angewendet werden kann. Diese Methode ist im Durchschnitt 20 Mal schneller als eine vergleichbare FindX-Methode. Ein Nachteil von Tabellen ist, daß sie nicht mit SQL-Abfragen bearbeitet werden können.

Welche Variante eines Recordset-Objekts erstellt wird, hängt davon ab, ob Sie mit dem Datensteuerelement oder direkt mit den Datenzugriffsobjekten arbeitet. Das Datensteuerelement arbeitet standardmäßig mit Dynasets. Über die Eigenschaft RecordsetType können Sie sowohl zur Entwurfszeit als auch während der Programmausführung einen anderen Typ auswählen (die Datensatzgruppe muß dann aber neu aufgebaut werden). Greifen Sie dagegen über die Datenzugriffsobjekte auf ein Recordset-Objekt zu, wird dessen Typ beim Aufruf der OpenRecordset-Methode festgelegt.

*** Bild fehlt!!!

Bild 18.3: Eine Datensatzgruppe kann vom Typ Dynaset, Snapshot oder Tabelle sein.

Beispiel:

Das folgende Beispiel zeigt, wofür Dynasets im allgemeinen benutzt werden. Es wird ein Recordset-Objekt vom Typ Dynaset angelegt, in der nur jene Datensätze der Tabelle Titles enthalten sind, deren Feld Title mit dem Buchstaben »S« beginnt:

Dim Rs As Recordset
SQLString = "SELECT * FROM Titles WHERE Title LIKE 'S*'"
Set Rs = Db.OpenRecordset(SQLString, vbDbOpenDynaset)

Das Ergebnis ist eine Objektvariable Rs (Typ Recordset), die für eine Datensatzgruppe steht, in der jedes »Title«-Feld mit dem Buchstaben »S« beginnt. Dies ist ein typisches Anwendungsbeispiel für Dynasets.

Soll statt dessen ein Recordset-Objekt vom Typ Snapshot geöffnet werden, muß der Aufruf nur ein wenig modifiziert werden:

Dim Rs As Recordset
SQLString = "SELECT * FROM Titles WHERE Title LIKE 'S*'"
Set Rs = Db.OpenRecordset(SQLString, vbDbOpenSnapshot)

Die Datensatzgruppe Rs steht zwar für die gleiche Anzahl an Datensätzen wie im vorherigen Beispiel, die Felder können allerdings nicht überschrieben werden.

Tabelle 18.4:
Die drei Recordset-Objekttypen in der Gegenüberstellung

Datensatz-gruppentyp

Vorteile

Nachteile

Dynaset

Kann über eine SQL-Abfrage erstellt werden, editierbar.

Zugriff etwas langsam. Allerdings werden nur Schlüssel auf die Datensätze in den Arbeitsspeicher gelesen.

Snapshot

Schnellerer Zugriff als beim Dynaset.

Keine Editiermöglichkeit. Bei großen Datensatzgruppen macht sich der Umstand, daß stets der komplette Datensatz in den Speicher gelesen wird, nachteilig bemerkbar.

Tabelle

Sehr schneller Zugriff über Seek-Methode (indexbasierend).

Stets direktes Abbild einer Basistabelle, keine SQL-Abfragen möglich.

 

Die OpenRecordset-Methode

Zu den wichtigsten Methoden, die im Zusammenhang mit den Datenzugriffsobjekten der Jet-Engine eine Rolle spielen, gehört die OpenRecordset-Methode.

Syntax

Set Variable = Database.OpenRecordset(Quelle[,Typ[,Optionen]])
Set Variable = Recordset.OpenRecordset([Typ[, Optionen]])

Während der Parameter Typ den Typ des anzulegenden Recordset-Objekts festlegt (zur Auswahl stehen dbOpenDynaset, dbOpenSnapshot und dbOpenTable; wird nichts angegeben wird eine Tabelle angelegt), können über den Parameter Optionen wichtige Optionen eingestellt werden (mehr dazu gleich). Die OpenRecordset-Methode gibt es in zwei Varianten. Die erste Variante wird auf ein Database-Objekt angewendet und erstellt eine neue Datensatzgruppe, in der Regel basierend auf einer SQL-Abfrage. Wo die Datensätze herkommen, bestimmt der Source-Parameter. Die zweite Variante geht dagegen von einer bereits existierenden Datensatzgruppe aus, auf die sie angewendet wird. Ein Source-Parameter wird hier nicht benötigt. Haben Sie beim ersten Aufruf z. B. alle Mayers aus einer Basistabelle herausgefiltert und in ein Dynaset übertragen, können Sie durch vorheriges Setzen der Filter-Eigenschaft und dem anschließenden Aufruf der OpenRecordset-Methode auf das Dynaset nur jene Mayers übrig lassen, die in Lüdenscheid wohnen:

SQLText = "SELECT * FROM Kunden WHERE Nachname = 'Mayer'"
Set Rs = Db.OpenRecordset(SQLText, dbOpenDynaset)
Rs.Filter = "Ort = 'Lüdenscheid'"
Set Rs = Rs.OpenRecordset()

Die Syntax des zweiten Aufrufs ist ein wenig ungewöhnlich, denn beim Aufruf der OpenRecordset-Methode wird kein Parameter übergeben, da die Bedingung bereits über die Filter-Eigenschaft gesetzt wurde.

Für Umsteiger

Die alten Methoden CreateDynaset, CreateSnapshot und OpenTable werden von der Jet-Engine zwar noch unterstützt, sollten aber zugunsten der OpenRecordset-Methode nicht mehr verwendet werden.

Optionen beim Anlegen eines Recordset-Objekts

Eine wichtige Rolle spielen die Optionen, die beim Aufruf der OpenRecordset-Methode übergeben werden können, auch wenn sie bei den Beispielprogrammen in dieser Kapitel nicht angewendet werden. Besonders hervorzuheben ist dabei die Option SQLPassThrough, die bewirkt, daß eine SQL-Anfrage nicht von der Jet-Engine vorverarbeitet, sondern direkt an den SQL-Server weitergereicht wird. Allein das Setzen dieser Option, beim Datensteuerelement muß der Konstantenwert über die Options-Eigenschaft eingetragen werden, kann beim Zugriff auf einen Oracle-SQL-Server eine erhebliche Performance-Steigerung bewirken.

Tabelle 18.5:
Die Optionen, die beim Aufruf der OpenRecordset-Methode angegeben werden können.

Option

Wert

Bedeutung

dbDenyWrite

1

Andere Benutzer können die Datensätze weder editieren noch Datensätze hinzufügen.

dbDenyReadOther

2

Andere Benutzer können die Datensätze nicht lesen (nur bei Table-Typen möglich).

dbReadOnly

4

Die Datensätze können nur gelesen werden, andere Benutzer können allerdings Änderungen vornehmen.

dbAppendOnly

8

An die Datensatzgruppe dürfen lediglich Datensätze angehängt werden (nur bei Dynaset-Typen möglich).

dbInconsistent

16

Es sind inkonsistente, d. h. gegen die Regeln der referentiellen Integrität verstoßende Updates erlaubt (nur bei Dynaset-Typen).

dbForwardOnly

256

Es wird ein Datensatzgruppe vom Typ Snapshot angelegt, in der man sich (aus Performance-Gründen) nur (über die MoveNext-Methode) vorwärts bewegen kann (engl. »forward scrolling snapshot«).

dbSQLPassThrough

64

SQL-Statements werden nicht von der Jet-Engine ausgewertet, sondern direkt an den SQL-Server weitergereicht. Dies ist eine sehr wichtige Option, die die Performance beim SQL-Server-Zugriff erheblich verbessern kann und dafür sorgt, daß SQL-Parameter, die die Jet-Engine nicht kennt, verarbeitet werden können.

dbSeeChanges

512

Es wird ein Laufzeitfehler verursacht, wenn ein anderer Benutzer versucht, die gerade in Bearbeitung befindlichen Daten zu ändern.

Zugriff auf einzelne Felder

Wissen Sie noch, wo wir uns in der Objekthierarchie der DAOs befinden? Richtig, wir haben die Ebene der Recordset-Objekte erreicht. Bislang war von Engines, Arbeitsbereichen, Datenbanken und Datensatzgruppen die Rede. Doch was ist mit dem ganz simplen Zugriff auf ein einzelnes Feld? Nun, natürlich ist das kein Problem. Jedes Recordset-Objekt steht bekanntlich für eine Datensatzgruppe. Jede Datensatzgruppe enthält einen oder mehrere Datensätze. Jeder dieser Datensätze enthält eines oder mehrere Felder. Diese werden durch eine Fields-Auflistung repräsentiert, jedes einzelne Feld entsprechend durch ein Field-Objekt. Im Grunde also ganz simpel. Erschwert wird das ganze nur, weil man sich zunächst von oben an das Fields-Objekt »heranarbeiten« muß.

Beispiel:

Die folgenden Anweisungen geben den Inhalt des Feldes Title des ersten Datensatzes der Tabelle Titles der Datenbank Biblio.MDB aus.

Dim Db As Database, Rs As Recordset, F As String
Set Db = DBEngine.Workspaces(0).OpenDatabase("Biblio.mdb")
Set Rs = Db.OpenRecordset("Titles", dbOpenDynaset)
F = Rs1.Fields("Title").Value
MsgBox F

Achten Sie bitte genau darauf, wie sich in den Anweisungen die DAO-Objekthierarchie widerspiegelt. Wie muß eine Anweisung lauten, die statt dessen das erste Feld im aktuellen Datensatz der Tabelle Titles ausgibt? Zum Beispiel wie folgt:

Set Rs = Db.OpenRecordset("Title")
F = Rs!Title

Keine Angst, es geht noch alles mit rechten Dingen zu. Es wurde lediglich eine (völlig legale) Abkürzung für den Zugriff auf ein einzelnes Feld verwendet.

Indizes

Die Bedeutung der Indizes wurde bereits in Kapitel 17.10 erklärt, eine weitere Definition soll Ihnen daher erspart bleiben. Indizes sind, wie Fields-Objekte, Unterobjekte eines TableDef-Objekts, gehören also zur Tabellenstruktur. Jeder einzelnen Index, der für eine Tabelle festgelegt wurde, wird durch ein eigenes Index-Objekt repräsentiert, alle Index-Objekte werden in der Indexes-Auflistung zusammengefaßt. Soll ein Index für eine Datensatzgruppe vom Typ Tabelle ausgewählt werden, müssen Sie lediglich der Index-Eigenschaft den Namen des Index zuweisen:

Set Rs = Db.OpenRecordset("Authors", dbOpenTable)
Rs.Index = "AuID"

Das Zuweisen an die Index-Eigenschaft genügt, um die Sortierreihenfolge der Datensatzgruppe zu ändern.

Quizfrage: Wie findet man heraus, wie die Indizes einer Tabelle heißen? Zum Beispiel durch folgende Anweisungen:

For Each X in db.TableDefs("Titles").Indexes
Debug.Print X.Name
Next

Um über das Datensteuerelement auf die einzelnen Index-Objekte zugreifen zu können, ist ein kleiner Umweg über die Database-Eigenschaft notwendig:

Dim X As Index
For Each X In Data1.Database.Tabledefs("Titles").Indexes
Debug.Print X.Name
Next X

Anlegen von Indizes

Die Art und Weise, wie Indizes angelegt werden, hängt natürlich davon ab, auf welche Weise die Tabelle angelegt wurde. Verwenden Sie dafür Microsoft Access oder VisData in Visual Basic, genügt es, das Feld auszuwählen und die entsprechende Option oder Schaltfläche zu betätigen. Wird die Datenbank bzw. Tabelle dagegen programmatisch angelegt, müssen die entsprechenden Eigenschaft des Index-Objekt gesetzt werden.

Tabelle 18.6:
Die wichtigsten Eigenschaften eines Index-Objekts

Eigenschaft

Bedeutung, wenn True ...

Foreign

Bei dem Feld handelt es sich um einen Fremdschlüssel, der einen Datensatz in einer anderen Tabelle eindeutig identifiziert. In dieser Tabelle spielt das gleiche Feld die Rolle des Primärschlüssels.

IgnoreNulls

Felder, die den Wert Null besitzen, werden nicht in den Index übernommen.

Primary

Es handelt es sich um einen Primärindex.

Required

Das Feld darf keinen Nullwert besitzen.

Unique

Der Feldinhalt darf nur einmal vorkommen.

 

Für das Anlegen eines neuen Index benötigen Sie als erstes eine Variable vom Index und für jeden Feldnamen ein eigenes Field-Objekt. Nachdem alle Indexattribute gesetzt wurden, wird das Index-Objekt über eine Append-Methode in die Index-Auflistung des zuständigen TableDef-Objekts aufgenommen. Alles klar? Sie sehen an diesem Beispiel, daß die grundlegenden Gesetzmäßigkeiten beim Umgang mit Objekten, die bereits in Kapitel 5 und 6 (eindringlich) erläutert wurden, besonders bei den DAO-Objekten eine wichtige Rolle spielen.

Beispiel:

Natürlich können Sie auch zu jeder bestehenden Datenbank Indizes hinzufügen. In dem folgenden Beispiel wird die Tabelle Adressen um einen Index mit dem Namen inxNachnamen erweitert.

Private Sub cmdIndexAnlegen_Click()
' Die Datenbank wird geöffnet
Set Db = DBEngine.Workspaces(0).OpenDatabase _
(DatenbankName)
' Eine Datensatzgruppe vom Typ Tabelle wird geöffnet
Set Td = Db.TableDefs("Adressen")
' Ein neues Index-Objekt wird angelegt
Set NeuerIndex = Td.CreateIndex("inxNachname")
' Das Index-Objekt enthält ein Feld
Set NeuesIndexFeld = NeuerIndex.CreateField("Nachname")
' Die Attribute des Index-Objektes werden gesetzt
NeuerIndex.Required = True
NeuerIndex.Fields.Append NeuesIndexFeld
' Der Index wird in die Indexes-Auflistung der Tabelle
' aufgenommen
Td.Indexes.Append NeuerIndex
Db.Close
End Sub

Wer ist nun der Schnellere?

Besonders spannend ist natürlich die Frage, welchen Geschwindigkeitsvorteil die Verwendung von Indizes bringt. Um es vorweg zu nehmen, wie bereits ein einfacher Vergleich zeigt, ist der Performance-Gewinn beachtlich. Bei einer Suche in einer Tabelle mit 2000 Datensätzen ist die Seek-Methode und einer anschließenden Schleife aus MoveNext-Methoden gegenüber einer Kombination, aus einer FindFirst-Methode und weiteren FindNext-Methoden, die in einer Schleife so lange wiederholt werden, bis kein Datensatz mehr gefunden werden konnte, um den Faktor 20 bis 25 schneller. Auch wenn dieses Ergebnis nicht uneingeschränkt verallgemeinert werden darf, macht es doch die Performance-Unterschiede beider Suchverfahren deutlich.

Die Rolle der Schlüssel

Schlüssel sind ein fundamentaler Begriff in der Welt relationaler Datenbanken. Bei der Jet-Engine werden sie unter dem Namen Index »getarnt«. Es gibt daher keine Key-Objekte, denn ein Index-Objekt spielt die Rolle eines Schlüssels. Ein Schlüssel (engl. key) ist ein Feld in einem Datensatz, das diesen eindeutig identifiziert. Innerhalb einer Tabelle mit Kundenstammdaten wird das Feld mit der Kundennummer in der Regel die Rolle eines Schlüssels spielen. Ein Schlüssel wird als Primärschlüssel (engl. primary key) bezeichnet, wenn es sich um ein Feld handelt, dessen Inhalt in der gesamten Tabelle nur einmal vorkommt. Ein Beispiel wäre die Kundennummer, die nur einmal vergeben wird und daher einen Kundendatensatz eindeutig identifiziert. In einer Tabelle mit offenen Aufträgen zeigt das Feld mit der Kundennummer auf die Tabelle der Kundenstammdaten, in der z. B. die Anschrift abgelegt ist. Das Kundennummerfeld spielt in der Auftragstabelle die Rolle des Primärschlüssels. Das korrespondierende Feld in der Stammdatentabelle wird dagegen als Fremdschlüssel (engl. foreign key) bezeichnet.

Auch mehrere Felder können zu einem Primärschlüssel zusammengefaßt werden. In diesem Fall können die Inhalte der einzelnen Felder zwar doppelt vorkommen, eine Kombination aller zu dem Primärschlüssel gehörenden Felder muß jedoch eindeutig sein.

Merksatz:

Ein Primärschlüssel ist ein Index einer Tabelle, dessen Inhalt den Datensatz eindeutig identifiziert. Alle Felder anderer Tabellen, die den Primärschlüssel enthalten und dadurch einen Datensatz in der Tabelle mit dem Primärschlüssel eindeutig identifizieren, werden auch als Fremdschlüssel bezeichnet.

Ein Index wird zum Primärindex, wenn die Primary-Eigenschaft den Wert True besitzt. In diesem Fall erhält automatisch auch die Unique-Eigenschaft den Wert True. Besitzt nur die Unique-Eigenschaft den Wert True, ist der Schlüssel zwar eindeutig, er wird aber nicht als Primärschlüssel verwendet. Man spricht auch von einem Sekundärschlüssel, der lediglich zum Sortieren der Tabelle, nicht aber zur Identifizierung eines Datensatzes verwendet wird.

Die Festlegung, ob ein Feld die Rolle eines Schlüssels spielt, wird im allgemeinen beim Erstellen einer Tabelle festgelegt. Wird die Tabelle erstellt, müssen die entsprechenden Optionen angekreuzt werden. Soll das Festlegen eines Index dagegen im Programmcode geschehen, müssen die Eigenschaften Primary, Unique und Foreign des Index-Objekts entsprechend auf True gesetzt werden.

*** Bild fehlt!!!

Bild 18.4: VisData erlaubt auch das Anlegen von Schlüsseln, d. h. Index-Objekten.

»Beziehungskisten« mit Relationen

Zurück zur DAO-Objekthierarchie. Wir bewegen uns von den Fields- und Indexes-Objekten wieder eine Ebene nach oben und kommen zum Relations-Objekt, das, als Unterobjekt eines Database-Objekts, eine Auflistung von Relation-Objekten enthält. Was steckt dahinter? Über Relationen werden bei der Jet-Engine die Regeln der referentiellen Integrität eingehalten (mehr dazu gleich). In einer sog. Relation wird die Beziehung zwischen Feldern zweier Tabellen werden. Ein typisches Beispiel ist wieder einmal die Auftragsbearbeitung, in der es eine Kundenstammdatentabelle (Primärtabelle) und eine Auftragstabelle (Fremdtabelle) gibt. Wann immer ein neuer Auftrag in der Auftragstabelle angelegt werden soll, prüft die Jet-Engine automatisch, ob in der Kundenstammdatentabelle ein Datensatz existiert, der die angegebene Kundennummer als Primärschlüssel besitzt (dies wird über die Attributes-Eigenschaft eines Relation-Objekts festgelegt). Ist dies nicht der Fall, wird kein neuer Datensatz angelegt und ein Laufzeitfehler ist die Folge. Diese Form von Beziehungen werden über Relation-Objekte definiert. Alle Beziehungen zwischen Tabellen einer Datenbank werden in der Relation-Auflistung zusammengefaßt.

Relationen werden entweder mit Hilfe eines Tools, z. B. Microsoft Access (leider nicht mit VisData) oder im Programm über die CreateRelation-Methode des Database-Objekts angelegt.

Syntax

Set Variable = Database.CreateRelation([Name[, Primärtabelle _
[, Fremdtabelle[, Attribute]]]])

Die (optionalen) Parameter und ihre Bedeutung

Name

Name der Relation

Primärtabelle

Name der Primärtabelle, d. h. jener Tabelle, die den Primärschlüssel enthält und auf der linken Seite der 1:n-Beziehung (oder 1:1-Beziehung) steht.

Fremdtabelle

Name der Fremdtabelle, d. h. jener Tabelle, die den (oder die) Fremdschlüssel enthält und auf der rechten Seite der 1:n-Beziehung (oder 1:1-Beziehung) steht.

Attribute

Weitere Einstellungen, die festlegen, auf welche Weise die Regeln der referentiellen Integrität umgesetzt werden.

Die wichtigste Eigenschaft eines Relation-Objekts lautet Attributes, denn über sie wird die Art der Beziehung festgelegt. Die wichtigsten Einstellungen dieser Eigenschaft sind in Tabelle aufgelistet.

Tabelle 18.7:
Die wichtigsten Einstellungen der Attributes-Eigenschaft eines Relation-Objekts

Konstante

Bedeutung

dbRelationUnique

Es handelt sich um eine 1:1-Relation.

dbRelationDontEnforce

Es wird keine referentielle Integrität erzwungen, d. h. die Beziehung zwischen einzelnen Felder der Primär- und Fremdtabelle wird nicht dazu benutzt, die Regeln der referentiellen Integrität umzusetzen.

dbRelationUpdateCascade

Update-Operationen in der Primärtabelle wirken sich automatisch auch auf die Fremdtabelle aus. Wird z. B. die Kundennummer in der Primärtabelle geändert, paßt die Jet-Engine die Kundennummern in der Fremdtabelle an.

dbRelationDeleteCascade

Delete-Operationen in der Primärtabelle wirken sich automatisch auch auf die Fremdtabelle aus. Wird eine Datensatz in der Kundentabelle gelöscht, löscht die Jet-Engine alle Datensätze mit der Kundennummer in der Fremdtabelle.

 

Die Sache mit der referentiellen Integrität

Wozu werden Relationen überhaupt benötigt? Ganz einfach, Relationen legen »verwandtschaftliche« Beziehungen zwischen zwei Tabellen fest, die dazu führen, daß Änderungen in einer Tabelle automatisch dazu führen, daß die übrigen Tabellen, zwischen denen eine Relation besteht, aktualisiert werden. Die Kundentabelle muß dafür wieder als Beispiel herhalten. Wird eine Kundennummer in der Stammdatentabelle geändert, sollten sich idealerweise auch alle Felder, die die Kundennummer enthalten, in den übrigen Tabellen anpassen. Dadurch wird z. B. gewährleistet, daß die Tabelle mit den noch zu bearbeitenden Aufträgen die aktuelle Kundennummer enthält. Normalerweise müßte diese Aktualisierung manuell erfolgen. Existiert jedoch zwischen der Stammdatentabelle und der Auftragstabelle eine Relation (in diesem Fall 1:n), kümmert sich die Jet-Engine automatisch um die Aktualisierung. Dieser Vorgang wird unter dem Oberbegriff Referentielle Integrität zusammengefaßt. Seit der Jet-Engine 2.0, d. h. seit Visual Basic 4.0, können die Regeln der referentiellen Integrität auch direkt auf der Ebene der Datenzugriffsobjekte implementiert werden.

Merksatz:

Referentielle Integrität bedeutet, daß die Jet-Engine automatisch prüft, ob beim Löschen oder Aktualisieren eines Datensatzes in einer Primärtabelle Datensätze in der Fremdtabelle, zu der eine Relation besteht, betroffen sind und automatisch Anpassungen in der anderen Tabelle vornimmt. Referentielle Integrität soll die Integrität der Datenbank gewährleisten, ohne daß dies vom Programmierer durch Programmcode umgesetzt werden muß.

Beispiel:

In dem folgenden Beispiel wird die Datenbank Kundentest.MDB um eine Relation mit dem Namen AutomatischesKundenNrUpdate erweitert, die eine Beziehung zwischen dem Feld KundenNr der Tabelle Kundenstammdaten und dem Feld KundenNr der Tabelle Auftragsdaten herstellt.

' Neues Relationsobjekt mit 1:n-Relation anlegen
Dim NeueRelation As Relation
Dim NeuesFeld As Field
' Erst einmal die Datenbank öffnen
Set Db = Workspaces(0).OpenDatabase("Kundentest.mdb")
' Neue Relation anlegen
Set NeueRelation = Db.CreateRelation _
("AutomatischesKundenNrUpdate")
' Attribute festlegen
NeueRelation.Table = "Kundenstammdaten"
NeueRelation.ForeignTable = "Aufträge"
NeueRelation.Attributes = dbRelationUpdateCascade
' Neues Field-Objekt anlegen
Set NeuesFeld = NeueRelation.CreateField("KundenNr")
NeuesFeld.ForeignName = "KundenNr"
' Field-Objekt an neue Relation hängen
NeueRelation.Fields.Append NeuesFeld
' Neue Relation an Relations-Auflistung hängen
Db.Relations.Append NeueRelation

Die Relation AutomatischesKundenNrUpdate sorgt dafür, daß, wann immer sich in der Tabelle Kundenstammdaten eine Kundennummer ändert, alle KundenNr-Felder der Tabelle Auftragsdaten automatisch aktualisiert werden.

Tips:

Relationen werden am einfachsten innerhalb von Microsoft Access hergestellt, da Sie dort die Beziehungen mit der Maus herstellen können. Das Festlegen von Relationen durch das Definieren neuer Relation-Objekte sollte nur in Ausnahmefällen erfolgen.

*** Bild fehl!!!

Bild 18.5: Microsoft Access zeigt die angelegte Relation sehr übersichtlich an.

Gespeicherte Abfragen: QueryDef-Objekte

Wir bleiben im DAO-Objektmodell auf der gleichen Ebene und kommen jetzt zu den QueryDef-Objekten. Ein QueryDef-Objekt enthält eine SQL-Abfrage, die in der Datenbank gespeichert wird. Der Vorteil besteht darin, daß die Abfrage bereits in »übersetzter« Form vorliegt und daher in der Regel schneller ausgeführt werden kann als eine SQL-Abfrage, die in Gestalt eines SQL-Strings an die Jet-Engine geschickt wird und dort zunächst ausgewertet werden muß. Lassen Sie sich durch den etwas technisch klingenden Begriff nicht abschrecken (man könnte »QueryDef« mit Abfragedefinition übersetzen). Der Umgang mit QueryDefs ist wirklich ganz einfach, zumal die Gesetzmäßigkeiten, die bereits für die übrigen Datenzugriffsobjekte hervorgehoben wurden, natürlich auch für die QueryDef-Objekte gelten. QueryDef-Objekte sind Unterobjekte eines Database-Objekts und werden in einer QueryDefs-Auflistung zusammengefaßt. Sie stehen damit auf der gleichen Ebene wie die Recordset-Objekte.

QueryDefs werden entweder innerhalb von Microsoft Access als gespeicherte Datenbankabfragen oder in Visual Basic über eine CreateQueryDef-Methode (des Database-Objekts) anlegen.

Tabelle 18.8:
Die wichtigsten Eigenschaften eines QueryDef-Objekts

Eigenschaft

Bedeutung

DateCreated

Enthält das Datum, an dem das QueryDef-Objekt erstellt wurde.

LastUpdated

Erhält das Datum, an dem das QueryDef-Objekt zuletzt aktualisiert wurde.

Name

Name des gespeicherten QueryDef-Objekts.

RecordsAffected

Enthält die Anzahl an Datensätzen, die durch die zuletzt ausgeführte Execute-Methode betroffen wurden.

ReturnRecords

Legt fest, ob eine SQL-Abfrage, die über die SQLPassthrough-Option direkt an einen SQL-Server geschickt wird, Datensätze zurückgibt (ReturnRecords=True) oder nicht (ReturnRecords=False).

SQL

Enthält den Text der in dem QueryDef-Objekt gespeicherten SQL-Abfrage.

 

Tabelle 18.9:
Die wichtigsten Methoden eines QueryDef-Objekts

Methode

Bedeutung

Execute

Bringt eine Aktionsabfrage (engl. action query) zur Ausführung. Eine solche Abfrage gibt keine Datensätze zurück, sondern verändert Datensätze.

OpenRecordset

Erstellt ein neues Recordset-Objekt auf der Basis der gespeicherten Abfrage.

 

Ein QueryDef-Objekt zu öffnen heißt, eine vordefinierte Datenbankabfrage auszuführen. Über die SQL-Eigenschaft eines QueryDef-Objekts steht diese Abfrage innerhalb des Visual-Basic-Programms zur Verfügung. Alternativ kann man ein SQL-Statement auch innerhalb des Visual-Basic-Programms »zusammenbasteln« und der SQL-Eigenschaft zuweisen. Allerdings führt jede Änderung an der SQL-Eigenschaft dazu, daß das SQL-Statement von der Jet-Engine erneut übersetzt werden muß. Der Geschwindigkeitsvorteil geht dadurch unter Umständen verloren.

Die Eigenschaften und Methoden eines QueryDef-Objekts sind schnell abgehandelt (siehe Tabellen 18.8 und 18.9). Die wichtigste Eigenschaft kennen Sie bereits. Es ist die SQL-Eigenschaft, die das in der Abfrage gespeicherte SQL-Statement enthält.

Für Umsteiger

Die veraltete ListParameters-Methode der Jet-Engine 1.1 wird nicht mehr unterstützt. Statt dessen stehen die einzelnen Parameter, sofern vorhanden, über die Parameters-Auflistung eines QueryDef-Objekts zur Verfügung.

Natürlich ist der Umgang mit QueryDef-Objekten kein reiner Selbstzweck, sondern hat eine ganz bestimme Aufgabe: Die Durchführung von Abfragen zu beschleunigen. Da die Analyse und Optimierung des SQL-Statements bereits von der Jet-Engine vorgenommen wurde, werden gerade komplexe Statements oder Aktionsabfragen (z. B. UPDATE-Statement) schneller ausgeführt. Bei einfachen SELECT-Abfragen dürften sich dagegen keine Performance-Vorteile bemerkbar machen.

Öffnen eines QueryDef-Objekts

Um mit einem bereits angelegten QueryDef-Objekt zu arbeiten, d. h. die in ihm enthaltenen Abfrage ausführen zu können, muß das Objekt lediglich geöffnet werden:

Dim Q As QueryDef
Dim Db As Database
Dim Rs As Recordset
Set Db = DBEngine.Workspaces(0).OpenDatabase("Biblio.mdb")
Set Q = Db.QueryDefs("[All Titles]")
Set Rs = Q.OpenRecordset(dbOpenSnapshot)

Das Ergebnis ist eine Objektvariable Rs, die alle Datensätze umfaßt, die von der Abfrage zurückgegeben wurden.

Für Umsteiger

Die OpenQueryDef-Methode steht nur noch aus Kompatibilitätsgründen zur Verfügung und sollte nicht mehr verwendet werden. Ein QueryDef-Objekt wird nun direkt über die QueryDefs-Auflistung geöffnet.

QueryDefs in die Karten geschaut

Was ist zu tun, wenn man eine Abfrage benutzen möchte und ihren genauen Namen nicht kennt? Oder wenn man sich in einer »fremden« Datenbank einen Überblick über eventuell vorhandene Abfragen verschaffen möchte? Ganz klar, dafür ist die QueryDefs-Auflistung da. Um z. B. zu erfahren, ob eine bereits existierende Datenbank über QueryDef-Objekte verfügt, muß man sich lediglich alle Mitglieder der QueryDefs-Auflistung des aktuellen Database-Objekts ausgeben lassen:

Dim Db As Database
Dim X As QueryDef
Set Db = DBEngine.Workspaces(0).OpenDatabase("Biblio.mdb")
For Each X In Db.QueryDefs
Debug.Print X.Name, X.SQL
Next X

Bei der Ausführung dieses Beispiels werden Sie feststellen, daß die Demo-Datenbank Biblio.MDB bereits über ein QueryDef-Objekt mit dem Namen »All Titles« verfügt. In VisData wird dieses Objekt zwar auch als Tabelle aufgelistet, es handelt sich aber in Wirklichkeit, wie Sie jetzt wissen, um eine gespeicherte Abfrage, die beim Öffnen eine neue Datensatzgruppe anlegt.

      1. Anlegen neuer QueryDef-Objekte

Neue QueryDef-Objekte werden über die CreateQueryDef-Methode der QueryDefs-Auflistung hinzugefügt. Beim Aufruf dieser Methode werden der Name des QueryDef-Objekts und das später auszuführende SQL-Statement übergeben.

Beispiel:

Das folgende Beispiel legt ein QueryDef-Objekt mit dem Namen TestAbfrage an, das aus der Tabelle Kundenstammdaten alle Datensätze heraussucht, deren Feld Nachname den Wert »Schmitt« besitzt. Aus rein didaktischen Gründen wird das SQL-Statement innerhalb des Programms formuliert, was den Performance-Vorteil, sofern er überhaupt existiert, bei der ersten Abfrage zunichte macht, da die Jet-Engine die Abfrage erst einmal intern übersetzen muß. Es handelt sich zudem um eine relativ triviale Abfrage, doch im Unterschied zu den bisherigen Beispielen wird sie als QueryDef-Objekt in der Datenbank gespeichert:

Private Sub cmdQueryDefAnlegen_Click()
On Error GoTo cmdQueryDefAnlegen_Error
Dim Q As QueryDef
Dim Db As Database
Dim Rs As Recordset
Dim SQLString As String
Set Db = DBEngine.Workspaces(0).OpenDatabase _
("Visual Basic Kompendium – Probe\Sunybook.mdb")
SQLString = "SELECT * FROM Kundenstammdaten WHERE _
Nachname = 'Schmitt'"
Set Q = Db.CreateQueryDef("TestAbfrage", SQLString)
Set Rs = Q.OpenRecordset(dbOpenSnapshot)
Exit Sub
cmdQueryDefAnlegen_Error:
Select Case Err.Number
Case 3012
' QueryDef-Objekt existiert bereits – dann löschen
Db.QueryDefs.Delete "TestAbfrage"
Resume
Case Else
MsgBox Err.Description & " (" & Err.Number & ")", _
vbExclamation, "Laufzeitfehler"
End Select
End Sub

Da die Datenbank bereits eine Abfrage mit dem Namen TestAbfrage enthalten könnte, enthält die Prozedur eine Fehlerbehandlungsroutine, die in diesem Fall das QueryDef-Objekt über eine Delete-Methode aus der QueryDefs-Auflistung entfernt.

Die Fehler lauern (wie immer) überall

Insbesondere beim Umgang mit QueryDef-Objekten wird man die Notwendigkeit einer konsistenten Laufzeitfehlerbehandlung relativ schnell einsehen. Nicht, daß QueryDef-Objekte fehlerhaft implementiert werden können, doch beim Definieren neuer QueryDef-Objekte ergibt sich schnell die Situation, daß versucht wird, ein QueryDef-Objekt zu erstellen, das bereits existiert, oder ein QueryDef-Objekt zu öffnen, das noch nicht definiert wurde.

Beispiel:

Im folgenden wird ein kleines Beispiel vorgestellt, das die CreateQueryDef-Methode aufruft und für den Fall, daß das Objekt bereits existiert, auf die QueryDef-Auflistung zurückgreift.

Private Sub cmdAbfrage()
On Error GoTo cmdAbfrage_Error
Set Q = Db.CreateQueryDef("TestQuery")
Exit Sub
cmdAbfrage_Error:
Select Case Err.Number
Case 3012
Set Q = Db.QueryDefs("TestQuery")
Resume Next
Case Else
MsgBox Err.Description & "(" & Err.Number & ")", _
vbExclamation, "Laufzeitfehler"
End Select
End Sub

Und nun etwas Action bitte!

Normale Datenbankabfragen resultieren in einer Gruppe von Datensätzen, die als Ergebnis der Abfrage zurückgegeben wird. Ein SQL-Statement muß jedoch nicht zwangsläufig Datensätze zurückgeben. Sog. Aktionsabfragen führen eine Veränderung an der Datenbank durch, geben aber keine Datensätze zurück. Diese SQL-Statements werden über die Execute-Methode eines QueryDef-Objekts ausgeführt.

Beispiel:

Die folgende Aktionsabfrage trägt in alle Status-Felder der Tabelle Titles einen speziellen Wert ein, wenn der Wert des Feldes Ausgeliehen den Wert True besitzt.

Private Sub cmdAktion_Click()
Dim Q As QueryDef
Dim SQLString As String
' SQL-String zusammenbauen
SQLString = "UPDATE Titles SET Status = 'Nicht mehr da' _
WHERE Ausgeliehen = True"
' QueryDef-Objekt anlegen
Set Q = datTest.Database.CreateQueryDef("Aktion1")
Q.SQL = SQLString
' Aktionsabfrage ausführen
Q.Execute
' Recordset-Objekt des Datensteuerelements neu aufbauen
Q.Close
datTest.Refresh
End Sub

Hinweis:

Über die RecordsAffected-Eigenschaft des Database- bzw. QueryDef-Objekts erfährt man, wie viele Datensätze von der Execute-Methode betroffen wurden.

Jetzt ist es an der Zeit, Ihre bisherigen SQL-Kenntnisse zu testen. Wie muß ein SQL-Statement lauten, das alle Datensätze herausfiltert, in denen das Feld Ausgeliehen den Wert True besitzt? Wie werden diese Datensätze in das Recordset-Objekt des Datensteuerelements Data1 übertragen? Hier zunächst das SQL-Statement:

SQLString = "SELECT * FROM Titles WHERE Ausgeliehen = True"

Indem diese Stringvariable der RecordSource-Eigenschaft des Datensteuerelements zugewiesen wird, stehen die herausgefilterten Datensätze über das Datensteuerelement zur Verfügung. Vergessen Sie allerdings nicht, die Refresh-Methode auszuführen, die die Datensatzgruppe neu aufbaut:

Data1.RecordSource = SQLString
Data1.Refresh

Und jetzt das Ganze mit Parametern

Einen großen Teil ihrer Flexibilität verdanken QueryDef-Objekte dem Umstand, daß man den in ihnen enthaltenen SQL-Statements Parameter übergeben kann. Das setzt allerdings voraus, daß die Namen der einzelnen Parameter beim Erstellen des SQL-Statements über die Klausel PARAMETERS festgelegt wurden. Die benötigten Parameter werden dem QueryDef-Objekt vor seiner Ausführung als »Unterobjekte« vom Typ Parameter übergeben. Wie es sich gehört, werden alle Parameter-Objekte eines QueryDef-Objekts in einer Parameters-Auflistung zusammengefaßt.

Das QueryDef-Objekt aus dem vorletzten Beispiel besaß den Nachteil, daß die Abfrage stets gleich formuliert war. Sehr viel praktischer wäre es natürlich, wenn man den zu suchenden Nachnamen erst vor dem Öffnen des QueryDef-Objekts festlegen könnte. Möglich wird dies durch eine parametrisierte Abfrage. Möchte man neben den Schmitts auch die Mayers und Müllers dieser Welt ermitteln, muß das SQL-Statement als erstes einen Parameter erhalten:

SQLString = "PARAMETERS VarName1 Text; SELECT * FROM _
Kundenstammdaten WHERE Nachname = VarName1"

Damit ist die Jet-Engine bereits moralisch auf die Übergabe eines Parameters, in diesem Fall lautet sein Name VarName1, vorbereitet. Die eigentliche Übergabe erfolgt über den Zugriff auf ein Parameter-Objekt in der Parameters-Auflistung:

Q.Parameters("VarName1") = SQLParameter

Bei SQLParameter handelt es sich um eine gewöhnliche Stringvariable. Wird das QueryDef-Objekt Q geöffnet, werden automatisch alle Parameter-Objekte durch die über die PARAMETERS-Klausel festgelegten Variablen ersetzt.

Hinweis:

Dies für alle Fans des Datensteuerelements. Parametrisierte QueryDef-Abfragen sind auch über das Datensteuerelement möglich.

Beispiel:

Das folgende Beispiel zeigt eine einfache Anwendung über eine parametrisierte QueryDef-Abfrage.

Private Sub cmdQueryDefPara_Click()
On Error GoTo cmdQueryDefPara_Error
Dim Q As QueryDef, Db As Database, Rs As Recordset
Dim SQLString As String, SQLParameter As String
SQLParameter = txtQueryDef.Text
If SQLParameter = "" Then SQLParameter = "Schmitt"
Set Db = DBEngine.Workspaces(0).OpenDatabase _
("Visual Basic Kompendium – Probe\Sunybook.mdb")
SQLString = "PARAMETERS VarName1 Text; SELECT * " _
FROM Kundenstammdaten WHERE Nachname = VarName1"
Set Q = Db.CreateQueryDef("TestAbfrage", SQLString)
Q.Parameters("Varname1") = SQLParameter
Set Rs = Q.OpenRecordset(dbOpenSnapshot)
Exit Sub
cmdQueryDefPara_Error:
Select Case Err.Number
Case 3012
' QueryDef-Objekt existiert bereits – dann löschen
Db.QueryDefs.Delete "TestAbfrage"
Resume
Case Else
MsgBox Err.Description & " (" & Err.Number & ")", _
vbExclamation, "Laufzeitfehler"
End Sub

Da ein QueryDef-Objekt in einer Datenbank nur einmal angelegt werden kann, enthält die Prozedur eine Fehlerbehandlungsroutine, die den Fehler 3012 abfragt und das QueryDef-Objekt über die Delete-Methode aus der QueryDef-Auflistung löscht.

Die Beispieldatenbank stellt sich vor

Nachdem Sie im letzten Abschnitt die wichtigsten DAO-Objekte kennengelernt haben, wären jetzt eigentlich Beispiele an der Reihe, die zeigen, wie Sie z. B. Datensätze lokalisieren, löschen oder jene Dinge anstellen können, die Datenbankprogrammierer in aller Welt Tag ein und Tag aus mit Datenbanken unternehmen. Sie würden jedoch schnell feststellen, daß Ihnen das alles recht bekannt vorkäme, denn alle elementaren Datenbankoperationen wurden bereits in Kapitel 17 im Zusammenhang mit dem Recordset-Objekt des Datensteuerelements vorgestellt. Das Datensteuerelement und die DAOs sind kein Widerspruch. Im Gegenteil, sie ergänzen sich auf angenehme Weise. Was Sie in Kapitel 17 im Zusammenhang mit dem Datensteuerelement gelernt haben, sind es nichts anderes als exakt jene Datenoperationen, die Sie auch mit den DAO-Objekten durchführen können. Der einzige (und für die Datenbankpraxis durchaus bedeutende) Unterschied besteht darin, daß das Datensteuerelement auf der Ebene des Database-Objekts beginnt und die Workspace-Objekte nicht zugänglich sind.

Anstelle weiterer Beispiele wird im folgenden eine Mini-Datenbankanwendung vorgestellt, die ausschließlich mit DAO-Objekten realisiert wurde. Es handelt sich um eine Datenbank, mit der man eine Gebrauchtwagenbörse realisieren könnte. Sie finden die komplette Anwendung auf der Buch-CD-ROM in der Datei Autodemo.VBP. Um das Ganze überschaubar zu halten, wurden alle Daten in eine einzige Tabelle gepackt. In der Praxis würde man anders vorgehen und z. B. die Kfz-Daten in einer Tabelle ablegen. Falls Sie sich etwas näher mit Datenbankoperationen beschäftigen möchten, finden Sie eine erweiterte Variante auf der Buch-CD-ROM in der Datei AutodemoII.VBP. Diese Version arbeitet mit zwei Tabellen und ist um einiges komplizierter, da Sie hier zwei Tabellen miteinander synchronisieren müssen.

*** Bild fehlt!

Bild 18.6: Eine kleine Datenbankanwendung soll die Ausführung elementarer Datenbankoperationen mit den Datenzugriffsobjekten veranschaulichen.

Im folgenden wird das komplette Programmlisting der Datenbankanwendung vorgestellt. Da es ausführlich kommentiert ist, sollte es eigentlich keiner weiteren Erläuterungen bedürfen.

' ***********************************************
' Das Visual Basic 5 Kompendium – (C) Markt&Technik, 1997
'
' AutoDemoI.vdb – Zeigt den Umgang mit den Datenzugriffs-
' objekten (DAOs) beim Datenbankzugriff auf die
' Datenbank AutoDemo.mdb
' frmHaupt – Hauptformular des Programms
'
' Autor: Peter Monadjemi – Letzte Änderung: 19/5/97
'
' ***********************************************

Option Explicit
Public Db As Database
Public tdAutoTabelle As Recordset
Private NeuerDatensatz As Boolean
Private AnzahlDatensätze As Long
Private NeueDatenSatzNr As Long

' ***********************************************
' FelderAnzeigen – Überträgt den Inhalt des aktuellen
' Datensatzes in die Steuerelemente des Formulars
' ***********************************************
Sub FelderAnzeigen()
lblAutoNr.Caption = FeldEinlesen("AutoID")
txtAutotyp.Text = FeldEinlesen("Typ")
txtBaujahr.Text = FeldEinlesen("Baujahr")
txtBemerkung.Text = FeldEinlesen("Bemerkung")
txtFarbe.Text = FeldEinlesen("Farbe")
txtHersteller.Text = FeldEinlesen("Hersteller")
txtHubraum.Text = FeldEinlesen("Hubraum")
txtKmLeistung.Text = FeldEinlesen("KMLeistung")
txtPreis.Text = FeldEinlesen("Preis")
txtPS.Text = FeldEinlesen("PS")
txtVorbesitzer.Text = FeldEinlesen("AnzahlBesitzer")
txtZylinder.Text = FeldEinlesen("Zylinder")
chkMwstFähig.Value = Abs(FeldEinlesen("MwstFähig"))
chkEUImport.Value = Abs(FeldEinlesen("EUImport"))
If chkBildAnzeigen.Value = 1 Then
imgAuto.Picture = BildEinlesen()
Else
imgAuto.Picture = LoadPicture()
End If
DataChangedZurückSetzen
StatusAnzeigen
End Sub ' FelderAnzeigen

' ***********************************************
' FelderSpeichern – Überträgt den Inhalt der Steuerelemente
' des Formulars in den aktuellen Datensatz
' ***********************************************

Sub FelderSpeichern()
tdAutoTabelle.Fields("AutoID") = lblAutoNr.Caption
tdAutoTabelle.Fields("Typ") = txtAutotyp.Text
tdAutoTabelle.Fields("Baujahr") = txtBaujahr.Text
If txtBemerkung.Text = "" Then txtBemerkung.Text = _
"Keine besonderen Merkmale"
tdAutoTabelle.Fields("Bemerkung") = txtBemerkung.Text
tdAutoTabelle.Fields("Farbe") = txtFarbe.Text
tdAutoTabelle.Fields("Hersteller") = txtHersteller.Text
tdAutoTabelle.Fields("Hubraum") = txtHubraum.Text
tdAutoTabelle.Fields("KmLeistung") = txtKmLeistung.Text
tdAutoTabelle.Fields("Preis") = txtPreis.Text
tdAutoTabelle.Fields("PS") = txtPS.Text
tdAutoTabelle.Fields("AnzahlBesitzer") = _
txtVorbesitzer.Text
tdAutoTabelle.Fields("Zylinder") = txtZylinder.Text
tdAutoTabelle.Fields("MwstFähig") = _
CBool(chkMwstFähig.Value)
tdAutoTabelle.Fields("EUImport") = _
CBool(chkEUImport.Value)
If chkBildAnzeigen.Value = 1 Then
' Bilder erhalten eine Sonderbehandlung
BildSpeichern
End If
End Sub ' FelderSpeichern

' ***********************************************
' FeldInhalteLöschen – Löscht alle Felder
' ***********************************************
Sub FeldInhalteLöschen()
txtAutotyp.Text = ""
txtBaujahr.Text = ""
txtBemerkung.Text = ""
txtFarbe.Text = ""
txtHersteller.Text = ""
txtHubraum.Text = ""
txtKmLeistung.Text = ""
txtPreis.Text = ""
txtPS.Text = ""
txtVorbesitzer.Text = ""
txtZylinder.Text = ""
imgAuto.Picture = LoadPicture()
chkMwstFähig.Value = 0
chkEUImport = 0
End Sub ' FeldInhalteLöschen

' ***********************************************
' FeldÄnderungPrüfen – Feststellen, ob Änderung erfolgt ist
' ***********************************************
Sub FeldÄnderungPrüfen()
Dim tmpMsg As String
If DataChangedPrüfen Then
If mnuAutomatischSichern.Checked = True Then
cmdUpdate_Click
Else
tmpMsg = "Am aktuellen Datensatz wurden " & _
"Veränderungen vorgenommen." & vbCrLf
tmpMsg = tmpMsg & _
"Soll der Datensatz gespeichert werden?" & _
vbCrLf & vbCrLf
tmpMsg = tmpMsg & _
"Ja – Datensatz wird gespeichert" & vbCrLf
tmpMsg = tmpMsg & _
"Nein – Die Änderungen werden nicht übernommen"
If MsgBox(tmpMsg, vbQuestion + vbYesNo, _
"Datensatzsicherung") = vbYes Then
cmdUpdate_Click
End If
End If
End If
End Sub ' FeldÄnderungPrüfen

' ***********************************************
' DataChangedPrüfen – Prüft, ob die DataChanged-Eigenschaft
' den Wert True besitzt
' ***********************************************
Function DataChangedPrüfen() As Boolean
If txtHubraum.DataChanged Then GoTo Ende
If txtPS.DataChanged Then GoTo Ende
If txtZylinder.DataChanged Then GoTo Ende
If txtHersteller.DataChanged Then GoTo Ende
If txtAutotyp.DataChanged Then GoTo Ende
If txtBemerkung.DataChanged Then GoTo Ende
If txtFarbe.DataChanged Then GoTo Ende
If txtKmLeistung.DataChanged Then GoTo Ende
If txtPreis.DataChanged Then GoTo Ende
If txtVorbesitzer.DataChanged Then GoTo Ende
If chkEUImport.DataChanged Then GoTo Ende
If chkMwstFähig.DataChanged Then GoTo Ende
If imgAuto.DataChanged Then GoTo Ende
Exit Function
Ende:
DataChangedPrüfen = True
End Function ' DataChangedPrüfen

' ***********************************************
' DataChangedZurückSetzen – Setzt die DataChanged-Eigenschaft
' bei allen Eingabefeldern zurück
' ***********************************************
Sub DataChangedZurückSetzen()
txtHubraum.DataChanged = False
txtPS.DataChanged = False
txtZylinder.DataChanged = False
txtHersteller.DataChanged = False
txtAutotyp.DataChanged = False
txtBemerkung.DataChanged = False
txtFarbe.DataChanged = False
txtKmLeistung.DataChanged = False
txtPreis.DataChanged = False
txtVorbesitzer.DataChanged = False
chkEUImport.DataChanged = False
chkMwstFähig.DataChanged = False
imgAuto.DataChanged = False
End Sub ' DataChangedZurücksetzen

' ***********************************************
' chkBildAnzeigen_Click – Legt fest, ob Bilder angezeigt
' werden sollen oder nicht
' ***********************************************
Private Sub chkBildAnzeigen_Click()
cmdBildladen.Enabled = CBool(chkBildAnzeigen.Value)
End Sub ' chkBildAnzeigen_Click

' ***********************************************
' cmdBildladenen_Click – Lädt eine Bildatei
' ***********************************************
Private Sub cmdBildladen_Click()
Dim tmpDateiname As String
cdlDatei.Filter = "Bmp-Dateien|*.bmp|" & _
"Gif-Dateien|*.gif|Jpg-Dateien|*.jpg|Alle Dateien|*.*"
cdlDatei.ShowOpen
tmpDateiname = cdlDatei.filename
imgAuto.Picture = LoadPicture(tmpDateiname)
End Sub ' cmdBildladen_Click

' ***********************************************
' cmdFirst_Click – Geht einen Datensatz zurück
' ***********************************************
Private Sub cmdFirst_Click()
FeldÄnderungPrüfen
tdAutoTabelle.MoveFirst
FelderAnzeigen
End Sub ' cmdFirst_Click

' ***********************************************
' cmdLast_Click – Geht zum ersten Datensatz
' ***********************************************
Private Sub cmdLast_Click()
FeldÄnderungPrüfen
tdAutoTabelle.MoveLast
FelderAnzeigen
End Sub ' cmdLast_Click

' ***********************************************
' cmdLöschen_Click – Löscht einen Datensatz
' ***********************************************
Private Sub cmdLöschen_Click()
Dim tmpMsg As String
tmpMsg = "Datensatz wirklich löschen?" & vbCrLf & vbCrLf
tmpMsg = tmpMsg & _
"Die Daten sind unwiderbringlich verloren!"
If MsgBox(tmpMsg, vbQuestion + vbYesNo, _
"Datensatz soll gelöscht werden!") = vbYes Then
If NeuerDatensatz Then Exit Sub
tdAutoTabelle.Delete
tdAutoTabelle.MoveNext
If tdAutoTabelle.EOF Then tdAutoTabelle.MoveLast
AnzahlDatensätze = tdAutoTabelle.RecordCount
FelderAnzeigen
End If
End Sub ' cmdLöschen_Click


' ***********************************************
' cmdNeu_Click – Legt einen neuen Datensatz an
' ***********************************************
Private Sub cmdNeu_Click()
tdAutoTabelle.AddNew
NeuerDatensatz = True
FeldInhalteLöschen
DataChangedZurückSetzen
txtAutotyp.SetFocus
End Sub ' cmdNeu_Click

' ***********************************************
' cmdNext_Click – Geht zum nächsten Datensatz
' ***********************************************
Private Sub cmdNext_Click()
FeldÄnderungPrüfen
tdAutoTabelle.MoveNext
If tdAutoTabelle.EOF = True Then
MsgBox "Das Ende der Datensatzgruppe wurde erreicht." _
, vbExclamation, "Weiter geht es nicht mehr"
tdAutoTabelle.MoveLast
End If
FelderAnzeigen
End Sub ' cmdNext_Click

' ***********************************************
' cmdPrevious_Click – Geht zum zurückliegenden Datensatz
' ***********************************************
Private Sub cmdPrevious_Click()
FeldÄnderungPrüfen
tdAutoTabelle.MovePrevious
If tdAutoTabelle.BOF = True Then
MsgBox "Der Anfang der Datensatzgruppe wurde " & _
"erreicht.", vbExclamation, "Weiter geht es nicht mehr"
tdAutoTabelle.MoveFirst
End If
FelderAnzeigen
End Sub ' cmdPrevious_Click

' ***********************************************
' cmdUpdate_Click – Aktualisiert einen Datensatz
' ***********************************************
Private Sub cmdUpdate_Click()
On Error GoTo errUpdate
' Befinden wir uns im Append-Modus?
If NeuerDatensatz = True Then
NeuerDatensatz = False ' Jetzt nicht mehr
Else
' Tabelle in Edit-Modus schalten
tdAutoTabelle.Edit
End If
AnzahlDatensätze = tdAutoTabelle.RecordCount
StatusAnzeigen
FelderSpeichern
tdAutoTabelle.Update ' Tabelle aktualisieren
StatusAnzeigen "Datensatz Nr. " & NeueDatenSatzNr & _
" wurde gespeichert"
NeueDatenSatzNr = NeueDatenSatzNr + 1
' Daten geändert Flag zurücksetzen
DataChangedZurückSetzen
Exit Sub
errUpdate:
MsgBox "Laufzeitfehler: " & Err.Number, vbExclamation, _
Err.Description
Stop
End Sub ' cmdUpdate_Click

' ***********************************************
' Form_Load
' ***********************************************
Private Sub Form_Load()
On Error Resume Next
Dim DBName As String, tmpMsg As String
DBName = App.Path
If Right(DBName, 1) <> "\" Then DBName = DBName & "\"
DBName = DBName & "Autodemo1.mdb"
Set Db = DBEngine.Workspaces(0).OpenDatabase(DBName)
If Err.Number Then GoTo DatenbankNichtGefunden
Set tdAutoTabelle = Db.OpenRecordset("Autodaten", _
dbOpenDynaset)
tdAutoTabelle.MoveLast
tdAutoTabelle.MoveFirst
AnzahlDatensätze = tdAutoTabelle.RecordCount
NeueDatenSatzNr = AnzahlDatensätze
FelderAnzeigen
chkBildAnzeigen.Value = 0
cmdBildladen.Enabled = CBool(chkBildAnzeigen.Value)
Exit Sub
DatenbankNichtGefunden:
tmpMsg = "Achtung!" & vbCrLf & vbCrLf
tmpMsg = tmpMsg & "Die Datenbankdatei " & DBName & _
" konnte" & vbCrLf
tmpMsg = tmpMsg & "im aktuellen Verzeichnis " & CurDir & _
" nicht gefunden werden." & vbCrLf & vbCrLf
tmpMsg = tmpMsg & "Bitte kopieren Sie die Datei " & _
"in das aktuelle Verzeichnis und starten Sie" & vbCrLf
tmpMsg = tmpMsg & "das blöde Programm neu."
MsgBox tmpMsg, vbCritical, _
"Fehler beim Öffnen der Datenbank"
End
End Sub ' Form_Load

' ***********************************************
' Form_Unload
' ***********************************************
Private Sub Form_Unload(Cancel As Integer)
FeldÄnderungPrüfen ' Daten gegebenenfalls sichern
Db.Close ' Datenbank schließen
End Sub ' Form_Unload

' ***********************************************
' mnuAutomatischSichern_Click
' ***********************************************
Private Sub mnuAutomatischSichern_Click()
Static AutomatischSichern As Boolean
AutomatischSichern = Not AutomatischSichern
mnuAutomatischSichern.Checked = AutomatischSichern
End Sub ' mnuAutomatischeSichern_Click

' ***********************************************
' mnuBeenden_Click
' ***********************************************
Private Sub mnuBeenden_Click()
Unload Me
End Sub ' mnuBeenden_Click

' ***********************************************
' mnuGeheZu_Click
' ***********************************************
Private Sub mnuGehezu_Click()
Dim tmpDatensatzNr As Long, curBuchmarke As Variant
tmpDatensatzNr = InputBox _
("Geben Sie die Datensatznummer ein: ", _
"Auswahl eines Datensatzes", 1)
' Aktuellen Datensatz merken
curBuchmarke = tdAutoTabelle.Bookmark
tdAutoTabelle.FindFirst "AutoID=" & tmpDatensatzNr
' Wurde Datensatz gefunden?
If tdAutoTabelle.NoMatch Then
MsgBox "Diesen Datensatz gibt es leider nicht!", _
vbExclamation, "Alles bleibt beim alten"
' Alten Datensatz wieder zurück
tdAutoTabelle.Bookmark = curBuchmarke
Exit Sub
End If
FelderAnzeigen
End Sub ' mnuGehezu_Click

' ***********************************************
' StatusAnzeigen – gibt Datensatznummer aus
' ***********************************************
Sub StatusAnzeigen(Optional tmpStatustext As String)
Dim tmpText As String
tmpText = "Datensatz Nr. " & _
tdAutoTabelle.AbsolutePosition + 1
tmpText = tmpText & " (von " & AnzahlDatensätze & ")"
staStatus.Panels("DatensatzNr").Text = tmpText
If tmpStatustext <> "" Then
staStatus.Panels("Modus").Text = tmpStatustext
End If
End Sub ' StatusAnzeigen

' ***********************************************
' txtAutoID_GotFocus
' ***********************************************
Private Sub txtAutoID_GotFocus()
TextMarkieren
End Sub ' txtAutoID_GotFocus

' ***********************************************
' txtFarbe_GotFocus
' ***********************************************
Private Sub txtFarbe_GotFocus()
TextMarkieren
End Sub ' txtFarbe_GotFocus

' ***********************************************
' txtHersteller_GotFocus
' ***********************************************
Private Sub txtHersteller_GotFocus()
TextMarkieren
End Sub ' txtHersteller_GotFocus

' ***********************************************
' txtKmLeistung_GotFocus
' ***********************************************
Private Sub txtKmLeistung_GotFocus()
TextMarkieren
End Sub ' txtKmLeistung_GotFocus

' ***********************************************
' txtPreis_GotFocus
' ***********************************************
Private Sub txtPreis_GotFocus()
TextMarkieren
End Sub ' txtPreis_GotFocus

' ***********************************************
' txtPS_GotFocus
' ***********************************************
Private Sub txtPS_GotFocus()
TextMarkieren
End Sub ' txtPS_GotFocus

' ***********************************************
' txtVorbesitzer_GotFocus
' ***********************************************
Private Sub txtVorbesitzer_GotFocus()
TextMarkieren
End Sub ' txtVorbesitzer_GotFocus


' ***********************************************
' FeldEinlesen – Liest den Inhalt eines Datenbankfeldes ein
' ***********************************************
Function FeldEinlesen(FeldName As String)
' Soll Null-Wert eines Feldes d. h. kein Inhalt abfangen
On Error GoTo errFeldEinlesen
Dim tmpWert As Variant
Dim tmpPicture As Picture
If FeldName = "Bild" Then
BildEinlesen
Exit Function
End If
tmpWert = tdAutoTabelle.Fields(FeldName)
If IsNull(tmpWert) Then
FeldEinlesen = ""
Else
FeldEinlesen = tmpWert
End If
Exit Function
errFeldEinlesen:
MsgBox "Laufzeitfehler: " & Err.Number, vbExclamation, _
Err.Description
Stop
End Function ' FeldEinlesen

' ***********************************************
' BildEinlesen – Liest ein Bild aus der Datenbank ein
' ***********************************************
Function BildEinlesen() As Picture
Dim X() As Byte
Dim tmpPic As Picture
ReDim X(tdAutoTabelle("Bild").FieldSize)
If IsNull(tdAutoTabelle!Bild) Then
imgAuto.Picture = LoadPicture()
Exit Function
End If
X = tdAutoTabelle!Bild
Open "Bild.ole" For Binary As 1
Put #1, , X
Close 1
On Error Resume Next
Set tmpPic = LoadPicture("Bild.ole")
Set BildEinlesen = tmpPic
If Err.Number = 0 Then Exit Function
If Err.Number = 481 Then ' Ungültiges Bild
imgAuto.Picture = LoadPicture()
Else
MsgBox "Laufzeitfehler: " & Err.Number, vbExclamation, _
Err.Description
End If
End Function ' BildEinlesen


' ***********************************************
' BildSpeichern – Speichert ein Bild in der Datenbank
' ***********************************************
Sub BildSpeichern() ' As Boolean
Dim Dateinr As Integer
Dateinr = FreeFile
Dim X() As Byte
SavePicture imgAuto.Picture, "SaveBild.ole"
ReDim X(FileLen("SaveBild.ole"))
Open "SaveBild.ole" For Binary As Dateinr
Get #Dateinr, , X
Close Dateinr
tdAutoTabelle!Bild = X
End Sub ' BildSpeichern

' ***********************************************
' TextMarkieren
' ***********************************************
Sub TextMarkieren()
ActiveControl.SelStart = 0
ActiveControl.SelLength = Len(ActiveControl.Text)
End Sub ' TextMarkieren

' ***********************************************
' txtZylinder_GotFocus
' ***********************************************
Private Sub txtZylinder_GotFocus()
TextMarkieren
End Sub ' txtZylinder_GotFocus

' ***********************************************
' txtBaujahr_GotFocus
' ***********************************************
Private Sub txtBaujahr_GotFocus()
TextMarkieren
End Sub ' txtBaujahr_GotFocus

' ***********************************************
' txtAutoTyp_GotFocus
' ***********************************************
Private Sub txtAutoTyp_GotFocus()
TextMarkieren
End Sub ' txtAutoTyp_GotFocus

' ***********************************************
' txtHubraum_GotFocus
' ***********************************************
Private Sub txtHubraum_GotFocus()
TextMarkieren
End Sub ' txtHubraum_GotFocus

Spezielle Themen der Datenbankprogrammierung

In den folgenden Abschnitten werden einige Themen der Datenprogrammierung vorgestellt, die zwar nicht in die Kategorie »Einsteiger-ABC« fallen, die aber für die Umsetzung von Datenbankanwendungen sehr wichtig sind.

Mehrfachzugriff auf eine Datenbank

In sog. Multiuser-Umgebungen spielt das Thema »gleichzeitiger Zugriff auf einen Datensatz« naturgemäß eine wichtige Rolle. Eine solche Multiuser-Umgebung liegt immer dann vor, wenn mehrere Prozesse auf ein und denselben Recordset einer Datenbank zugreifen möchten. Dies kann ein Netzwerk sein, bei dem auf verschiedenen Arbeitsstationen Visual-Basic-Programme laufen, die auf die auf dem Server-PC befindliche Datenbank zugreifen. Dies kann aber auch ein einzelner PC sein, auf dem zwei oder mehr Programme laufen, die auf einen Recordset der gleichen lokalen Datenbank zugreifen. Sobald mehrere Prozesse auf ein und denselben Datensatz zugreifen möchten, liegt ein Zugriffskonflikt vor. Ein solcher Zugriffskonflikt stellt grundsätzlich kein Problem dar, denn die Jet-Engine bietet einen einfachen, allerdings nicht immer sehr flexiblen Mechanismus, der verhindern soll, daß sich zwei Prozesse (Benutzer) beim gleichzeitigen Zugriff auf einen Datensatz in die Quere kommen. Dadurch kann z. B. wirkungsvoll verhindert werden, daß Benutzer B an diesem Datensatz Änderungen vornimmt, während Benutzer A einen Datensatz bearbeitet.

Datensätze werden seitenweise gesperrt

Allerdings gilt es eine entscheidende Besonderheit der Jet-Engine zu beachten. Die Jet-Engine ist (wahrscheinlich aus Performance-Gründen) nicht in der Lage, einen einzelnen Datensatz zu sperren. Statt dessen wird eine bestimmte Anzahl an Datensätzen, die um den eigentlichen Datensatz herumliegen, gesperrt. Dieser Bereich wird als Seite (engl. page) bezeichnet und ist 2 Kbyte groß. Bei kleinen Datensätzen kann dies bei der Sperre von Datensatz Nr. 5 bedeuten, daß auch die Datensätze Nrn. 6 bis 8 gesperrt werden. Versucht ein Benutzer auf Datensatz Nr. 7 zugreifen, wird ein Laufzeitfehler ausgelöst, auch wenn dieser Datensatz von keinem anderen Benutzer bearbeitet wird.

Vollständige oder teilweise Sperre?

Die Jet-Engine kennt zwei unterschiedliche Sperrmechanismen: die teilweise Sperre (engl. optimistic locking) und die vollständige Sperre (engl. pessimistic locking). Welche Sperre für ein Recordset-Objekt verwendet wird, wird über die LockEdits-Eigenschaft des Recordset-Objekts eingestellt. Bei der vollständigen Sperre wird der aktuelle Datensatz gesperrt, sobald ein Prozeß für das Recordset-Objekt die Edit-Methode ausführt. Alle übrigen Prozesse können die Edit-Methode für diesen Datensatz (und die benachbarten Datensätze, denn die Jet-Engine sperrt stets einen 2 Kbyte großen Bereich) nicht mehr aufrufen und erhalten bei einem solchen Versuch den Laufzeitfehler 3260. Erst die Ausführung der Update-Methode gibt den Datensatz wieder frei. Diese Sperrmethode ist zwar sehr zuverlässig, bietet aber den Nachteil, daß der Datensatz relativ lange für die anderen Benutzer gesperrt ist. Macht der Benutzer vor dem Ausführen der Update-Methode eine Mittagspause, kann in dieser Zeit niemand an den gesperrten Datensätzen irgendwelche Änderungen vornehmen. Im Gegensatz dazu ist die teilweise Sperre etwas großzügiger. Hier wird die Sperre erst mit dem Aufruf der Update-Methode eingerichtet. Andere Benutzer können in dieser Zeit ebenfalls die Edit-Methode aufrufen, um den Datensatz zu bearbeiten. Nach dem Motto »Wer zuerst kommt, ...« führt derjenige Benutzer die Änderungen in der Datenbank durch, der zuerst die Update-Methode ausführt. Würde in dieser (allerdings sehr kurzen) Zeitspanne ein anderer Benutzer versuchen, die Edit-Methode auszuführen, wäre ein Laufzeitfehler 3260 die Folge. Doch was ist, wenn der andere Benutzer, der bereits die Edit-Methode, noch nicht aber die Update-Methode ausgeführt hatte, dies nun nachholt? Ganz einfach, die Jet-Engine stellt fest, daß die Daten nach der Ausführung der Edit-Methode geändert wurden und gibt den Laufzeitfehler 3197 (»Daten wurden verändert; Operation angehalten«) aus.

Hinweis:

Die Fehlermeldung »Aktualisieren nicht möglich; momentane Sperrung durch Benutzer Admin auf Computer <Name >« (Fehler Nr. 3260) hat nichts damit zu tun, ob der PC in einem Netzwerk eingesetzt wird. Bei Admin handelt es sich um einen der beiden Default-Benutzer, die durch das Sicherheitskonzept der Jet-Engine im Default-Workspace eingerichtet werden.

Das heißt aber nicht, daß es nicht vorkommen kann, daß ein Benutzer einen Datensatz sieht, der von einem anderen Benutzer bereits verändert wurde. Sehen Benutzer A und Benutzer B den gleichen Datensatz auf ihrem Bildschirm und führt Benutzer A Änderungen an dem Datensatz durch, sieht Benutzer B diese Änderung erst dann, wenn er den Datensatz erneut ansteuert oder (z. B. über eine Refresh-Methode) das Recordset-Objekt neu aufbaut. Tut er dies nicht, führt er Änderungen an Daten durch, die in dieser Form unter Umständen nicht mehr existieren.

Nachdem Sie nun wissen, wie die Jet-Engine einen Zugriffsschutz realisiert, muß noch die Frage beantwortet werden, wie ein Programm auf eine solche Situation reagieren muß. Die Antwortet ist klar, durch Abfangen der entsprechenden Laufzeitfehlermeldungen. Würde dies nicht geschehen, wäre bei einem Zugriffskonflikt (der keinen Fehler, sondern eher eine besondere Situation darstellt), ein Programmabbruch die Folge.

Grundsätzlich gibt es zwei Möglichkeiten, wie ein Programm im Rahmen einer Fehlerbehandlungsroutine verfahren kann:

1. Selbständig Maßnahmen ergreifen und z. B. nach einer bestimmten Zeitspanne die Edit- oder Update-Methode erneut aufrufen.

Variante 1 dürfte bei einfachen Zugriffskonflikten (wie einem gesperrten Datensatz) die beste Empfehlung sein. Der Benutzer bemerkt in diesem Fall lediglich eine kurze Verzögerung, der Programmablauf wird nicht gestört. Variante 2 ist dagegen zu empfehlen, wenn ein Programm versucht hat, auf einen gelöschten oder geänderten Datensatz zuzugreifen. In diesem Fall sollte der Benutzer die Datensatzgruppe neu aufbauen, was ihm über eine Mitteilung kundgetan werden kann.

Beispiel:

Das folgende Beispiel zeigt, wie eine Prozedur, die den Wert eines Feldes unter Zuhilfenahme des Datensteuerelements in einer Datenbank ändern möchte, auf einen Zugriffskonflikt reagieren kann. Stellt das Programm fest, daß ein Zugriff aufgrund einer Sperre nicht möglich ist, werden in kurzen Abständen (realisiert über die API-Funktion Sleep) drei weitere Versuche unternommen. Schlagen auch die Wiederholungsversuche fehlt, erhält der Benutzer eine entsprechende Mitteilung und kann dann den Vorgang entweder noch einmal versuchen oder ganz abbrechen.

Private Sub cmdEdit_Click()
On Error GoTo cmdEdit_Error
Static Versuche As Integer
Data1.Recordset.Edit
' Weitere Anweisungen
Exit Sub
cmdEdit_Error:
Select Case Err.Number
Case 3260 ' Zugriffssperre aktiv
If Versuche <= 3 Then
Sleep (1000)
Versuche = Versuche + 1
Resume
Else
If MsgBox "Datensatz für Zugriff zur Zeit gesperrt" & _
" – Zugriff wiederholen?", vbQuestion + vbYesNo, _
"Zugriffssperre") = vbYes Then
Versuche = 0
Resume
Else
Exit Sub
End If
End If
Case Else
MsgBox Err.Description & " (" & Err.Number & ")", _
vbExclamation, "Laufzeitfehler"
Err.Clear
End Select
End Sub

Die EditLocks-Eigenschaft

Über die EditLocks-Eigenschaft wird für ein Recordset-Objekt eingestellt, ob es mit der vollständigen Sperre (EditLocks=True) oder der teilweisen Sperre (EditLocks=False) arbeiten soll

Syntax

Objekt.EditLocks=True|False

Standardmäßig, d. h. wenn nichts anderes vereinbart wird, ist für eine Datensatzgruppe die vollständige Sperre aktiv.

Die Idle-Methode

Die Jet-Engine ist, wie ein echter Motor, ein Gegenstand, in dem stets mehrere Dinge gleichzeitig passieren. Wie in jedem Multitaskingsystem kann es passieren, daß ein zeitaufwendiger Prozeß die übrigen Prozesse blockiert. Damit die Jet-Engine ihre eigenen »Hausaufgaben« im Hintergrund zügig erledigen kann, sollte zwischen größeren Datenbankoperationen die Idle-Methode des DbEngine-Objekts aufgerufen werden. Sie ist das Pendant der DoEvents-Anweisung für die internen Operationen der Jet-Engine.

Syntax

DBEngine.Idle [dbFreeLocks]

Wird als Parameter dbFreeLocks angegeben, wird die Programmausführung nicht eher fortgesetzt, bis alle internen Sperren aufgehoben wurden.

Merksatz:

In umfangreicheren Datenbankoperationen sollte in einer Multiuser-Umgebung, d. h. wenn mehrere Programme gleichzeitig auf ein und dieselbe Tabelle zugreifen können, zwischendurch die FreeLocks-Anweisung ausgeführt werden, damit die Jet-Engine die Gelegenheit erhält, interne Verwaltungsaufgaben (vor allem das Aufheben von Sperren) durchzuführen.

Für Umsteiger

Bei Visual Basic 3 wurde anstelle der Idle-Methode die FreeLock-Anweisung eingesetzt, die aus Kompatibilitätsgründen nach wie vor unterstützt wird.

Sperren von Datenbanken oder Datensätzen

Bislang wurde stets davon ausgegangen, daß eine Datenbank oder ein Recordset-Objekt für den Mehrfachzugriff, d. h. nicht exklusiv geöffnet wurde (dies ist die Standardeinstellung). Um Zugriffskonflikte gar nicht erst entstehen zu lassen, kann sowohl die Datenbank als auch das Recordset-Objekt von Anfang an mit einer Zugriffsbeschränkung versehen werden. Bei der OpenDatabase-Methode muß dazu der zweite Parameter auf True gesetzt werden:

Set Db = OpenDatabase("Biblio.mdb", True)

Durch diese Anweisung wird die Datenbank exklusiv geöffnet, d. h. kein weiterer Benutzer (Prozeß) kann gleichzeitig auf die Datenbank zugreifen. Da eine solche Sperre keinem anderen Programm den Zugriff gestattet, wird man sie nur selten anwenden. Alternativ besteht die Möglichkeit, eine Zugriffssperre auf der Ebene eines Recordset-Objekts einzurichten:

Set Rs = Db.OpenRecordset("Titles",dbOpenDynaset,dbDenyWrite)

Durch den zusätzlichen Parameter dbDenyWrite können andere Programme die Datensatzgruppe zwar lesen, nicht aber ihren Inhalt verändern.

Transaktionen

Eine Transaktion ist eine Zusammenfassung mehrerer Datenbankoperationen, die von der Jet-Engine als eine Einheit bezeichnet wird. Transaktionen bieten zwei entscheidende Vorteile:

1. Sie können rückgängig gemacht werden und schützen damit die Integrität der Datenbank.

2. Sie sind in der Regel schneller als ihre Einzeloperationen, da die Jet-Engine die einzelnen Anweisungen besser optimieren kann.

Wenn Sie bei Transaktionen an Bankgeschäfte denken, liegen Sie damit bereits goldrichtig. Im Bankgewerbe ist es üblich, z. B. Überweisungen so durchzuführen, daß die endgültige Änderung an einem Konto erst dann durchgeführt wird, wenn alle vorherigen Operationen fehlerfrei erledigt werden konnten. Auf diese Weise soll verhindert werden, daß ein Konto, z. B. durch eine Störung in der Übertragungsleitung, in einen undefinierten Zustand versetzt wird oder ein anderes Konto eine Buchung erhält, zu der keine Gegenbuchung existiert. Erst wenn der Transaktionsmanager sein OK gibt, werden alle bereits übertragenen Änderungsbefehle wirksam. Erkennt der Transaktionsmanager dagegen einen Fehler, wird die Transaktion abgebrochen und das Konto wird wieder in seinen ursprünglichen Zustand versetzt. Transaktionen spielen nicht nur bei Banken eine wichtige Rolle, auch das Dateisystem von Windows NT arbeitet nach diesem Prinzip. Hier werden mehrere aufeinanderfolgende Schreiboperationen zu einer Transaktion zusammengefaßt. Auf diese Weise können verlorene Zuordnungseinheiten oder andere »Unfeinheiten« vermieden werden, da bei einem fehlerhaften Schreibzugriff die Transaktion einfach abgebrochen und das NTFS-Dateisystem in seinen alten, stabilen Zustand wieder zurückversetzt wird.

Transaktionen werden vor allem dann eingesetzt, wenn durch eine Änderung mehrere Tabellen betroffen sind, denn in diesem Fall ist die Gefahr groß, daß durch einen vorzeitigen Abbruch der Änderung die Integrität der Datenbank gefährdet wird. Transaktionen verlaufen bei der Jet-Engine stets nach dem gleichen Prinzip. Alle Datenbankoperationen müssen in ein »Korsett« eingebunden werden, das folgenden Aufbau besitzt:

Database.BeginTrans
' Datenbankoperationen
If Error Then
' Alles wieder in den alten Zuständen
Database.Rollback
Else
' Alle Änderungen werden bestätigt
Database.CommitTrans
End If

Die Transaktions-Methoden können wahlweise auf ein Database- oder ein Workspace-Objekt angewendet werden, wirken aber immer im Kontext eines Workspace-Objekts. Das bedeutet, daß innerhalb einer Transaktion auch Änderungen an mehreren Datenbanken durchgeführt werden können, die im Rahmen des Workspace-Objekts geöffnet wurden.

Eingeleitet wird eine Transaktion stets durch die BeginTrans-Methode, die wahlweise auf ein Database- oder ein Workspace-Objekt angewendet werden kann. Anschließend werden alle Datenzugriffe auf exakt die gleiche Weise durchgeführt, als wenn keine Transaktion im Spiel wäre. Sind alle Datenbankzugriffe abgewickelt, kommt die Stunde der Entscheidung. Das Programm muß prüfen, ob alle Änderungen fehlerfrei durchgeführt wurden und sich gegebenenfalls beim Benutzer noch eine Bestätigung holen. Ging alles gut, werden die Änderungen über die CommitTrans-Methode in der Datenbank durchgeführt. Gibt es jedoch Grund zu Beanstandungen, wird die Datenbank über die Rollback-Methode in ihren ursprünglichen Zustand zurückversetzt und alles ist wieder so, wie es vor der Ausführung der BeginTrans-Methode war.

Beispiel:

Das folgende Beispiel durchsucht alle Datensätze der Tabelle Titles der Datenbank BiblioX.MDB (einer leicht modifizierten Version unsere beliebten Demo-Datenbank). Besitzt das Feld Status der Tabelle Titles (dieses Feld existiert in der Originaldatenbank nicht) den Wert »Nicht mehr da«, wird statt dessen der Wert »Ausgemustert« eingetragen. Nachdem alle Datensätze durchsucht wurden, erhält der Benutzer die Gelegenheit, die Änderungen zu bestätigen oder sie zu verwerfen.

Private Sub cmdStart_Click()
On Error GoTo cmdStart_Error:
Dim Buchmarke As String, Anzahl As Integer
' Position des Datensatzzeigers merken
Buchmarke = datTest.Recordset.Bookmark
' Transaktion beginnen
datTest.Database.BeginTrans
' Datensatzzeiger auf den ersten Datensatz
datTest.Recordset.MoveFirst
' Alle Datensätze der Reihe nach durchgehen
Do While Not datTest.Recordset.EOF
' Enthält das Status-Feld den Wert "Nicht mehr da"?
' Leerzeichen beachten
If RTrim(datTest.Recordset!Status) = _
"Nicht mehr da" Then
' Datensatz in Edit-Modus
datTest.Recordset.Edit
' Feldinhalt ändern
datTest.Recordset!Status = "Ausgemustert"
' Feldinhalt aktualisieren
datTest.Recordset.Update
' Anzahl der aktualisierten Datensätze zählen
Anzahl = Anzahl + 1
End If
' Auf zum nächsten Datensatz
datTest.Recordset.MoveNext
Loop
' Wurden Änderungen durchgeführt?
If Anzahl > 0 Then
If MsgBox("Änderungen an " & Anzahl & _
" Datensätzen – Durchführen?", vbQuestion + _
vbYesNo, "Transaktion fertig") = vbYes Then
' Alle Änderungen tatsächlich durchführen
datTest.Database.CommitTrans
Else
' Änderungen wieder verwerfen – Datenbank bleibt unverändert
datTest.Database.Rollback
End If
Else
' Es wurden keine Datensätze gefunden – also auch keine
' Änderungen
datTest.Database.Rollback
End If
' Datensatzzeiger wieder auf alte Position
datTest.Recordset.Bookmark = Buchmarke
Exit Sub
' Man kann nie wissen
cmdStart_Error:
Select Case Err.Number
' Hier können spezielle Fehler abgefangen werden
Case Else
MsgBox Err.Description & " (" & Err.Number & ")", _
vbExclamation, "Laufzeitfehler"
Stop
End Select
End Sub

Mit dem Aufruf der CommitTrans-Methode werden die Änderungen an der Datenbank durchgeführt. Das Rückgängigmachen dieser Änderungen ist nur dann möglich, wenn diese Transaktion Teil einer weiteren Transaktion war. Laut Visual-Basic-Hilfe können bis zu fünf Transaktionen verschachtelt werden (geht nicht bei ODBC-Datenbanken). Wird eine äußere Transaktion abgebrochen, werden damit auch alle inneren und bereits abgeschlossenen Transaktionen rückgängig gemacht. Möchten Sie mehrere Transaktionen unabhängig und asynchron zueinander durchführen, müssen Sie mit mehreren Workspace-Objekten arbeiten.

Für Umsteiger

Die Anweisungen BeginTrans, CommitTrans und Rollback stehen aus Kompatibilitätsgründen noch zur Verfügung, sollten aber nicht mehr eingesetzt werden.

*** E N D E ***