FoxProFoxPro Developer's Conference '94 |
Session 242
Data-Driven
Programming
Rainer Becker
ISYS GmbH
Einleitung
Die meisten von Ihnen werden bereits einmal von den Vorteilen gehört haben, welche Data-Driven Programming bietet. Es vereinfacht die alltägliche Arbeit, indem Metadaten - Daten über Tabellen und Felder - in eigenen Tabellen organisiert werden. Die meisten der bekannten Programmierer, wie George Gooley oder Y. Alan Griver, gaben uns hierfür exzellente Beispiele in ihren Büchern und Training-Sessions. Nun möchte ich Ihnen zeigen, wie leicht man diese Methode so erweitern und anpassen kann, daß sie in den eigenen Anwendungen Verwendung finden kann.
Dabei möchte ich Ihnen nicht etwas völlig Neues, das niemand in seinen Applikationen einbauen kann, präsentieren. Ich kann keinen Sinn darin finden, Luftschlösser oder umfassende Universalkonzepte zu erstellen, die dann doch nicht realisiert werden können, weil die tägliche Arbeit es nicht zuläßt, daß man sie für einige Monate der Entwicklungsaufgaben einfach unterbricht. Daher muß alles, was verwirklichbar sein soll, auf existierende Konzepte zurückgreifen können. Die Konsequenz daraus ist natürlich, daß Ihnen hier viele Dinge begegnen werden, die Sie schon einmal in einem anderen Zusammenhang gehört haben mögen - der Trick liegt in der neuen Zusammenstellung und einer geringfügigen Erweiterung.
Dieser Artikel besteht aus drei Teilen. Im ersten Teil wird ein Überblick über das Potential des Data-Driven Programming mit einem aktiven Data Dictionary gegeben. Der zweite Teil beschäftigt sich mit einer Art von Function- und/oder Message-Dictionary, die einigen schon bekannt sein könnte. Neue Wege zumindest für FoxPro werden dann im dritten Teil beschritten: Es geht um die Implementierung von Funktionsaufrufen in eine Multi-User Umgebung auf der Basis von Tabellen.
Teil 1: Data Dictionary
Grundsätzlich gilt, daß ein Data Dictionary wichtige Informationen über Daten, die sogenannten Metadaten, enthält. Die vier grundlegenden Tabellen eines Dictionary's enthalten die Metadaten zu Tabellen, Feldern, Indices und Relationen. Die gespeicherte Information wird sowohl während des Kompilierens als auch in der Run-Time gebraucht. Werfen wir nun einen Blick auf diese grundlegenden Tabellen des Dictionary's.
Tabellen
Informationen über Tabellen werden immer dann benötigt, wenn man eine solche öffnet oder darauf zugreift. Anstatt die Tabellennamen und Zugriffsoptionen in einem Programm direkt zu codieren ist es sinnvoller Tabellen zu öffnen oder darauf zuzugreifen, indem man eine Standard-Öffnungs-Funktion verwendet, welche auf Metadaten zurückgreifen kann, um diese Aufgabe zu erledigen.
DATABASE TABLES | FoxPro Equivalents |
Comments |
DBFNAME | char |
name of the database |
PATH | memo |
complete or relative path |
ORDER | char |
primary key, order to start |
READONLY | flag |
open file readonly |
EXCLUSIVE | flag |
open file exclusive |
TEMPORARY | char |
it is a cursor and no database (cursor name) |
DESCRIPT | memo |
Description |
Eine typische Funktion dieser Art öffnet die Tabelle DBFNAME, welche sich im Pfad PATH findet und den Index ORDER hat. Die Datei kann als READONLY oder EXCLUSIVE geöffnet werden. Falls sie TEMPORARY ist, kann sie als Cursor erzeugt werden. Ob es noch weitere Parameter gibt, hängt ganz von den Gegebenheiten ihrer Applikation ab. Im zweiten Teil werden noch einige Möglichkeiten vorgestellt werden.
Die grundsätzlichen Vorteile eines Tabellen-Dictionary sind:
Felder
Ein einfaches Felder-Dictionary kann recht
schnell erzeugt werden, da es nur Informationen wie Feldname,
Feldtyp und Feldgröße enthält, welche bereits in der
DBF-Struktur definiert sind.
DATABASE FIELDS | FoxPro Equivalents |
Comments |
DBFNAME | char |
name of the database |
FLDNAME | char |
field name |
FLDTYPE | char |
type of field |
FLDLEN | numeric |
field length |
DECIMALS | numeric |
number of decimals |
Zusätzlich zur bereits gelieferten
Feldinformation sollte ein Dictionary noch folgende Punkte
enthalten:
DATABASE FIELDS | FoxPro Equivalents |
Comments |
TITLE | char |
header of field for browse window |
NAME | char |
name of field in getexpr-dialog |
DEFAULT | memo |
default value |
HELPTEXT | memo |
text for FOXHELP.DBF |
DESCRIPT | memo |
Description |
Ein Felder-Dictionary ist der Ort dafür, Hilfe-Informationen über ein Feld abzuspeichern. Man kann die Eingaben in die Datei FOXHELP übernehmen und dabei Topic durch NAME und Class durch DBFNAME.FLDNAME ersetzen. Darüberhinaus kann das Dictionary typische Spaltentitel für Browsefenster enthalten.
Die grundsätzlichen Vorteile eines Felder-Dictionary sind:
Ein Dictionary kann durchaus auch noch die WHEN, VALID, PICTURE, MESSAGE und RANGE-Snippets eines Feldes enthalten. Ein um die Snippet-Informationen erweitertes Felder-Dictionary verringert die Redundanz, ist allerdings zu komplex, um von Hand codiert zu werden. Unter FoxPro verwendet man für diesen Zweck deshalb gerne GENSCRNX.
Indices
Die folgende Tabelle enthält die
Indexdefintionen für alle Tabellen:
DATABASE INDICES | FoxPro Equivalents |
Comments |
DBFNAME | char |
name of database |
TAGNAME | char |
name of index |
FIELDS | memo |
fields or expression |
FOR | memo |
FOR-clause for index |
NAME | char |
name for getorder-dialog |
DESCRIPT | memo |
description |
Indices können erzeugt oder verändert werden, indem man DBFNAME auswählt und die Felder FIELDS nach dem Index TAGNAME indiziert, gegebenenfalls unter Verwendung von FOR. Ein GetOrder-Dialog wird dadurch ausgefüllt, indem der NAME eines Index ausgewählt wird wenn DBFNAME=Alias(), geordnet nach NAME.
Die grundsätzlichen Vorteile eines Index-Dictionary sind:
Relationen
Die folgende Tabelle enthält die Defintionen
der Relationen:
RELATIONS | FoxPro Equivalents |
Comments |
PARENTDBF | char |
master table, e.g. bill |
PARENTFLD | char |
field to relate, e.g. number |
CHILDDBF | char |
child database, e.g. items |
CHILDFLD | char |
field to relate, e.g. bill-number |
NODELETE | flag |
do not delete if childs exist |
MUSTOPEN | flag |
open child table with parent table |
DESCRIPT | memo |
description |
Wird für eine Relation das Flag MUSTOPEN gesetzt, dann bewirkt dies, daß die Child-Tabelle automatisch zusammen mit der Parent-Tabelle geöffnet wird. Ist das Flag NODELETE gesetzt, so bedeutet das, daß ein Benutzer nicht in der Lage ist, einen Parent-Datensatz zu löschen, solange noch ein Child-Datensatz existiert. Die Alternative dazu ist ein absteigendes Löschen, d.h. die Child-Datensätze werden automatisch gemeinsam mit dem Parent-Datensatz gelöscht.
Die grundsätzlichen Vorteile eines Relations-Dictionary sind:
Struktur-Datei
Oft ist es recht kompliziert, gleichzeitig mit so vielen geöffneten Dateien zu hantieren. Darüberhinaus kann man entweder das ganze Dictionary nur für eine Applikation verwenden oder man muß sich für jede Applikation eine angepaßte Version erstellen. DAher ist es gelegentlich nützlich, mit einer denormalisierten vereinheitlichten Version dieser vier Tabellen zu arbeiten. In meinen eigenen Anwendungen begann ich damit, für jede Datenbank mit denormalisierten Struktur-Dateien zu arbeiten - aber bald gingen mir für Netzwerke die Datei-Handler aus als die Anwendung größer wurde.
Zusammenfassung
Man findent hervorragende Beispiele im Codebook von Alan Griver oder in George Gooleys Buch. Ein komplettes Data-Dictionary wird von Stonefield geliefert. Wirft man einen Blick auf Access 2.0, dann kann man dort vielleicht ein paar Dinge finden, die es für FoPro in der Version 3.0 geben dürfte. Ich biete Ihnen hier keine komplette eigene Version dieser Funktionen an, da diese sicher nicht besser sind als diejenigen, welche den meisten von Ihnen bereits zur Verfügung stehen. Wie der Großteil der FoxPro-Gemeinde warte auch ich hier auf die nächste FoxPro-Generation. Dieser erste Teil diente lediglich dazu, ein gemeinsames Minimum des Data-Driven Programming festzulegen. Nun können wir darüber hinausgehen.
Teil 2: Function-Dictionary
Das Entwicklungssystem MAGIC gewann ganz klar die beiden letzten DevCon-Entwicklerwettbewerbe. Der Grund für dieses hervorragende Abschneiden lag meines Erachtens darin, daß es mit MAGIC möglich ist, Parameter für Standardfunktionen in Masken einzusetzen. So kann man es sich sparen, dies alles von Hand zu codieren.
Genau wie im Fall des Data-Dictionary ist ein Function-Dictionary nur dann von Nutzen, wenn die entsprechende Funktion mehr als einmal aufgerufen wird. Funktionen, welche nnur ein einziges Mal aufgerufen werden, sollten auch weiterhin bei den Anwendungen und Masken bleiben, die sie aufrufen.
Die Bezeichnung "Function-Dictionary" ist ein wenig irreführend, da ich hierbei auch Aufrufe von Komponenten wie Sub-Applikationen oder Reports als Funktionen bezeichne. Gleiches gilt für standardisierte Funktionsaufrufe. Die gewählte Bezeichnung soll einfach nahelegen, daß im Prinzip fast alles, das mindestens zweimal aufgerufen wird, nicht mit hartcodierten Parametern benutzt werden muß, sondern in ein Data-Driven-Konzept eingepaßt werden kann.
Anwendungen
Alle meine Anwendungen werden über eine
Loader-Applikation gestartet, welche die einzige .EXE-Datei ist.
Diese Hauptanwendung überprüft alle Pfade und Zugriffsrechte
bevor sie das gewünschte Programm aufruft. Darüberhinaus
enthält sie alle Standardmasken, -Menüs, -Reports sowie
-Funktionen die benötigt werden. Für jede Anwendung existiert
eine Eintragung in der Apps-Tabelle.
DATABASE APPS | FoxPro Equivalents |
Comments |
APPNAME | char |
application |
PROJPATH | memo |
path for project |
TEMPPATH | memo |
temporary path |
APPPATH | memo |
application master path |
Ein Applikationen-Dictionary ist recht
hilfreich beim Wiedererstellen aller Module. Man scannt die
Tabelle durch und ruft dann das Kommando BUILD auf. Danach und
wenn kein Fehler auftrat bringt man die Anwendung im Pfad APPATH
unter. Auf die selbe Weise lassen sich auch der Projektpfad und
der temporäre Pfad wiedererstellen.
DATABASE APPS ext. | FoxPro Equivalents |
Comments |
DATE | date |
date build |
TIME | char |
time build |
SIZE | numeric |
application size of last build |
DOWNLOADIT | flag |
download to workstation |
Um die Startup- und die Reload-Zeit einer
Anwendung in einem Netzwerk zu verkürzenist es eine gute Idee,
die Anwendung auf die lokale Festplatte downzuloaden. Dabei
werden DATE, TIME und SIZE mit der Applikation verglichen.
DATABASE APPS ext. | FoxPro Equivalents |
Comments |
TABLE | char |
main table |
SCREEN | char |
main screen |
MENUE | char |
main menue |
PROGINIT | char |
startup procedure |
PROGEXIT | char |
cleanup procedure |
Da sich diese Dinge in verschiedenen
Anwendungen wiederholen können, gibt es Felder für die beim
Start zu öffnende Haupttabelle, die zu Beginn erscheinende
Hauptmaske und das aufgerufene Hauptmenü. Die Felder
PROGINIT/PROGEXIT dienen dazu, Startup und Cleanup anzupassen.
Sie können mit einem DO ausgeführt werden, wenn der Inhalt
nicht leer ist.
DATABASE APPS ext. | FoxPro Equivalents |
Comments |
MENUWIND | logical |
add standard window-pad |
MENUSATZ | logical |
add standard record-pad |
MENUTEXT | logical |
add standard edit-pad |
MENUTOOL | logical |
add standard tool-pad |
MENUHELP | logical |
add standard help-pad |
MENUDISK | logical |
add standard-disk-pad |
Da sich einer hoher Prozentsatz der Menüeinträge in jeder Anwendung wiederholt, entwickelte ich mir ein halbes Dutzend standardisierter Menüs die sich selbst an der passenden Stelle einfügen (außer dem Menü MENUWIND, welches sich links einordnet). Bei den Menüs TEXT, TOOL und HELP gibt es zwischen der Windows- und der DOS-Version leichte Unterschiede.
Funktionen
Funktionsaufrufe können hinter Buttons
"versteckt" oder über einen Menüpunkt erreichbar
sein. Oft ist es auch so, daß ein Benutzer beide Wege wählen
kann, eine Funktion aufzurufen. Bei Funktionsaufrufen aus Menüs
verwende ich die folgende Namenskonvention:
MENUNAME.PADNAME.PROMPT . Die Aufrufe werden in der
untenstehenden Tabelle gespeichert.
DATABASE BASEFILE | FoxPro Equivalents |
Comments |
CODE | char |
unique function ID |
TYPE | char |
name of standard function |
FILE | char |
depends, normally database name |
FIELDS | memo |
allowed/used fields |
ORDER | memo |
sorting sequence |
FROM | memo |
depends, normally database names |
WHERE | memo |
depends, where- or for-clause |
VALUE | memo |
depends, value to compare with |
EXTRA | memo |
depends, additional value |
TITLE | memo |
Window-title or text |
FLAG | flag |
depends, logical value |
SKIPFOR | memo |
skipfor-clause for menue |
DESCRIPT | memo |
description |
HELPTEXT | memo |
text-entry for FOXHELP.DBF |
Diese Datenbank ist denormalisiert, da es ziemlich sinnlos wäre, für jede spezielle Funktion eine eigene Tabelle zu erzeugen; dies würde das Öffnen um ein Vielfaches erschweren.
Gleich zu Beginn wird die SKIPFOR-Klausel ausgewertet. Die anderen Felder sind von Funktion zu Funktion unterschiedlich, wobei es sich hauptsächlich um Memo-Felder handelt, welche der Funktion als Parameter übergeben werden. Die Bezeichnungen der einzelnen Felder legen jeweils nahe, was wo abgelegt werden sollte.
Beispiele
Die Funktionen, welche ich am Häufigsten in meinen Anwendungen verwende sind REPORT FORM, BROWSE und ein SQL-SELECT in Verbindung mit einem Browse. Dies sind Kommandos und keine Funktionen? - Richtig, aber Function-Dictionary klingt doch viel besser als Command-Dictionary, oder?
Und da man diese Befehle niemals allein für sich stehenlassen kann, da sie noch Setup und Cleanup benötigen, hat man doch wieder so eine Art Funktionsaufruf. Ich nenne diese Funktionen dann F_REPO, F_BROWSE und F_BROSEL (BROwse + SELect).
Reports
Ein Block des Menüs der Anwendung ist
normalerweise für Reports reserviert. Oft werden Reports aus
anderen Funtionen heraus aufgerufen. Fast noch häufiger kommt
der Fall vor, daß ein Bericht mehr als einmal aufgerufen wird,
wobei sich lediglich die FOR-Klausel ändert. Man kann den
Standard-Reportaufruf auf folgende Parameter beschränken:
Datenbank, Ordnung der Datenbank, FOR-Bedingung, Name des Reports
und einen angezeigten Titel. Auf diese Art und Weise lassen sich
in kleineren Anwendungen 90 Prozent der Reportaufrufe erledigen.
FUNCTION REPOLIST | FoxPro Equivalents |
Comments |
FILE | char |
main database or alias if already open |
FIELDS | memo |
(to store fields used) |
ORDER | char |
order |
FOR | memo |
FOR-clause |
FROM | char |
name of report, default alias |
TITLE | memo |
optional, title |
EXTRA | numeric |
optional, number of records |
Beispiel:
=F_REPORT("customer","name","ZIP=65760","custlist",;
"short list of customers",20)
Das normale Kommando REPORT FORM benötigt eine geöffnete Datenbank. Wird der Funktion ein "_TEMP" als Dateiname übergeben, so erzeugt sie einen temporären Cursor mit einem Datensatz, z.B. zum Drucken des Clipboardinhalts. Umgebungsbedingungen wie Work Area, Satzzahl, Ordnung, Filter und Jahrhundert werden gespeichert und hinterher wiederhergestellt. Die Funktion prüft die Existenz der Datenbank und des Reports, Gültigkeit der Datensätze, den benötigten Speicherplatz und ob der Drucker bereit ist. Das Drucken kann mittels der Escape-Taste abgebrochen werden. In einem Netzwerk wird dabei auch der Drucker-Port freigegeben.
Eine kurze Suche nach Report-Befehlen in der Basisdatei führt zur Anzahl der Reports, welche davon betroffen sind, wenn die Dateistrukturen geändert werden. Außerdem läßt sich feststellen, wie viele Aufrufe eines Reports betroffen sind, wenn das Layout dieses Berichts geändert wird. Dies ist dann besonders wichtig, wenn man in verschiedenen Tabellen identische Feldnamen verwendet und so den selben Report für eine Vielzahl von Tabellen aufruft. Das Feld FIELDS wird in diesem Funktionsaufruf nicht verwendet. Möchte man analysieren, wieviel Arbeit eine Änderung der Dateistruktur verursachen würde, muß man die Feldnamen der .FRX-Datei in dieses Memofeld kopieren. Siehe unten.
Browse
Ich kann natürlich nicht wissen, ob meine
Kunden stärker an einer Vielzahl verschiedener Browse-Fenster
interessiert sind als die Ihren - aber ich zumindest benötige
das Kommando Browse wirklich sehr oft. Wiederum ist es möglich
die Anzahl der Parameter auf das Nötigste zu begrenzen:
Datenbank, Ordnung, Felder und FOR-Klausel. Öfter als die
Report-Funktion wird ein Browse von einem Button statt einem
Menü aus aufgerufen.
FUNC F_BROWSE | FoxPro Equivalents |
Comments |
FILE | char |
database name, default alias |
ORDER | char |
order for database |
EXTRA | memo |
editing allowed, default yes, (NODEL,NOAPP) |
FIELDS | memo |
fields for browse-command |
FLAG | flag |
lock records, default no |
FOR | memo |
FOR-condition |
VALUE | memo |
results in a BROWSE KEY FOR,VALUE |
Beispiel:
=F_BROWSE("customer","number",.T.,"number,name,town",;
.T.,"number>0")
Umgebungsbedingungen wie Work Area, Satzzahl, Ordnung, Filter und Felder werden gespeichert und hinterher wiederhergestellt. Die Funktion prüft die Existenz der Datenbank und stellt fest, wieviel Speicher benötigt wird. Enter sowie die rechte Maustaste bewirken dasselbe wie Ctrl-W. Die Header der Felder und alle anderen für die Felder benötigten Klauseln können aus dem Data-Dictionary entnommen werden und müssen nicht von Hand codiert werden.
Ich benutze eine annähernd identische Version dieser Funktion mit dem Namen F_JKEY. Diese verbindet ein Browse-Kommando mit dem hübschen Utility JKEY. Da man dabei sogar noch weniger Parameter benötigt, als für das Browse alleine, können wir dies hier überspringen.
Queries
Auch wenn das Kommando Browse alleine schon ein
sehr mächtiges Werkzeug ist, so läßt sich seine Wirkung noch
durch eine Kombination aus einem SQL-Select einem Cursor mit
anschließendem Browse übertreffen. Da es sich bei dem Befehl
SQL-Select meiner Meinung nach um FoxPro's stärkste Waffe
handelt, benötigt man nun auch noch einige zusätzliche
Parameter.
FUNC F_BROSEL | FoxPro Equivalents |
Comments |
FROM | memo |
database, default alias |
FIELDS | memo |
fields, default all |
WHERE | memo |
FOR/WHERE-condition |
VALUE | memo |
optional, additional comparing value |
ORDER | memo |
Order |
EXTRA | char |
optional, return value (evaluated) |
FILE | char |
INTO-clause/cursor-name, default QUERY |
TITLE | memo |
header for browse-window |
Beim Rückgabewert handelt es sich um den Namen eines Datenbankfeldes, dessen Wert dadurch ermittelt wird, daß der Parameter evaluiert wird, wenn der Benutzer nicht die Taste Escape drückt, um diesen Datensatz zu verlassen. Der zusätzliche Vergleichswert wird automatisch in eine Zeichenkette umgewandelt und der WHERE-Klausel übergeben. Dies erleichtert das Anstellen von Vergleichen (man kann dann etwa Kunden.Nummer anstelle von LTRIM(STR(Kunden.Nummer)) verwenden).
Die Menüs werden mittels einer globalen Speichervariable abgestellt. Umgebungsbedingungen wie Work Area, Satzzahl, Ordnung, Filter und Jahrhundert werden gespeichert und hinterher wiederhergestellt. Die Funktion prüft die Existenz der Datenbank, die Gültigkeit der Datensätze und stellt fest, wieviel Speicher benötigt wird. Enter sowie die rechte Maustaste bewirken dasselbe wie Ctrl-W. Bleibt nur ein einziger
Datensatz als Ergebnis übrig, so wird dieser in einem Edit-Fenster und nicht in einem Browse dargestellt. In meiner aktuellen Fassung dieser Funktion muß eine Ordnung definiert werden, falls eine FOR-Klausel existiert.
Ein wirklich gutes und auch umfangreiches Beispiel für die Koordination eines Data-Dictionary mit Reports, Queries und Browse-Fenstern ist FoxFire!. Daher möchte ich auch meine Beispiele nicht allzusehr in diese Richtung ausdehnen, da jeder Interessierte eine kompetentere Lösung in Form eines Third-Party-Tools erstehen kann.
Ersetzen und Löschen
Möchte man das Function-Dictionary als eine
Übersicht darüber verwenden, wo und wie Felder und Tabellen
verwendet werden, so ist ein weiterer wichtiger Bereich das
Ändern von Werten in Feldern und das Löschen von Datensätzen.
FUNC F_REPLACE | FoxPro Equivalents |
Comments |
FIELDS | memo |
Replace-string (<field> with <value>) |
FILE | char |
database, default alias |
ORDER | char |
order, default none |
FOR | memo |
FOR-clause |
TITLE | memo |
message for alertbox |
FLAG | logical |
optional, second confirmation needed |
Falls das Feld TITLE nicht leer ist, so wird eine Warnungsbox angezeigt, mit der um Bestätigung gebeten wird. Ist der logische Wert von FLAG .T. (also wahr), dann ersucht die Funktion um eine zweite Bestätigung. Falls keine FOR-Klausel existiert, so beziehen sich die Änderungen lediglich auf den aktuellen Datensatz.
Die Funktion F_DELETE verwendet die gleichen Parameter mit Ausnahme von FIELDS. In meinen Programmen werden Eintragungen zu Änderungen und Löschungen mit einer zweiten Bestätigung verbunden.
Masken
Ein wichtiger Punkt fehlt nun noch. Die Masken
zur Dateneingabe enthalten ebenfalls Felder. In meinen
Anwendungen wird jeder Maske eine Funktionstaste zugewiesen.
TABLE SCREENS | FoxPro Equivalents |
Comments |
FILE | char |
screen file, default alias |
FIELDS | memo |
(to store used fields) |
FROM | memo |
database, default alias |
VALUE | char |
function key to access window |
EXTRA | char |
name of first get field |
TITLE | char |
Window-title |
FLAG | logical |
optional, start with disabled gets |
Jetzt sind alle Möglichkeiten abgedeckt, bei denen Felder vorkommen: Reports, Browse-Fenster, Queries, Änderungs- und Eingabe-Masken. Falls man keine einmaligen Feldnamen verwendet, kann man Masken mit mehr als einer Datenbank benutzen.
Andere Beispiele
Nun möchte ich einige weitere Beispiele
standardisierter und typischer Funktionen bringen, welche man
leicht in ein Function-Dictionary einbauen kann. Oftmals muß man
nur einen einzigen Datensatz anzeigen lassen, z.B. den
Kundennamen zu einer Rechnung oder die Kontonummer eines Kunden.
In diesen Fällen verwende ich die Funktion F_SHOW.
FUNCTION F_SHOW | FoxPro Equivalents |
Comments |
FILE | char |
database, default alias |
ORDER | char |
order, default number, should be unique |
VALUE | memo |
search value |
TITLE | memo |
title for edit-window |
Beispiel:
=F-SHOW("customer","number",1000,"This is Cust.#1000)
Die Funktion F_SHOW kann um einen Maskenaufruf erweitert werden. Dies geschieht mit dem Rückgabewert eines F_BROSEL.
Ein anderes weitverbreitetes Problem ist die
Auswahl eines bestimmten Datensatzes aus einem Set oder einer
Übersichtstabelle. Für diesen Zweck habe ich eine typische
Übersichtsfunktion namens F_SELECT.
FUNCTION F_SELECT | FoxPro Equivalents |
Comments |
FILE | char |
database |
ORDER | memo |
order |
FIELDS | memo |
field, could be more than one field |
WHERE | memo |
FOR-clause |
TITLE | memo |
optional, title within window, default provided |
VALUE | memo |
optional, field to be replaced with fetched value |
EXTRA | memo |
optional, field containing value if not FIELDS |
Diese Funktion wählt FIELDS aus der Datei FILE nach der Bedingung WHERE aus und ordnet sie nach ORDER in einem Array an. Dieses Array wird durch eine Auswahlmaske mit einem Titelfeld dargestellt. Wird ein Wert gewählt, so kann er in einem Feld der Datenbank oder in einem anderen Feld des selben Datensatzes gespeichert werden.
Damit möchte ich dieses Thema abschließen. Es dürfte klar geworden sein, daß jede Funktion, die mehr als einmal verwendet wird und welche Felder in Tabellen ändert oder liest, in einem Function-Dictionary untergebracht werden kann. Der indirekte Aufruf über eine Funktonstabelle verlangsamt den Ablauf ein wenig.
Message-Dictionary
Da es keine autmatische Konversion der Codepage gibt, ist es sicher eine gute Idee, den Text der Alarmboxen und der Wait-Fenster in eine Datenbank zu extrahieren. Meiner Meinung nach, ist eine Alarmbox sowieso auch wieder eine Art von Funktionsaufruf. Für diese Zwecke benötigt man zwei Funktionen:
FUNKTION F_ALERT
FUNKTION F_WAIT
DA es hierfür aber gute Beispiele auf CompuServe gibt, muß ich hier keine eigenen anbieten. Felder werden in Nachrichten ohnehin selten benutzt, daher ist dieser Punkt für Überlegungen zum Thema Strukturwechsel auch nur von geringerer Bedeutung; ich habe ihn hier lediglich angeführt, damit niemand glaubt, daß ich ihn unterschlagen wolle.
Mögliche Erweiterungen
Procedure-Dictionary
Die Idee, alle Prozeduren und Funktionen aus Programmen, Masken und der Procedure-Datei selbst in eine Datenbank zu extrahieren, um die die Dokumentatiuon zu erleichtern, wurde bereits viel diskutiert. Aber erstens müßte man dann später doch wieder alles zurück in Programmdateien übertragen und zweitens kommt nach hinzu, daß, wenn man alle Parameter, Änderungen und wichtige Teile der Funktionen im Quellcode beschreibt, es einfacher ist diese in eine Dokumentationsdatei zu übertragen als umgekehrt. In diesem Fall steuern die Daten nichts und wir können die Sache abhaken.
Endanwender-definierte Programme
In FoxPro läßt sich der Parser für Textdateien verwenden. Man kann eine Datenbank mit einem Memofeld erzeugen, welches den vom Endanwender definierten Quellcode aufnimmt. Dieser wird in einen Cursor kopiert und dort mittels Macro-Expansion Zeile für Zeile verarbeitet. Für Schleifen und IF/CASE-Statements muß man ncoh einige Überlegungen anstellen, aber im Prinzip ist die Sache nicht sehr schwierig. So läßt sich ein Maximum an Anpassung an die individuellen Anwender realisieren. Ein biischen wie VBA für FoxPro.
Ein Beispiel hierfür findet sich in dem gewerblichen Programm WEEKEND. Um allerdings einen Blick auf den Quellcode werfen zu können, muß man über die Tool-Box ASSIST von Halogram verfügen.
Gruppierte Funktionen
Zur Zeit beschäftige ich mich damit, hierfür eine Art Macro-Funktion zu entwerfen. Oft ist es so, daß eine bestimmte Aufgabe durch eine Kombination von Standardfunktionen gelöst werden kann. Beispielsweise könnte man von einer Alarmbox ausgehen, dann ein SQL-Select auf die Daten anwenden, diese dann browsen und anschließend einen Bericht zu den angezeigten Daten ausdrucken. Eine Standardfunktion könnte diese Macros steuern und ein Cleanup durchführen, falls einer der Aufrufe ein FALSE zurückgibt.
Zusammenfassung
Es ist relativ einfach, standardisierte Funktionen in ein Function-Dictionary einzubauen. Dazu muß man nur den Originalfunktionsaufruf aus der .MNX-Datei extrahieren und die Delimiter zählen (siehe FUNKTION CUTPARMS). Der wichtigste Vorteil der Verwendung eines Function-Dictionary liegt meines Erachtens darin, daß man die Möglichkeit erhält, potentielle Probleme bei geplanten Strukturänderungen vorauszusehen. Dies ist eine große Hilfe bei der Abschätzung von Zeit und Kosten der Wartung. Ein weiterer Vorteil ist, daß sich die eigenene Programme so wesentlich leichter an die Bedürfnisse der einzelnen Anwender anpassen lassen, ohne daß der Code geändert werden muß.
Die wichtigsten Vorteile eines Function-Directory sind:
Die wichtigsten Vorteile eines Message-Directory sind:
Teil 3: Remote Function Calls - Fernaufruf von Funktionen
Einleitung
Wenn man schon mit einem Function-Dictionary arbeitet und somit Funktionen über eine Tabelle aufruft, dann ist es kein großer Schritt mehr, eine weitere Tabelle dazwischen einzuschieben. Man könnte sich das etwa wie folgt vorstellen: Man schreibt den Namen der standardisierten Funktion in eine Tabelle, welche dann eine andere Station durcharbeitet, die diese Funktion auch ausführt. Wozu das gut sein soll? Zum Beispiel in folgender Situation: Ihr Chef wartet vor seinem Rechner auf die Tagesauswertung während er gleichzeitig eine Liste der überfälligen Rechnungen bekommen und Zahlungserinnerungen auf den Weg bringen möchte. Da FoxPro bis jetzt zu echtem Multitasking nicht fähig ist, muß man sich damit behelfen, die Arbeit an eine andere Maschine im Netzwerk zu übertragen. Dieser andere Rechner braucht nur die Jobs-Datenbank in einer Endlos-Schleife zu durchsuchen.
Eine Bemerkung gibt es hierbei noch: Auch ohne ein Function-Dictionary kann diese Aufgabe bewältigt werden. Anstatt nur den Funktionsaufruf zu senden, muß man den kompletten Befehl in einem Memo-Feld der Jobs-Datenbank speichern.
Jobs-Datenbank
Die erste und zugleich leichtere Methode wird als eine Art spezieller Background-Server für Jobs verwirklicht. Dieser Rechner läuft mit einer Endlos-Schleife welche die Jobs-Datenbank nach Neueinträgen durchsucht. Wurde ein neuer Eintrag gefunden, so wird dieser abgearbeitet und danach als FINISHED (erledigt) markiert.
DATABASE JOBS | FoxPro Equivalents |
Comments |
STATION | char |
station ID |
JOBNAME | char |
name of function |
PARAMS | memo |
parameters for function |
DATE | date |
date entered |
TIME | char |
time entered |
FINISHED | flag |
flag it if finished |
MESSAGE | memo |
message for end user |
Soll ein neuer Job eingetragen werden, so fügt man einen Datensatz an die Job-Tabelle an. Die Funktion muß allerdings noch klären, ob der Job bereits in der Jobs-Datenbank vorhanden ist. Die benötigten Parameter müssen ermittelt und im Block PARAMS gespeichert werden. Nun ist der neue Job bereit, von einer anderen Station aus aufgerufen zu werden. Klickt der Chef noch einmal den Button oder das Icon für die Tagesauswertung an, dann muß ermittelt werden, ob der Job bereits erledigt war. Falls nicht, sollte eine Nachricht übermitteln, daß der Job noch bearbeitet wird. Sonst führt man einfach die Resultate vor und löscht anschließend den Job-Eintrag. Falls man die Workgroup Extensions oder das SEND-Kommando von Novell verwendet, kann man noch die Station notieren, von welcher aus der abgearbeitete Job kam.
Work While Wait - Arbeiten Anstatt Abwarten
Wie man weiß, sind nicht alle Stationen eines Netzwerks immer bei einem Server eingeloggt. Normalerweise sind etwa 5-10 Prozent der Stationen nicht aktiv. Man kann also, anstelle eines bestimmten Background-Servers der die Jobs abarbeitet, auch eine eigene Login-Maske zur Verteilung der Hintergrundaufgaben an verschiedene Stationen verwenden. Diese können die Jobs dann abarbeiten, während sie auf einen Benutzer warten. Das größte Problem hierbei ist, daß der Benutzer zurückkommen kann während der Job noch bearbeitet wird. Möchte man ihm dann nicht eine Nachricht präsentieren und ihn auf die Erledigung des Jobs vertrösten, muß man die Bearbeitung des Jobs unter- oder sogar abbrechen. Da der Job nicht als erledigt markiert wird, übernimmt eine andere Station diese Aufgabe.
Multiple Jobs
Für mich war der Hauptgrund, diese Funktion zu entwickeln, mein Problem damit, alle Indices über Nacht wiederherzustellen. Da meine Hauptapplikation über 200 Tabellen verwendet, ist es faktisch unmöglich, alle Indices auf einem Rechner in einer Nacht zu rekonstruieren. Die Befehle Reindex, Pack und Pack Memo sind extrem zeitaufwändig wenn die Tabellen sehr großen Umfang annehmen und sie verwenden viele Indices. Das Reindizieren einer Tabelle mit 100.000 Datensätzen und 40 Indices dauert etwa vier Stunden. Gibt es also nur noch drei weitere Dateien dieser Größenordnung, ist die Arbeit von einer einzigen Station in einer Nacht nicht zu bewältigen. Alle Stationen zusammengenommen schaffen die Arbeit allerdings leicht bis Mitternacht.
Eine Verbindung zum Data-Dictionary kann in diesem Zusammenhang recht hilfreich sein. Man markiert lediglich alle Tabellen die über Nacht bzw. übers Wochenende gepackt, memo-gepackt oder reindiziert werden sollen im Tables-Dictionary und kopiert das Dictionary versehen mit den entsprechenden Parametern in die Jobliste.
Nun braucht man ncoh ein neues Memo-Feld MAINTAIN. Dort behandelt man die verschiedenen Möglichkeiten als EOD, EOW, EOM oder EOJ mit entsprechenden Kommandos REINDEX, PACK oder PACK MEMO dahinter. Um die Gesamtarbeitszeit zu verringern, ist es praktischer, sollten die Tabellen absteigend nach Dateigröße multipliziert mit der Anzahl der Indices sortiert werden. So wird verhindert, daß die größte Datei als letzte abgearbeitet wird.
Noch ein Hinweis: Die Netzwerkkonfiguration ist für die Verarbeitungsgeschwindigkeit multipler Jobs äußerst wichtig, da es schnell dazu kommen kann, daß man mit allen "Flaschenhälsen" gleichzeitig zu kämpfen hat. Die folgenden Tips wurden für Novell Networks Version 3.12 getestet: Die Übertragungsgeschwindigkeit läßt sich dadurch steigern, daß man bis zu vier Netzwerkkarten in den Server einbaut und größere Jobs auf Rechnern laufen läßt, die über ein gesondertes Kabel verbunden sind. Die Anzahl der Datensätze überhaupt (100K) und auf einer Station (10K) läßt sich dadurch maximieren, daß man 10 MB als Kurzzeitspeicher verwendet. Anstelle einer Partition mit 1 GB verwendet man 5x200 MB oder eine ähnliche Kombination. Dies erhöht die Anzahl der Festplattenleseköpfe.
Zeitgesteuerte Ereignisse
Möchte man einen Job zu einer bestimmten Zeit
starten, also beispielsweise nachts oder am Wochenende, dann muß
man die Jobs-Datenbank noch um einige Felder ergänzen.
DATABASE JOBS ext. | Equivalents FoxPro |
Comments |
STARTDATE | date |
starting date |
STARTTIME | char |
starting time |
REPEATIT | memo |
rythm for repeating |
LASTDATE | date |
last date called |
Das Feld REPEATIT kann für Einträge verwendet werden, welche sich jeden Tag oder jede Woche wiederholen und unbeaufsichtigt laufen können. Erlaubte Einträge sind hier DAILY, MONDAY-SATURDAY und END-OF-DAY/WEEK/MONTH/YEAR. Für die END-OF-Funktionen ist die Uhrzeit lediglich ein Sortierkriterium für einen gesonderten Durchlauf, welcher seperat gestartet werden muß.
Die Endlos-Schleife muß so erweitert werden, daß sie auch nach solchen zeitgesteuerten Ereignissen sucht. Wird ein Ausführungszeitpunkt erreicht, dann wird der Job gesperrt, ein neuer Jobeintrag für die zu erledigenden Arbeiten wird erzeugt und unter LASTDATE wird das aktuelle Datum eingetragen. Für zeitgesteuerte Ereignisse wird ein gesonderter Background-Server benötigt, um sicherzustellen, daß der Job zum richtigen Zeitpunkt ausgeführt wird und um zu verhindern, daß allzuviel Netzwerkverkehr entsteht, wenn auf den Stationen immer wieder nach zeitgesteuerten Ereignissen gesucht wird.
Validation
Manchmal sind Validationen bereits zu komplex um noch während einer hacktischen <WORTSPIEL, der Übersezzer> Dateneingabe durchgeführt zu werden. Es kommt auch vor, daß die Anwender eine spezielle Warnmeldung bekommen wollen, wenn ein bestimmtes Feld geändert oder ein gegebener Wert erreicht wird. Man kann soetwas nicht im Code selbst festlegen, da es sich von Benutzer zu Benutzer ändern kann. Hier kommt der Begriff Trigger ins Spiel: So nennt man einen Mechanismus, welcher spezifische Funktionen aufruft, wenn bestimmte Daten geändert werden. Aber zuerst braucht man natürlich noch eine History-Datei, die alle Änderungen mitprotokolliert. Immer wenn ein Datensatz neu eingesetzt, geändert oder gelöscht wird, macht die Bestätigungsfunktion einen Eintrag in eine Protokolldatei.
FUNKTION J_CHANGE
PARAMETER cTYPE
übergibt Alias() an DATABASE, Recno() an
RECNO, cType an TYPE und das Array OldArray an OLDVALUE (das
Array muß vor dem Editieren erstellt werden)
DATABASE CHANGES | Equivalents FoxPro |
Comments |
DATABASE | char |
name of database |
RECNO | number |
record number of changed record |
TYPE | char |
type of change (insert, delete, edit, reverse) |
OLDVALUE | memo |
array of former values |
FINISHED | flag |
flag |
Für das Feld TYPE sind vier verschiedene Einträge möglich. Ein Datensatz kann eingesetzt, geändert, gelöscht oder umgekehrt werden. Die Unterscheidung zwischen Umkehrung und Löschung ist wichtig für Buchhaltungssysteme.
Im Feld OLDVALUE stehen die Werte des Datensatzes vor der Änderung, gespeichert in einem Memofeld. Bei einem Triggeraufruf lassen sich so Vergleiche zwischen den alten und den neuen Werten anstellen. Dadurch wird es möglich, Trigger auch auf die Veränderung von Werten aufzubauen.
Die Struktur der Protokolldatei kann noch um die Felder "Station", "Benutzer", "Datum", "Uhrzeit" und vielleicht auch noch "Grund der Änderung" ergänzt werden, um die Daten zu vervollständigen.
Die zweite Datenbank enthält die Trigger
selbst:
DATABASE TRIGGERS | Equivalents FoxPro |
Comments |
DATABASE | char |
name of database |
TYPE | char |
type of change (insert, delete, edit, reverse) |
WHEN | memo |
evaluated trigger-clause |
FUNCTION | memo |
function to be called |
FINISHED | flag |
flag |
Diesmal ist der Prozeß ein wenig komplizierter. Ein mit dem Server verbundener Rechner arbeitet als Background-Server für die Validationen. Wieder gibt es eine Endlos-Schleife, mit den folgenden Schritten:
(am Schnellsten mittels eines Index auf NOT FINISHED)
(wird etwa ein Kunde aufgenommen, suche nach DATABASE=custommer und TYPE=Insert)
Nun ist der für die Validation zuständige Background-Server in der Lage, alle Änderungen in jeder Datenbank mitzuverfolgen, die alten und die neuen Einträge zu vergleichen und eine Funktion aus dem Function-Dictionary aufzurufen. Diese Art von Validation im Hintergrund ist von den Apllikationen völlig unabhängig. Es kann von geschulten Endanwendern auf die jeweils eigenen Bedürfnisse angepaßt werden. Meine eigene Buchhaltungssoftware basiert auf dieser Idee und erlaubt so Erweiterungen in alle Richtungen, wie etwa Inrechnungstellen von Adressänderungen bei Kunden (nicht geplant) oder das direkte Übergeben von fälligen Rechnungen an den Rechtsanwalt, falls die Rechnungssumme einen bestimmten Betrag überschreitet. Man kann das alles natürlich auch direkt in die Anwendungen hineinprogrammieren, verliert hierdurch aber ein gewisses Maß an Flexibilität. Das Beste ist die Tatsache, daß FoxPro 3.0 ODBC Level 2 verwendet und somit in der Lage ist, Tabellen anderer Datenbankverwaltungssysteme zu lesen. Es wird also möglich sein, Trigger auf Änderungen von Daten in verschiedenen Programmen aufzubauen.
Arbeitet man mit Tabellen anderer Datenbankverwaltungssysteme oder braucht nicht mehr als ein Dutzend Tabellen, kann man auf eine Protokolldatei verzichten und die Datenbankdateien direkt nach Änderungen durchsuchen. FÜr meine eigenen Anwendungen bleibe ich allerdings lieber bei einer Protokolldatei, da sie Daten für statistische Auswertungen liefert und mir vielleicht aus der Patsche hift, wenn irgendetwas schief gelaufen war. Hat man auch die Memofelder gespeichert, so kann man die alten Werte wiederherstellen und und eine Art Transaction machen. Man sollte auch nicht vergessen, die Protokolldatei wenigstens einmal in der Woche zu packen.
Noch eine Bemerkung zum Schluß: Verwendet man in der Trigger-Datenbank Validationen, welche aus irgendeinem Grund den Originaldatensatz wiederherstellen, dann benötigt man ein zusätzliches Feld in allen Tabellen. Mit diesem Feld wird sichergestellt, daß die Änderungen geprüft wurden, um zu verhindern, daß die Benutzer die Eingaben wieder ändern. Eine andere Möglichkeit wäre, in der Protokolldatei die "geänderten" Eingaben zu sperren, bis der Benutzer den Rest des Editierens beendet hat und danach dann alle Änderungen auf einmal zu verifizieren.
Zusammenfassung
In diesem Artikel verwende ich eine Menge hochrangige Begriffe wie REMOTE PROCEDURE CALL, TRIGGER, TIMED EVENTS und BACKGROUND SERVER. Diese Dinge finden sich auf einem wesentlich höheren und komplexeren Niveau als hier vorgeführt in großen Datenbanksystemen wie z.B. Oracle, Informix oder Sybase. Wie bei den Data-Dictionaries sind meine Ideen nur Miniaturausgaben ihrer großen Brüder - lassen Sie sich also nicht verwirren, wenn diese Begriffe in anderen Zusammenhängen fallen. Das zugrundliegende Konzept wird wohl ähnlich sein, aber die Ausführung ist wirklich sehr verschieden. Meine Vorstellung war dabei, diese Konzepte aus luftiger Höhe hinunter auf ein praktikables Niveau zu bringen, welches es ermöglicht, sie direkt in FoxPro-Anwendungen zu integrieren.
Die wichtigsten Vorteile des Background Processing (der Hintergrundverarbeitung) sind:
(falls eine Aufgabe von einer Station allein nicht zu bewältigen ist)
Ich hoffe, Sie haben viel Freude an den Vorteilen , welche Ihnen die oben dargestellten Konzepte bieten. Ich nenne es "Client/Server ohne einen Server" - stattdessen mit einigen Rechnern die sich der Sache annehmen.
Data-Driven Programming
(c)1994 Rainer Becker