DBC-Extensibility

von Alf Borrmann
Der Datenbankcontainer in VFP Bau einer DBC-Manager-Klasse
Aufbau der Felder und Indizes Die Initialisierung des Managers
Anlegen und Füllen von Properties Die Konfiguration
Zusatzfunktionen Zusammenfassung

Datei-Download mit Dokumentation, Code und Beispielen

Der Datenbankcontainer in VFP

Die Struktur des Datenbankcontainers in Visual FoxPro wurde in der Version 5.0 gegenüber der Version 3.0 zwar nicht geändert, das Handling durch die VFP-Datenbankengine erfuhr jedoch einige Verbesserungen. Diese Neuerungen erlauben nun Manipulationen, die vorher so nicht möglich waren.

Der Zweck des Datenbankcontainers (DBC) in VFP ist, anders, als der Name vermuten läßt, nicht, die Daten einer Datenbank aufzunehmen, sondern lediglich, die Zusatzinformationen zu den Tabellen, Views, Feldern bzw. Spalten, Indexen und Relationen zu sammeln. Für jede der genannten Entitäten wird dabei im DBC ein Datensatz angelegt. Innerhalb des DBC werden dabei nicht die Strukturinformationen zu Tabellen und die Indexausdrücke selbst gespeichert. Dies begründet Microsoft damit, daß dies redundante Information wäre und diese in die .DBF- (Tabellen) und .CDX-Dateien (Indizes) gehören. Dadurch läßt sich aber die Datenbank nicht durch die im DBC enthaltenen Metainformationen neu aufbauen bzw. reparieren.

Der DBC beinhaltet neben den Zusatzinformationen zu diesen Dateien auch den Quellcode und die compilierten Funktionen für die referenzielle Integrität sowie einige andere Informationen. Die Zusammensetzung dieser Zusatzinformationen ist jedoch durch die Funktionalität der Engine selbst beschränkt.

 

Abbildung 1: der Tabellendesigner in VFP, im unteren Teil die Zusatzinformationen zu den Feldern

Während der Laufzeit eines Programms kann auf diese Metadaten, in VFP auch als Properties bezeichnet, über die Funktionen DBGetProp() und DBSetProp() zugegriffen werden. In VFP eingebaute Funktionen wie etwa DBAddProp() und DBDeleteProp() fehlen jedoch, somit lassen sich z.B. zu den Spalten von Tabellen nur Werte zu folgenden Properties abspeichern:

Property Zugriff zur Laufzeit Datentyp Zweck
Caption Schreib-Lese C die Überschrift für die Anzeige der Spalte
Comment Schreib-Lese C ein Kommentar zum Feld
DefaultValue Nur Lese C der Vorgabewert für das Feld
DisplayClass Schreib-Lese C der Name der Klasse, die als Vorgabe im Formdesigner verwendet werden soll
DisplayClassLibrary Schreib-Lese C Name und Pfad der Klassenbibliothek, die die "DisplayClass" enthält
Format Schreib-Lese C "Format"-Einstellungen für das Feld
InputMask Schreib-Lese C Formatangabe für Eingaben
RuleExpression Nur Lese C Ausdruck für die Gültigkeitsprüfung
RuleText Nur Lese C bei ungültigem Eingabewert anzuzeigender Text

Tabelle 1: Properties zu Feldern

Damit lassen sich mit den Mitteln von VFP keine weiteren Informationen zu den Entitäten einer Datenbank ablegen. Notwendig wäre dies aber z.B. für Informationen zu Datentyp und Länge von Tabellenspalten, für Schlüssel für die Internationalisierung von Überschriften, für feldspezifische Help-Context-IDs o.ä. Zur Umgehung dieser Einschränkung bieten sich zwei Möglichkeiten an:

  1. das Anlegen speziellen Klassen mit Properties für die Zusatzinformationen zu den Entitäten der Datenbank und
  2. die Anlage kompletter zusätzlicher Tabellen für die Metainformationen.

Die erste Möglichkeit scheidet jedoch praktisch von selbst aus, da hier für jede Tabelle, jedes Feld, jede View etc. eine eigene Klasse erzeugt werden müßte. Zudem wären die Informationen in den Klassenstrukturen sehr gut "versteckt", ein ernsthaftes Wartungsproblem wäre die Folge.

Bleibt zunächst nur der Aufbau eines kompletten Datadictionaries aus zusätzlichen Tabellen. Dies ist eine klassische Lösung. Sie hat jedoch den Nachteil, daß für die Pflege dieses Datadictionaries eine erhebliche Menge Code entwickelt werden muß. Abgesehen davon, muß in diesem Datadictionary wiederum die gesamte Datenbank abgebildet werden, obwohl diese ja im Datenbankcontainer bereits schon einmal existiert. Tatsächlich bietet sich bei genauerer Betrachtung ein weiterer Ort für die Speicherung von Metainformationen an: der DBC selbst.

Wie viele andere Dateien in VFP, z.B. die Formulare (SCX), Klassenbibliotheken (VCX) und die Berichte (FRX) ist der Datenbankcontainer nämlich eine normale Tabelle im bekannten .DBF-Format. Dadurch kann man ihn - wie auch diese anderen Tabellen - mit dem USE-Befehl öffnen. Die Struktur des DBC ist folgende:

Feldname Datentyp Zweck
OBJECTID I eindeutige ID des Entities
PARENTID I ID des Parentdatensatzes, z.B. bei Feldern die ID des Tabelleneintrags
OBJECTTYPE C Typ des Eintrags, z.B. "Table" oder "Field"
OBJECTNAME C Name des Eintrags
PROPERTY M (binary) die von VFP zu dieser Entität gespeicherten Properties
CODE M (binary) wird nur für den DBC selbst zur Speicherung des RI-Codes und der Stored Procedures verwendet, für andere Entitäten leer
RIINFO C wird nur für Relationen verwendet als Kennung für die Generierung des RI-Codes
USER M für Verwendung durch den Benutzer

Tabelle 2: Struktur der Datenbankcontainer-Tabelle in VFP

Hier fällt zunächst das Feld User auf, das dafür vorgesehen zu sein scheint, Zusatzinfos wie die oben genannten zu beherbergen. Im Gegensatz zur Version 3.0 von VFP wird der Inhalt des Feldes in der Version 5.0 auch bei Änderungen über den Datenbankdesigner mitgepflegt. Bei Änderung auch nur der Feldlänge eines Feldes in einer Tabelle der Datenbank werden von der VFP-Engine nämlich die Datensätze für alle Felder als gelöscht markiert und dann neu an das Ende der DBC-Tabelle angehängt. Die Version 3.0 kopierte dabei den Inhalt des USER-Feldes nicht mit, die Version 5.0 tut dies jetzt. Außerdem läßt sich der Datenbankcontainer in der Version 5.0 durch ein open database <DBC-Name>… shared nicht-exklusiv öffnen, was zunächst dazu dient, mehreren Entwicklern im Netz gleichzeitiges Arbeiten zu ermöglichen. Diese Eigenschaft von VFP kann aber auch dazu genutzt werden, durch ein use <DBC-Name> shared einen gleichzeitigen Zugriff auf die Tabelle zu erhalten, die sich dann mit normalen xBase Tabellenbefehlen manipulieren läßt.

Abbildung 2: Ausschnitt aus dem Inhalt der DBC-Tabelle

Bei weiteren Überlegungen bereitet aber auch dieser Ansatz zwei Probleme:

  1. wenn ein auf den DBC aufsetzendes System Zusatzinformationen im USER-Feld ablegt, was passiert, wenn weitere Aufsätze Platz benötigen? Und
  2. noch gravierender: beim Zugriff auf Datensätze für Tabellenfelder kann der entsprechende Datensatz nur mit einer zweistufigen Suche gefunden werden: es muß zunächst nach der ID der Tabelle und dann nach dem Feldnamen mit der richtigen ParentID gesucht werden.

Da wir in den Zusatzproperties aber auch Daten unterbringen wollen, die zur Laufzeit sehr häufige Zugriffe bedingen (z.B. verschiedene Überschriften oder Schriftartinformationen zur Darstellung der Felder), ist ein schnelles Auffinden der Daten unabdingbar, um z.B. den Bildschirmaufbau von Formularen nicht zu bremsen.

Als weitere Möglichkeit für die Unterbringung von zusätzlichen Daten kommt so nur noch in Betracht, die DBC-Tabelle selbst zu modifizieren. Überraschenderweise ist dies in VFP 5.0 möglich, ohne daß die Funktionalität der VFP-Engine dadurch beeinträchtigt würde. Ja, sogar die Inhalte dieser selbstdefinierten Felder werden beim oben beschriebenen Kopieren der Datensätze mitgepflegt. Zusätzlich zu den Feldern lassen sich auch Indexe erzeugen, die den späteren Suchzugriff auf die Informationen im DBC ermöglichen.

Ein so manipulierter DBC könnte also folgende Struktur und Inhalte haben:

Abbildung 3: die manipulierte DBC-Tabelle

Hier sind ein Memofeld für die Zusatzproperties (WPROPERTY) und ein Feld für das schnelle Finden von Sätzen (WKEY) hinzugefügt. Auf das Feld WKEY ist ein Index gelegt.

Bau einer DBC-Manager-Klasse

Mit diesen Kenntnissen können wir nun daran gehen, eine Klasse zu erstellen, die die vorgestellten Funktionalitäten vereinigt. Diese Klasse kann zur Pflege der Metadaten und zum Einsatz in Applikationen verwendet werden. Diese Klasse muß folgendes können:

Alle diese Forderungen lassen sich problemlos mit Hilfe normalen VFP-Codes in einer Klasse realisieren. Wie sich dies umsetzen läßt, wird im folgenden anhand von markanten Punkten im Code erläutert. Der gesamte Code inklusive Dokumentation der Klasse steht auf der Internet-Site www.wizards-builders.com zum Download bereit.

Aufbau der Felder und Indizes

Beim ersten Aufruf des DBC-Managers werden nach einer Abfrage automatisch die benötigten Felder und Indizes aufgebaut. Dazu muß die Tabelle exklusiv geöffnet sein, was das gleichzeitige Öffnen des DBCs als Datenbank ausschließt. Vor dem Öffnen des DBC finden umfangreiche Fehlerprüfungen und Sicherungen der Umgebung statt. Die hierzu benötigten Funktionen und Befehle sind DBUsed(), DBC(), Set("DATABASE"), Open Database, Close Database und Set Database to. Im dem Falle, daß der Datenbankcontainer in der Entwicklungsumgebung innerhalb eines Projektes geöffnet ist, läßt er sich nicht per Close Database schließen und damit auch nicht exklusiv als Tabelle öffnen. Der DBC-Manager bricht in diesem Falle mit einer Fehlermeldung ab.

Das Öffnen des DBC als Tabelle erfolgt in folgendem Codestück:

*	DBC-Manager Methode OpenDatabase
...
this.lErroroccurred = .f.
*	DBC ist schon geöffnet
if this.lDBUsed
	set database to ( this.cDBC)
	this.lIsExclusive = isexclusive( set( "DATABASE"), 2)
else
this.lErroroccurred = .f.
open database ( this.cDBC) shared
if this.lErroroccurred
	*	DBC ließ sich nicht öffnen: Fehlermeldung
	= messagebox( ERRORONOPENDBC_LOC,;
	MB_ICONSTOP;
	+ MB_OK,;
	this.cTitle)
	return .f.
else
	*	Abspeichern des Zugriffsmodus für den DBC
	this.lIsExclusive = isexclusive( set( "DATABASE"), 2)
	endif
endif
local loDataSession,;
loSetSelect
*	Datasession auf default setzen
*	Klasse aus EnvLib wird zur Sicherung der Umgebung verwendet
loDataSession = createobject( "SetDataSession", 1)
set datasession to 1
if used( this.cDBCAlias)
	*	Klasse aus EnvLib wird zur Sicherung der Umgebung verwendet
	loSetSelect = createobject( "SetSelect", this.cDBCAlias)
else
	select 0
endif
*	Öffnen als Tabelle triggert ggf. Errormethode
*	dort wird dann das Property lErroroccurred gesetzt
this.lErroroccurred = .f.
if this.lIsExclusive
	*	Klasse aus EnvLib wird zur Sicherung der Umgebung verwendet
	loDatabase = createobject( "SetDatabase", this.cDBC)
	close database
	use ( this.cDBC) alias ( this.cDBCAlias) shared again
	open database ( this.cDBC) shared
else
	use ( this.cDBC) alias ( this.cDBCAlias) shared again
endif
if this.lErroroccurred
	this.lIsExclusive = !empty( alias( )) and isexclusive( alias( ))
	= messagebox( ERRORONOPENDBC_LOC,;
	MB_ICONSTOP;
	+ MB_OK,;
	this.cTitle)
	return .f.
endif

Listing 1: Öffnen der DBC-Tabelle

Zur Kapselung des Arbeitsbereiches, in dem die Tabelle geöffnet wird, erhält diese einen privaten, nur dem DBC-Manager bekannten Alias, der in dem Property cDBCAlias abgelegt ist. Dieses Property wurde in der Init-Methode mit Hilfe der Sys( 2015)-Funktion von VFP gefüllt, die einen 10stelligen String zurückliefert, der mit einem Unterstrich beginnt. Dieser String ist damit als Aliasname geeignet. Er wird aus Systemzeit und Seriennummer der Maschine gewonnen wird und ist damit praktisch einmalig.

Die anzulegenden Felder werden mit Hilfe des Befehls Alter Table angelegt, nachdem anhand der in das Array laDBCFields eingelesenen Struktur der Tabelle überprüft wurde, ob sie entweder noch nicht angelegt wurden oder den falschen Typ haben. Die anzulegenden Felder werden über die Konstantendeklarationen (DBCMEMOFIELDNAME (=WPROPERTY), DBCINDEXFIELDNAME (=WKEY)), die wie auch die Strings für Ausgaben in der Headerdatei abgelegt sind, konfiguriert:

= afields( laDBCFields, this.cDBCAlias)
* wenn WPROPERTY nicht existiert...
if ascan( laDBCFields, DBCMEMOFIELDNAME)> 0
	lnPropertyField = asubscript( laDBCFields,;
	ascan( laDBCFields, DBCMEMOFIELDNAME), 1)
	* wenn WPROPERTY nicht Type Memo, dann dahingehend ändern
	if laDBCFields[lnPropertyField,2] # T_MEMO;
	and this.OpenDBFExclusive( )
		alter table ( this.cDbc);
		alter column DBCMEMOFIELDNAME T_MEMO nocptrans
	endif
else
	if this.OpenDBFExclusive( )
		alter table ( this.cDbc);
		add column DBCMEMOFIELDNAME T_MEMO nocptrans
	endif
endif
* wenn WKEY nicht existier...
if ascan( laDBCFields, DBCINDEXFIELDNAME) > 0
	lnIndexField = asubscript( laDBCFields, ascan( laDBCFields,;
	 DBCINDEXFIELDNAME), 1)
	* wenn WKEY nicht Type Character oder Länge nicht optimal
	if ( laDBCFields[ lnIndexField,2] # T_CHARACTER;
	or laDBCFields[ lnIndexField,3] # this.nIndexFldLen);
	and this.OpenDBFExclusive( )
		alter table ( this.cDbc);
		alter column DBCINDEXFIELDNAME;
		T_CHARACTER( this.nIndexFldLen) nocptrans
	endif
else
	if this.OpenDBFExclusive( )
		alter table ( this.cDbc);
		add column DBCINDEXFIELDNAME;
		T_CHARACTER( this.nIndexFldLen) nocptrans
	endif
endif

Listing 2: Anlegen der Felder

Für das Feld, das die Strings zum Suchen aufnimmt, wird vorher die optimale Länge mit Hilfe eines SQL-Select-Statements aus der maximalen Länge eines Eintrags für Tabellen und dem maximalen Wert für Feldnamen im Feld OBJECTNAME berechnet und im Property nIndexFldLen abgelegt:

select distinct child.PARENTID,;
	max( len( alltrim( child.OBJECTNAME)));
	+ len( alltrim( parent.OBJECTNAME)) as FLDLEN,;
	parent.OBJECTID;
	from ( dbf( this.cDBCAlias)) parent,;
	( dbf( this.cDBCAlias)) child;
	where child.PARENTID;
	in ( select distinct OBJECTID;
	from ( dbf( this.cDBCAlias)) parent;
	where inlist( alltrim( OBJECTTYPE), "Table", "View"));
	and child.PARENTID = parent.OBJECTID;
	union select PARENTID,;
	max( len( alltrim( OBJECTNAME))) as FLDLEN,;
	OBJECTID;
	from ( dbf( this.cDBCAlias));
	where alltrim( OBJECTTYPE) = "Database";
	into array laFieldLen

Listing 3: Berechnung der optimalen Länge für Indexfeld

Nach dem Anlegen der benötigten Felder und Indizes wird das Feld WKEY automatisch gefüllt (s.a. Abbildung 3: die manipulierte DBC-Tabelle). Dabei wird zum späteren schnelleren Finden ein String aus dem Eintragstyp, dem Parentnamen und dem Entitynamen gebildet.

Die Initialisierung des Managers

Beim Starten des DBC-Managers wird der DBC mit Hilfe der in Listing 1: Öffnen der DBC-Tabelle gezeigten Routine geöffnet. Je nachdem, ob inzwischen Tabellen zu der Datenbank hinzu kamen, kann natürlich das Feld WKEY noch leer sein. Zum möglichst schnellen Auffüllen dieses Feldes ist folgender Index für den DBC definiert:

lcIndexField = DBCINDEXFIELDNAME
index on OBJECTNAME ;
	for !( alltrim( lower( OBJECTNAME)) == ;
	right( alltrim( &lcIndexField.),;
	len( alltrim( &lcIndexField.)) - rat( ".", alltrim( &lcIndexField.)))) ;
	and !deleted( );
	tag DBCINDEXWKEYDIFF of ( juststem( this.cDBC) + ".dcx")

Listing 4: Aufbau des Indexbaumes für das Indexfeld

Der Trick liegt hier in der for-Klausel, die nur dann einen Eintrag in den Indexbaum erlaubt, wenn sich der Inhalt des Feldes WKEY von dem des Feldes OBJECTNAME unterscheidet. Mit einer Scan-Schleife, die über diesen Index läuft, werden dadurch nur die Felder der (relative wenigen) Datensätze neu gefüllt, bei denen das nötig ist. Der Index wird, wie auch die schon von VFP selbst zur Beschleunigung seiner Zugriffe angelegten, in der Datei mit der Endung .DCX abgelegt. Da meist nur eine Anzahl von weniger als 20 Sätzen des DBC bei Strukturänderungen oder Ergänzungen der Datenbank betroffen sind, kann der Aufbau der Inhalte des Feldes WKEY ohne große Performance-Verluste bei jedem Start des DBC-Managers erfolgen, eine separat zu triggernde "Refresh()"-Methode erübrigt sich so.

Anlegen und Füllen von Properties

Nachdem der DBC-Manager erfolgreich gestartet wurde, steht ihm in der Datasession 1 unter dem nur ihm bekannten Alias die DBC-Tabelle für normale Zugriffe zur Verfügung. Im folgenden wird davon ausgegangen, daß sich eine Referenz auf das Objekt in der Variablen loDBCMgr befindet. Die Schnittstelle zum Zugriff auf die Zusatzproperties für Entities im DBC wurde kompatibel zu den Funktionen DBGetProp() und DBSetProp() gehalten, die in VFP eingebaut sind. Im einzelnen:

Ein neues Property für ein bestimmtes Feld läßt sich über den Aufruf loDBCMgr.DBAddProp( "authors.firstname", "field", "nHelpContextID", "N") anlegen, wobei der 3. Parameter den Namen des Properties und der 4. Parameter den Variablentyp angibt. Durch diesen Aufruf wird eine neue Zeile in dem Feld WPROPERTY angelegt.

Danach läßt sich das Property mit dem Aufruf von loDBCMgr.DBSetProp( "authors.firstname", "field", "nHelpContextID", 213) mit einem Wert füllen und per loDBCMgr.DBGetProp( "authors.firstname", "field", "nHelpContextID") wieder auslesen. Die Methode DBDelProp("authors.firstname", "field", "nHelpContextID") erlaubt das Löschen von nicht mehr benötigten Properties.

Der Zugriff auf die einzelnen Zeilen des Feldes WPROPERTY erfolgt dabei in zwei Phasen: zunächst sucht der DBC-Mananger anhand der beiden ersten übergebenen Parameter und dem Index auf dem Feld WKEY den richtigen Datensatz in der DBC-Tabelle, dann übernimmt ein universeller Parser, der zu dem DBC-Manager per Aggregation hinzugefügt wurde, den Zugriff auf die einzelnen Zeilen im Feld WPROPERTY.

Zusatzfunktionen

Wenn man den DBC schon einmal als normale Tabelle geöffnet hat, bieten sich zusätzlich zu der Kernfunktionalität zur Verwaltung der Properties weitere Schnittstellen an. So ist das Auslesen des DBC zur übersichtlichen Darstellung aller Properties z.B. für enthaltene Tabellen mit den Methoden DBGetFirstParent(), DBGetNextParent(), DBGetFirstProp() und DBGetNextProp() möglich. Mit Hilfe dieser Methoden und den analog für die Entities (Felder) implementierten kann leicht eine Oberfläche erstellt werden, die die komfortable Bearbeitung des Inhalts des DBC erlaubt.

Die Konfiguration

Zur Konfiguration des DBC-Managers dient die Headerdatei DBCMGR.H. Sie enthält Konstantendeklarationen, über die die Namen der Felder und Indizes, die angelegt werden, festgelegt werden. Über das Verändern dieser Konstanten und das anschließende Neucompilieren des Klasssencodes lassen sich so verschiedene Manager erstellen. Aufgrund der vollständigen Kapselung der Implementation können damit mehrere DBC-Manager sogar gleichzeitig auf einen DBC zugreifen. daneben enthält die Klasse Code, der sämtliche Felder und Indizes wieder löscht und damit den Datenbankcontainer wieder in seinen ursprünglichen Zustand versetzt.

Zusammenfassung

Die Notwendigkeit, ein Zusatztool für den Aufbau eines Datadictionaries schreiben zu müssen, könnte als Schwäche von Visual FoxPro betrachtet werden. Daß dies aber mit relativ einfachen Mitteln (der Code für den Manager hat inklusive Errorhandling und aller Kommentare gerade 2000 Zeilen) und sehr elegant zu lösen ist, zeigt einmal mehr, daß die offene Architektur der VFP-Engine sehr mächtige Tools und Applikationen ermöglicht.

Sämtlicher Quellcode und eine komplette Dokumentation zu der Klasse, die in diesem Artikel gezeigt wird, ist von der Firma Wizards & Builders als Freeware veröffentlicht. Er steht auf der Website www.wizards-builders.com und auf dem CIS-Forum der deutschen FoxPro Usergroup zum Download bereit.

Alf Borrmann

erschienen im Objekt Fokus 1/98