PDF file - IDB - Universität Bonn

Werbung
Rheinische Friedrich-Wilhelms-Universität Bonn
Institut für Informatik
Abteilung III
Diplomarbeit
DatalogLab: Ein Tool zur Verwaltung
regeldefinierter Daten in Datalog
Bastian Kraemer
29. März 2007
Erstgutachter: Professor Dr. R. Manthey
Inhaltsverzeichnis
1. Einleitung................................................................................................................... 1
2. Datalog ....................................................................................................................... 4
2.1 Syntax von Datalog ............................................................................................... 4
2.1.1 Regel- und Faktendarstellung......................................................................... 5
2.1.2 Erweiterung um Built-In-Operatoren ............................................................. 8
2.1.3 DDL und DML ............................................................................................. 10
2.2 Semantik von Datalog ......................................................................................... 13
2.2.1 Stratifikation ................................................................................................. 14
2.2.2 Fixpunktsemantik für stratifizierbare Regelmengen .................................... 15
2.2.3 Fixpunktsemantik für nichtstratifizierbare Regelmengen ............................ 17
2.2.4 Semantik von Anfragen und Änderungen .................................................... 21
2.2.5 Semantik von Built-In-Operatoren ............................................................... 24
3. Verfahren zur Fixpunktberechnung..................................................................... 26
3.1 Fixpunktberechnung für stratifizierbare Regelmengen....................................... 26
3.1.1 Naive Fixpunktberechnung .......................................................................... 27
3.1.2 Semi-naive Fixpunktberechnung.................................................................. 29
3.1.3 Iterierte Fixpunktberechnung ....................................................................... 32
3.2 Fixpunktberechnung für nichtstratifizierbare Regelmengen............................... 34
3.2.1 Fixpunktberechnung mit bedingten Fakten.................................................. 34
3.2.2 Alternierende Fixpunktberechnung.............................................................. 35
4. Entwurf des DatalogLabs....................................................................................... 37
4.1 Anforderungsanalyse........................................................................................... 37
4.1.1 Aufgaben des DatalogLabs........................................................................... 37
4.1.2 Entwurfskriterien für die graphische Oberfläche ......................................... 41
4.2 Kriterien für die Wahl der Programmiersprache Java ........................................ 45
4.3 Systemarchitektur................................................................................................ 46
5. Implementierung der Datenbankfunktionalitäten .............................................. 51
5.1 Darstellung von Regeln und Fakten in Java........................................................ 52
5.2 Schemamanager .................................................................................................. 62
5.3 Datenbankmanager.............................................................................................. 66
5.4 FPI-Verfahren...................................................................................................... 67
5.4.1 Naive Fixpunktberechnung .......................................................................... 68
5.4.2 Semi-naive Fixpunktberechnung.................................................................. 70
5.4.3 Iterierte Fixpunktberechnung ....................................................................... 71
5.5 Datenbankschnittstelle ........................................................................................ 73
5.6 Anfragemanager .................................................................................................. 74
5.7 Integritäts- und Transaktionsmanager................................................................. 76
5.8 Sonstige Klassen (und wichtige Methoden)........................................................ 78
5.8.1 Die Methoden zur Variablensubstitution ..................................................... 78
5.8.2 Die Vergleichesmethoden der Datenstruktur ............................................... 79
5.8.3 Datalog-Parser .............................................................................................. 81
5.8.4 Hilfsklassen und deren Methoden ................................................................ 85
6. Implementierung der graphischen Oberfläche.................................................... 88
6.1 Darstellung von Anwendungskomponenten ....................................................... 92
6.1.1 Schema ......................................................................................................... 92
6.1.2 Regeln........................................................................................................... 94
6.1.3 Fakten ........................................................................................................... 95
6.1.4 Integritätsbedingungen ................................................................................. 98
6.1.5 Stratifikation ................................................................................................. 99
6.2 Anfrageeditor .................................................................................................... 100
6.3 Statistische Informationen................................................................................. 101
6.4 Statusfenster ...................................................................................................... 102
7. Beispielanwendung des DatalogLabs .................................................................. 105
8. Zusammenfassung und Ausblick......................................................................... 111
Literaturverzeichnis ................................................................................................. 116
Anhang ....................................................................................................................... 118
Abbildungsverzeichnis
Abbildung 3.1: Algorithmus zur naiven Fixpunktberechnung .................................... 27
Abbildung 3.2: Algorithmus zur semi-naiven Fixpunktberechnung ........................... 30
Abbildung 3.3: Algorithmus zur bedingten Fixpunktberechnung (CFP) .................... 35
Abbildung 3.4: Algorithmus zur alternierenden Fixpunktberechnung („doubled
program“ AFPI).................................................................................................. 36
Abbildung 4.1:Architekturübersicht des deduktiven Datenbanksystems DatalogLab 48
Abbildung 5.1: UML-Klassendiagramm zur Argumenthierarchie .............................. 58
Abbildung 5.2: UML-Klassendiagramm zur Literalhierarchie .................................... 59
Abbildung 5.3: UML-Klassendiagramm zum Regelaufbau ....................................... 60
Abbildung 5.4: Aufbau der Anfragebeantwortung ..................................................... 76
Abbildung 6.1: Hauptansicht der graphischen Oberfläche........................................ 89
Abbildung 6.2: Symbolleiste mit Erklärungen als eigenständiges Fenster................ 90
Abbildung 6.3: Deklarationsdialog ............................................................................ 93
Abbildung 6.4: Symbolleiste der Schemaansicht...................................................... 94
Abbildung 6.5: Symbolleiste der Regelansicht.......................................................... 95
Abbildung 6.6: Symbolleiste der Faktenansicht ........................................................ 96
Abbildung 6.7: Faktenansicht mit geöffneten Faktenfenstern ................................... 97
Abbildung 6.8: Symbolleiste der Faktenfenster ........................................................ 98
Abbildung 6.9: Symbolleiste der Integritätsansicht ................................................... 98
Abbildung 6.10: Hauptbereich mit zweischichtiger Stratifikation..............................100
Abbildung 6.11: Anfrageeditor mit einigen Beispielen .............................................101
Abbildung 6.12: Fenster mit Integritätsbedingungen, gegen die verstoßen wird .....103
Abbildung 6.13: Optionsfenster ...............................................................................104
Abbildung 7.1: Neueingefügte Deklarationen ..........................................................106
Abbildung 7.2: Statusleiste Hauptfenster.................................................................106
Abbildung 7.3: graphische Darstellung des Beispiels .........................................107
Abbildung 7.4: Eingefügte Fakten............................................................................107
Abbildung 7.5: Eingefügte Regeln ...........................................................................108
Abbildung 7.6: Ergebnis der Fixpunktiteration .........................................................109
Kapitel 1
Einleitung
Die meisten Datenbanksysteme ermöglichen neben ihrer Hauptaufgabe, der Informationsspeicherung und Verwaltung, auch die Ableitung neuer Daten aus den bereits bekannten. Solche Datenbanksysteme werden als deduktive Systeme bezeichnet. Sie bieten dem Anwender die Möglichkeit, Ableitungsregeln zu formulieren und ebenfalls im
System zu speichern. Zu den deduktiven Datenbanksystem zählen alle Systeme, die
Sichten (engl. views) [13] erlauben, wie sie z.B. aus der Datenbanksprache Structured
Query Language (SQL) [13] bekannt sind. In der theoretischen Datenbankforschung
hat sich aus unterschiedlichen Gründen im Zusammenhang mit deduktiven Systemen
nicht die bei relationalen Datenbanken verbreitete Sprache SQL, sondern Datalog als
die gängige Anfrage- und Regeldefinitionssprache etabliert. Ein Grund dafür ist die
einfache Syntax, die im Kern aus einem minimalen Satz an Operatoren (Konjunktion
und Negation) besteht. Eine wesentliche Stärke von Datalog gegenüber den meisten
anderen Datenbanksprachen ist die vollständige Unterstützung jeglicher rekursiver
Ableitungsregeln.
Für die Ableitung der Daten bietet ein deduktives Datenbankmanagementsystem
(DBMS) eine eigene Ableitungskomponente an. Für die Semantikdefinition rekursiver
Spezifikationen existieren zwei unterschiedliche Vorgehensweisen: Zum einen die
modelltheoretische Semantik und zum anderen die Fixpunktsemantik, wobei letzteres
intuitiver ist und zugleich ein konstruktives Verfahren zur iterativen Herleitung aller
ableitbaren Fakten darstellt. In dieser Arbeit soll nur der fixpunktbasierte Ansatz verfolgt werden.
Da viele Methoden zur effizienten Verarbeitung von Daten in Datalog vorgestellt wurden, ist es ein Ziel der vorliegenden Diplomarbeit, Datalog für Lehrzwecke besser
nutzbar zu machen. Weiter soll auch die Möglichkeit zum empirischen Vergleich unterschiedlicher Algorithmen geschaffen werden (beispielsweise zum Ableiten neuer
Informationen). Zu diesem Zweck soll das DatalogLab konzipiert und entwickelt werden. Es soll aus einem mit allen grundlegenden Funktionalitäten ausgestatteten deduktiven Datenbankmanagementsystem bestehen und eine graphische Benutzeroberfläche
zu Steuerung des DBMS beinhalten. Zu den grundlegenden Funktionalitäten des
DBMS sollen neben der Datenverwaltung vor allem auch die Herleitung neuer Fakten,
sowie die naive Beantwortung von Anfragen durch das DBMS und einer Komponente
zur Verwaltung von Integritätsbedingungen (IB), gehören.
1
Die Aufgabe der graphischen Oberfläche besteht darin, die vom DBMS realisierten
Funktionen für den Anwender vollständig nutzbar zu machen. Dabei soll der Benutzer
durch vereinfachte Dialoge, intuitive Programmsteuerung, automatisch angezeigte
Hilfstexte und, soweit möglich, auch durch vorgenerierte Ausdrücke unterstützt werden.
All diese Funktionalitäten müssen in einer geeigneten Programmiersprache umgesetzt
werden. Zur Realisierung soll dabei die Programmiersprache Java benutzt werden.
Diese zeichnet sich neben ihrer Plattformunabhängigkeit auch durch eine sehr umfangreiche Standardbibliothek aus, welche vor allem im Bereich der Oberflächenprogrammierung die Entwicklung erleichtert. Ein weiterer Grund für die Wahl von Java ist die
Tatsache, dass es sich um eine objektorientierte Programmiersprache handelt. Damit
lässt sich durch eine strukturierte und modularisierte Klassenstruktur eine weitere
wichtige Eigenschaft des DatalogLabs, nämlich die Erweiterbarkeit um die im Rahmen dieser Arbeit nicht vollständig zu entwickelnden und umzusetzenden Komponenten und Methoden, realisieren.
Im Gegensatz zu den Realisierungsansätzen von S. Lobko [16] und V. Bill [2], bei denen der Schwerpunkt der Arbeit auf einer Teilkomponente eines deduktiven Datenbanksystems lag, soll im Rahmen dieser Arbeit ein Programm geschaffen werden,
welches bereits alle nötigen Komponenten enthält, um die oben beschriebenen Aufgaben zu erfüllen. Dadurch kann sichergestellt werden, dass die unterschiedlichen Komponenten kompatibel zueinander sind. Die Zusammenarbeit der eigenständige Komponente zur Anfragebearbeitung von S. Lobko [16] und der Komponenten zur FPI von
V. Bill [2] ist nicht möglich, da beide zwar ähnliche aber dennoch im Detail unterschiedliche Datenstrukturen nutzen.
Das Resultat dieser Arbeit ist ein für Lehrzwecke einsetzbares Programm zur Verwaltung von Fakten und Regeln mit der Datenbanksprache Datalog. Das DatalogLab erlaubt das Herleiten neuer Fakten, sowie das Stellen von Anfragen an die Datenbank.
Für die Fixpunktiteration zur Faktenherleitung können unterschiedliche Vorgehensweisen und Algorithmen ausgewählt werden. Außerdem ermöglicht das DatalogLab
das Protokollieren des Fixpunktprozesses, um empirisch die Effizienz der unterschiedlichen Methoden zu vergleichen. Bei der Erstellung des DatalogLabs wurden potentielle Verbesserungen thematisiert und beim Entwurf auf nahe liegende Weiterentwicklungen hingewiesen.
Die hier vorliegende Arbeit ist wie folgt aufgebaut: Nach der Einleitung werden im
zweiten Kapitel die Syntax und die Semantik von Datalog definiert. Dies ist nötig, da
für Datalog keine einheitliche Syntax existiert, obwohl Datalog in der Forschung weit
verbreitet ist. Dies betrifft vor allem die Datalog-Erweiterungen wie Anfragen, Integritätsbedingungen und Build-In-Funktionen. Im dritten Kapitel werden effiziente Methoden zur Ableitung neuer Fakten formal betrachtet und einige der gängigen Fixpunktverfahren erläutert. Im vierten Kapitel werden die Anforderungsanalyse und der
Entwurf des DatalogLabs vorgestellt. In den Kapiteln zwei, drei und vier erfolgt somit
eine umfangreiche theoretische Beschreibung von Datalog und deduktiven Datenbanken und die Anforderungsanalyse des DatalogLabs. Die folgenden Kapitel fünf und
2
sechs befassen sich hingegen mit der konkreten Realisierung des Programms. Die Darstellung deduktiver Regeln und aller dazu benötigten Klassen werden im fünften Kapitel erläutert. Das Kapitel befasst sich weiterhin mit dem Aufbau und der Realisierung
der deduktiven DBMS-Komponenten. Zu diesen Komponenten gehört auch der Fixpunktmanager, der die implementierten Fixpunktmethoden zur Verfügung stellt. Das
sechste Kapitel befasst sich mit der Implementierung einer graphischen Oberfläche für
das entworfene deduktive DBMS. Die Umsetzung erfolgt ebenfalls in Java. Im siebten
Kapitel wird die Nutzung des DatalogLabs an einem kurzen Beispiel demonstriert. In
Kapitel 8 folgt schließlich eine Zusammenfassung der gesamten Arbeit. Außerdem
wird abschließend ein Ausblick über mögliche Erweiterungen gegeben.
3
Kapitel 2
Datalog
In diesem Kapitel werden die Syntax und Semantik von Datalog vorgestellt. Dabei
werden [6] sowie [4] und vor allem [17] als Grundlage dienen. Dabei ist im Folgenden
mit Datalog das um Negation erweiterte Datalog⌐ (oder auch DatalogNeg) gemeint. In
Abschnitt 2.2.5. wird Datalog dann noch um verschiedene Built-In-Operatoren erweitert. Obwohl weitgehend Einigkeit über die Syntax und Semantik der Datalog-Fakten
und -Regeln herrscht, gilt dies nicht für die erweiterten Konzepte, wie Integritätsbedingungen, Anfragen, Änderungen oder den Aufbau des Datenbankschemas. Auch im
Hinblick auf eine spätere Nutzung des zu erstellenden Programms wird bei unterschiedlichen Notationen in der Fachliteratur immer der Darstellung von [17] gefolgt.
2.1 Syntax von Datalog
Das Datalog-Alphabet besteht aus:
• Konstanten,
• Variablen,
• Prädikatsymbolen,
• Operatoren und
• Schlüsselwörtern.
Konstanten sind beliebige in Hochkommata eingeschlossene Zeichenketten (z.B.: ’12,
aaB’), Zahlen und Zeichen (z.B.: a) bzw. Zeichenketten, die mit einem Kleinbuchstaben beginnen (z.B.: xYZ).
Variablen müssen durch Großbuchstaben (z.B.: X) oder Zeichenketten, die mit Großbuchstaben beginnen (z.B.: Variable_1A) definiert werden. Außerdem können lokale
Variablen durch den Unterstrich („_“) abgekürzt werden, wenn keine Bindung zu dieser Position benötigt wird. Diese werden als „don’t cares“ bezeichnet.
Prädikatsymbole bestehen, ähnlich wie Konstanten, aus Kleinbuchstaben (z.B.: p) oder
Zeichenfolgen, die mit einem Kleinbuchstaben beginnen (z.B.: parent_of). Sie bezeichnen Relationen und haben eine Stelligkeit größer gleich Null. Konstanten und
Prädikate lassen sich immer auf Grund ihrer Position eindeutig unterscheiden.
4
Die Menge der Operatoren besteht aus dem Negationssymbol „⌐“, alternativ kann dies
auch durch die Zeichenfolge not ausgedrückt werden, dem Implikationspfeil „←“ und
dem logischen Konjunktionsoperator, der durch ein Komma „,“ dargestellt wird. Weiter gehören der Punkt „.“, das Ausrufezeichen „!“, das Fragezeichen „?“, die geschweiften Klammern „{ … }“ sowie die runden Klammern „( … )“, der Doppelpunkt
„:“ und das Semikolon „;“, das Plus- „+“ und das Minuszeichen „-“ zu den DatalogOperatoren.
Der Punkt dient zum Abschließen von Fakten, Regeln und Schemadefinitionen. Das
Fragezeichen beendet Anfragen und das Ausrufezeichen Änderungen und Transaktionen. Das Plus- und das Minuszeichen stehen in Datalog nicht für die arithmetischen
Operationen „Plus“ und „Minus“, sondern dienen zum Einfügen und Löschen von
Fakten. Bei den Built-In-Operatoren werden sie dann überladen und erhalten ihre bekannten arithmetischen Bedeutungen.
Zu den Schlüsselwörtern zählen derived, stored und constraint die für die Schemaverwaltung benötigt werden. Außerdem with und where die bei Änderungen, Anfragen
und zur Regelergänzung benutzt werden. Ein weiters Schlüsselwort ist die Konstante
true.
2.1.1 Regel- und Faktendarstellung
Ein Datalog-Programm besteht aus einer endlichen (duplikatfreien) Menge von Fakten
und Regeln. Diese bestehen aus positiven und negativen atomaren Formeln, auch Literale genannt. Literale bestehen wiederum aus Termen.
Definition 2.1 (Term)
Ein Term besteht aus Variablen oder Konstanten.
Definition 2.2 (atomare Formel)
Sei p ein n-stelliges Prädikatensymbol und seien ti (i=1, …, n mit n ≥ 0) Terme, dann
ist
p(t1, …, tn)
eine atomare Formel, bzw. ein Atom. Für ein Atom A = p(t1, …, tn) bezeichnet
pred(A) das zu A gehörende Prädikatensymbol p und arity(A) die Anzahl der Parameter ti, also arity(A) = n. Atomare Formeln werden auch als Literale bezeichnet.
5
Definition 2.3 (Formel)
Formeln sind:
1) die Konstante true,
2) positive atomare Formeln (auch positive Literale genannt) A,
3) negierte atomare Formeln (negierte Literale) ⌐ A und
4) Konjunktionen von Literalen L1, …, Ln wobei Li (i=1, …, n und n ≥ 1) Literale
sind.
Definition 2.4 (Regel)
Unter einer Regel R versteht man einen Ausdruck der Form
R:
A←W
Wobei A ein positives Literal ist und als Kopf der Regel bezeichnet wird. W ist eine
Formel und wird Regelrumpf, oder einfach nur Rumpf genannt. Entartete Regeln, deren
Rumpf nur aus der Konstante true bestehen, werden als künstliche bedingte Fakten
bezeichnet und vereinfachen die Formalisierung der bedingten Fixpunktoperation, die
später ausführlich erklärt wird. Deswegen soll diese Darstellungsform von Fakten erlaubt bleiben. Ist W ≡ true, und lässt man den unnötigen Regelrumpf weg, so bezeichnet man A als (gewöhnliches) Fakt. Fakten oder Regeln bezeichnet man auch als Datenbankklauseln.
Um jedoch sicherzustellen, dass das Ergebnis einer Regelauswertung endlich ist, bedarf es einiger weiterer Definitionen.
Definition 2.5 (Variablenvorkommen)
Für eine Formel W bezeichnet man vars(W) als die duplikatfreie Menge von Variablen, die in W vorkommen. Gilt vars(W) = Ø, dann wird die Formel W als grundinstanziiert bezeichnet.
Man unterscheidet weiter zwischen Input- und Outputvariablen. Inputvariablen sind
dabei solche, die nur in negativen Literalen vorkommen und schon vor Auswertung
dieser Literale gebunden, d.h. mit einem Wert belegt sein müssen. Alle anderen Variablen, also diese die in mindestens einem positiven Literal vorkommen, bezeichnet
man Outputvariablen.
6
Definition 2.6 (Input- und Outputvariablen)
Sei W eine Formel, dann sind die Menge der Inputvariablen vars+(W) und die Menge
der Outputvariablen vars-(W) definiert als:
1) vars+(W) := Ø und vars-(W) := vars(A), wenn W ≡ A ein positives Literal ist,
d.h. alle Variablen eines positiven Literals sind Outputvariablen.
2) vars+(W) := vars(⌐A) und vars-(W) := Ø, wenn W ≡ ⌐A ein negatives Literal
ist, d.h. alle Variablen eines negativen Literals sind Inputvariablen.
3) vars+(W) := Ui=1,…,n vars+(Ai) / Ui=1,…,n vars-(Ai) und
vars-(W) := Ui=1,…,n vars-(Ai),
wenn W = A1,..., An mit n ≥ 1 und Ai (i=1, ..., n) Literale sind.
Inputvariablen bezeichnet man auch als ungebundene und Outputvariablen als gebundene Variablen.
Mit diesen Definitionen lassen sich nun sichere deduktive Regeln definieren.
Definition 2.7 (sichere deduktive Regel)
Eine sichere deduktive Regel ist eine Regel R := A ← W für die gilt:
vars(A) ⊆ vars-(W) und vars+(W) = Ø
Es darf also weder im Regelkopf noch in negativen Literalen des Rumpfs ungebundene Variablen bezüglich des gesamten Ausdrucks geben. Dies gilt im speziellen auch
für Fakten, so dass diese keine Variablen enthalten dürfen.
Im Weiteren ist mit Regel oder deduktiver Regel immer eine sichere deduktive Regel
gemeint.
Definition 2.8 (Relation)
Eine Relation wird entweder durch eine einzige Datenbankklausel oder durch eine
Menge von Datenbankklauseln definiert. Dabei definiert die Datenbankklausel C die
Relation p/X, wenn gilt pred(C) = p und arity(C) = X. Dadurch ist es möglich in einer
Datenbank verschiedene Relationen mit dem gleichen Prädikatsymbol aber unterschiedlicher Stelligkeit zu definieren. Für eine Menge M von Datenbankklauseln ist
def(M) die Menge aller definierten Relationen durch die Datenbankklauseln Ci (i=0,
…, n, n = |M|) aus M.
Definition 2.9 (Integritätsbedingung)
Eine statische Integritätsbedingung oder auch normative Regel wird durch ein positives oder negatives Grundliteral (also ein variablenfreies Literal) definiert und wird
durch das Schlüsselwort constraint eingeleitet.
constraint [not] p(c1, …, cn). mit n ≥ 0
7
Integritätsbedingungen stellen Gesetzmäßigkeiten dar, die in jedem Datenbankzustand
wahr sein müssen.
Nun kann man den Begriff der deduktiven Datenbank formal definieren:
Definition 2.10 (deduktive Datenbank)
Eine deduktive Datenbank D ist ein Tupel (F, R, I) wobei F die Menge der Basisfakten, R die Menge der deduktiven Regeln und I die Menge der Integritätsbedingungen
von D bezeichnet. Dabei gilt, dass def(F) ∩ def(R) = Ø. Es darf also keine Relationen
geben, die sowohl durch Fakten als auch durch Regeln definiert werden. Die Menge
aller Relationen von D wird als RelD bezeichnet. Eine Relation r ∈ def(D) wird als
abgeleitete Relation bezeichnet, wenn r ∈ def(R) gilt. Von Basisrelationen spricht
man, wenn p ∈ def(F) gilt.
Entsprechend spricht man von abgeleiteten Literalen und Basisliteralen, wenn diese
abgeleitete Relationen oder Basisrelationen referenzieren.
Abgeleitete Relationen können sich nicht nur auf Basisrelationen beziehen, sondern
auch wiederum auf abgeleitete Relationen und dabei wieder auf sich selbst. Man
spricht dann von einer rekursiven Abhängigkeit. Weitere Arten von Abhängigkeiten
zwischen den einzelnen Relationen sind zum einen positive bzw. negative Abhängigkeiten (je nachdem, ob das entsprechende Literal negiert wurde), zum anderen direkte
und indirekte Abhängigkeiten. So sind z.B. die Relationen p und s indirekt voneinander abhängig, wenn p (direkt) von q abhängt und q seinerseits von s abhängt.
Eine genauere Untersuchung der Abhängigkeiten wird besonders im Zusammenhang
mit Fixpunktoperationen nötig sein und in Kapitel 3 erfolgen.
2.1.2 Erweiterung um Built-In-Operatoren
Wie gesehen, bietet der klassische Kern von Datalog keine Möglichkeit einfache arithmetische Operationen oder Vergleiche irgendeiner Form durchzuführen. Um den
praktischen Nutzen von Datalog deutlich zu steigern, wird in diesem Abschnitt die
Erweiterung durch zusätzliche Operatoren betrachten. All diese Konstrukte sind in
einer externen Programmiersprache umzusetzen [4] [17].
2.1.2.1 Built-In-Prädikate
Um mit Datalog die Möglichkeit zu haben Vergleiche durchzuführen, müssen zusätzlich folgende Vergleichsprädikate mit in die Syntax eingebaut werden:
„<“, „<=“, „=“, „/=“, „>=“, „>“
8
Aus Sicht der Logik sind dies ebenfalls normale Relationssymbole, die aber in den
meisten Fällen unendliche Relationen darstellen. Man könnte sie also einfach in Präfixnotation schreiben und erhielt z.B.: <(X, Y). Da man aber durch die Unendlichkeit
dieser Relationen sowieso gezwungen ist, Built-In-Prädikate in einer externen Programmiersprache zu realisieren, bleibt man bei der für den Benutzer natürlichen Infixnotation (X < Y).
Weiter darf, um die Sicherheit von Regeln mit Built-In-Prädikaten zu gewährleisten,
kein Built-In-Argument zum Zeitpunkt der Auswertung mehr ungebunden sein. Es
dürfen also nur Konstanten oder Variablen, die in mindestens einem positiven
Rumpfliteral auftauchen, enthalten sein. Das heißt nicht, dass die Argumente der BuiltIn-Prädikate ausschließlich atomare Terme sein müssen; es kann sich auch um funktionale Terme handeln. Funktionale Terme werden durch das Konkatenieren von weiteren Argumenten mit Built-In-Operatoren gebildet. Dies wird im Abschnitt „Built-InFunktionen“ ausführlich beschrieben.
Einen besonderen Fall unter den Built-In-Prädikaten stellt das Zuweisungsprädikat
„:=“ dar. Mit ihm wird einem Argument (einer Variablen) ein Wert zugewiesen. Auf
der linken Seite des Zuweisungsprädikats steht also eine ungebunden Variable. Auf
der rechten Seite des Zuweisungsprädikats, dem so genannten definierendem Ausdruck, kann ein beliebiger Term stehen.
Damit ist die Besonderheit des Zuweisungsoperators auch direkt klar, er verlangt nur
für den definierenden Ausdruck Sicherheitsbedingungen, wie sie für negative und
„normale“ Prädikatsliterale bereits eingeführt wurden. Dies liegt daran, dass seine
Endlichkeit gewährleistet ist.
Damit kann man durch den Zuweisungsoperator umgekehrt die Sicherheitsanforderungen für Negative- und Prädikatsliterale abschwächen, da diese sich jetzt auch auf Variablen beziehen dürfen, die durch den Zuweisungsoperator definiert werden.
2.1.2.2 Built-In-Funktionen
Eine weiter ebenfalls wichtige Erweiterung stellen spezielle eingebaute Funktionen
dar. Zu ihnen gehören vor allem die arithmetischen Grundoperationen („+“, „-“, „*“,
„/“), es ist aber auch eine Erweiterung um andere Hilfsoperationen, wie sie zum Beispiel aus SQL bekannt sind, möglich. Es wird hier aber nur die die Erweiterung um die
arithmetischen Grundoperationen betrachtet.
Auch bei den Built-In-Funktionen gelten wieder die bekannten Sicherheitsanforderungen, d.h. es dürfen keine ungebundenen Variablen in ihnen vorkommen. Weiter gilt,
aus den gleichen Gründen wie bei den Prädikatsliteralen auch, dass sie in einer externen Programmiersprache realisiert werden müssen.
Built-In-Funktionen können als Argumente sowohl Konstanten und Variablen als auch
selbst wieder funktionale Terme haben. Es ist demnach also erlaubt Funktionalliterale
9
zu schachteln und Ausdrücke der Form: X + Y - 2 zu bilden. Klammerungen sind dabei aber nicht erlaubt.
Alle Built-In-Operatoren sind dabei nur in den speziellen Built-In-Literalen erlaubt
und nicht in normalen positiven oder negativen Literalen. Der Grund dafür liegt in der
Regelauswertung; diese geschieht durch den Abgleich von Fakten mit den gegebenen
Literalen. Built-In-Operatoren würden diesen Vorgang deutlich erschweren. Man kann
also keine Literale der Form: p(X, Y + 1) erstellen, da dies den Substitutionsvorgang
bei der Faktenbildung behindern würde, ohne einen Vorteil zu gewinnen. Denn Literale der Form p(X, Y + 1) mit Built-In-Operatoren können immer unter zu Hilfenahme
des Zuweisungsoperators in die Form: p(X, Z), Z := Y +1 überführet werden.
Abschließend bleibt zu den Built-In-Operatoren noch zu sagen, dass obwohl Datalog
eigentlich keine Datentypen unterscheidet, dies für die Built-In-Operatoren nötig ist.
Diese Datentypbestimmung kann auf die extern realisierten Methoden verschoben
werden, so dass sich für den Datalog-Kern keine Änderung ergibt. Eine andere Möglichkeit unterschiedliche Typen von Daten zu zulassen, wird in [6] vor geschlagen.
Dazu werden für jeden Datentyp einstellige Prädikatssymbole hinzugefügt. Wenn man
nun ein Literal p mit drei Parametern X, Y, Z hat und Typsicherheit verlangen will,
muss man dass Literal p(X, Y, Z) folgendermaßen transformieren:
p(X, Y, Z), string(X), int(Y), double(Z). Dabei sind string/1, int/1 und double/1
die Hilfsrelationen, die Texte, Ganzzahlen und Realzahlen definieren.
Jetzt werden also z.B. nur Eingaben berücksichtigt, bei denen Y mit einer Ganzzahl
definiert wird.
2.1.3 DDL und DML
Die meisten der bisher erläuterten Datalog-Elemente werden in der Forschungsliteratur
weitgehend einheitlich präsentiert. Für die Datendefinitionssprache (DDL) und für die
Datenmanipulationssprache (DML) gibt es aber in der Fachliteratur keine einheitliche
Notation. Sie sind aber unablässig, um mit einer Datenbank arbeiten zu können. Im
folgendem werden für die Darstellungen der DDL und der DML die Notationen von
[17] zugrunde gelegt.
2.1.3.1 DDL
Mit der Datendefinitionssprache wird das Schema definiert. Zum Schema gehört sowohl eine Deklaration bzw. Definition jeder Relation, als auch die die deduktiven Regeln. Ein weiteres wichtiges Element des Schemas sind die Integritätsbedingungen.
Definition 2.11 (Relationen)
In Datalog werden Relationen durch ihren Namen und die Anzahl der Parameter (auch
Stelligkeit genannt) unterschieden. Es ist also, anderes als z.B. in SQL, möglich ver10
schiedene Relationen mit dem gleichen Namen (aber unterschiedlicher Stelligkeit) zu
definieren. Es ist aber nicht möglich diese Relationen weiter durch Datentypen oder
Wertebereiche zu unterscheiden. Bei der Deklaration einer Relation muss in Datalog
aber noch die Definitionsart angegeben werden.
Es gibt drei verschiedenen Definitionsarten, die durch die Schlüsselwörter derived und
stored bzw. deren Kombination festgelegt werden:
1) Basisrelationen, welche nur mit dem Schlüsselwort stored versehen werden.
2) virtuelle Relationen, welche nur mit dem Schlüsselwort derived versehen
werden.
3) materialisierte Relationen, welche sowohl mit stored als auch mit derived versehen werden müssen.
Der Name ist ein Prädikatsymbol, wie in Abschnitt 2.1 definiert. Die Stelligkeit ist
eine beliebige ganze Zahl größer gleich Null und gibt die Anzahl der Relationsparameter an. Daraus ergibt sich folgende Definitionsform:
<Definitionsart> <Name> / <Stelligkeit>.
die, wie alle Schemaanweisungen, mit einem Punkt („.“) endet.
Definition 2.12 (Ableitungsregeln)
Ableitungsregeln sind sichere deduktive Regeln, wie sie in Definition 2.7 definiert
wurden. Sie bestehen aus einem Kopf und einem Rumpf. Der Kopf muss ein positives
Literal sein. Der Rumpf besteht aus einer Konjunktion von endlich vielen Literalen,
oder auch nur aus einem positiven Literal.
<Kopf> <- <Rumpf>.
Jede abgeleitete Relation muss durch mindestens eine Regel definiert sein.
Definition 2.13 (Integritätsbedingungen)
Integritätsbedingungen können im Datalog-Schema durch folgende Anweisung definiert werden:
constraint <Literal>.
Das Schlüsselwort constraint leitet eine Integritätsbedingung ein und wird von einem
positiven oder negativen (variablenfreien) Grundliteral gefolgt, der auch als Bedingungsteil bezeichnet wird.
Die Prüfung einer so definierten Integritätsbedingung muss in jedem Datenbankzustand „Wahr“ zurückgeben. Das heißt für positive Bedingungen, dass sie immer wahr
sein müssen und für negative Bedingungen, dass sie nie wahr sein dürfen.
11
2.1.3.2 DML
In diesem Abschnitt wird kurz die Syntax von Anfragen- und Änderungsoperationen,
aus welchen die Datenmanipulationssprache besteht, erläutert.
Darstellung von Anfragen
Durch Anfragen werden in Datalog im Prinzip nur temporäre Antwortrelationen formuliert. Eine Anfrage hat keinen Bezeichner und wird auch in keiner Form in der Datenbank gespeichert. Es existieren zwei verschiedene Typen von Anfragen. Zum einen
gibt es Mengenanfragen, zum anderen Test- oder Existenzanfragen. Beide Typen von
Anfragen enden immer mit einem Fragezeichen „?“.
Bei Mengenanfragen erhält man eine Menge von Antwortfakten, wenn die Anfrage
erfolgreich war, sonst die leere Menge. Sie haben folgende Form:
{(<Zielliste>) : <Qualifikation> } ?
Die Zielliste besteht aus einer Menge von atomaren Datalog-Termen und lässt sich mit
dem Kopf einer Regel bzw. dessen Parametern vergleichen. Die Qualifikation hingegen besteht aus einem oder mehreren durch Konjunktionen verbundenen Literalen. Die
Qualifikation entspricht dem Regelrumpf und es gelten die gleichen Sicherheitsanforderungen, um ebenfalls keine unendlichen Antworten zu erhalten. In der Zielliste dürfen auch nur durch positive Literale der Qualifikation gebundene Variablen enthalten
sein.
Zwar lassen sich Existenzanfragen als Extremfall von Mengenanfragen auffassen, indem man eine leere Zielliste zulässt, allerdings ist dies keine sonderlich natürliche
Weise. Deswegen besitzt Datalog auch eine spezielle Syntax für Testanfragen:
<Qualifikation> ?
Hierbei gelten die gleichen Forderungen und Regeln wie für die Qualifikation von
Mengenanfragen. Testanfragen sind zum Beantworten von ja/nein-Fragen nötig.
Um die Ausdrucksstärke von Anfragen zu erhöhen, bietet Datalog Hilfsrelationen an.
Diese Hilfsrelationen werden direkt nach der Anfrage (ohne Fragezeichen „?“) temporär definiert. Dazu werden sie durch das Schlüsselwort with eingeleitet und ggf. durch
Semikolons „;“ getrennt. Sie können dann auch nur in der Anfrage benutzt werden.
Abgeschlossen wird die gesamte Anfrage durch ein Fragezeichen („?“) nach der letzten lokalen Regel. Die Syntax für Anfragen mit Hilfsregeln hat folgende Form:
<Anfrage> with <Regel1>;
…
<Regeln> ? (mit n ≥ 0)
12
Darstellung von Änderungen
Änderungen sind nötig um Fakten einer bestehenden Datenbank hinzuzufügen oder zu
löschen. Dies ist aber nur bei Basisrelationen erlaubt. Weiter müssen nach jeder Änderungsoperation alle Integritätsbedingungen erfüllt bleiben. Datalog bietet verschiedene
Änderungsoperationen an, die alle durch das Ausrufezeichen „!“ abgeschlossen werden. Die einfachste Änderungsoperation ist die Elementaränderung. Sie weißt folgende
Struktur auf:
+|- <Grundliteral> !
Einfügungen beginnen dabei mit einem Pluszeichen „+“ und Löschungen werden
durch einem Minuszeichen „-“ eingeleitet.
Der zweite Änderungstyp ist die bedingte Änderung; sie hat folgendes Format:
+|- <dynamisches Literal> where <Bedingung> (with <Regelliste>) !
Auch bedingte Änderungen beginnen immer mit einem Plus- „+“ oder Minuszeichen
„-“ um festzulegen, ob es sich um eine Einfügung oder Löschung handelt. Im dynamischen Literal dürfen sich nun auch Variablen befinden, die aber durch die Bedingung
im where-Teil positiv gebunden sein müssen. Es folgen analoge Sicherheitsbedingungen wie schon für Regeln und Anfragen. Optional können auch wieder lokale Regeln,
eingeleitet durch with, angefügt werden.
Eine spezielle Art Änderungen an der Datenbank durchzuführen sind Transaktionen.
Eine Transaktion stellt eine Folge von Änderungen, an deren Ende erst die Integritätsbedingungen überprüft werden, dar. Diese Änderungen werden nur alle auf einmal
wirksam, oder es wird keine von ihnen ausgeführt. Die verschiedenen Änderungen
werden, um als Transaktion zusammengefasst zu werden, in geschweiften Klammern
aufgezählt, mit Semikolons getrennt. Es folgt dann nicht nach jeder einzelnen Änderungsanweisung ein Ausrufezeichen. Das Ausrufezeichen folgt erst nach der schließenden Klammer:
{U1; …; Un} ! (mit n ≥ 1)
Dabei sind die einzelnen Änderungen U einfache oder bedingte Änderungen oder
selbst wieder Transaktionen.
2.2 Semantik von Datalog
Nachdem gezeigt wurde, wie die unterschiedlichen Aufgaben syntaktisch formuliert
werden, muss nun betrachtet werden, welche Bedeutung sie haben. Dazu wird zuerst
die fixpunktbasierte Semantik von Datalog-Regeln betrachtet und um drauf aufbauend
kurz die Bedeutung von Anfragen und Änderungen zu erläutern. Das Folgende richtet
13
sich dabei nach [6] [17]. Dabei sind alle bisher gemachten Erweiterungen ausgeschlossen. Auch Integritätsbedingungen werden erst einmal nicht mit betrachtet.
Um jedoch mit der Fixpunktsemantik zu beginnen, bedarf es noch einiger Definitionen
und Erklärungen, z.B. wie man Regeln in verschiedene Regelklassen aufteilt. Dazu
wird zunächst das Konzept der Stratifikation erläutert.
2.2.1 Stratifikation
Stratifikation („Schichtung“) bedeutet, dass die gegebene Regelmenge in verschiedene
Schichten unterteilt wird. Diese Unterteilung hängt mit den unterschiedlichen Abhängigkeiten der Relationen zusammen.
Definition 2.14 (Relations-Abhängigkeitsgraph)
Sei D eine deduktive Datenbank wie in Definition 2.10 definiert. Dann ist der Relations-Abhängigkeitsgraph von D ein gerichteter Graph GD = (V, E), wobei V = RelD die
Menge aller Relationen aus D ist. E ist die Menge aller Kanten, die folgendermaßen
definiert sind:
Sei A ← W ∈ D eine deduktive Regel aus D, L ∈ W ein Prädikatsliteral, pred(A) = p
und pred(L) = q, dann gilt:
1) Falls L ein positives Literal ist, dann enthält GD eine positive, gerichtet Kante
von q nach p, die nicht beschriftet ist.
2) Falls L ein negatives Literal ist, dann enthält GD eine negative, gerichtet Kante von q nach p, die mit not beschriftet ist.
Definition 2.15 (Relations-Abhängigkeiten)
D sei wieder eine deduktive Datenbank und p, q ∈ pred(D) Relationen und GD der zugehörige Relations-Abhängigkeitsgraph. Dann heißt:
1) p abhängig von q (in Zeichen: p < q) genau dann, wenn GD einen Pfad der
Länge n ≥ 1 von q nach p enthält.
2) p positiv von q abhängig (p <+ q) genau dann, wenn p < q und der Pfad keine
negative Kante enthält.
3) p negativ von q abhängig (p <- q) genau dann, wenn p < q und der Pfad mindestens eine negative Kante enthält.
4) p und q voneinander abhängig (p ≈ q) genau dann, wenn p < q und q < p gilt.
Damit ist GD also zyklisch und die Regelmenge wird dann als rekursiv bezeichnet.
14
Definition 2.16 (Stratifikation)
Sei D eine deduktive Datenbank und R die zugehörige Menge deduktiver Regeln.
Dann ist eine Stratifikation λ der Regelmenge R eine eindeutige Abbildung von der
Menge RelD in die Menge der natürlichen Zahlen:
λ : RelD → N0,
so dass für alle p, q ∈ RelD gilt:
1) Falls p positiv von q abhängt, dann ist λ(q) ≤ λ(p).
2) Falls p negativ von q abhängt, dann ist λ(q) < λ(p).
Eine deduktive Datenbank für die es mindestens eine Stratifikation gibt, heißt stratifizierbar, ansonsten wird sie als nichtstratifizierbar bezeichnet. Eine deduktive Datenbank in der es nur Relationen gibt, die sich auf tiefere Schichten beziehen, heißt hierarchisch. Analog werden Mengen von Regeln oder Relationen bezeichnet.
Grundsätzlich basiert eine Stratifikation auf den Relationen. Um aber eine effizientere
Arbeitsweise der Fixpunktoperationen zu erhalten, kann man von dieser Forderung
abweichen. Man spricht dann von einer so genannten schwachen Stratifikation, bei
denen die einzelnen Regeln und nicht die gesamten Relationen in einzelne Schichten
eingeteilt werden. Für die Implementierung aller im Rahmen dieser Arbeit vorgestellten Algorithmen wird aber immer von einer starken Stratifikation ausgegangen.
2.2.2 Fixpunktsemantik für stratifizierbare Regelmengen
In diesem Abschnitt werden die Fixpunktsemantik für positive (enthalten keine Regeln
mit negativen Abhängigkeiten), semi-positive (enthalten nur Regeln, die höchstens von
Basisrelationen negativ abhängen) und stratifizierbare deduktive Datenbanken betrachtet.
Eine deduktive Datenbank besteht aus einer endlichen Menge von Regeln R und Fakten F. Ziel ist es die Faktenmenge F* zu erhalten, welche alle Fakten enthält die durch
die Regeln R aus F ableitbar sind. Dazu muss man zuerst verstehen, was unter der
Anwendung einer Regel Ri auf eine Faktenmenge F gemeint ist.
Definition 2.17 (Ableitungsfunktion)
Sei Ri ∈ R eine deduktive Regel der Form:
Ri = A <- L1, …, Lm, Lm+1, …,Ln. wobei alle Lj mit 1 ≤ j ≤ m positive
Literale und alle Lj mit m+1 ≤ j ≤ n negative Literale sind.
Dies ist o.B.d.A. möglich, da die Literalreihenfolge beliebig umsortiert werden darf.
15
Dann ist die Ableitungsfunktion folgendermaßen definiert:
T[Ri] (F) := { A σ | σ ist eine konsistente Variablensubstitution, so dass
∀ 1 ≤ j ≤ m: Ljσ ∈ F und
∀ m+1 ≤ j ≤ n: Ljσ ∉ F gilt}
Mit konsistenter Variablensubstitution ist eine Ersetzung aller Variablen durch Konstanten gemeint, wobei jedes Vorkommen derselben Variablen auch durch dieselbe
Konstante ersetzt werden muss.
Die Auswertung von negativen Literalen erfolgt durch das „negation as failure“Prinzip, das bedeutet, dass negierte Literale genau dann wahr sind, wenn das Literal
selbst falsch ist. Da aber in deduktiven Datenbanken keine negativen Fakten gespeichert werden, bedarf es einer weiteren Annahme, der „closed world assumption“
(CWA). Sie besagt, dass alles was nicht wahr ist, oder als wahr hergeleitet werden
kann, falsch ist. Es wird angenommen, dass alle Fakten die nicht direkt oder indirekt in
der Datenbank enthalten sind, falsch sind.
In dieser Art der Auswertung negativer Fakten liegt auch ein Grund für die Sicherheitsanforderungen für deduktive Regeln, Anfragen und bedingte Änderungen.
Die gemeinsame Durchführung aller Regeln wird durch die Vereinigung der einfachen
Ableitungsfunktion erreicht. Jede Regel wird dabei aber auf die gleiche InputFaktenmenge angewendet. Neue Fakten haben daher keinen Einfluss auf die aktuelle
Anwendung. Die kollektive Ableitungsfunktion ist folgendermaßen definiert:
T[R] (F) := URi ∈ R T[Ri] (F)
Allerdings fehlt diesen Ableitungsfunktionen die ursprüngliche Faktenmenge. Es wird
also noch eine letzte Erweiterung benötigt um die Ableitungsfunktion iterativ anwenden zu können. Die so genannte kumulative Ableitungsfunktion, die die ursprüngliche
Faktenmenge noch hinzufügt:
T*[R] (F) := T[R] (F) ∪ F
Für positive und semi-positive deduktive Datenbanken kann dieser Operator verwendet werden, um iterativ alle herleitbaren Fakten zu bestimmen bzw. zu beschreiben.
Bei deduktiven Datenbanken mit negativen Abhängigkeiten zwischen abgeleiteten
Relationen hingegen, spielt die Abarbeitungsreihenfolge eine Rolle. Es ist zusätzlich
noch nötig das Konzept der Stratifikation mit der kumulativen Ableitungsfunktion zu
verbinden. Damit lässt sich die Semantik von stratifizierbaren deduktiven Datenbanken aufstellen. Die Semantik wird durch einen Grenzwertprozess, der am Ende einen
Fixpunkt erreicht, beschrieben. Dazu aber vorher noch eine Definition:
16
Definition 2.18 (kleinster Fixpunkt)
Der kleinste Fixpunkt einer Funktion f, der das Argument M enthält, wird mit lfp(f, M)
bezeichnet. „lfp“ ist die Abkürzung für „least fix point“.
Definition 2.19 (Fixpunktsemantik von stratifizierbaren deduktiven Datenbanken)
Sei D eine deduktive Datenbank mit der Faktenmenge F und den Regeln R:
1) Falls D eine positive oder semi-positive deduktive Datenbank ist, dann ist die
Bedeutung F* von D lfp(T*R, F).
2) Falls D eine stratifizierbare deduktive Datenbank ist dann sei λ eine Stratifikation von R mit den Schichten R#1,…R#k, dann gilt:
F0 := F
Fi := lfp(T*[R#i], Fi-1) für 1 ≤ i ≤ k.
Die Bedeutung von (F, R) ist dann definiert als das Ergebnis der lokalen Fixpunktberechnung auf dem letzten Stratum R#k:
F* := Fk
F* nennt man den iterierten Fixpunkt von T*[R] bzgl. F.
Für stratifizierbare deduktive Datenbanken D = (F, R) in Datalog existiert immer ein
eindeutiger kleinster Fixpunkt von T*R, der F ganz enthält. Dabei ist lfp(T*R, F) auch
immer endlich. Das liegt an der Endlichkeit von R und F und den für Regeln geforderten Sicherheitsbedingungen und der Tatsache, dass keine Funktionssymbole zugelassen sind.
Durch die Kombination von Rekursion und Negation kann es dazu kommen das eine
Regelmenge gerade nicht mehr stratifizierbar ist. Dann benötigt man einen anderen
Ansatz um die Semantik von deduktiven Datenbanken zu bestimmen.
2.2.3 Fixpunktsemantik für nichtstratifizierbare Regelmengen
In diesem Abschnitt wird, der Vollständigkeit wegen, ein kurzer Überblick über die
Fixpunktsemantik für nichtstratifizierbare Regelmengen gegeben. Da keine Methode
zur Berechung von Fixpunkten für nichtstratifizierbare Regelmengen implementiert
werden soll, werden auch keinen, oder nur wenige, formale Definitionen erfolgen.
In nichtstratifizierbaren deduktiven Datenbanken kann es dazu kommen, dass man einige Fakten nicht eindeutig ableiten kann. Der einfachste Fall ist eine Datenbank, die
nur die Regel R = p(1) <- s(1,1), not p(1). enthält. Hierbei ist es nicht mehr möglich,
zweifelsfrei den Wahrheitswert für p(1) zu bestimmen. Dies geschieht allgemein dann,
wenn Rekursion mit Negation auftritt. Es gibt in der Fachliteratur zwei verschiedene
Ansätze, wie man mit solchen Fakten bzw. Regelmengen umgeht. Der erste Ansatz,
17
der nicht weiter betrachtet werden soll, ist die gesamte Datenbank für undefiniert zu
erklären, sobald ein solches Fakt auftritt.
Der zweite und produktivere Weg schlägt vor eine dreiwertige Logik zu Grunde zu
legen. Fakten, denen nicht eindeutig wahr oder falsch zugeschrieben werden kann,
bekommen den Wert undefiniert (undefind) zugewiesen.
Die Komplementbildung zur Bestimmung negativer Fakten, wie es bei der „closed
world assumption“ geschieht, ist dann nicht mehr möglich. Man muss auch die negativen Fakten explizit bestimmen.
Dazu zerlegt man die Herbrand-Basis HD der deduktiven Datenbank D. Die HerbrandBasis besteht aus der Menge der vorkommenden Relationssymbole mit der deren Stelligkeit, auch als Signatur RelD von D bezeichnet und der Menge aller in D vorkommenden Konstanten. Die Menge aller vorkommenden Konstanten wird HerbrandUniversum UD genannt. Man erhält folgende Aufteilung für die Herbrand-Basis:
-
die bekannte Menge F* der positiven Fakten,
die Menge Fneg* der negativen Fakten und
die Menge Fundef* der undefinierbaren Fakten.
Um die Bedeutung einer nichtstratifizierbaren deduktiven Datenbank D zu bestimmen,
muss man mindestens zwei der drei Mengen explizit herleiten. Die dritte Menge kann
dann durch Komplementbildung angeben werden. Hat man beispielsweise F* und
Fneg* hergeleitet kann man Fundef* folgendermaßen berechnen:
Fundef* := HD / (F* ∪ Fneg*)
Um die „well-founded“ Semantik einer Datenbank konstruktiv zu bestimmen, gibt es
verschiedene Methoden. Um die Semantik von nichtstratifizierbaren deduktiven Datenbanken zu bestimmen wird hier die Fixpunktiteration von Bry (1989) mit bedingten
Fakten betrachten, die unter dem Namen „conditional fixpoint computation“ CFP bekannt geworden ist. Die zweite bekannte Methode ist die alternierende Fixpunktmethode (AFP) von van Gelder (1989), die später in Kapitel 3 noch einmal aufgegriffen
wird.
Bedingte Fakten sind variablenfreie Regeln, deren Rumpf nur aus der Konstante true
besteht, oder deren Rumpfliterale alle negiert sind.
Die CFP- Methode lässt sich in zwei Phasen unterteilen:
-
eine Expansionsphase; in ihr werden zunächst aus der Basisfaktenmenge F
durch Anwendung der Regeln R alle herleitbaren bedingten Fakten bestimmt.
Dabei wird die Negationsbehandlung auf die zweite Phase verschoben.
18
-
und eine Reduktionsphase; in der eindeutig bestimmbare Literale in bedingten
Fakten entfernt werden, und bedingte Fakten gelöscht werden, die sicher
falsch sind.
-
Schließlich werden die einzelnen Fakten den Mengen F*, Fneg* oder Fundef*
zugeordnet.
In der ersten Phase werden alle positiven Literale ausgewertet; dies kann auch durch
einsetzten von bedingten Fakten geschehen. Dadurch kann es zu einer exponentialen
Regelrumpfgröße kommen.
Da in der ersten Phase alle potentiell herleitbaren Fakten erstellt wurden, können in der
zweiten Phase alle negativen Literale, zu denen es kein passendes bedingtes Fakt gibt,
entfernt werden. Diese sind sicher falsch.
Beide Phasen lassen sich als Fixpunktiterationen mit verschiedenen Operationen darstellen. Und mit ihnen auch eine formale Definition der Semantik geben. Zuerst bedarf
es dazu eines bedingten Ableitungsoperators für einzelne Regeln, dann folgt der bedingte Ableitungsoperator für Regelmengen. Die Expansionsphase arbeitet dabei weitgehend analog zur „normalen“ Fixpunktiteration.
Definition 2.20 (Bedingter Ableitungsoperator)
Sei D eine deduktive Datenbank mit der Regelmenge R und der bedingten Faktenmenge CF; weiter sei Ri ∈ R mit:
Ri:
A <- B1, …, Bn, not C1, …, not Cm.
Der bedingte Ableitungsoperator für einzelne Regeln lautet dann:
Tcond[Ri] (CF) := {A σ <- prune[(D1, …, Dn, not C1, …, not Cm) σ] |
σ ist eine konsistente Variablensubstitution, so dass
œ 1 ≤ j ≤ n: (Bj <- Dj) σ ∈ CF gilt}
Der Absorptionsoperator prune ist nötig, um aus der Konjunktion von Literalen alle
Duplikate und alle Vorkommen von true zu eliminieren.
Der kollektive Ableitungsoperator ist dann wieder wie folgt definiert:
Tcond[R] (CF) := URi ∈ R Tcond[Ri] (CF)
Um bei Iterationen nicht bereits erzeugte Resultate zu verlieren, muss immer die ursprüngliche bedingte Faktenmenge hinzugefügt werden. Man erhält den endgültigen
kumulativen Tcond*-Operator für die Expansionsphase:
Tcond*[R] (CF) := Tcond[R] (CF) c CF
19
Daraus ergibt sich zu einer gegebenen transformierten Basisfaktenmenge Fcond als Ergebnis der CFP-Semantik wieder der kleinste Fixpunkt von Tcond*[R], der Fcond ganz
enthält:
Fcond* := lim i → ∞ Tcond*[R]i (Fcond)
Nun bedarf es noch einiger Operatoren zur formalen Beschreibung der Reduktionsphase. Als Hilfsmittel wird der Operator heads definiert, der die Kopfliterale herausfiltert.
heads(CF) := { A | (A <- B) ∈ CF}
Nun kann der Operator zur Eliminierung negativer Literale definiert werden. Dieser
hat die Aufgabe, jedes negative Literal not L durch true zu ersetzen, wenn L nicht in
heads(CF), also den bedingten Fakten, vorkommt. Anschließend kann es dann durch
den prune-Operator eliminiert werden. Es werden alle jene negative Literale entfernt,
für die es garantiert keine mögliche Herleitung mehr geben kann.
Definition 2.21 (positiver Reduktionsoperator)
Sei CF eine bedingte Faktenmenge, dann ist der positive Reduktionsoperator Redtrue
wie folgt definiert:
Redtrue (CF) := A <- prune[Cred1, …,Credn] | A <- not C1, …,Cn) ∈ CF}
true, wenn Ci ∉ heads (CF)
mit Credi :=
not Ci sonst
für 1 ≤ i ≤ n.
Weiter lassen sich ganze bedingte Fakten entfernen, wenn für ein negatives Literal not
L der positive Anteil L definitiv erfüllt ist. Dieses Literal kann durch false ersetzt werden. Sobald aber in der Konjunktion von Literalen false auftritt, kann diese nicht mehr
true ergeben und das bedingte Fakt gelöscht werden. Existiert zu irgendeinem negativen Literal not L ein Fakt der Form L <- true, kann das bedingte Fakt, das not L enthält, entfernt werden.
Definition 2.22 (negativer Reduktionsoperator)
Sei CF eine bedingte Faktenmenge, dann ist der negative Reduktionsoperator Redfalse
wie folgt definiert:
Redfalse := (CF) := CF - CFfalse
mit CFfalse := { A <- not C1, …, Cn | A <- not C1, …,Cn) ∈ CF und
∃ 1 ≤ i ≤ n: Ci <- true ∈ CF}
Nun müssen beide Operatoren hintereinander ausgeführt werden. Das Ergebnis ist der
allgemeine Reduktionsoperator:
Red(CF) := Redfalse (Redtrue (CF))
20
In der Reduktionsphase werden beide Operatoren bzw. einfach der allgemeine Reduktionsoperator solange durchgeführt, bis keine Änderung mehr an der Faktenmenge
Auftritt. Der kleinste Fixpunkt von Red ist:
Fred* := lim i → ∞ Redi (Fcond*)
Der Reduktion beginnt dabei mit dem Resultat des kumulativen Ableitungsoperators
für bedingte Fakten.
Nun kann die Bedeutung nichtstratifizierbarer deduktiver Datenbanken definiert werden.
Definition 2.23 (Fixpunksemantik von nichtstratifizierbaren deduktiven Datenbanken)
Sei D = (F, R) eine deduktive Datenbank und HD die zugehörige Herbrand-Basis, dann
setzt sich die Bedeutung aus wahren, falschen und undefinierten Fakten zusammen.
Diese sind wie folgt definiert:
Fpos*
Fundef*
Fneg*
:= { A | A <- true ∈ Fred*}
:= heads(Fred*) – Fpos*
:= HD – heads(Fred*)
2.2.4 Semantik von Anfragen und Änderungen
Im letzten Abschnitt dieses Kapitels wird der Überblick über die Syntax und Semantik
von Datalog abgerundet und kurz die Bedeutung von Anfragen und Änderungen erläutert.
2.2.4.1 Anfragen
Die Bedeutung einer Anfrage ist deren Antwort, also eine Menge von Fakten im Fall
der Mengenanfrage oder ein Wahrheitswert im Fall der Testfrage. Dabei lässt sich die
Anfrage als eine temporäre Relation auffassen. Der Kopf der Relation steht dabei für
die Zielliste. Damit lässt sich mit Hilfe der Fixpunktsemantik die Bedeutung bestimmen. Dies führt zu folgender formaler Definition:
Definition 2.24 (Semantik einer Datalog-Anfrage)
Sei D = (F, R) eine deduktive Datenbank und sei A = {(t1, …, tm): Q} with R1; …; Rn?
(mit m, n ≥ 0) eine Datalog-Anfrage, wobei Q die Qualifikation der Anfrage ist. Dann
wird eine neue Regel RAnfrage generiert, die folgende Struktur besitzt:
RAnfrage = temp_anfrage(t1, …, tm) <- Q.
21
Dabei darf es in der Datenbank keine m-stellige Relation temp_anfrage geben, sonst
ist ein anderer Name zu wählen. Mit Hilfe von RAnfrage und den lokalen Regeln Ri wird
die erweiterte Regelmenge Rerw gebildet:
Rerw = R ∪ RAnfrage ∪ R1 ∪ … ∪ Rn mit n ≥ 0
F* sei im Weiteren die Bedeutung der erweiterten Datenbank Derw = (Rerw, F). Dann ist
die Bedeutung der Anfrage A:
{(t1 σ, …, tm σ) | σ ist eine konsistente Substitution und
∀ L ∈ Q : L σ ∈ F*}.
Damit hat man auch direkt die Bedeutung einer Testanfrage AT = Q? bestimmt. AT ist
wahr, wenn mindestens eine konsistente Variablensubstitution σ, wie oben, für eine
leere Zielliste (m = 0) existiert.
2.2.4.2 Änderungen
Um die Semantik von unbedingten Änderungen zu beschreiben, werden diese einfach
als bedingte Anweisungen mit wahrem Bedingungsteil betrachtet. Dadurch muss nur
noch die Semantik für zwei Änderungstypen beschrieben werden. Zum eine werden
die individuellen Änderungsanweisungen, zu denen die bedingten und unbedingten
Änderungen zählen und zum anderen die Semantik von Transaktionen, erklärt.
Für individuelle Änderungsanweisungen ergeben sich drei Schritte, die nach einander
abgearbeitet werden müssen. Als Erstes muss der Bedingungsteil über der aktuellen
Faktenmenge ausgewertet werden. Dann müssen alle redundanten Änderungen, d.h.
Löschungen von nicht vorhandenen und Einfügungen von bereits enthaltenen Fakten,
eliminiert werden. Im letzten Schritt werden alle verbleibenden Änderungen ausgeführt. Die Bedingungsauswertung wird analog zur Regelauswertung durchgeführt:
Definition 2.25 (Semantik einer individuellen Datalog-Änderung)
Sei U ≡ +/- A where B1, …, Bn! eine Anfrage, F die aktuelle Faktenmenge und F* die
Bedeutung der Datenbank. Dann ist die Bedingungsauswertung folgendermaßen definiert:
δ [U](F) := { Aσ | σ ist eine konsistente Substitution und
∀ 1 ≤ i ≤ n: Bi σ ∈ F*}
Die Eliminierung redundanter Änderungen erfolgt durch:
δ [U](F) - F, falls U eine Einfügung ist.
∆ [U](F) :=
δ [U](F) ∩ F, falls U eine Löschung ist.
22
Und zuletzt die formale Definition des Effekts eines individuellen Zustandübergangs
auf die Basisdaten durch die Transition τ:
F ∪ ∆ [U](F) , falls U eine Einfügung ist.
Τ [U](F) :=
F - ∆ [U](F) , falls U eine Löschung ist.
Eine spezielle Art von Änderungsanweisungen sind Transaktionen. Transaktionen sind
Folgen von Änderungen, die entweder alle gemeinsam oder überhaupt nicht ausgeführt
werden. Die Änderungen erfolgen dabei ohne Festlegung der Reihenfolge der Einzelanweisungen.
Obwohl Transaktionen verschachtelt und mit Bedingungen gekoppelt sein können,
werden im Folgenden nur die Semantik für einfache Transaktionen ohne Schachtelung
und Bedingung betrachtet. Die Semantik für bedingte Transaktionen ist analog zur
Bedeutung von bedingten Einzelanweisungen zu definieren.
Zur Bedeutungsbestimmung von Transaktionen wird die Transaktion zuerst in ihren
positiven und negativen Anteil aufgeteilt, dann wird ebenfalls in drei Schritten ihre
Bedeutung bestimmt. Zuerst werden die einzelnen Auswirkungen berechnet, dann
wird der Nettoeffekt bestimmt. Am Ende werden alle Änderungen simultan durchgeführt.
Definition 2.26 (Semantik einer Datalog-Transaktion)
Sei D = (F, R) eine deduktive Datenbank, T ≡ {U1; …; Un}! eine einfache Transaktion.
Sei T+ die Menge aller Einfügungen aus T und T- die Menge aller Löschungen aus T.
Dann ist die Semantik von T durch die folgenden Schritte definiert:
1) ∆+ [T] (F) := U(U ∈ T+) δ[U](F) und
∆- [T](F) := U(U ∈ T-) δ[U](F)
2) Π+ [T] (F) := ∆+ [T](F) - ∆- [T](F) und
Π- [T](F) := ∆- [T](F) - ∆+ [T](F)
3) Damit ist die von der Transaktion T bewirkte Transition:
τ [T] (F) := (F - Π- [T](F)) ∪ Π+ [T] (F)
Zuletzt sollen noch kurz Änderungen im Zusammenhang mit Integritätsbedingungen
betrachtet werden, die bisher nicht betrachtet wurden.
Definition 2.27 (Semantik einer Datalog-Transaktion)
Sei nun die Datenbank D = (F, R, I) um Integritätsbedingungen erweitert und F* erfüllt
diese Integritätsbedingungen I. Weiter sei I+ die Menge aller positiven Integritätsbedingungen und I- die Menge aller negativen Integritätsbedingungen. Dann ergibt sich
folgende Transition:
23
τ I [T] (F) :=
τ [T] (F),
falls ∀ (constraint L) ∈ I+ gilt: L ∈ τ [T] (F)*
und ∀ (constraint not L) ∈ I- gilt: L ∉ τ [T] (F)*
F,
sonst
Individuelle Anweisungen lassen sich natürlich als einelementige Transaktionen auffassen und werden im Zusammenhang mit Integritätsbedingungen analog behandelt.
2.2.5 Semantik von Built-In-Operatoren
Bisher wurde die Semantik ausschließlich für einfache Literale ohne die oben definierten Built-In-Operatoren betrachtet. Nun soll eine Erweiterung der Fixpunktoperatoren
um genau diese Elemente vorgestellt werden.
Vergleichsliterale können, wie schon negative Literale, keine neuen Variablenbindungen erzeugen. Sie können nur bereits gefundene Variablenbindungen verifizieren bzw.
zurückweisen. Das Erfülltsein von Vergleichen kann dabei, wie schon in Abschnitt
2.1.2 erklärt, nicht durch den Zugriff auf die Faktenmengen bestimmt werden, sondern
muss in einer extern programmierten Methode getestet werden. Formalisiert werden
soll dies durch Einführung eines Booleschen Hilfsprädikats „is_satisfied/1“. Dieses
liefert true, wenn die Vergleichsrelation auf die Parameter zutrifft, ansonsten false.
Daraus ergibt sich folgende Änderung des Ableitungsoperators:
Ri = A <- L1, …, Lm, …, Ln, V1,…, Vl. wobei
alle Lj mit 1 ≤ j ≤ m positive Literale,
alle Lj mit m+1 ≤ j ≤ n negative Literale und
alle Vk mit 1≤ k ≤ l Vergleichsliterale sind.
T[Ri] (F) := {A σ | σ ist eine konsistente Variablensubstitution, so dass
∀ 1 ≤ j ≤ m: Ljσ ∈ F,
∀ m+1 ≤ j ≤ n: Ljσ ∉ F und
∀ 1≤ k ≤ l: is_satisfied(Vk σ) gilt}
Um auch Funktionen in Regeln zu zulassen, bedarf es noch einer weiteren Anpassung
des Ableitungsoperators für Regeln. Vor der Auswertung von Vergleichsliteralen, die
funktional Terme enthalten, müssen alle Terme berechnet werden. Dies geschieht wieder mit externen Prozeduren, was durch die Hilfsrelation „eval/1“ formalisiert wird.
Ihre Aufgabe ist es alle funktionalen Terme auszurechnen und durch die Ergebnisse zu
ersetzten.
T[Ri] (F) := {A σ | σ ist eine konsistente Variablensubstitution, so dass
∀ 1 ≤ j ≤ m: Ljσ ∈ F,
∀ m+1 ≤ j ≤ n: Ljσ ∉ F und
∀ 1≤ k ≤ l: is_satisfied( eval(Vk σ) ) gilt}
24
Die Relation eval braucht nur auf Built-In-Literale angewandt werden, da andere Literale nach der Definition in Abschnitt 2.1.2 keine funktional Terme enthalten dürfen.
Wichtig bei ist, dass es in Termkonstrukten nicht zu Rekursionen kommen darf, da
sonst unendliche Relationen entstehen können.
25
Kapitel 3
Verfahren zur Fixpunktberechnung
Die Fixpunktberechnung stellt ein Kernstück eines deduktiven Datenbankmanagementsystems dar. Sie wird zur Materialisierung von Fakten und damit auch zur (effizienten) Anfragebeantwortung benötigt. In diesem Kapitel werden daher verschiedene
Algorithmen betrachtet, die zur Bestimmung der Fixpunktsemantik von deduktiven
Datenbanken geeignet sind.
Vor allem werden Methoden betrachtet, die nur auf eingeschränkte Regelmengen, wie
z.B. semi-positive Regeln, angewendet werden dürfen. Der große Vorteil dieser Algorithmen ist ihre einfache Struktur und ihre gute kurze Laufzeit. In Abschnitt 3.2 werden zwei Verfahren für nichtstratifizierbare Regelmengen vorgestellt. Dies geschieht
nicht so ausführlich wie die Vorstellung der stratifizierbaren Regelmengen in Abschnitt 3.1, da keine Methode für unstratifizierbare Regelmengen im Rahmen dieser
Arbeit implementiert worden ist.
Die Verfahren zur Fixpunktberechnung werden in diesem Kapitel unabhängig von der
genutzten Programmiersprache erläutert, um das Verständnis der allgemeinen Abläufe
nicht durch Besonderheiten der Programmiersprache zu erschweren. In Abschnitt 5.1
wird gesondert auf die Implementierung in Java eingegangen. Die Beschreibungen
richten sich dabei an [17] und [18].
3.1 Fixpunktberechnung für stratifizierbare Regelmengen
In diesem Abschnitt werden zuerst die naive und die semi-naive Fixpunktberechnung
vorgestellt. Beide Verfahren können nur auf positive oder semi-positive Regelmengen
angewendet werden. Bei allgemein stratifizierbaren Regelmengen liefern diese Verfahren möglicherweise ein falsches Ergebnis. Für allgemein stratifizierbare Regelmengen wird daraufhin die iterierte Fixpunktberechung vorgestellt. Für die iterierte Fixpunktberechnung muss eine Stratifikation der gegebenen Regelmenge berechnet werden.
26
Keine der Methoden in diesem Abschnitt lässt sich auf unstratifizierbare Regelmengen
anwenden. Sie würden nur in wenigen Ausnahmen bzw. nur für eine Teilmenge der
Regeln eine korrekte Lösung liefern.
3.1.1 Naive Fixpunktberechnung
Die naive Fixpunktberechnung richtet sich strikt nach der Definition der Fixpunktsemantik, da dieses bereits ein konstruktives Verfahren zur Berechnung der Bedeutung
F* einer deduktiven Datenbank ist. Es werden also zu jeder Regel r aus der Regelmenge R mit D = (F, R) alle herleitbaren Fakten durch Substitution mit Hilfe des Ableitungsoperators generiert. Dabei werden in derselben Iterationsrunde nur Herleitungen bezüglich der aktuellen Faktenmenge F gemacht. Die neuen Fakten Fnew werden
dabei sukzessiv erweitert und am Ende der Iterationsrunde mit den aktuellen Fakten
vereinigt. Der Algorithmus terminiert, wenn keine neuen Fakten mehr gefunden werden konnten und somit die alte Faktenmenge gleich der neuen Faktenmenge ist.
Fnew = ∅;
do {
for all (r ∈ R)
Fnew = T[r] (F) ∪ Fnew;
F = Fnew ∪ F;
} while (Fnew != ∅);
Abbildung 3.1: Algorithmus zur naiven Fixpunktberechnung
Zur Berechnung von T[r](F) wird für jedes im Regelrumpf vorhandene Literal in der
aktuellen Faktenmenge (F) nach Fakten gesucht. Dabei werden die Variablenbindungen erstellt. Nachdem alle Literale untersucht wurden, werden die Variablenbindungen
auf den Regelkopf übertragen. Die dabei erhaltenen Fakten sind das Ergebnis des Ableitungsprozesses T[r](F).
Um diesen und die folgenden Algorithmen besser verstehen zu können, werden sie
immer kurz durch möglichst ähnliche Beispiele verdeutlicht.
Beispiel 3.1 (naive Fixpunktberechnung)
Sei D = (F, R) eine deduktive Datenbank mit folgenden Fakten- und Regelmengen:
F = {s(1, 2); s(2, 3); q(3)}
p(X) <- s(X, Y), p(Y).
R1
p(Y) <- q(Y).
R2
R = {R1; R2}
27
In der ersten Iterationsrunde kann nur die Regel R2 neue Fakten hervorbringen. Da
noch keine p-Fakten existieren, kann es keine Substitution für die Y Variable in Regel
R1 geben. Regel R2 liefert das p-Fakt p(3) und man erhält als neue Gesamtfaktenmenge am Ende der Iterationsrunde:
F = {s(1, 2); s(2, 3); q(3); p(3)} nach Iteration 1
Die zweite Iterationsrunde beginnt. Dieses Mal kann auch Regel R1 zur Herleitung von
Fakten herangezogen werden. Sie produziert das Fakt p(2). Auch die Regel R2 kann
wieder ein Fakt herleiten und zwar wieder das Fakt p(3). Dann werden die alten und
neuen Fakten duplikatfrei vereinigt (Fnew ∪ F), wodurch die, nur um das Fakt p(2) erweiterte, neue Faktenmenge F heraus kommt:
F = {s(1, 2); s(2, 3); q(3); p(3); p(2)} nach Iteration 2
Ein weiterer Iterationsschritt ist nötig, da sich die Faktenmenge wieder geändert hat.
Im dritten Durchgang produziert Regel R1 wieder das bereits vorhandene Fakt p(2)
und das neue Fakt p(1). R2 erzeugt wieder das Fakt p(3). Das Ergebnis ist also die Faktenmenge:
F = {s(1, 2); s(2, 3); q(3); p(3); p(2); p(1)} nach Iteration 3
In der vierten Iterationsrunde kann keine der beiden Regeln noch neue Fakten herleiten. Es werden aber alle bisher erzeugten Fakten erneut abgeleitet. Die neue Faktenmenge ist damit wieder:
F = {s(1, 2); s(2, 3); q(3); p(3); p(2); p(1)} nach Iteration 4
Da die dritte und vierte Iteration die gleiche Faktenmenge erzeugt hat, ist der Fixpunkt
erreicht. Man sieht leicht ein, dass die naive Fixpunktiteration nicht sonderlich effizient ist. Da zum Erzeugen von drei Fakten insgesamt zehnmal Fakten hergeleitet
wurden, die meisten davon aber bei der Duplikateliminierung verworfen wurden. Im
obigen Beispiel sind Duplikate nur durch erneute Herleitung aufgetreten. Dies ist aber
auch durch unterschiedliche Ableitungsmöglichkeiten des gleichen Fakts möglich. In
Beispiel 3.1 kann dies durch Einfügen des Basisfakts s(3, 3) erreicht werden. Dann ist
Regel R1 in der zweiten Iterationsrunde auch in der Lage das Fakt p(3) herzuleiten. Es
bietet sich daher an, überall mit duplikatfreien Mengen zu arbeiten und dies nicht nur
bei der Vereinigung von Fnew und F sicherzustellen.
28
3.1.2 Semi-naive Fixpunktberechnung
Die semi-naive Fixpunktberechnung stellt zwar kein mächtigeres Werkzeug zur Berechnung von komplexeren Regelmengen dar, ist aber deutlich effizienter. Der Ansatzpunkt ist dabei die in Beispiel 3.1 beobachteten Mehrfachherleitungen durch wiederholtes Herleiten. Dieses soll verhindert werden. Dazu soll, durch Regeltransformation die Auswertung der Regeln so beschränken werden, dass immer mindestens ein
Fakt der letzten Iteration bei der Findung neuer Fakten beteiligt sein muss. Dazu werden so genannte Delta-Relationen erstellt.
In den Delta-Relationen werden nur die Fakten der aktuellen Iterationsrunde gespeichert. Nach Auswertung der Regelmenge in einer Iterationsrunde werden aus den Delta-Relationen alle Fakten gelöscht und die neu erstellten Fakten dieser Runde in ihnen
gespeichert. Gleichzeitig müssen natürlich auch die Faktenmengen der „normalen“
Relationen aktualisiert werden. Weiter werden in den Delta-Relationen auch zu jeder
Ableitungsregel aus der zugehörigen „normalen“ Relation entsprechende Delta-Regeln
erstellt. Dabei ist es meist nötig, mehr als eine Delta-Regel für eine „normale“ Regel
zu erstellen. Die Delta-Regeln sind dabei folgendermaßen definiert:
Definition 3.1 (Delta-Regeln)
Sei D = (F, R) eine deduktive Datenbank, dann sind die Delta-Regeln ∆R wie folgt
definiert:
1) ∀ Regeln r ∈ R mit r ≡ A <- L1,…, Ln gelten folgenden Regeln:
a. Wenn die Regel r nur von Regeln aus tieferen Schichten abhängig ist,
also nicht selbst rekursiv ist, dann sei r in ∆R enthalten.
b. Wenn die Regel r aber die Form: r ≡ A <- L1,…, Lm, Lm+1, …Ln mit 0
< m ≤ n wobei Li (1 ≤ i ≤ m) positive, rekursiv von A abhängige Literale sind, hat, dann müssen folgende Regeln zur Delta-Relation hinzugefügt werden:
∆R1 : δA <- δL1, L2, …, Lm, Lm+1, …, Ln
∆R2 : δA <- L1, δL2, …, Lm, Lm+1, …, Ln
...
∆Rm : δA <- L1, L2, …, δLm, Lm+1, …, Ln
2) Keine anderen Regeln werden zu ∆R hinzugefügt.
Bei der Auswertung dieser Regeln dürfen, wie schon im naiven Fall, keine neuen Fakten aus der aktuellen Iterationsrunde einfließen.
Beim Iterationsprozess sind Regeln, die keine regeldefinierten Literale enthalten, nur
einmal auszuwerten. Mit regeldefinierten Literalen sind solche Literale gemeint, die
selbst ein Kopfliteral einer Regel der betrachteten Regelmenge sind. Es sind im Falle
29
von Teilmengen der ursprünglichen Datenbankregelmenge R nicht nur Regeln gemeint, die nur auf Basisrelationen zugreifen.
Beispiel 3.2 (Delta-Regeln)
Sei D = (F, R) eine deduktive Datenbank mit folgender Regelmenge:
R1
R2
p(X, Y) <- p(Y, X).
p(X, Y) <- q(X, Z), q(Z, Y).
R3
q(X, Y) <- s(X, Z).
R = {R1; R2, R3}; s sei eine Basisrelation.
Es müssen dann die nachstehenden Delta-Regeln erstellt werden:
∆R1 δp(X, Y) <- p(Y, X).
∆R2 δp(X, Y) <- δq(X, Z), q(Z, Y).
∆R2’ δp(X, Y) <- q(X, Z), δq(Z, Y).
∆R3
δq(X, Y) <- s(X, Z).
Aus der Regel R2 werden die Regeln ∆R2’ und ∆R2’ gebildet.
Der Iterationsprozess teilt sich dann in zwei Teile: in die erste Iteration, in der alle
Delta-Regeln ausgewertet werden, und in die restlichen Iterationen, in denen nur Delta-Regeln ausgewertet werden müssen, die selbst wieder ein Delta-Literal enthalten.
Regeln die kein Delta-Literal enthalten, können in weiteren Iterationsrunden auch keine neuen Fakten generieren, da sie ja auch nicht auf neue Fakten zugreifen können.
first = true;
∆R = createDeltaRelations(R);
do{
Fnew = ∅;
for all(r ∈ ∆R)
if (first | hasDeltaLiteral(r)) Fnew = Fnew ∪ T[r] (F);
removeAllDeltaFacts(F);
F = F ∪ Fnew ∪ transform(Fnew);
first = false;
} while (Fnew != ∅);
Abbildung 3.2: Algorithmus zur semi-naiven Fixpunktberechnung
30
Die Besonderheit des Algorithmus liegt in der Materialisierung der Fakten, dargestellt
durch die Vereinigung von F, Fnew und den überführten Fakten transform(Fnew). In Fnew
werden alle neuen Fakten einer Iterationsrunde zwischengespeichert, damit sie keinen
Einfluss auf die aktuelle Regelableitungen haben. Die Vereinigung von F und Fnew
stellt das Materialisieren der neuen Fakten in ihren Delta-Relationen dar. Es ist aber
noch nötig, dass die neuen Fakten auch in ihren korrespondierenden „normalen“ Relationen gespeichert werden. Würde diese doppelte Speicherung der neuen Fakten nicht
geschehen, könnten in späteren Iterationszyklen nicht alle Fakten hergeleitet werden.
Vor der Materialisierung müssen zuerst aus der aktuellen Faktenmenge F alle alten
Delta-Fakten gelöscht werden.
Die redundante Speicherung der neuen Fakten in den Delta- und den „normalen“ Relationen lässt sich durch Erweiterung der Delta-Regeln erreichen. Allerdings hat dies
den Nachteil, dass man von einer linearen Anzahl von Regeln zu einer exponentiellen
Anzahl von neuen Delta-Regeln gelangt.
Der Algorithmus lässt sich durch einige einfache Anpassungen leicht weiter verbessern. Um z.B. die Effizienz der Delta-Regel-Auswertung weiter zu erhöhen, sollte man
immer mit dem Delta-Literal δLi bei der Auswertung beginnen. Man spart dadurch
unter Umständen eine große Menge potentieller Variablensubstitutionen ein. Diese
potentiellen und unvollständigen Substitutionen würden erst beim Erreichen des DeltaLiterals wieder verworfen werden. Auch eine Duplikatbildung durch unterschiedliche
Ableitungswege lässt sich durch eine Erweiterung der Delta-Regeln verhindern. Dazu
muss man am Ende jeder Delta-Regel noch das negierte Kopfliteral der korrespondierenden „normalen“ Regel einfügen. Dadurch wird immer geprüft, ob in der alten Faktenmenge nicht schon ein solches Fakt besteht (direkte Duplikateliminierung).
Beispiel 3.3 (semi-naive Fixpunktiteration)
Dieses einfache Beispiel soll die Arbeitsweise des semi-naiven Algorithmus verdeutlichen und die Einsparungen bei der Materialisierung zeigen. Sei D = (F, R) eine deduktive Datenbank wie in Beispiel 3.1 mit den zugehörigen Delta-Regeln:
F = {s(1, 2); s(2, 3); q(3)}
R1
R2
p(X) <- s(X, Y), p(Y).
p(Y) <- q(Y).
∆R1
∆R2
p(X) <- s(X, Y), δp(Y).
p(Y) <- q(Y).
R = {R1; R2; ∆R1; ∆R2}
In der ersten Iterationsrunde kann nur die Regel ∆R2 neue Fakten hervorbringen. Da
noch keine p-Fakten existieren, kann es keine Substitution für die Y Variable in Regel
∆R1 geben. Würde man hier nun die Literale von links nach rechts auswerten, bekäme
man zuerst mindestens eine potentielle Variablensubstitution für die Variablen X und
Y, die dann aber durch Auswertung des δp-Literals wieder verworfen würden. Die
31
Regel ∆R2 liefert das Delta-Fakt δp(3), materialisiert wird aber auch das Fakt p(3) und
man erhält folgende neue Faktenmenge:
F = {s(1, 2); s(2, 3); q(3); p(3); δp(3)} nach Iteration 1
In der zweiten Iterationsrunde wird die Regel ∆R2 nicht mehr beachtet, da sie kein
Delta-Literal enthält. Die Regel ∆R1 erzeugt aber das Fakt δp(2). Nachdem alle neuen
Fakten berechnet wurden, werden die alten Delta-Fakten, in diesem Beispiel nur δp(3),
gelöscht. Am Ende des Iterationsschritts wird die Materialisierung durchgeführt und
man erhält die Faktenmenge:
F = {s(1, 2); s(2, 3); q(3); p(3); p(2); δp(2)} nach Iteration 2
Im dritten Durchlauf kann mit der Regel ∆R2 wieder genau ein Fakt δp(1) hergeleitet
werden. Andere, vor allem doppelte, Herleitungen sind nicht möglich.
F = {s(1, 2); s(2, 3); q(3); p(3); p(2); δp(1); p(1)} nach Iteration 3
In der letzten Iterationsphase werden keine neuen Fakten mehr gefunden und Fnew ist
leer. Es werden allerdings wieder alle alten Delta-Fakten gelöscht. Man erhält diese
Faktenmenge:
F = {s(1, 2); s(2, 3); q(3); p(3); p(2); p(1)} nach Iteration 4
Nachdem die Schleife terminiert ist, kann man noch alle Delta-Relationen löschen.
Alternativ kann man sie aber auch im Schema belassen, um damit zukünftige seminaive Fixpunktberechnungen etwas schneller durchführen zu können.
Selbst bei einer so kleinen Fakten- und Regelmenge spart die semi-naive Fixpunktberechnung schon die Berechnung vieler Fakten ein. Es werden genau die drei Fakten
berechnet, um die die Faktenmenge erweitert werden muss. Nur die doppelte Speicherung der Fakten stellt überflüssige Arbeit dar.
3.1.3 Iterierte Fixpunktberechnung
Das schichtenbasierte Verfahren zur Fixpunktberechnung ist in der Lage, die Bedeutung jeglicher stratifizierbarer Regelmenge zu bestimmen. Die iterierte Fixpunktberechnung stützt sich dabei auf die naive oder semi-naive Fixpunktmethode. Zuerst
muss eine Stratifikation für die gegebene Regelmenge berechnet werden. Für ein Stratum i können alle Relationen, die in einer tiefern Schicht (<i) bereits berechnet wurden, als Basisrelationen angesehen werden. Die Regelmengen der einzelnen Schichten
lassen sich also als semi-positive Regelmenge betrachten, da sie sich nicht negativ auf
Regeln der gleichen Schicht beziehen dürfen. Somit lassen sich sowohl der naive, als
auch der semi-naive Algorithmus für diese Teilmengen benutzten. Der iterative Algorithmus muss für jedes Stratum den semi-naiven (oder den naiven) Algorithmus mit
der entsprechenden Regelmenge als Parameter aufrufen. Dabei muss er beim kleinsten
32
Stratum beginnen und dann immer die Bedeutung der nächst höheren Schicht berechnen.
Sei D = (F, R) und S eine beliebige Stratifikation von R und S[i] das i. - Stratum.
Algorithmus zur iterativen Fixpunktberechnung:
for all (int i=0; i<S.size(); i++)
semiNaiveFPI( rulesIn(S[i]) );
Beispiel 3.4 (iterative Fixpunktiteration)
Sei D = (F, R) eine deduktive Datenbank und S eine Stratifikation von R:
F = {r(1, 2, 3); r(2, 3, 4); r(3, 1, 2); t(1,4) ; t(3, 2)}
R4
s(X) <- t(X, Y), not p(X, Y).
R#3 = {R4}
R3
R2
p(X, Y) <- q(X, Y), not q(Y, X).
p(X, Y) <- q(X, Z), p(Z, Y).
R#2 = {R2; R3}
R1
q(X, Y) <- r(X, Y, Z).
R#1 = {R1}
R = {R1; R2; R3; R4}
S = {R#3; R#2; R#1}
Im ersten Durchlauf wird von der semi-naiven Fixpunktberechnung nur die Regel R1
ausgewertet. Dabei entstehen die Fakten q(1, 2), q(2, 3) und q(3, 1) und man erhält die
gesamt Faktenmenge:
F = {r(1, 2, 3); r(2, 3, 4); r(3, 1, 2); t(1,4); t(3, 2); q(1, 2); q(2, 3); q(3, 1)}
Im zweiten Iterationsschritt werden die Regeln R2 und R3 ausgewertet. Dabei werden
zuerst durch die Regel R3 die Fakten p(1, 2), p(2, 3) und p(3, 1) hergeleitet und dann
die Fakten p(3, 2), p(1, 3) und p(2, 1) gefunden. Zum Schluss werden die Fakten p(3,
3), p(2, 2) und p(1, 1) von dem internen semi-naiven Aufruf abgeleitet. Am Ende des
zweiten Schleifendurchlaufs ist das Ergebnis die folgende Faktenmenge:
F = {r(1, 2, 3); r(2, 3, 4); r(3, 1, 2); t(1,4); t(3, 2); q(1, 2); q(2, 3); q(3, 1);
p(1, 2);p(2, 3); p(3, 1); p(3, 2); p(1, 3); p(2, 1); p(3, 3); p(2, 2); p(1, 1) }
Der letzte Durchlauf liefert nur noch das Fakt s(1). Damit erhält man die endgültige
Faktenmenge:
F = {r(1, 2, 3); r(2, 3, 4); r(3, 1, 2); t(1,4); t(3, 2); q(1, 2); q(2, 3); q(3, 1);
p(1, 2); p(2, 3); p(3, 1); p(3, 2); p(1, 3); p(2, 1); p(3, 3); p(2, 2);
p(1, 1); s(1) }
33
Bei einer direkten Anwendung des naiven oder semi-naiven Algorithmus auf die gesamte Regelmenge, wäre fälschlicher Weise auch das Fakt s(3) erzeugt worden.
3.2 Fixpunktberechnung für nichtstratifizierbare Regelmengen
In diesem Unterkapitel werden kurz die beiden gängigen Verfahren für die Fixpunkterechnung von nichtstratifizierbaren Regelmengen vorgestellt. Das erste Verfahren ist
das in Kapitel 2 vorgestellte CFP-Verfahren mit dessen Hilfe die Semantik von nichtstratifizierbaren deduktiven Datenbanken definiert wurde. Das zweite Verfahren ist die
alternierende Fixpunktberechnung (AFPI) von Van Gelder. Beide Verfahren weisen
einer deduktiven Datenbank immer dieselbe „well-founded“-Semantik zu. Ebenso liefern beide Verfahren auch zu jeder stratifizierbaren deduktiven Datenbank die in Kapitel 2 definierte Semantik.
Die alternierende Fixpunktberechnung ist zwar besser dokumentiert und scheint auch
für eine effizientere Implementierung besser geeignet zu sein, aber ein Beweis dafür
steht noch aus [17]. Eine Diskussion der Vor- und Nachteile wird nicht geführt, es
werden lediglich die Vorraussetzungen der Algorithmen dargestellt. Dies ist nötig, um
eine bessere Erweiterbarkeit des zu entwerfenden Programms um diese Algorithmen
zu erleichtern.
3.2.1 Fixpunktberechnung mit bedingten Fakten
Der Ablauf der bedingten Fixpunktiteration ist durch die Definitionen in Kapitel 2 erfolgt. Zur einfacheren Handhabung empfiehlt es sich vor Beginn der Iteration alle
„normalen“ Fakten ebenfalls in bedingte Fakten zu überführen. Damit lässt sich das
Verwalten und Auswerten unterschiedlich strukturierter Daten, welche die gleiche Bedeutung haben, ersparen. Im Anschluss muss dann die erste Iteration durchgeführt
werden. Diese erste Phase ist eine „gewöhnliche“ Fixpunktiteration ohne Auswertung
von negativen Literalen. Das Ergebnis ist somit ausschließlich eine Menge von bedingten Fakten. Es verbleiben keine Variablen mehr in den Regelrümpfen und man
erhält alle potenziell herleitbaren Fakten. In der zweiten Phase müssen nun die negativen Literale ausgewertet werden. Dies unterteilt sich wieder in zwei Abschnitte. Zuerst
werden alle sicher wahren Literale eliminiert. Sicher wahr sind all diese Literale, zu
denen ein bedingtes Fakt existiert, dessen Rumpf nur die Konstante true enthält. Als
zweites müssen in der Reduktionsphase alle bedingten Fakten gelöscht werden, die ein
sicher falsches Literal enthalten. Mit Sicherheit falsch sind alle Fakten, die nicht mehr
als bedingte Fakten vorhanden sind. Diese zweite Iterationsphase muss solange wiederholt werden, bis keine Veränderung mehr an der Faktenmenge stattfindet. In Abbildung 3.3 ist der Algorithmus zur bedingten Fixpunktberechnung noch einmal in Pseudocode angegeben.
34
tF = transformToConditionalFacts(F);
do {
/* Phase 1 */
tFold = tF;
tF = tF ∪ Tcond[R] (tF);
} while (tF != tFold)
do {
/* Phase 2 */
tFold = tF;
tF = Redtrue (tF);
tF = tF - computeFalseFacts(tF);
/* oder: tF = Redfalse (tF); */
} while (tF != tFold)
Fpos* = LiteralheadsWithTrueBody(tF);
Fundef* = heads(tF) - Fpos*;
Fneg* = computeHerbrandBasis(D) – heads(tF);
Abbildung 3.3: Algorithmus zur bedingten Fixpunktberechnung (CFP)
Zur Implementierung der bedingten Fixpunktberechnung benötigt man also vor allem
eine Datenstruktur, die bedingte Fakten darstellen und verwalten kann. Weiter ist zu
beachten, dass die bedingte Fixpunktberechnung neue Operationen benötigt und nicht
auf Algorithmen zur Berechnung von stratifizierbaren deduktiven Datenbanken zurückgreifen kann.
3.2.2 Alternierende Fixpunktberechnung
Die alternierende Fixpunktberechnung von van Gelder arbeitet mit vier verschiedenen
Faktenmengen:
-
den definitiv falschen Fakten,
den wahrscheinlich wahren Fakten,
den wahrscheinlich falschen Fakten und
den definitiv wahren Fakten
Zu Beginn enthält die Menge der wahrscheinlich wahren Fakten die gesamte Herbrand-Basis. Alle anderen Mengen sind leer. Dann wird eine Fixpunktbestimmung mit
fixierter Referenz auf die Menge der definitiv falschen Faktenmenge durchgeführt. Die
Menge der definitiv falschen Fakten kann durch Komplementbildung der wahrscheinlich wahren Fakten bezüglich der Herbrand-Basis berechnet werden. Dadurch, dass
negative Literale nur über eine feste Referenzmenge ausgewertet werden, erhält man
35
eine semi-positive Datenbank und kann die Methoden für stratifizierbare deduktive
Datenbanken anwenden. Dabei muss die feste Referenzmenge mit übergeben werden
und es darf nicht mehr das „negation as failure“-Prinzip angewandt werden. Nach der
Bestimmung von definitiv wahren Fakten muss nun auch eine Fixpunktberechnung auf
dieser Teilmenge bzw. ihrem Komplement ausgeführt werden. Dadurch erhält man
ggf. eine neue (kleinere) Menge von wahrscheinlich wahren Fakten und der gesamte
Prozess muss von vorne begonnen werden. Diese Doppeliteration wird global solange
wiederholt, bis keine Veränderung mehr an den Faktenmengen auftritt.
Der große Nachteil dieses Algorithmus ist die explizite Nutzung der negativen Fakten.
Ein alternativer Ansatz („doubled program“) von Kemp, Stuckey und Srivastava [12]
bietet den Vorteil, dass die negativen Fakten nur implizit angegeben werden müssen.
Dadurch kann man bei der Implementierung des Algorithmus auch direkt auf die unveränderten Fixpunktoperatoren für stratifizierbare deduktive Datenbanken zurückgreifen. Der vereinfachte Algorithmus ist in Abbildung 3.4 dargestellt. Dies bedeutet
vor allem auch, dass für die Implementierung der alternierenden Fixpunktberechnung
keine zusätzlichen Bedingungen an die Datenstruktur gestellt werden müssen. Zusätzlich bedarf es einer weiteren Regeltransformationskomponente, die ohne zusätzliche
Anforderungen an die Datenstruktur zu stellen, implementiert werden kann.
defTrueRules = createDefTrueRules(R);
possFalseRules = createPosFalseRules(R);
semiNaiveFPI(possFalseRules);
do {
semiNaiveFPI(defTrueRules);
semiNaiveFPI(possFalseRules);
} while (hasChanged(Facts))
Abbildung 3.4: Algorithmus zur alternierenden Fixpunktberechnung („doubled
program“ AFPI)
36
Kapitel 4
Entwurf des DatalogLabs
In diesem Kapitel erfolgt die Anforderungsanalyse für das DatalogLab auf der theoretischen Grundlage der ersten Kapitel. Dabei wird auch die gute Erweiterbarkeit in Bezug auf im Rahmen dieser Arbeit nicht zu implementierende Programmkomponenten
beachtet. Im Anschluss wird die resultierende Systemarchitektur vorgestellt.
4.1 Anforderungsanalyse
Dieses Unterkapitel besteht aus drei Teilen. Zuerst werden die allgemeinen Aufgaben
spezifiziert, die das DatalogLab erfüllen soll. Dabei werden auch die Aufgaben des
DBMS-Teils untersucht und festgelegt. In Abschnitt 4.1.2 folgen dann die Anforderungen an die graphische Oberfläche.
4.1.1 Aufgaben des DatalogLabs
Die Aufgaben des DatalogLabs wurden in der Einleitung bereits genannt und sollen in
diesem Abschnitt genauer beschrieben werden. Das DatalogLab soll als Lehrmittel für
die Anfragesprache Datalog in der theoretischen Datenbankforschung dienen. Dazu
muss das DatalogLab aus einem deduktiven Datenbanksystem und einer darauf zugeschnittenen graphischen Oberfläche bestehen. Das deduktive Datenbanksystem muss
zumindest alle Operationen bereitstellen, die zum Ausführen von grundlegenden Aufgaben benötigt werden. Zu diesen grundlegenden Aufgaben eines deduktiven Datenbanksystems gehören:
-
das Erstellen und Löschen von Relationen,
die Verwaltung von Regeln und Integritätsbedingungen,
das Einfügen und Löschen von Fakten aus der Datenbank,
das Ableiten neuer Daten aus den Basisfakten,
die Anfragebearbeitung,
das Parsen von Datalog-Ausdrücken im Textformat,
die Datenausgabe an andere Anwendungen und
das Speichern und Laden der Datenbank auf einem permanenten Speichermedium in der gängigen Datalog-Syntax.
37
Weiter soll die empirische Analyse unterschiedlicher Algorithmen ermöglicht werden,
dazu muss in das DBMS auch eine Komponente eingebaut werden, die statistische
Werte über die durchgeführten Operationen speichern und später ausgeben kann. Zusätzliche Aufgaben wie die Realisierung der Integritätsüberwachung und der Transaktionsverwaltung sind nicht Ziel dieser Arbeit. Allerdings stellt eine möglichst einfache
Erweiterung auch eines der wichtigen Ziele des gesamten DatalogLabs dar. Dadurch
ist es unumgänglich, auch Anforderungen von nicht entwickelten Komponenten mit
einzubeziehen. Dies gilt sowohl für den DBMS-Teil als auch für die graphische Oberfläche des DatalogLabs.
Die Aufgaben der Benutzeroberfläche leiten sich zum größten Teil aus den Möglichkeiten des DBMS ab. So muss sie:
-
alle angebotenen Funktionalitäten des DBMS nutzbar machen,
die vom DBMS gelieferten Informationen visualisieren,
leicht bedienbar,
möglichst flexibel und
strukturiert sein.
Aus diesen übergeordneten Aufgaben des DatalogLabs ergeben sich weitere spezialisierte Anforderungen, die erfüllt sein müssen. Die Hauptaufgabe des DBMS liegt, wie
bereits erklärt, in der effizienten Datenverwaltung. Daher ist der beste Einstiegspunkt
für die genaue Anforderungsanalyse die Datenstruktur. Dabei darf die aktuell untersuchte Komponente nie losgelöst von der gesamten Datenstruktur betrachtet werden.
Würde man beispielsweise eine Darstellung für Fakten wählen, die einen minimalen
Speicherverbrauch hat, hieße dies nicht, dass auch die Zugriffszeiten im Rahmen der
Regelauswertung effizient sind. Dies ist jedoch bezogen auf die Aufgabe der Datenstruktur sicher die wichtigere Eigenschaft. Da Regeln Fakten hervorbringen, wird zuerst eine möglichst gute Darstellungsform für die Regeln gesucht und im Anschluss
eine passende, aus den Regeln resultierende, Struktur für die Fakten vorgestellt.
Zuerst sollen jedoch einige allgemein gültige Forderungen erläutert werden:
Beispielsweise darf das DBMS keine Speicherreferenzen auf die gespeicherten Daten
übermitteln, sondern nur Kopien von diesen Informationen an andere Anwendungen
weitergeben. Ansonsten wäre es Programmen möglich, die Datenbankinhalte ohne die
Kontrolle des DBMS zu verändern und damit ggf. die Datenintegrität zu verletzen.
Eine andere Anforderung an das DBMS folgt aus dem Ziel, die Datenbankinhalte in
der Datalog-Syntax zu speichern bzw. zu laden. Dies bietet den Vorteil, dass das DatalogLab eine gewisse Grundkompatibilität mit anderen deduktiven Datenbanksystemen
aufweist, die auf Datalog basieren. Außerdem lassen sich Dateien, welche die Informationen in der Datalog-Syntax enthalten, mit normalen Texteditoren bearbeiten. Allerdings hat diese Art der Speicherung auch einen großen Nachteil: Beim Laden muss die
Textdarstellung jedes Mal durch das DBMS neu geparst und auf Konsistenz geprüft
werden. Dies ist nicht notwendig, wenn eine der internen Datenstruktur nachempfundene feste Speicherfolge gewählt wird. Da beide Arten des Ladens und Speicherns
deutliche Vorteile besitzen, sollten sie auch beide implementiert werden. Weiter ist es
38
sinnvoll, die Methoden zum Laden und Speichern so zu realisieren, dass sie einfach
erweitert werden können, um auch andere Formate zu unterstützen.
Für die Anforderungsanalyse der Datenstruktur sollen hier nur einige allgemeine Forderungen formuliert werden. In Kapitel 5 werden dann noch weitere Entwurfsentscheidungen erläutert und begründet. Da es sich bei diesen Entscheidungen um Realisierungsdetails und nicht um grundsätzliche Forderungen an die Datenstruktur handelt,
werden sie erst im nächsten Kapitel beschrieben.
Als Ausgangspunkt der folgenden Analyse und der späteren Entwicklung sollen die
Datalog-Regeln und Fakten dienen, da diese den Kern eines deduktiven Datenbanksystems darstellen und von fast allen Komponenten verwendet werden. Dabei ist es besonders wichtig, dass die gewählten Darstellungen in einer kompakten Form ohne die
Speicherung von redundanten Daten umgesetzt werden und außerdem einen schnellen
Zugriff auf die zugehörigen Daten ermöglichen. Dazu müssen diese Daten gut strukturiert gespeichert werden. Außerdem ist es vor allem bei den Regeln nötig, sie mit dynamischen Datenstrukturen zu realisieren, damit möglichst leicht und effizient Methoden realisiert werden können, die auf Regeltransformationen basieren. Weiter ist es
sehr wichtig, dass während der Arbeit mit den Daten möglichst keine Parsingoperationen von Texten durchgeführt werden, da diese Operationen zu einer schlechten Laufzeit führen. Beim Parsen selbst sollten reguläre Ausdrücke verwendet werden, weil
diese den Code deutlich vereinfachen und der Code übersichtlicher und besser lesbar
ist. Außerdem ist bei der Implementierung darauf zu achten, den gesamten Code möglichst gut zu kapseln, um doppelte Implementierungen zu vermeiden.
Für die Darstellung von Fakten hingegen gibt es verschiedene Ansätze, die alle unterschiedliche Stärken und Schwächen aufweisen. Grundsätzlich ist es möglich, Fakten
als Regeln zu speichern. Der Regelrumpf dieser Regeln bestünde dann nur aus der
Konstanten true, die sich wiederum als nullstellige immer wahre Relation auffassen
ließe. Für einige Algorithmen ist diese Betrachtungsweise von Fakten, die dann auch
als bedingte Fakten bezeichnet werden, nötig. Um eine zukünftige Implementierung
dieser Algorithmen als Ergänzung des zugehörigen Programms zu ermöglichen, soll
diese Art der Faktendarstellung grundsätzlich auch möglich sein. Allerdings ist es keine effiziente Methode um Fakten allgemein zu speichern. Dazu ist es sinnvoller, Fakten als eigenständige Objekte zu realisieren. Diese sollen, wie bereits oben gefordert,
kompakt sein. Daher empfiehlt es sich bei der Speicherung der gesamten Faktenmenge
nur die Listen der Konstanten ohne den Relationsbezeichner bei jedem Fakt zu speichern. Andererseits muss es zumindest für die Eingabe von Fakten eine Darstellungsform geben, die auch erlaubt, den Relationsnamen direkt mit der Liste der Konstanten
zu speichern. Insgesamt bedarf es also drei verschiedener Darstellungsformen, die alle
neben einander implementiert werden müssen.
Die nächsten wichtigen Punkte sind die Schemaevolution und die Datenverwaltung.
Da es bei der Schemaevolution dazu kommen kann, dass auch die Datenbankfakten
angepasst werden müssen bzw. bei der Datenmanipulation auch Schemadaten geprüft
werden müssen, könnte der Ansatz verfolgt werden, beide Aufgaben in einer Komponente zu vereinigen und gemeinsam zu realisieren. Betrachtet man aber die potentielle
39
Größe von Datenbanken, sieht man schnell, dass diese ggf. nicht komplett in den
Hauptspeicher passen. Das Problem liegt dabei nicht an der Größe des Schemas, sondern meist an der großen Faktenmenge. Um aber Schemaevolution effizient betreiben
zu können, ist es für den Benutzer sinnvoll, das gesamte Schema gleichzeitig bearbeiten zu können. Dazu sollte es komplett in den Hauptspeicher geladen werden. Wären
nun aber Fakten und Schemainformationen aufgrund der internen Datenstruktur eine
Einheit, wären auch Veränderungen am Schema mit langsamen Zugriffen auf den Hintergrundspeicher verbunden. Um dieses Problem zu umgehen, müsste man also wieder
eine künstliche Trennung in der internen Darstellung erzwingen.
Weiter erhöht eine Trennung der beiden Komponenten auch die Übersichtlichkeit des
DBMS. Außerdem gibt es Schemaänderungen, die keine Änderung der Daten nach
sich ziehen können. Der wichtigste Grund aber ist die Tatsache, dass bei einer Änderung an einer der beiden Komponenten die Struktur der anderen nicht oder nur sehr
geringfügig geändert werden muss. Sollte also beispielsweise die Speicherung, oder
gar die Datenstruktur des Datenbankmanagers optimiert werden, kann der Schemamanager unverändert bleiben.
Eine der Aufgaben beider Komponenten ist das Laden und Speichern der verwalteten
Daten. Dazu sollte auch die aktuelle Versionsnummer in der internen Darstellung gespeichert werden. Durch eine solche Versionsnummer ist es möglich, nach Änderung
der Dateiausgabe noch alte Datenbankdateien zu laden. Die unbekannten Werte, die
im alten Dateiformat noch nicht gespeichert wurden, müssen dann automatisch mit
passenden Werten gefüllt werden.
Zuletzt sollen in der Anforderungsanalyse Integritätsbedingungen betrachtet werden.
Grundsätzlich können Integritätsbedingungen neben den in Kapitel 2 definierten Eigenschaften auch weitere Merkmale aufweisen. So gibt es beispielsweise in der Datenbanksprache SQL die Möglichkeit zu bestimmen, wann Integritätsbedingungen
während einer Transaktion geprüft werden. In SQL werden dazu die Schlüsselworte
IMMEDIATE (Prüfung unmittelbar nach jedem Zustandswechsel) und DEFERRED
(Prüfung erst am Ende der Transaktion) verwendet. Des Weiteren wäre eine Einbindung von Hilfsregeln oder komplexen Ausdrücken auch einfacher möglich. Um die
Erweiterung um solche Eigenschaften zu gewährleisten, muss für Integritätsbedingungen eine eigene Komponente realisiert werden. Durch die gespeicherten Versionsnummern ist dann die leichte Erweiterbarkeit der Integritätsbedingungskomponente
ohne Datenverlust bzw. zusätzliche Datenkonvertierungsmethoden gewährleistet.
Für alle übrigen Datalog-Konzepte und -Ausdrücke gelten keine speziellen zusätzlichen Anforderungen, außer den allgemeinen Bedingungen, die in diesem Abschnitt
aufgestellt wurden, und den Aufgaben der Ausdrücke, die sie laut ihrer Definition in
Kapitel 2 erfüllen müssen.
40
4.1.2 Entwurfskriterien für die graphische Oberfläche
Wie bereits im Abschnitt 4.1.1 kurz erläutert hat die graphische Benutzeroberfläche
(GUI) die Aufgabe, die Kommunikation zwischen dem Benutzer und dem DBMS zu
ermöglichen und zu vereinfachen. Dazu gehören alle Schemaanweisungen, wie z.B.
das Erstellen von neuen Relationen oder das Einfügen und Löschen von Regeln oder
Integritätsbedingungen. Genauso soll die GUI aber auch alle Änderungsanweisungen
entgegennehmen und an die Datenbank weiterleiten können. Dazu zählen sowohl einfach Änderungen als auch Transaktionen. Die dritte sehr wichtige Aufgabe besteht in
der Anfragebearbeitung. Bei all diesen Operationen soll die GUI vor allem auch in der
Lage sein, die Daten übersichtlich darzustellen. Weiter soll die GUI das Einstellen von
Optionen ermöglichen und zusätzliche Informationen über die Datenbank anzeigen
können. Auch die Auswertung aller Regeln der Datenbank soll von der GUI direkt
gestartet werden können. Die graphische Oberfläche hat also die Aufgabe, dem Benutzer alle Möglichkeiten des DBMS wiederzuspiegeln. Dabei bleibt eine gute und einfache Benutzbarkeit immer eine der wichtigsten Qualitäten der GUI.
Für häufig wiederkehrende und sehr ähnliche Aufgaben werden meist die spezialisierten Anwendungsprogramme benutzt. Diese sind leicht zu erlernen und effizient zu bedienen. Meist sind sie allerdings nicht so ausdrucksstark wie (zumindest teilweise)
textbasierte Programme, die ein direktes Arbeiten mit der zum Datenbanksystem gehörenden Anfragesprache ermöglichen. Diese Schnittstellenprogramme sind meist flexibler und mächtiger als ihre spezialisierten Vertreter. Die im Rahmen dieser Arbeit entwickelte graphische Benutzerschnittstelle soll beide Ansätze vereinen. Und dabei zum
einen unerfahrene Benutzer bei der Anlage einer Datenbank unterstützen, zum anderen
aber auch möglichst alle Datalog-Anweisungen verarbeiten können. Dazu soll die
Möglichkeit bestehen, jeden beliebigen Datalog-Ausdruck über eine entsprechend
vorgesehene Dialogbox einzugeben.
Auf der anderen Seite sollten für allen zu realisierten Datenbankbereichen unterstützende Eingabefenster zur Verfügung stehen, die speziell für eine Aufgabe zugeschnitten sind. Ein Vorteil dieser Komponenten ist, dass sie in der Lage sind, gewisse
Grundstrukturen beispielsweise vor der Eingabe vorzugenerieren. Daneben ist es viel
einfacher und schneller möglich, dem Benutzer Fehlerhinweise zu geben und Lösungsmöglichkeiten vorzuschlagen. Beides ist nur möglich, weil im Kontext des Programms klar ist, welche Art von Datalog-Anweisung der Benutzer eingeben will. Die
Dialogbox für allgemeine Anweisungen kann erst einmal nur feststellen, dass es sich
um einen ungültigen Datalog-Ausdruck handelt. Dann müsste versucht werden festzustellen, welchem Ausdruck der durch den Benutzer eingegebene Befehl am ähnlichsten ist, um ggf. gezielte Fehlerhinweise geben zu können.
Neben einer intuitiven und benutzerfreundlichen Eingabe ist die zweite wichtige Aufgabe der graphischen Benutzeroberfläche die Ausgabe der Daten auf dem Bildschirm.
Dabei ist es wichtig, dass die visualisierten Daten immer möglichst übersichtlich dargestellt werden. Dazu kann auch die Anzeige zusätzlicher Sekundärdaten gehören,
wenn dies vom Benutzer gewünscht wird. Diese zusätzlichen Daten sollten dann ein
besseres Verständnis der Primärdaten ermöglichen. Eine Vorgehensweise für die Dar41
stellung unterschiedlicher Informationen besteht darin, für jede Visualisierung von
Daten ein eigenes Fenster zu öffnen. Damit kann jeder Benutzer in einem gewissen
Rahmen seinen individuellen Oberflächenaufbau erstellen. Allerdings können zu viele
gleichzeitig geöffnete Fenster die Übersichtlichkeit deutlich mindern und die Arbeitsgeschwindigkeit des Nutzers einschränken, wenn der Anwender immer alte Fenster
schließen und neue Fenster öffnen bzw. (neu) anordnen muss. Dies könnte man durch
Speichern der alten Fensterpositionen und die Angabe einer maximal offenen Fensteranzahl verbessern, es verursacht aber insgesamt immer einen deutlich höheren Verwaltungsaufwand.
Alternativ kann man das gesamte Programm in einem Hauptfenster ablaufen lassen.
Dabei sind natürlich einige Ausnahmen wie z.B. Dialog- und Nachrichtenboxen oder
Optionsfenster erlaubt. Um aber auch hier unterschiedliche Daten gleichzeitig anzeigen zu können, bietet es sich an, das Fenster in verschiedene Bereiche zu unterteilen.
Dabei sollten die unterschiedlichen Bereiche aber in allen Ansichten immer ähnliche
Aufgaben haben, damit die Benutzung möglichst intuitiv geschehen kann. Zudem bietet dieser Ansatz ebenso eine gewisse Flexibilität, indem man es dem Anwender erlaubt, die Größe der verschiedenen Bereiche selbst festzulegen. Zusätzlich könnte man
den Benutzer auch noch über die Programmeinstellungen festlegen lassen, was bei
welchen Ansichten in welchem Fensterabschnitt angezeigt werden soll. Dann wären
beide Alternativen fast gleichwertig, wobei es bei der mehr Fensterlösung immer noch
passieren kann, dass einige Fenster in den Hintergrund gestellt werden und dadurch für
den Anwender nicht mehr sofort nutzbar sind.
Da grundsätzlich die zweite Alternative bei allgemeinen graphischen Oberflächen dem
gängigen Modell zu entsprechen scheint und aus den oben genannten Gründen leichte
Vorteile dem ersten Ansatz gegenüber aufweist, soll dieses Modell als Grundlage der
vorliegenden Arbeit dienen. Dabei soll allerdings darauf verzichtet werden, dem Anwender zu erlauben, die Art der angezeigten Daten in den verschiedenen Ansichten zu
verändern. Jedoch soll es möglich sein, einzelne Teilbereiche (temporär) auszublenden
(ihre Größe auf Null zu setzten). Damit kann auch der gesamte Bildschirm zur Ausgabe von großen Datenmengen genutzt werden.
Neben der Eingabe und der Visualisierung sollte bei der Entwicklung einer graphischen Oberfläche auch eine einfache und intuitive Programmsteuerung im Vordergrund stehen. Dazu gibt es einige verschiedene Ansätze und Möglichkeiten, die aber
im Gegensatz zu den unterschiedlichen Realisierungsformen bei der Darstellung nicht
mit einander konkurrieren. Das heißt, es ist möglich, verschiedene Steuerungsmechanismen gleichzeitig zu implementieren. Dies sollte auch immer geschehen, da man so
die unterschiedlichen Gewohnheiten verschiedener Benutzer unterstützen kann.
Übliche Programmsteuerungsmethoden sind die Steuerung über eine Menüleiste, über
Symbolleisten und die Steuerung durch Tastenkürzel, so genannte Shortcuts, die direkt
über die Tastatur eingegeben werden. Diese sind deshalb besonders wichtig, weil sie
das Wechseln zwischen Tastatur und Maus überflüssig machen und somit der geübte
Benutzer deutlich schneller mit dem Programm arbeiten kann.
42
Weiter existiert noch die Möglichkeit, die Navigation über ein Hauptmenü mit der
Maus zu realisieren. Bei der Steuerung über ein Hauptmenü, sollte beachtet werden,
dass dieses größere Bedienelemente enthält als die Toolleiste. Dadurch ist der Benutzer auch wirklich in der Lage, schneller über dieses Menü zu navigieren, da zum einen
die Darstellung besser zu erkennen sein sollte und zum anderen der Aktivierungsbereich größer ist. Die Symbolleiste, auch Toolbar oder Toolleiste genannt, soll hingegen
das Ausführen häufig benutzter Optionen beschleunigen. Bei der Nutzung unterschiedlicher Symbolleisten sollte darauf geachtet werden, dass ähnliche Befehle mit ähnlichen bzw. gleichen Symbolen dargestellt werden.
Alle Elemente der Steuerungsleisten sind meist nur durch Symbole oder Schlagworte
visualisiert. Um auch neuen und unerfahrenen Benutzern einen möglichst einfachen
Einstieg zu erlauben, sollten alle Schaltflächen der graphischen Oberfläche mit so genannten Tooltipps versehen werden. Tooltipps sind kurze Hilfstexte, die automatisch
angezeigt werden, wenn der Benutzer eine kurze Zeit lang über einem Objekt mit dem
Mauszeiger verweilt. Diese Tooltipps können und sollten selbstverständlich auch für
alle anderen Objekte genutzt werden.
Die Menüleiste sollte im Gegensatz zur Symbolleiste und dem Hauptmenü immer die
Möglichkeit bieten alle ausführbaren Aktionen zu starten, besonders auch solche, die
der Benutzer in der Regel nur selten ausführt und deswegen nicht in einem der anderen
Steuerleisten auswählbar ist. Es sollte dem Benutzer also möglich sein, die gesamte
Steuerung des Programms nur über die Menüleiste durchzuführen. Damit dies auch
effizient geschehen kann, sollten die verschiedenen Befehle ordentlich strukturiert und
in entsprechende Unterbereiche eingeordnet sein. Dabei sollte darauf geachtet werden,
dass die Verzweigungen im Menübaum nicht zu tief werden und dass in einem Menüpunkt nicht zu viele Untereinträge angeordnet sind. Allgemeine Aussagen über absolute Zahlen lassen sich hierbei nicht unbedingt machen, da dies stark von der Anzahl der
insgesamt vorhandenen Befehle des Programms abhängt. Als Faustregel für die Gesamttiefe gilt, dass es in der Regel nicht mehr als zwei Untermenüs geben sollte (also
eine Gesamttiefe von drei). Weiter sollten Menüpunkte mit nur einem Eintrag vermieden werden [19].
Neben diesen Steuerungsmöglichkeiten sollte auch immer das Starten über eine alternative Tastenkombination implementiert werden. Dabei ist es wichtig, möglichst verwandte Buchstaben zu den Aktionen zu bestimmen. Zum Beispiel sollte man den ersten Buchstaben des assoziierten Schlagworts oder des entsprechenden Menüeintrags
wählen. Bei doppelter Belegung eines solchen Buchstabens muss für jede weitere Aktion natürlich ein anderer Buchstabe oder aber eine andere Steuerungstaste (z.B.: Strg
oder Alt) gefunden werden. Allerdings sollten alle Aktionen des gleichen Aufgabenbereichs auch mit der gleichen Steuertaste verbunden sein. Sehr ungeschickt ist es z.B.
das Laden über die Tastenkombination „Strg + L“ und das Speichern von Daten über
die Tastenkombination „Alt + S“ zu realisieren. Um doppelte Belegungen der gleichen
Taste zu verhindern, sollte von der vermutlich weniger benutzten Aktion der zweite
oder ein anderer Buchstabe genutzt werden.
43
Grundsätzlich sollte bei der Wahl der Tasten nicht von den gängigen Tastenkombinationen abgewichen werden. Zum Laden sollte zum Beispiel die Tastenkombination
„Strg + L“ beibehalten werden. Weiter sollten Konflikte mit Tastenkombinationen des
Betriebssystems vermieden werden, z.B. sollte die Tastenkombination „Strg + C“
nicht mit einer anderen Aufgabe als zum Kopieren eines ausgewählten Objektes oder
Textes benutzt werden. Am besten überlädt man solche Kombinationen gar nicht, da
das Verhalten verschiedener Betriebssysteme nicht unbedingt gleich sein muss. So
könnten einige Betriebssysteme beide Befehle ausführen (sofern dies möglich ist).
Andere Betriebssysteme könnten aber nur eine der beiden Aktionen ausführen, im
schlimmsten Fall nur die des Betriebssystems. Da die graphische Oberfläche mit Java
entwickelt werden soll, sollten solche Konflikte vermieden werden, um die Plattformunabhängigkeit nicht einzuschränken.
Weitere Möglichkeiten eine graphische Oberfläche benutzerfreundlich zu gestallten
sind die Erweiterungen um Animationen und akustischen Signale. Mit Warnsignalen
kann der Benutzer z.B. besonders gut zusätzlich auf das fehlerhafte Ende einer Operation hingewiesen werden. Animationen lassen sich hingegen gut nutzen, um das Andauern langwieriger Rechenoperationen anzuzeigen. Damit ist es für den Benutzer
immer ersichtlich, was das Programm gerade macht und das es noch arbeitet. Gängige
Methoden sind dabei das Umwandeln des Mauszeigers in das Sanduhrsymbol oder die
Anzeige von Fortschrittsfenstern.
Zusammenfassend sind also die wichtigsten Aspekte bei der Entwicklung einer graphischen Oberfläche die Benutzbarkeit, die Gestaltung und die Konstruktion. Dabei gelten natürlich ebenso die grundsätzlichen Anforderungen der Softwareentwicklung.
Dazu zählt, wie schon bei der Entwicklung des Datenbanksystems, eine gute Modularisierung eingeständiger Elemente, sowie die teilweise schon daraus resultierende
gute Erweiterbarkeit um neue Aufgabenbereiche. Vor allem sollte dies für Aufgaben
gelten, die in der ersten Implementierung noch nicht realisiert werden sollen, aber
schon fest für zukünftige Erweiterungen geplant sind. In der vorliegenden Arbeit gehört dazu im Bereich der graphischen Oberfläche z.B. die Visualisierung des Abhängigkeitsgraphen.
Die Anpassbarkeit der graphischen Oberfläche durch den Benutzer wurde schon im
Zusammenhang mit der Darstellung verschiedener Visualisierungsarten für Daten erläutert. Allerdings bezieht sich eine potentielle Anpassung nicht nur auf die Gestaltung
und Anordnung der Fenster und Daten, sondern auch auf die individuelle Einrichtung
von Toolleisten und anderen Menüs. Damit können Anwender mit unterschiedlichen
Aufgaben nur solche Befehle und Aktionen in der Symbolleiste aktivieren, die sie
wirklich brauchen. Eine solche individuelle Symbolleiste ist allerdings erst ab einer
größeren Anzahl von Befehlen wirklich sinnvoll.
Grundsätzlich sollten alle Anpassungen an der graphischen Oberfläche, die durch den
Benutzer vorgenommen werden, nach Beendigung des Programms gespeichert und
beim Neustart wieder geladen werden. Dadurch ist der Benutzer nicht gezwungen seine Einstellungen von Hand erneut durchzuführen. Zu diesen Einstellungen können
auch die Position, die Größe und die Anordnung der Fenster gehören.
44
Eine weitere gängige Art der Aktions- und Befehlsausführung in graphischen Oberflächen findet durch Popup-Menüs statt. Diese werden aber normalerweise nicht für allgemeine Aktionen benutzt, sondern beziehen sich meist auf die gerade dargestellten
Daten. So werden sie in der Regel durch das Betätigen der rechten Maustaste ausgeführt und abhängig zu den an den Koordinaten des Mauszeigers visualisierten Daten
erstellt. Die zugehörigen Daten können beispielsweise in einem Fließtext das Wort
oder der Satz im Bereich des Mauszeigers sein. In Tabellendarstellungen beziehen sich
die Befehle hingegen meist auf die einzelne Zelle oder die gesamte Zeile bzw. Spalte.
Da sie sich kontextabhängig verändern, werden Popup-Menüs oft auch als Kontextmenüs bezeichnet. Globale Befehle, wie z.B. das Laden und Speichern der gesamten
Daten, sollten in der Regel nicht durch Popup-Menüs unterstützt werden.
4.2 Kriterien für die Wahl der Programmiersprache Java
Aus diesen Zielsetzungen lassen sich viele Gründe für die Realisierung mit der Programmiersprache Java herleiten. Einige der Gründe, die für die Wahl von Java als
Programmiersprache sprechen, sollen hier kurz genannt und mit den Anforderungen an
das DatalogLab in Verbindung gebracht werden:
Neben den objektorientierten Eigenschaften zeichnet sich Java vor allem durch dynamische Methodenaufrufe und dem Konzept der späten Bindung zur Laufzeit aus.
Durch die Objektorientierung lassen sich sehr einfach stark modularisierte Programme
entwerfen. Diese sind leichter erweiterbar und benötigen bei Änderungen an einzelnen
Komponenten oft keine Anpassungen an anderen Programmteilen. Auch das Prinzip
der späten Bindung zur Laufzeit unterstützt dabei diese einfacheren Erweiterungen.
Da der Schwerpunkt dieser Arbeit nicht in einer effizienten und laufzeitstarken Implementierung der Datenverwaltung auf der untersten Ebene des DBMS liegt, stellt
auch das automatische Speichermanagement einen Vorteil für die Implementierung
dar. Speicheranforderungen erfolgen zwar über explizite Aufrufe, aber die Speicherrückgabe erfolgt über den Garbage-Collector-Prozess von Java automatisch. Gerade
bei der Datenverwaltung von großen (temporären) Datenmengen kommt es bei anderen Programmiersprachen (wie z.B. C++) zu schwer auffindbaren Programmierfehlern
und so genannten Speicherlecks [15].
Auch die strukturierte Ausnahmeverwaltung (Exceptionhandling) von Java lässt sich
gut für den Entwurf eines Datenbanksystems, und hierbei besonders für den Teil des
Parsers, nutzen. Ausnahmen unter Java ermöglichen eine sofortige Benachrichtigung
der aufrufenden Methode mit einer beliebig komplexen Fehlerangabe. Dieses ist ohne
komplizierte Rückgabewerte möglich.
Ebenfalls ein großer Vorteil liegt nicht direkt an den Spracheigenschaften von Java,
sondern vielmehr an der großen Standardbibliothek. So enthält sie beispielsweise viele
vordefinierte und effizient programmierte Datenstrukturen. Dazu gehören verschiedene Listen, Hashtabellen und Baumstrukturen. Auch die grafischen Fähigkeiten der
mitgelieferten Bibliotheken stellen ein wichtiges Werkzeug für den Entwickler dar. So
45
bieten die bereitgestellten Klassen eine große Auswahl an Funktionalitäten und ermöglichen so die Entwicklung komplexer graphischer Benutzerschnittstellen.
Die Plattformunabhängigkeit von Java ist grundsätzlich ein Vorteil bei der Entwicklung von Datenbanksystemen, da dieses den Entwicklungsaufwand von Benutzerschnittstellen verringert. Es ist mit Java nämlich nicht nötig, für unterschiedliche Betriebssysteme unterschiedliche Quellcodes zu entwickeln und zu pflegen. Das im
Rahmen dieser Arbeit entwickelte Programm ist zwar erst einmal nur als Einzelplatzversion konzipiert, dennoch bietet die Plattformunabhängigkeit natürlich Vorteile. Das
resultierende Programm soll, wie bereits einleitend erwähnt, vor allem der Lehre dienen. Durch die Plattformunabhängigkeit lässt sich dies deutlich einfacher realisieren.
Und im Hinblick auf die potentielle Trennung zwischen DBMS und der graphischen
Benutzerschnittstelle ist die Plattformunabhängigkeit von Java ein entscheidender Vorteil. Dadurch ist es nach der Trennung sofort möglich, die Benutzerschnittstelle als
Client auf unterschiedlichen Betriebssystemen einzusetzen.
Aus diesen Gründen, sowie der weiten Verbreitung von Java, ist Java als Realisierungssprache gewählt worden. Das DatalogLab wurde mit Java Version 5.0 (1.5.0_10)
umgesetzt und auch unter Java 6.0 (1.6.0) getestet. Das Betriebssystem bei der Entwicklung und den Tests des DatalogLabs war Windows XP (Service Pack 2). Als
Entwicklungsumgebung wurde Eclipse in den Versionen 3.2.1 und 3.2.2 benutzt. Bei
einer Weiterentwicklung des Paketes sollte auf Java 6.0 umgestiegen werden. Dies war
bei der Fertigstellung des Programms nicht möglich, da zu diesem Zeitpunkt Java 6.0
nur in einer Betaversion verfügbar war. Nachträglich ist jedoch festzuhalten, dass mit
Java 6.0 z.B. deutlich erweiterte Zugriffe auf das Dateisystem möglich sind. Damit ist
es beispielsweise möglich vor dem Anlegen einer neuen Datei zu prüfen, ob genug
Speicherplatz auf dem gewählten Medium zur Verfügung steht. Außerdem zeichnet
sich Java 6.0 durch allgemeine Verbesserungen an der grafischen Benutzeroberfläche
und durch Performance-Verbesserungen am Kern aus [10].
4.3 Systemarchitektur
In den ersten Abschnitten dieses Kapitels wurden allgemeinere Anforderungen und
Realisierungsgründe dargestellt und erläutert. In diesem Abschnitt geht es um die daraus resultierende Systemarchitektur für das DatalogLab und dabei vor allem um den
Aufbau des DBMS.
Ein Datenbanksystem (DBS) lässt sich in drei Schichten unterteilen. In die oberste
Schicht werden die Benutzerschnittstellen eingeordnet. Diese können Kommandozeilen oder graphische Schnittstellen, sowie spezialisierte Anwendungsprogramme sein.
Bei der Realisierung des DatalogLabs soll, wie oben beschrieben, eine graphische Oberfläche benutzt werden. Die mittlere Ebene des DBS umfasst das eigentliche Datenbankmanagementsystem, um das es hauptsächlich in diesem Kapitel geht. Die unterste
Ebene enthält die effiziente Datenverwaltung und die Speicherung der Daten auf einem permanenten Hintergrundspeicher.
46
Der Schwerpunkt dieser Arbeit liegt in den ersten beiden Schichten. Die Datenverwaltung für ein deduktives Datenbanksystem unterscheidet sich kaum von der Datenverwaltung anderer Datenbanksysteme. Daher wird auf eine genaue Untersuchung dieser
Komponente verzichtet.
Bei der Realisierung eines Datenbanksystems ist es wichtig, die unterschiedlichen
Schichten zu trennen. Das hat verschiedene Gründe. Zum einen ist es aus Performancegründen für ein DBS nötig, nicht nur auf dem Hintergrundspeicher zu arbeiten, sondern auch Daten im Hauptspeicher bereitzuhalten. Da es aber ein Ziel von Datenbanksystemen ist, viele Benutzer zugleich zuzulassen, ist dann eine 1-Prozess-Architektur
nicht möglich. Ein weiterer wichtiger Punkt, der schon bei Einzelnutzung des DBS
auftritt, ist die Realisierung von Zugriffskontrollen. Ohne Kapselung wäre ein direkter
Zugriff auf die Datenbankinhalte möglich, d.h. die Zugriffskomponenten wären umgehbar, wie bereits in Abschnitt 4.1.1 beschrieben.
Die beste Möglichkeit für ein vollständiges DBS ist, alle drei Schichten je durch einen
eigenen Prozess zu realisieren. Dies ermöglicht vor allem auch eine einfache verteilte
Nutzung der Datenbank in einem Netzwerk [11].
Abbildung 4.1 zeigt die vereinfachte Struktur des deduktiven Datenbanksystems DatalogLab. Im Folgenden werden die einzelnen Komponenten genau beschrieben.
Der Schemamanager hat die Aufgabe, die Relationsdeklarationen und die zugehörigen
Regeln zu verwalten. Er stellt dabei sicher, dass keine syntaktisch oder semantisch
falschen Anweisungen verarbeitet und gespeichert werden. Weiter verwaltet er auch
die Integritätsbedingungen. Die syntaktische Korrektheit überprüft er dabei durch Aufrufe von Methoden aus dem Parsermodul. Der Schemamanager stellt eine Schnittstelle
zur tiefsten Ebene, zum Datenwörterbuch (data dictionary), dar. In diesem Datenwörterbuch werden die vom Schemamanager verwalteten und in eine interne Darstellung
übertragenen Daten gespeichert.
Der Datenbankmanager hat die Aufgabe, die in einer Datenbank enthaltenen Fakten
zu verwalten und Zugriffs-, Lösch- und Änderungsanweisungen entgegenzunehmen.
Er stellt die andere Schnittstelle zur untersten Ebene dar und ermöglicht den Zugriff
auf die Datenbank. Bei jeder Anweisung muss vor Änderung des Datenbankzustands
durch eine Anfrage beim Integritätsmodul geprüft werden, ob nach Ausführung der
Anweisung weiterhin ein konsistenter Datenbankzustand herrscht.
Das Integritätsmodul stellt eine Verbindung zwischen dem Schemamanager und dem
Datenbankmanager dar und dient zum effizienten Auswertung und Kontrolle der Integritätsbedingungen. Es liefert Methoden, die prüfen, ob Änderungen am Schema oder an der Faktenbasis einen gültigen Datenbankzustand zur Folge haben.
47
Benutzerschnittstelle
graphische Benutzeroberfläche
Parser
modul
Database
access
DBMS
Query
manager
Transaction
manager
Fixpoint
manager
Statistic
manager
Schema
manager
Database
manager
Built-in
manager
Datenverwaltung
Abbildung 4.1:
Integrity
modul
Data
Dictionary
Database
Architekturübersicht des deduktiven Datenbanksystems
DatalogLab
Eine der wichtigsten Komponenten von deduktiven Datenbankmanagementsystemen
ist der Fixpunkmanager. Er wird zur Ableitung aller Fakten der deduktiven Datenbank
benötig. Teil des Fixpunktmanagers sind Methoden der vorgestellten Fixpunktalgorithmen, wie z.B. naive, semi-naive und iterierende Fixpunktberechnung. Nach der
Fixpunktberechnung werden die neuen Fakten zur Speicherung an den Datenbankmanager weitergegeben.
Der Anfragemanager soll möglichst effizient gegebene Anfragen beantworten und die
Ergebnisse zurückgeben. Dazu muss er verschiedene Methoden mit unterschiedlichen
komplexen und effizienten Algorithmen anbieten. Im Rahmen der vorliegenden Arbeit
soll allerdings nur ein naiver Algorithmus zur Anfragebeantwortung implementiert
werden. Dieser führt immer eine komplette Fixpunktiteration durch und bestimmt
dann die Antwort.
Neben diesen grundlegenden DBMS-Komponenten gibt es noch weitere Module die
für ein deduktives Datenbanksystem nicht zwingend nötig sind, bzw. deren Funktionalitäten auch in die oben erläuterten Komponenten eingebaut werden könnte. Es gibt
aber für diese Komponenten gute Gründe, sie als gekapselte Module zu realisieren.
48
Der Transaktionsmanager ist neben seiner Aufgabe sich um Transaktionen zu kümmern, auch für einfache Änderungsanweisungen zuständig. Einfache bedingte oder
unbedingte Änderungen lassen sich als einelementige Transaktionen auffassen und
somit gehören sie auch zum Aufgabenfeld des Transaktionsmanagers. Der Transaktionsmanager muss, ebenfalls auf den Fixpunktmanager zugreifen, um bedingte Änderungen auszuwerten. Obwohl der Transaktionsmanager nicht mit realisiert werden soll,
soll er dennoch in der Systemanalyse betrachtet werden, um die Erweiterung möglichst
zu vereinfachen.
Die Datenbankschnittstelle stellt die einzige Schnittstelle zwischen der Benutzerschicht und dem DBMS dar. Sie hat die Aufgabe, die ankommenden Befehle an die
entsprechenden DBMS-Komponenten zu delegieren. Die Datenbankschnittstelle beinhaltet demnach selbst keinen produktiven Code. Dies hat für eine konkrete Implementierung den großen Vorteil, dass die Gefahr von Sicherheitslücken durch falsche Datenübergaben gering ist. Der zweite große Vorteil ist, dass ganz einfach Änderungen
am DBMS durchgeführt werden können, ohne dass Anpassungen an irgendwelchen
Anwendungsprogrammen, wie der GUI, durchgeführt werden müssen. Weiterhin ist es
bei der Entwicklung einer Benutzerschnittstelle nicht nötig, alle Komponenten des
DBMS zu verstehen und zu kennen, da die direkte Kommunikation über die Datenbankschnittstelle geleitet wird.
Das Parsermodul ist das einzige Modul, das die Datenbankschnittstelle umgeht. Es ist
also möglich, direkt Anfragen an dieses Modul aus der Benutzerschicht zu stellen. Da
das Parsermodul aber keinen Zugriff auf die Datenbank hat, hebt es den Vorteil der
Datenbankschnittstelle nicht auf. Der direkte Zugriff ermöglicht aber ein effizienteres
Parsen von Datenbankbefehlen, ohne diese direkt auszuführen. Es bietet den Benutzerschnittstellen vor allem die Möglichkeit auch Teilausdrücke zu prüfen und damit eine
genauere und schnellere Fehleranalyse. Das Benutzerprogramm hat damit die Möglichkeit, den Syntaxfehler genauer zu lokalisieren.
Im Optionsmodul werden alle möglichen Einstellungen eines Datenbanksystems gespeichert und verwaltet. Zum einen vereinfacht dies wieder die Kommunikation zwischen den einzelnen Komponenten, zum anderen vereinfacht dies auch die Speicherung der gesetzten Optionen.
Das Statistikmodul dient schließlich zum dokumentieren und späteren Auswerten von
Informationen über das Datenbankschema und die Datenbank selbst. Diese Informationen werden während entsprechender Operationen direkt vom Schema- bzw. Datenbankmanager im Statistikmodul gespeichert. Weiter sammelt das Statistikmodul noch
Informationen über die letzte durchgeführte (Fixpunkt-)Operation.
Beim Entwurf eines (deduktiven) Datenbanksystems sollte grundsätzlich auch immer
an eine DBMS-Komponente zur Mehrbenutzersynchronisation gedacht werden. Dieses
Modul sollte auch direkt für die Fehlerbehandlung durch synchrone Nutzung zuständig
sein. Dazu muss es Archivkopien und Protokollinformationen speichern, um damit
einem Datenverlust vorzubeugen und Recoveryoperationen durchzuführen.
49
Obwohl die Datenverwaltung bei der Realisierung der vorliegenden Arbeit keinen
großen Teil einnimmt, soll sie kurz betrachtet werden. Denn eine gute Realisierung
dieses Teils erlaubt erst den Umgang mit sehr großen Datenmengen. Auch sollte die
Datenverwaltung eines voll nutzbaren Datenbanksystems möglichst robust gegen externe Fehler sein, um einen (teilweisen) Datenverlust zu verhindern. Dazu gibt es verschiedene Speicher- und Organisationsarten, die maßgeblich nach ihren Zugriffszeiten,
ihrem Organisationsaufwand und dem Platzbedarf der Daten beurteilt werden.
Da die Hauptaufgabe der vorliegenden Arbeit nicht die effiziente Realisierung der Datenverwaltung ist, werden nur die grundsätzlichen Aufgaben dieser DBS-Schicht erläutert, ohne auf spezielle Verfahren genauer einzugehen.
Die Datenbasis eines Datenbanksystems wird im Hintergrundspeicher aufbewahrt und
bei Bedarf teilweise in den Hauptspeicher geladen. Veränderungen durch Datenbankoperationen, werden immer auf den im Hauptspeicher befindlichen Daten durchgeführt. Das Speichersystem des Datenbanksystems übernimmt dabei alle anfallenden
Aufgaben. Dies geschieht in der Regel voll automatisch und ohne direkte Handlungen
des Benutzers. Bei dem, im Rahmen dieser Arbeit erstellten, deduktiven Datenbanksystems ist ein sehr vereinfachtes Speichersystem implementiert worden. Dabei ist die
vereinfachte Annahme gemacht worden, dass die komplette Datenbank zu jeder Zeit in
den Hauptspeicher passt. Das Laden und Speichern von Daten vom Sekundärspeicher
wird daher nur auf direkten Befehl des Benutzers ausgeführt.
Durch das Laden der gesamten Datenbank in den Hauptspeicher sind gute Speicherstrukturen zum Arbeiten auf dem Hintergrundspeicher nicht nötig. Daher wurde im
vorliegenden Programm eine sequenzielle Organisation gewählt. Andere Möglichkeiten wären z.B. Indexstrukturen, B-Bäume oder Hash-Organisationen. Diese oder ähnliche Strukturen bieten sich teilweise auch für die Verwaltung der Daten im Hauptspeicher an [13].
50
Kapitel 5
Implementierung der Datenbankfunktionalitäten
In Kapitel 2 wurden Syntax und Semantik von Datalog ausführlich definiert und besprochen. Im vierten Kapitel wurde darauf basierden die allgemeine Andorderungsanalyse für das DatalogLab bzw. des zugehörigen DBMS erstellt und in diesem Kapitel
wird es um die konkrete Realisierung dieser Konzepte in der Programmiersprache Java
gehen. Abschnitt 5.1 beschäftigt sich mit der Implementierung von Datalog-Regeln
und Fakten. In Abschnitt 5.2 wird dann die Umsetzung der Schemaevolution und in
5.3 die Umsetzung der Datenmanipulation vorgestellt. Abschnitt 5.4 zeigt die Implementierung der FPI-Verfahren und 5.5 erläutert die Umsetzung der Datenbankschnittstelle. Im Abschnitt 5.6 wird die Umsetzung des Anfragemanagers. Abschnitt 5.6 erläutert die zum Teil nur vorbereitende Implementierung des Integritäts- und des Transaktionsmanagers. Im letzten Abschnitt werden schließlich alle weiteren wichtigen
Klassen und Methoden erklärt.
Dabei wird in den einzelnen Abschnitten nicht der genaue Code erklärt, der zur Umsetzung der behandelten Komponenten nötig ist. Meist werden auch nicht alle implementierten Methoden aufgezählt, sondern lediglich die verwendeten Algorithmen erläutert und deren Verwendung begründet und auf Besonderheiten und Vorteile bei der
Realisierung dieser Komponenten durch Java eingegangen. Für eine ausführliche
Klassen- und Methodenbeschreibung sei auf die HTML-Dokumentation des Programms verwiesen. Die HTML-Dokumentation liegt dem zu dieser Arbeit gehörenden
Programm bei.
Grundsätzlich sei noch gesagt, dass beim Großteil der für die Datenbankfunktionalitäten notwendigen Klassen die in Java üblichen Standardmethoden implementiert wurden. Zu diesen gehören Methoden wie die „Getter-“ und „Setter-Methoden“, die für
Zugriffe auf interne Variablen und Teilstrukturen gebraucht werden. Außerdem werden die Methoden equals(…) und compareTo(…) von fast allen Klassen überschrieben, um besonders einfach mit in Java vordefinierten Datenstrukturen umgehen zu
können. Diese Datenstrukturen benutzen die Methoden equals(…) und compareTo(…), um beliebige in ihnen gespeicherte Objekte vergleichen zu können und somit
gesuchte Elemente zu identifizieren.
51
Außerdem musste noch eine Methode equalsStrict(…) implementiert werden, in dieser
wird auf strenge Gleichheit getestet und nicht mehr auf starke Ähnlichkeit wie bei den
implementierten Versionen der equals(…)-Methoden. Die equalsStrict(…)-Methoden
haben für die meisten Klassen den gleichen Aufbau wie die equals(…)-Methoden. Sie
ruft allerdings ihrerseits für alle zu vergleichenden Objekte wieder equalsStrict(…)
auf. Nur für Konstanten und Variablen unterscheiden sich die beiden Methoden stark.
Der Grund dafür liegt in den unterschiedlichen Anforderungen an die VergleichOperatoren für Anfragen und Regeln, dies wird in Abschnitt 5.4.2 noch einmal genau
begründet. Eine weitere Methode, die in den meisten Klassen überschrieben wurde, ist
die toString(…)-Methode. Diese Standardmethode wird von Java-Programmen normalerweise für die Textausgabe nutzen. Und dies ist (soweit möglich) auch in dieser Arbeit geschehen.
Viele der Datenstrukturklassen enthalten außerdem Methoden zum Laden und Speichern der in ihnen enthaltenen Daten in eine Datei im Dateisystem. Diese Methoden
gehören im Gegensatz zu den oben vorgestellten Methoden nicht zu den in Java üblichen Methoden. Die load(…)- und save(…)-Methoden haben immer zwei Parameter.
Der eine gibt die Eingabe- bzw. Ausgabedatei an, der andere Parameter legt den Dateityp fest. Zurzeit sind nur zwei Dateitypen erlaubt. Der erste Typ ist die gewöhnliche
Textdatei, die in der üblichen Datalog-Syntax die Daten als Text ausgibt bzw. einliest.
Der zweite Dateityp ist die Java interne Darstellungsform der Daten, die nur in maschinenlesbarer Form gespeichert wird. Sie setzen damit die in der Anforderungsanalyse verlangten Speicherarten um. Weiter ist, wie in der Anforderungsanalyse gefordert, die Struktur der load(…)- und save(…)-Methoden so aufgebaut, dass sich leicht
weiter Dateitypen hinzufügen lassen. So wäre z.B. auch ein Dateityp zur Anbindung
an gängige Tabellenkalkulationsprogramme einfach zu realisieren.
5.1 Darstellung von Regeln und Fakten in Java
Datalog-Regeln sollen als eigenes Objekt realisiert werden und stellen, wie in der Anforderunganalyse festgelegt den Ausgangspunkt der Entwicklung dar. Der Grund für
die Implementierung als eigene neue Klasse liegt in ihrer großen Bedeutung für deduktive Datenbanken. Das heißt vor allem, dass Regeln und darauf aufbauende bzw. davon abgeleitete Strukturen sehr häufig vorkommen. Daher eignet sich eine eigenständige Realisierung um später von dieser Klasse oder verwendeten Unterklassen Teile
wieder zu verwenden. Weiter können in dieser Klasse auch direkt Methoden zur Regeltransformation programmiert werden.
Eine Regel besteht nach der Definition aus Kapitel 2 aus einem Kopf und einem
Rumpf. Sie muss also aus syntaktischer Sicht nur in der Lage sein, eine Menge von
Literalen effizient zu speichern. Weiter muss eine Regel den semantischen Anforderungen gerecht werden, wie z.B. den Sicherheitsanforderungen im Zusammenhang mit
negativen und funktionalen Literalen. Die Java-Klasse, die eine Regel darstellen soll,
wird mit Rule bezeichnet.
52
Um der Regel möglichst einfach die Fähigkeit zu geben Literale verwalten zu können,
wird eine weitere Klasse RelationalLiteral verwendet. Die Klasse RelationalLiteral hat
die Aufgabe, eine (positive) atomare Formel darzustellen. Damit lässt sich der Regelkopf direkt durch ein RelationalLiteral definieren. Da Datalog verschiedene Arten von
Literalen kennt und diese gemeinsame Eigenschaften haben bzw. Methoden besitzen
sollen, die auf die gleiche Art und Weise aufgerufen werden, ist es sinnvoll, alle Literale von einer abstrakten Basisklasse Literal abzuleiten.
Eine abstrakte Klasse schreibt in Java, ähnlich wie ein Interface, allen von ihr abgeleiteten Klassen vor, bestimmte Methoden zu implementieren. Zusätzlich zu einem Interface kann sie aber auch schon fertig implementierte Methoden bereitstellen, die alle
abgeleiteten Klassen nutzen können. Bei dieser Implementierung kann auch auf nicht
realisierte (abstrakte) Methoden dieser Klasse zugegriffen werden. Daher ist das
Erstellen einer Instanz einer abstrakten Klasse selbst nicht möglich.
Ein Beispiel für eine Methode, die alle Literal-Klassen aufweisen müssen, ist die Methode getSubstitutions(…), die alle möglichen Variablensubstitutionen zu einer gegebenen Faktenmenge zurückgibt. Die Methode getSubstitutions(…) bekommt dafür als
Parameter die gesamte Faktenmenge mitgeliefert. Wie diese Ersetzungen gefunden
werden, ist nun Aufgabe jeder von Literal abgeleiteten Klasse.
Um den Regelrumpf zu modellieren, bestünde die Möglichkeit, die im Regelrumpf
enthaltenen Literale direkt in einer Liste oder ähnlichen Datenstruktur zu speichern,
wie es in vergleichbaren Implementierungen [2] [16] oft geschehen ist. Da aber auch
anderen Datalog-Strukturen Konjunktionen von Literalen enthalten (z.B. in bedingten
Anfragen und Änderungen), ist es sinnvoller für die Literalliste ebenfalls eine neue
Java-Klasse zu erstellen. Diese Klasse erhält den Namen LiteralContainer und hat die
Aufgabe, eine beliebige Anzahl an Objekten vom Typ Literal zu verwalten, d.h. sie
soll in der Lage sein, alle von Literal abgeleiteten Klassen zu speichern und zu verwalten.
Weitere Informationen müssen nicht gespeichert werden, um die Regel-Klasse effizient nutzen zu können. Abzusehen ist vor allem von einer zusätzlichen Regelidentifikationsnummer, da jede Regel eindeutig durch ihren Kopf und ihren Rumpf identifizierbar ist. Eine zusätzliche Regelidentifikationsnummer benötigt nur mehr Speicher
und bedarf eines höheren Verwaltungsaufwands.
Die Regel-Klasse ist nun in der Lage eine syntaktisch korrekte Regel zu speichern.
Weiter muss sichergestellt werden, dass sie auch in der Lage ist, Sicherheitsbedingungen zu prüfen, Regeltransformationen durchzuführen und grundsätzliche Anfragen an
die Regel zu beantworten. Diese und andere wichtige Methoden, die z.B. für die Fixpunktiteration benötigt werden und in der Klasse Rule implementiert wurden, werden
später unter anderem im Abschnitt 5.3 erklärt.
Zunächst soll die Klasse RelationalLiteral betrachtet werden und damit die Darstellungsform von Literalen. Eine atomare Formel besteht laut Definition 2.2 aus einem
53
Prädikatensymbol und einer Menge von Termen, wobei ein Term entweder eine Konstante oder eine Variable ist.
Das Prädikatsymbol ist am einfachsten durch ein String-Objekt zu verwalten. Es wäre
auch möglich, das Prädikatsymbol durch eine eigene Klasse zu beschreiben und beispielsweise Parseraufgaben in dieser Klasse zu realisieren.
Wird die Parserkomponenten der unterschiedlichen Datalog-Strukturen immer als Teil
einer jeweils neuen Klasse implementiert, ergeben sich daraus mehrere Probleme. Zum
einen muss, um dieses Konzept konsequent zu verfolgen, für jeden einzeln zu parsenden Datalog-Ausdruck eine eigene Klasse programmiert werden. Dies muss auch dann
geschehen, wenn zwei Ausdrücke zwar auf die gleiche Art verarbeitet werden, aber in
unterschiedlichen Ausdrücken vorkommen, d.h. die direkte Wiederverwendbarkeit von
gleichem Code wäre stark eingeschränkt, bzw. nur über unsaubere Queraufrufe unter
den grundsätzlich unabhängigen Klassen möglich. Zum anderen hat es auch den Nachteil, dass die Parserkomponente in vielen verschiedenen Klassen verteilt, implementiert ist. Eine spätere Anpassung oder Erweiterung der Syntax hätte zur Folge, dass
eine Änderung sehr vieler Klassen nötig ist. Und dies erhöht den Aufwand für die Erweiterung des Programms stark. Außerdem macht die große Anzahl vieler kleiner
Klassen die Programmstruktur sehr unübersichtlich.
Die Terme der Literale sollen hingegen wieder durch eine bzw. mehrere neue Klassen
realisiert werden. Dabei solle eine Klasse das Verwalten von Konstanten, die andere
das von Variablen übernehmen. Da Variablen und Konstanten beide einen aktuellen
Wert und einen Datentyp haben bzw. haben können, bietet es sich sogar an, eine dritte
abstrakte Klasse Term zu programmieren, die die grundlegende Verwaltung vom aktuellen Wert und dem Datentyp übernimmt. Dadurch ist in den abgeleiteten Klassen Variable und Constant keine doppelte Implementierungsarbeit nötig. Außerdem ist sichergestellt, dass beide Klassen ähnliches Verhalten aufweisen, bzw. die gleichen
Zugriffmethoden implementieren.
Die Speicherung des Datentyps von Variablen und Konstanten widerspricht zwar im
Grundsatz der Typlosigkeit von Datalog, macht aber die Implementierung der BuiltIn-Operationen deutlich einfacher. Um die grundsätzliche Eigenschaft der Typlosigkeit von Datalog nicht zu verlieren, bringt der gespeicherte Datentyp keinerlei Anforderungen an neu zuzuweisende Daten. Es wird hingegen bei jeder Zuweisung zu einer
Variablen der Typ neu überprüft. Damit ist dies nicht bei jedem Zugriff im Rahmen
einer Fixpunktiteration nötig. Und dadurch haben Fixpunktoperationen bei einer Regelmenge, die Built-In-Operationen enthält, eine deutlich schnellere Laufzeit.
Weiter ermöglicht diese Implementierung bei einer Erweiterung sehr viel einfacher,
ggf. doch eine gewisse Typsicherheit zu fordern. Dazu kann die Klasse Termtype, die
zur Speicherung des Datentyps implementiert wurde, einfach genutzt werden um eine
solche Restriktion durchzusetzen. Weiter kann die gewählte Implementierung auch
effizient dazu genutzt werden, um den Typ eines Relationsparameters zu bestimmen
bzw. bei der jetzigen Implementierung aufgrund der aktuell vorhandenen Faktenmenge
zu vermuten. Dies ist besonders bei Basisrelationen relevant, da damit dem Benutzer
54
angezeigt werden kann, welcher Typ erwartet wird. Damit kann er dann weitere Fakten mit den gleichen erwarteten Parametertypen angeben. Somit lassen sich aus Regeln
mit Built-In-Operationen, die auf die neuen Fakten zugreifen, auch wirklich neue Daten ableiten. Die Parametertypangabe kann z.B. sehr gut bei der Visualisierung von
Fakten durch eine Benutzerschnittstelle genutzt werden.
Zurück zur Verwaltung der Konstanten und Variablen:
Obwohl sich beide Klassen recht ähnlich sind und ihre Verwaltung auch mit nur einer
Klasse realisiert werden könnte, gibt es einige Gründe die dagegen sprechen. Ein
Grund Variablen und Konstanten durch zwei verschiedene Klassen zu realisieren liegt
einfach in der Speicherplatzersparnis. Variablen brauchen zusätzlich noch ein Objekt
um den Variablennamen zu speichern, was bei Konstanten nicht nötig ist. Weiter ist
bei der Zwei-Klassen-Lösung der Unterschied zwischen Variablen und Konstanten
direkt am Klassentyp erkennbar. Man benötigt also keine zusätzlichen internen Methoden, oder gar eine zusätzliche Variable, um diese zu unterscheiden.
Um aber auch erweiterte Argumente, wie z.B. Built-In-Terme, als Literalparameter zu
ermöglichen, sollte ähnlich wie bei den Literalen selbst eine abstrakte Oberklasse Argument definiert werden. Von dieser Klasse muss die Klasse Term abgleitet werden.
Die Klasse Argument legt fest, welche Methoden von einem Argument implementiert
werden müssen. Dazu gehören Methoden, die die Zugriffe auf die in einem Argument
enthaltenen Variablen ermöglichen, aber auch Vergleichsmethoden und Methoden, die
das Laden und Speichern der Objekte ermöglichen. Die Vergleichmethoden erlauben
den direkten Vergleich verschiedener Objekte vom Typ Argument.
Die Liste von Termen in der RelationalLiteral-Klasse wird einfach durch den vorgegebenen Datentyp ArrayList dargestellt. Dabei wird diese Container-Klasse nur für
Term-Klassen bzw. deren Unterklassen zugelassen, d.h. bei der Implementierung im
Rahmen dieser Arbeit wurden keine komplexen Argumente (Built-In-Terme) als Literalparameter vorgesehen.
Die Klasse ArrayList ist seit Java 5.0 ein generischer Datentyp. Der Programmierer ist
damit in der Lage, bei generischen Datentypen auch den Datentyp der inneren Objekte
festzulegen. Die korrekte Typdeklaration würde demnach auch ArrayList<Term> lauten. Im Weiteren wird aber immer auf diese exaktere Typdefinition verzichtet, wenn
sie eindeutig aus dem Text hervorgeht. Es ist sogar möglich, auf die genaue Typisierung der ArrayList im Quellcode zu verzichten. Java erwartet dann einfach ein Objekt
vom allgemeinen Typ Object. Da jede Klasse in Java aber automatisch von der Klasse
Object abgeleitet ist, kann man auch jedes beliebige Klassen-Objekt statt eines ObjectObjekts einsetzen. Allerdings bringt die explizite Typangabe deutlich mehr Sicherheit
und Übersichtlichkeit in den Quellcode. Für die genauen Typennamen gibt auch die
HTML-Hilfe zu den implementierten Klassen schnell Auskunft.
Statt des dynamischen Datentyps ArrayList ist es an einigen Stellen möglich direkt mit
festen Arrays zu arbeiten. Um aber in allen Klassen mit den gleichen Datenstrukturen
zu arbeiten und eine spätere Erweiterung so gut wie möglich vorzubereiten, wird auch
an diesen Stellen mit ArrayList-Objekten gearbeitet, da sie z.B. für eine sortierte Liste
55
deutlich besser geeignet sind. Außerdem ist der Performanceverlust sehr gering, da die
Klasse ArrayList intern auf einem festen Array arbeitet und damit Zugriffe ebenfalls in
konstanter Zeit ermöglicht.
Es ist auch möglich, alle vorgestellten Klassen oder zumindest einen großen Teil von
ihnen einzusparen. Das hätte den Vorteil einer deutlich kleineren Datenstruktur. Es
wäre z.B. möglich komplexe Strukturen in einem gemeinsamen String-Objekt speichern. Um aber dann mit diesen Objekten arbeiten zu können, müsste bei jeder Operation das String-Objekt geparst und zerlegt werden. Dieser Aufwand würde die Laufzeit
jeder Operation deutlich erhöhen und ein effizientes Arbeiten unmöglich machen. Die
Darstellung von Daten als String-Objekt ist nur dann effizient, wenn nie (oder nur unter sehr seltenen Vorraussetzungen) bestimmte Teile des gespeicherten Werts einzeln
benötigt werden.
Nachdem der Regelkopf mit all den zusätzlich genutzten Klassen erläutert und motiviert wurde, soll als nächstes der Regelrumpf besprochen werden. Wie bereits oben
erwähnt, ist es besser auch für den Regelrumpf eine eigene Klasse zu implementieren.
Diese Klasse soll LiteralContainer heißen und hat die Aufgabe eine beliebige Menge
von verschieden Literalen zu verwalten. Da für verschiedene Operationen nur bestimmte Literaltypen benötigt werden, bietet es sich an, in der Klasse LiteralContainer
für jeden Literaltyp eine eigene Liste bereitzustellen. Für die Listen sollte dabei wieder
die Klasse ArrayList genutzt werden, da eine dynamische Datenstruktur nötig ist, um
z.B. das Anhängen weiterer Literale effizient zu ermöglichen. Es wird also je eine Liste für positive, eine für negative, eine für Zuweisungsliterale und eine für Built-InLiterale vorgehalten. Dies hat auch noch den Vorteil, dass man nicht zwischen positiven und negativen Literalen in der Klasse RelationalLiteral unterscheiden muss. Es ist
also keine zusätzliche Boolesche Variable für diese Unterscheidung nötig. Auch um zu
prüfen, ob eine gegebene Variable eine positive Bindung hat, bedarf es nur einer
Durchmusterung der positiven Literale und der Zuweisungsliterale. Durch die strikte
Trennung bei der Speicherung spart man dabei jegliche Zugriffe auf negative und
Built-In-Literale ein und benötigt auch keinen zusätzlichen Testaufruf zur Prüfung des
Literaltyps. Dies bringt zur Laufzeit deutliche Performancegewinne, unabhängig davon, ob die Daten unsortiert oder in einer speziellen (sortierten) Datenstruktur gespeichert werden und erfüllt damit die Forderung der schnellen Zugriffe aus der Anforderungsanalyse für die Datenstruktur.
Die positive und negative Literalliste besteht dabei lediglich aus den bereits bekannten
RelationalLiteral-Objekten. Die Liste der Zuweisungsoperationen besteht aus Objekten der Klasse AssignmentLiteral, die Liste der Vergleichsliterale enthält hingegen Objekte der Klasse CompareLiteral. Diese zusätzliche Unterscheidung zwischen normalen Built-In-Literalen und Zuweisungsliteralen liegt an den in Kapitel 2 beschriebenen
unterschiedlichen Eigenschaften dieser beiden Literalarten. Zuweisungsliterale und
Vergleichsliterale gehören zu den Built-In-Literalen und werden deshalb auch gemeinsam im getrennten Paket built_in gebündelt. Dieses Paket soll grundsätzlich alle Klassen enthalten, die nicht zum eigentlichen Datalog-Kern gehören. Einzige Ausnahme
stellt die statische Klasse DatalogParser dar. Diese soll alle Aufgaben zum Parsen
56
abarbeiten, auch jene für die Built-In-Literale. Es wird also keine eigene Klasse zum
Parsen der Built-In-Objekte angelegt.
Der Aufbau des built_in-Pakets soll zum einen eine möglichst leichte Weiterentwicklung um neue Built-In-Funktionen ermöglichen und zum anderen eine möglichst gute
Kapselung der Built-In-Konzepte vom Datalog-Kern erreichen. Dazu sollten alle
Built-In-Literal-Klassen von einer gemeinsamen abstrakten Klasse BuiltInLiteral erben. Alternativ ist es auch möglich alle Built-In-Literale von einem Interface abzuleiten, aber abstrakte Klassen sind immer in der Lage ihren Unterklassen schon voll ausprogrammierte Methoden bereitzustellen und wenn dies möglich ist sind diese vorzuziehen. Die abstrakte Klasse BuiltInLiteral muss ihrerseits aber auch die abstrakte
Klasse Literal beerben; dadurch können in der Klasse LiteralContainer alle Literale
einheitlich behandelt werden. Bei der Implementierung der Klasse LiteralContainer ist
es nicht nötig, den genauen Typ eines Literals zu kennen bzw. zu bestimmen. Diese
Bestimmung kann aber, wenn nötig, jeder Zeit über den genauen Klassennamen und
den Java-Befehl instanceof gemacht werden.
Um ein Zuweisungsliteral zu verwalten, braucht man nur die Variable, der ein Wert
zugewiesen wird und das Argument, das den Wert bereithält, zu speichern. Ein zusätzliches Speichern des Operators ist nicht nötig. Hier zahlt sich nun das Definieren der
abstrakten Klasse Argument aus. Ein Argument für Built-In-Operatoren kann nämlich
auch einfach aus einem Term bestehen. Dafür kann dann direkt die Klasse Term, die
von Argument abgeleitet ist, benutzt werden. Das Verwalten von Konstanten und Variablen geschieht wie bei den Prädikatsliteralen. Zusätzlich sind aber noch einfache
Funktionsterme aus dem Built_in-Paket erlaubt.
Funktionsterme bestehen aus einem Operator (z.B. dem arithmetischen Plus) und zwei
Termen. Geschachtelte Operationen sind, um das Parsen möglichst einfach zu halten,
nicht erlaubt. Außerdem wäre es bei geschachtelten Ausdrücken nötig, jedem Operator
eine Auswertungspriorität zuzuweisen. Die Ausdrücke müssten dann entsprechend
dieser Prioritäten ausgewertet werden (z.B. Multiplikation vor Addition). Um die volle
Ausdrucksstärke zu bekommen, müssten dann auch geklammerte Ausdrücke erlaubt
sein. Dies bedarf einer deutlich komplexeren Auswertungsstrategie als bei den einfachen Ausdrücken.
Für die Repräsentation von Funktionstermen empfiehlt sich wieder eine eigene Klasse.
Die Klasse BinaryTerm, die die Funktionsterme verwalten soll, benötigt ein Objekt
zum Speichern des Operators. Der Einfachheit wegen bietet sich hier ein String-Objekt
an. Für die üblichen arithmetischen Funktionen würde zwar ein einzelnes Zeichen reichen, aber für ggf. komplexere Erweiterungen (z.B. den Modulo-Operator, der meist
mit der Zeichenfolge „mod“ abgekürzt wird) würde ein einzelnes Zeichen nicht mehr
ausreichen.
Alternativ könnte man für komplexere Operatoren auch eigene Operator-Klassen
erstellen. Das würde zwar zu einer etwas besseren Trennung der unterschiedlichen
Operatoren führen und die Klasse BinaryTerm wäre wirklich nur für die Verwaltung
der Objekte zuständig. Zuerst einmal sollen aber nur die arithmetischen Grundoperato57
ren implementiert werden. Diese können in Java meist direkt in einer Zeile implementiert werden. Dafür sollten keine neuen Klassen implementiert werden.
Weiter muss die Klasse BinaryTerm auch die beiden Argumente speichern. Da nur
einfache Terme als Argumente zugelassen sind, benötigt man nur zwei Objekte vom
Typ Term. Damit erhält man insgesamt die in Abbildung 5.1 dargestellt Argumenthierarchie.
Der Aufbau des Vergleichsliterals hat Ähnlichkeit mit den gerade beschriebenen BinaryTerm-Objekten. Es hat aber eine völlig andere Aufgabe als der Funktionsterm. Die
Klasse CompareLiteral wird wieder von der Klasse BuiltInLiteral abgeleitet, aber sie
besteht anderes als die Klasse AssignLiteral aus zwei Argumenten und irgendeinem
Vergleichsoperator. Die Argumente sind vom oben erklärten Argument-Typ. Der Operator wird wieder aus den gleichen Gründen wie bei der BinaryTerm Klasse als StringObjekt gespeichert.
abstract Argument
…
abstract Term
BinaryTerm
value : String;
type : Termtyp;
op : String;
arg1, arg2 : Term
…
…
Constant
…
Variable
identifier : String;
…
Abbildung 5.1: UML-Klassendiagramm zur Argumenthierarchie
Die Auswertung aller Built-In-Literale geschieht, genau wie die Auswertung der anderen Literale, über die in der abstrakten Klasse Literal definierten. Die Implementierung
weicht dabei jedoch stark von der Implementierung in der Klasse RelationalLiteral ab.
Die Literalhierarchie wird noch einmal in der Abbildung 5.2 verdeutlich.
Da für bestimmte Algorithmen die Abarbeitungsreihenfolge im Gegensatz zur grundsätzlichen Datalog-Philosophie nicht beliebig ist, wird in der Klasse LiteralContainer
ein Objekt benötigt, mit dem man die Abarbeitungsreihenfolge der Literale festlegen
kann. Durch diese Abarbeitungsreihenfolge wird der Informationsfluss durch den Re58
gelrumpf festgelegt. Dies wird auch als „sideways information passing strategy“ (kurz
SIPS) bezeichnet. Und wird durch die Klasse SIPS realisiert. Die SIPS-Klasse dient
nur als eine Abbildung der gewünschten Abarbeitungsreihenfolge auf die interne feste
Reihenfolge der Literale. Dies hat den großen Vorteil, dass bei einer Veränderung der
Abarbeitungsreihenfolge nur das interne SIPS-Objekt angepasst werden muss und keine Änderungen an der Datenstruktur selbst erfolgen müssen. Das Kopieren und Verschieben der Daten ist meist mit einem recht hohen Zeitaufwand verbunden und sollte,
wenn möglich, vermieden werden.
abstract Literal
…
RelationalLiteral
abstract BuiltInLiteral
identifier : String;
parameters : Array-
arg2 : Argument;
…
…
AssignmentLiteral
arg1 : Variable;
…
CompareLiteral
arg1 : Argument;
op : String;
…
Abbildung 5.2: UML-Klassendiagramm zur Literalhierarchie
Um einer Regel ein neues SIPS-Objekt zuweisen zu können, muss immer zuerst geprüft werden, ob es sich auch um eine sichere Abarbeitungsreihenfolge handelt. Dies
geschieht vor dem Setzen der übergebenen Reihenfolge in der Methode setSIPS(…).
Das interne SIPS-Objekt der Klasse Rule wird nur dann durch das neue als Parameter
übergeben Objekt ersetzt, wenn der Methodenaufruf isSafeSIPS(…) „Wahr“ zurückgibt. Durch diese Sicherheitsprüfungen, die nur einmal beim Erstellen einer neuen
SIPS zu einer Regel durchgeführt werden müssen, spart man sich später vor jeder FPI
eine ggf. langwierige Überprüfung der gesamten Regelmenge.
Grundsätzlich sei noch gesagt, dass auf alle Attribute aller Klassen von außen nicht
direkt zugegriffen werden kann (so auch nicht auf das zu einer Regel gehörende SIPSObjekt). Einzige Ausnahme stellen Konstanten dar. Diese sind von außen, d.h. von
anderen Klassen, in der Regel direkt nutzbar. Dadurch kann man sicherstellen, dass die
Syntax und die Semantik der repräsentierten Strukturen konsistent bleiben. Dazu müssen alle von außen ausführbaren Methoden der Klasse natürlich immer prüfen, ob die
Anweisung wieder zu einem konsistenten Zustand führt. Eine Überprüfung der Argu59
mente durch die aufrufende Methode ist also nicht nötig. Allerdings muss im Fehlerfall entsprechend reagiert werden. Um möglichst genau den Grund für einen solchen
Fehler weitergeben zu können, wirft die Klasse Rule ein Objekt vom selbst definierten
Typ RuleException zurück. RuleException ist von der allgemeinen Fehlerklasse Exception abgeleitet und kann damit durch den Java-Befehl throw weitergegeben werden.
RuleException bietet dabei die Möglichkeit, weitere Informationen, wie z.B. das auslösende Literal, zu speichern. Diese Informationen können dann für genaue Fehlermeldungen oder sogar Lösungsvorschläge durch eine Benutzerschnittstelle genutzt werden.
Eine weitere Besonderheit der Datenstruktur ist das Erben der Klasse Literal von der
Klasse Argument. Damit ist es z.B. möglich ganze Literale in späteren Versionen als
Argument von entsprechenden Operatoren nutzen zu können. Dadurch ist es leicht
möglich beliebige Literale mit einem Textausdruck zu vergleichen. Der größte Nutzen
liegt aber wohl in der einfachen Verbindung von Built-In-Vergleichsliteralen und den
Built-In-Funktionen, die sich durch diese einfache Maßnahmen nahezu beliebig kombinieren lassen. Allerdings bedarf dies eines deutlich komplexeren Parsers, der auch
rekursive Ausdrücke auswerten kann. Dieser ist aber auch für andere Erweiterung z.B.
für das Umsetzen des Transaktionskonzepts nötig. Denn auch Transaktionen können
selbst wieder Transaktionen enthalten.
Rule
LiteralContainer
head : RelationalLiteral;
body : LiteralContainer;
sips : SIPS;
positivLiterals, negativLiterals :
ArrayList<RelationalList>;
assignLiterals :
ArrayList<AssignmentLiteral>;
compareLiterals : Array-
isSafe(); …
throws
…
RuleException extends Exception
SIPS
unsecureLiteral : RelationalLiteral;
unsecureVariable : Variable;
position : int[];
…
…
Abbildung 5.3: UML-Klassendiagramm zum Regelaufbau
Nachdem Aufbau der Regelstrukturen kann mit der Erstellung der Faktendarstellung
begonnen werden. Die in der Anforderungsanalyse geforderte Faktendarstellung durch
Regeln, ist bereits durch die gewählte Implementierungsform der Datalog-Regeln
durch die Klasse Rule gegeben. Um aber Fakten effizient als eigenständige Objekte zu
verwalten, bedarf es eines neuen Ansatzes. Die erste Idee könnte sein, das Fakt als Objekt vom Typ String zu speichern. Dies hätte den Vorteil, dass man verschiedene Fakten direkt mit der vorgegebenen Methode equals(…) der Klasse String vergleichen
könnte. Dieser Ansatz erfordert aber eine ständiges parsen der Fakten, wenn auf ein60
zelne Werte zugegriffen werden soll. Dies ist aber wie in Kapitel 4 beschrieben, wegen
der schlechten Laufzeit, zu vermeiden.
Auch die Überlegung Fakten einfach als RelationalLiteral-Objekte, wie sie für die Regeln verwendet werden, zu speichern, führt leider nicht zum Ziel, da RelationalLiteral
auch Variablen als Argument haben dürfen. Es wäre also immer, wenn man mit Fakten
arbeiten will, zuerst nötig, das RelationalLiteral darauf zu testen, ob alle Argumente
auch wirklich Konstanten sind. Man hätte folglich wieder eine erhöhte Laufzeit. Und
durch die Möglichkeit von RelationalLiteral-Objekten auch Variablen zu speichern hat
man bei dieser Vorgehensweise ebenfalls einen unnötig erhöhten Speicherbedarf.
Besser ist die Darstellung durch eine neue Klasse Fact. Diese könnte dann das Prädikat in einem String-Objekt speichern und die Konstanten in einer eigenen Liste. Wenn
man nur diese Klasse Fact nutzen würde, hätte dies aber den offensichtlichen Nachteil,
dass man beim Speichern und Verwalten der gesamten Faktenmenge bei jedem Fakt
auch den Namen der zugehörigen Relation speichern muss. Man spart natürlich sehr
viel Speicherplatz, wenn man für jede Relation eine Faktenliste vorbehält, in der dann
die Fakten ohne Relationsnamen gespeichert werden. Da an einigen Stellen aber auch
Fakten mit ihrem Relationsnamen gespeichert werden müssen, ist der beste Weg, beide Möglichkeiten nebeneinander zu implementieren.
Als erstes benötigt man eine Klasse, die eine beliebige Menge von Konstanten verwaltet. Diese Klasse ist mit dem Namen ConstantList implementiert worden. Als Listenstruktur wurde wieder eine ArrayList gewählt. Der Vorteil einer neuen Klasse ist an
dieser Stelle vielleicht nicht direkt offensichtlich, da die grundsätzlichen Eigenschaften
der Klasse ArrayList einfach übernommen werden. Aber in der Klasse ConstantList
können wichtige Methoden implementiert werden, die bei der Verwaltung von Fakten
hilfreich sind und die auch dann benötigt werden, wenn das Fakt unabhängig von seinem Relationsnamen gespeichert wird. Es ist zum Beispiel möglich durch eine geschickte Implementierung der Methode equals(…) Fact- und ConstantList-Objekte
direkt zu vergleichen, ohne sie in einander zu überführen. Dies wäre bei der direkten
Nutzung der Klasse ArrayList nicht möglich.
Der zweite große Vorteil ist, dass man die Fact Klasse direkt von der Klasse
ConstantList ableiten kann. Diese ist damit ohne weiteren Programmieraufwand in der
Lage, Konstanten zu verwalten. In der Klasse Fact ist also das Definieren einer neuen
Liste zum Verwalten der Konstanten nicht nötig.
Zusätzliche Informationen, wie z.B. die Stelligkeit des Fakts, in einer gesonderten Variable der Klasse Fact zu speichern ist unnötig, denn die Stelligkeit entspricht der Anzahl an Konstanten. Daher kann diese Information einfach aus der Länge der Liste gewonnen werden. Zudem sind Zugriffe dieser Art bei der Klasse ArrayList in konstanter
Zeit durchführbar und somit genauso zeiteffizient wie die Nutzung einer zusätzlichen
Variablen. Damit sind alle in der Anforderungsanalyse gestellten Bedingungen erfüllt.
Eine wichtige Eigenschaft von Datalog ist die Duplikatfreiheit der verwalteten Mengen. Besonders gilt dies für die verwaltete Faktenmenge. Um diese Duplikatfreiheit
61
sicherzustellen werden bei allen Operationen auf der Faktenmenge die statischen und
generischen Mengenmanipulationsmethoden aus der Klasse Tools benutzt. Durch ihre
generische Implementierung können sie die duplikatfreie Vereinigung von Menge
(bzw. Listen) unterschiedlicher Typen sicherstellen. Dadurch ist es nicht nötig eine
explizite Duplikateliminierung nach irgendwelchen Operationen anzustoßen. Dies hat
den Vorteil, dass schon während der Laufzeit der Algorithmen die Faktenmenge möglichst klein gehalten wird. Durch das Verwenden von statischen Methoden ist der implementierte Code am besten wieder zu verwenden. Vor allem lassen sich diese Methoden direkt in beliebigen Klassen aufrufen. Dadurch benötigt der Algorithmus meist
keine genaue Kenntnis von der Datenstruktur mehr. Daraus folgt der größte Vorteil.
Dieser liegt allerdings in einer potenziellen Veränderung der internen Datenverwaltung. Sollte z.B. die Speicherung der Fakten zukünftig in sortierten Listen oder einer
völlig anderen Struktur erfolgen, müssen nur diese statischen Methoden der Klasse
Tools angepasst werden und nicht unbedingt der gesamte Code aller realisierten Algorithmen.
5.2 Schemamanager
Der Schemamanager hat die Aufgabe, alle zum Schema gehörenden Informationen zu
verwalten. Dazu gehören, wie in Abschnitt 2.1.3 definiert, die Relationsdeklaration,
die Regeln und die Integritätsbedingungen. Für die Darstellung einer Regel wird dabei
die in Abschnitt 5.1 eingeführte Klasse Rule benutzt. Aber auch für die beiden anderen
Konzepte bieten sich wieder eigene Java-Klassen an. Für die anderen zu speichernden
Daten bedarf es weiterer Hilfsklassen.
Bei der Deklaration von Relationen werden der Name und die Stelligkeit der Relation
angegeben. Zusätzlich muss noch der Relationstyp festgelegt werden. Die Klasse, die
dies realisieren soll, heißt Declaration. Um den Typ zu speichern, empfehlen sich zwei
Boolesche Variablen: eine Variable, um zu speichern, ob die Fakten der Relation abgeleitet (derived) werden und die andere Variable, um zu speichern, ob die Fakten der
Relation gespeichert (stored) werden. So ist die Syntax von Deklarationen wieder gespiegelt, ohne unnötig Speicherplatz durch die Verwendung von String-Objekten zu
blockieren. Da häufig eine eindeutige Identifikation einer Relation benötigt wird, empfiehlt sich eine weitere Klasse RelationID. In dieser Klasse werden der Name und die
Stelligkeit verwaltet, da diese die Relation eindeutig bestimmen. Die Declaration
Klasse enthält also auch ein Objekt vom Typ RelationID und erfüllt damit alle Anforderungen, um eine Relationsdeklaration darzustellen.
Die Klasse RelationID speichert den Namen in einem String-Objekt und die Stelligkeit
als eine ganze Zahl vom Typ int. Die Klasse prüft, wie alle anderen Klassen auch, einfache Sicherheitsanforderungen: z.B. darf die Stelligkeit nie kleiner Null sein. Ebenso
enthält sie einige Methoden zum Anpassen der Attribute. Dies wird zum Teil zur Regeltransformation benötigt.
RelationID-Objekte können von vielen Klassen erzeugt bzw. abgefragt werden. Zu
diesen Klassen gehören Fact, Rule, Declaration, RelationalLiteral und weitere Klas62
sen, die später vorgestellt werden. Durch diese Klassen ist es möglich, effizient und
einfach die equals(…)-Methode zu überschreiben. Diese wird von allen vordefinierten
Listentypen zum Suchen bzw. Identifizieren eines Objekts benutzt. Ohne dieses Überladen der equals(…)-Methode könnten die vordefinierten Datenstrukturen von Java
nicht effizient genutzt werden. Der einfachere Umgang mit den verschiedenen Objekten durch die Einführung der Klasse RelationID wird im Weiteren noch deutlicher.
Eine weitere Aufgabe des Schemamanagers ist die Verwaltung von Integritätsbedingungen: Neben dem bereits in der Anforderungsanalyse genannten Grund zur Realisierung der Integritätsbedingung in einer eigenen Klasse sprechen noch weitere Gründe
dafür. Da eine Integritätsbedingung aus einem positiven oder negativen Grundliteral
besteht und in der gewählten Datenstruktur die Negation von Literalen nicht in der
entsprechenden Klasse gespeichert werden kann und Literale vor allem auch Variablen
enthalten dürfen, kann man eine Integritätsbedingung nicht einfach durch Speichern
eines Literals vom Typ RelationalLiteral realisieren. Weil in Datalog auch nur positive
Fakten gespeichert werden, kann auch nicht Klasse Fact genutzt werden. Es wäre also
nötig, ähnlich wie bei der Speicherung der Literale, mit zwei Listen zu arbeiten und in
einer alle positiven und in der anderen alle negativen Integritätsbedingungen in Form
von Fakten zu speichern. Die andere Möglichkeit besteht darin, eine weitere Klasse zu
benutzten. Diese könnte dann entweder von Fact abgeleitet sein oder würde ein Attribut vom Typ Fact besitzen. Weiter muss die Klasse eine Boolesche Variable enthalten,
die festlegt, ob es sich um eine positive oder negative Integritätsbedingung handelt.
Die Ersparnis der zusätzlichen Booleschen Variablen stellt keinen bedeutenden Entscheidungsgrund dar. Aber da bei der Löschung von Fakten nur positive Integritätsbedingungen verletzt werden können und damit auch nur diese geprüft werden müssen,
spart die geteilte Speicherung wieder Zeit beim Durchmustern der Liste. Für das Einfügen von Fakten in die Datenbank gilt das gleiche Argument für die negativen Integritätsbedingungen.
Ein weiterer Vorteil einer neuen Klasse ist die Tatsache, dass diese sofort eine ordentliche toString()-Methode implementieren kann, die die Ausgabe deutlich vereinfacht.
Auch das Speichern von Integritätsbedingungen in reiner Textform ist damit leichter.
Die beste Lösung ist daher wieder eine Verbindung beider Ansätze. Es wird, wie bereits in der Anforderungsanalyse gefordert, eine neue Klasse Constraint implementiert.
Diese wird von der Klasse Fact abgeleitet und überschreibt einige wichtige Methoden,
erhält aber durch die Vererbung die Verwaltung der Grundliteralen (Fakten). Diese
neue Klasse wird dann in zwei getrennten Listen gespeichert, um auch die oben aufgezählten Vorteile der getrennten Speicherung zu nutzen.
Die Klasse, die den Schemamanager repräsentieren soll, muss also vier Listen bereithalten. Eine Liste, die die Relationsdeklarationen speichert, eine für positive und eine
für negative Integritätsbedingungen und eine für die Regeln. Bei der Speicherung der
Regeln ist es allerdings äußerst ineffizient alle Regeln unstrukturiert in einer Liste zu
speichern. Effizienter ist es, alle Regeln, die zu einer Relation gehören, auch gemeinsam zu speichern, da diese auch meist alle zusammen benötigt werden. Dazu ist es am
einfachsten, wieder eine Klasse RelationRules zu erstellen. In ihr wird eine Liste aller
63
zu einer Relation gehörenden Regeln gespeichert. Weiter enthält die Klasse RelationRules ein Objekt vom Typ RelationID, um direkt feststellen zu können, zu welcher
Relation die interne Liste gehört. Dieses RelationID-Objekt kann auch nicht eingespart
werden, da ja zu Beginn keine Regeln existieren und somit die zugehörige Relation
nicht durch die internen Regeln bestimmt werden kann. Zusätzlich können in der Klasse RelationRules effizient Methoden, wie z.B. dependsOn(…), implementiert werden.
Die dependsOn(…)-Methode aus der Klasse RelationRules gibt alle Relationen, auf
die sich die zugehörige Relation direkt bezieht, zurück. Dabei muss die Methode auf
alle definierten Regeln dieser Relation zugreifen können.
Zusammenfassend lässt sich also sagen, dass die Klasse Schemamanager die Möglichkeit bietet, Deklarationen, Regeln und Integritätsbedingungen einzufügen und zu löschen. Durch diese Operationen werden nötige Aktualisierungen anderer Daten und
die Überprüfung jeglicher Sicherheitsbedingungen automatisch durchgeführt. Zu den
Sicherheitsbedingungen zählen die grundsätzlichen semantischen Bedingungen, die
durch Datalog vorgegeben werden.
Wird z.B. eine Relationsdeklaration gelöscht, werden auch alle Fakten und Regeln
dieser Relation gelöscht. Regeln anderer Relationen, die auf die gerade gelöschte
zugreifen, bleiben allerdings unberührt. Beim Einfügen würde nur überprüft, ob nicht
schon eine Deklaration mit dem gleichen RelationID-Objekt, also mit gleichem Namen und gleicher Stelligkeit, existiert.
Weiter bietet die Klasse Schemamanager eine Methode zum Testen von Regeln an.
Sie gibt „Wahr“ zurück, wenn alle in ihr definierten Literale nur auf Relationen zugreifen, die im Schema enthalten sind. Sonst gibt sie „Falsch“ zurück.
Eine Besonderheit weißt der Schemamanager noch auf: Er lässt nicht das Erstellen
neuer Instanzen von sich selbst zu. Andere Klassen können also keine neuen Objekte
vom Typ Schemamanager anlegen; sie können nur über eine statische Methode eine
Instanz dieser Klasse abfragen. Klassen mit dieser Eigenschaft werden auch als Singleton bezeichnet. Dieses Verhalten stellt sicher, dass es von einer Klasse nur eine Ausprägung gibt. Der Sinn dieses Verhaltens ist, dass es nicht mehrere SchemamanagerInstanzen mit ggf. unterschiedlichem Inhalt über die gleiche Datenbank geben darf.
Weiter kann so sehr einfach sichergestellt werden, dass keine alten Instanzen unnötig
Speicher verbrauchen.
Die Klasse Datenbankmanager ist ebenso als Singleton realisiert. An ihr wird der
zweite Grund für die Realisierung einer Klasse als Singleton deutlicher: Würde versucht werden, die gesamte Faktenmenge einer Datenbank mehrfach, oder verschiedene
Faktenmengen von verschiedenen Datenbanken im Hauptspeicher zu hinterlegen, kann
dies sehr schnell zu einem Hauptspeicherüberlauf führen. Die Folge wären wieder unnötige, teure Zugriffe auf den Hintergrundspeicher. Diese Zugriffe wären dann entweder durch das Betriebssystem zu realisieren (Auslagerungsdatei des Hauptspeichers)
oder aber durch das DBMS selbst. Die Auslagerung über das DBMS kann in der Regel
deutlich effizienter ausgeführt werden, da der Programmierer Kenntnis über die internen Datenstrukturen hat und diese effizient auslagern kann. Diese Probleme könnten
64
auch schon bei kleinen Beispieldatenbanken, für die das zugehörige Programm entwickelt wurde, auftreten.
Weiter müssen im Schemamanager neben den zusätzlichen Methoden für statistische
Informationen über die Datenbank auch die bereits oben erwähnten Standardmethoden
(z.B. „Getter“ und „Setter“ oder Methoden zum Laden und Speichern) implementiert
werden. Zu den Methoden für die statistischen Informationen gehören z.B. getNumberOfRules(), getNumberOfConstraints() und getNumberOfRelations(), die je die Anzahl
der Regeln, der Integritätsbedingungen und der Relationen zurückgeben.
Eine der Standardmethoden (die save(…)-Methode zum Speichern von Daten) soll nun
an dieser Stelle auch etwas ausführlicher erklärt werden. Damit soll auch direkt die
Delegation von Aufgaben an die Unterklassen erläutert und verdeutlicht werden. Es
soll dabei aber auch die gewählte Speicherart für Datalog-Konstrukte, hier speziell für
die Schemadaten, motiviert und erklärt werden.
Ist als Ausgabeformat die gewöhnliche Textform gewählt, speichert die save(…)Methode einfach jedes Element der verschiedenen Listen. Dazu geht die save(…)Methode die Listen sequenziell durch und ruft für jedes enthaltene Objekt wieder die
zugehörige save(…)-Methode mit den gleichen Parametern auf. Für jedes Objekt vom
Typ Declaration aus der Deklarationsliste wird z.B. die save(…)-Methode aus der
Klasse Declaration aufgerufen. Diese schreibt in Datalog-Syntax die gesamte Deklaration als Text in die als Parameter übergebene Datei. Dabei nutzt sie ihre überlagerte
toString()-Methode, die eine entsprechend formatierte Ausgabe zurückgibt. Für die
Basisrelation „edge“ mit der Stelligkeit zwei ist dann das Ergebnis „stored edge / 2.“.
Ist als Ausgabeformat allerdings die interne Datalog-Darstellungsform gewählt (der
Dateityp lautet JAVA), schreibt die save(…)-Methode erst die Länge der Listen in die
übergebene Datei und dann wird wieder für jedes Listenobjekt die eigene save(…)Methode aufgerufen und damit gespeichert. Ist das gerade zu speichernde Listenobjekt
z.B. eine Liste der zu einer Relation gehörenden Regeln, so wird die save(…)Methode der Klasse RelationRules aufgerufen. Diese speichert dann wieder erst die
Anzahl der in ihr enthaltenen Regeln und ruft dann für jede Regel die Methode Rule.save(…) auf. Diese delegiert die Aufgabe wieder an die save(…)-Methoden von
RelationalLiteral und von LiteralContainer, um ihren Kopf bzw. Rumpf zu speichern.
Diese Methoden speichern dann zum Teil wieder zusätzliche Informationen und nutzen weitere save(…)-Methoden oder speichern die Informationen direkt. Bei der direkten Speicherung werden die Java-eigenen Ausgabemethoden für die verschiedenen
Java-Objekte benutzt. Dazu zählen z.B. writeInt(…) für ganze Zahlen oder writeUTF(…) für String-Objekte.
Durch das oben beschriebene Durchreichen der Aufgaben wird eine möglichst gute
Kapselung erreicht. Sollte also eine der Klassen angepasst oder optimiert werden,
können alle anderen Klassen diese direkt weiter nutzen, ohne das eine Veränderung an
ihnen nötig ist. Das Delegieren von Teilaufgaben wird auch bei vielen anderen Methoden durchgeführt, z.B. bei der oben erwähnten toString()-Methode.
65
Die Implementierung der load(…)-Methoden findet analog zum Speichern statt. Bei
der internen Darstellung ist allerdings darauf zu achten, dass die Daten in der gleichen
Reihenfolge bearbeitet werden wie beim Speichern. Dadurch ist es möglich, ohne Datenanalyse (z.B. das Parsen von Texten bei der Textform) die Informationen direkt
durch ein Objekt der Datenstruktur einzulesen und mit den Werten aus der Datei zu
füllen. Dafür sind in den Klassen spezielle Konstruktoren vorhanden, welche wie die
load(…)-Methode als Parameter eine Eingabedatei und den Dateityp übergeben bekommen. Intern ruft dieser Konstruktor einfach die load(…)-Methode selbst auf. Wird
versucht eine Datenbank mit Daten aus einer Textdatei zufüllen, werden die ggf. erfolglos eingelesenen Daten in einer Protokolldatei mit möglichen Fehlerhinweisen gespeichert.
5.3 Datenbankmanager
Der Datenbankmanager hat als Aufgabe den Datenbestand, also die Faktenmenge, zu
verwalten. Dabei muss sichergestellt werden, dass der mengenorientierte Charakter
von Datalog umgesetzt wird und keine doppelten Fakten gespeichert werden. Die Datenspeicherung, speziell die Möglichkeiten die Datenbankfakten auf verschiedenen
Speichermedien aufzuteilen, sollte nicht oder nur sehr rudimentär im Rahmen dieser
Arbeit implementiert werden. Aus diesem Grund wurden die verbliebenen Datenverwaltungsaufgaben an den Schema- bzw. den Datenbankmanager angehängt. Nur einen
Teil der Datenbankfakten in den Hauptspeicher zu laden und somit auch sehr große
Datenmengen bearbeiten zu können, ist nicht möglich.
Um das Verwalten der Fakten effizient zu implementieren, muss vorher klar sein, wie
diese gespeichert werden soll. Aus dem gleichen Grund wie bei den Regeln empfiehlt
sich die Speicherung der Fakten in zu den Relationen gehörenden Gruppen. Dazu wird
die Klasse RelationFacts erschaffen. Zusätzlich hat dies noch den bereits in Abschnitt
5.1 vorgestellten Vorteil, dass nicht zu jedem Fakt der Relationsbezeichner mitgespeichert wird. Durch die Klasse RelationFacts wird die Faktensuche beschleunigt und der
Speicherverbrauch minimiert. In der Klasse RelationFacts wird wieder ein Objekt vom
Typ RelationID gespeichert und eine Liste von ConstantList-Objekten. Durch diese
gebündelte Verwaltung der zusammengehörenden Daten ist es möglich, den Datenbankmanager so zu erweitern, dass er nur den gerade verwendeten Teil der Daten in
den Primärspeicher lädt. Dafür ist keine Änderung an der Datenstruktur vorzunehmen.
Der Datenbankmanager ermöglicht, teilweise durch die Delegation der Aufgaben an
die richtige RelationFacts-Instanz, das Einfügen, das Löschen und das Aktualisieren
von Fakten. Dies geschieht durch die Methoden addFact(…), removeFact(…) und
updateFact(…). Die beiden ersten erhalten als Parameter ein Fact-Objekt. Beim Einfügen wird vorher geprüft, ob es sich bei der entsprechenden Relation überhaupt um
eine Basisrelation handelt und ein solcher Zugriff erlaubt ist. Eigentlich müsste auch
vor der Löschoperation diese Prüfung vorgenommen werden. Aber um mit Hilfe einer
Benutzerschnittstelle auch materialisierte Faktenmengen nach Löschungen von Basisfakten von Hand wieder in einen konsistenten Zustand zu bringen, bleibt diese Operation für alle Relationen erlaubt. Die Einfügungen in die materialisierte Faktenmenge
66
können effizient durch eine erneute Fixpunktiteration bestimmt werden und sollten
daher nicht durch den Benutzer durchgeführt werden können.
Die updateFact(…)-Methode gehört zwar eigentlich nicht zum Datalog-Standard und
kann selbstverständlich durch eine Löschung und eine Einfügung simuliert werden, ist
aber deutlich effizienter und trägt vor allem zu einer benutzerfreundlicheren Faktenverwaltung durch ein Anwendungsprogramm bei.
Alle drei Methoden geben einen Booleschen Wert zurück. Dieser Rückgabewert ist
„Wahr“, wenn die Operation erfolgreich durchgeführt wurde, und sonst „Falsch“. Diese zusätzliche Ergebnisrückgabe wird bei vielen Methoden angeboten. Wichtig ist,
dass in beiden Fällen die Operation durchaus ohne Fehler beendet worden sein kann.
Syntax oder Semantikfehler werden durch die Methoden mit Java-Exceptions an die
aufrufende Methode weitergegeben werden. Mit „erfolgreich“ ist im Fall des Hinzufügens gemeint, dass sich am Ende wirklich ein neues Fakt in der Faktenmenge befindet.
Dieser Test wird beispielsweise gebraucht, um festzustellen, ob bei der aktuellen Fixpunktiteration wirklich neue Fakten hinzugefügt wurden und sich damit die Faktenmenge geändert hat.
Der Sinn dieses Rückgabewerts wird beim Löschen eines Fakt noch deutlicher. Versucht der Anwender z.B. das Fakt „Läufer“ zu löschen, würde auch dann kein Fehler
auftreten, wenn vor dem Aufruf der removeFact(…)-Methode gar kein solches Fakt
existiert hat. Durch den zusätzlichen Rückgabewert ist die Benutzerschnittstelle aber
in der Lage, dem Anwender den Hinweis zu geben, dass kein Fakt entfernt wurde. Der
Benutzer kann diese Information dann dafür nutzen, um z.B. die Rechtschreibung seiner Eingabe zu prüfen und anschließend das Fakt „Laeufer“ löschen. Dieser Eingabefehler wäre dem Anwender sonst vermutlich nicht sofort aufgefallen.
Weiter existieren noch einige Methoden, die Mengen von Fakten hinzufügen oder löschen können. Dazu können die Fakten in den verschiedenen, oben vorgestellten Darstellungsformen als Parameter übergeben werden. Es gibt beispielsweise auch Methoden, die ein ConstantList-Objekt und ein RelationID-Objekt als Parameter erwarten.
Für eine genaue Methodenübersicht und deren Parameterstruktur sei wieder auf die
HTML-Dokumentation verwiesen.
Zusätzlich weist auch die Databasemanger-Klasse, wie die Klasse Schemamanager,
einige Methoden auf, die statistische Informationen zurückgeben. Dazu zählt z.B. die
Anzahl aller Fakten in der Datenbank oder auch die Anzahl der Basisfakten.
5.4 FPI-Verfahren
In diesem Abschnitt sollen die im Rahmen dieser Arbeit implementierten Algorithmen
zur Fixpunktbestimmung vorgestellt werden. Es wurden nur solche Fixpunktverfahren
implementiert, die maximal stratifizierbare Regelmengen auswerten können. Da die
Algorithmen und die meisten zugehörigen Methoden komplett in der Klasse Fix67
pointManager gekapselt werden konnten, ist eine Erweiterung unter Nutzung der bereits implementierten Methoden leicht möglich.
Für alle implementierten Verfahren existieren immer zwei Methoden. Die erste Methode hat die Aufgabe, für die gesamte Regelmenge einer übergebenen Datenbank die
Bedeutung zu bestimmen. Die zweite Methode verlangt zusätzlich noch eine Regelmenge, für die die Fixpunktiteration durchgeführt werden soll. Die erste Methode delegiert die Arbeit aber intern an die zweite Methode weiter, indem sie einfach die gesamte Regelmenge als Parameter angibt. In den weiteren Unterabschnitten wird nur
die zweite Methode mit zusätzlichem Parameter ausführlich erklärt. Alle Methoden
der Klasse FixpointManager sind statische Methoden. Es ist also nicht nötig, sie über
eine spezielle Instanz der Klasse aufzurufen. Dafür muss man sie jedes Mal mit der zu
betrachtenden Datenbank als Parameter aufrufen. Die Ergebnisse der Fixpunktiteration
werden auch direkt in der übergebenen Datenbank gespeichert. Durch die spezielle
Realisierung der Datenbankmanager als Singleton-Klassen können aber nicht mehrere
Instanzen im Speicher unnötig Platz verbrauchen, wodurch die Übergabe als Parameter keine Effizienzprobleme bedeutet.
Zusätzlich zu den speziellen Fixpunktmethoden, die direkt einen der in Kapitel 3 vorgestellten Algorithmen implementiert, existiert die Methode runFPI(…). Dies ist die
einzige Methode, die von außen direkt aufgerufen werden kann (öffentliche Methode).
Alle anderen sind nach außen nicht sichtbar. Durch die Methode runFPI(…) kann aber
gesteuert werden, welcher der implementierten Algorithmen verwendet werden soll.
Dazu gibt es den Parameter FPIType und passende Konstanten in der Klasse Constant.
Die Klasse Constant verwaltet alle im Rahmen dieser Arbeit definierten öffentlichen
Konstanten. Für eine genauere Erklärung der Klasse sei auf Abschnitt 5.4.4 verwiesen.
Statt explizit einen Algorithmus durch einen gegebenen Parameter festzulegen, kann
man die Methode auch anweisen, die eingestellten Optionen der Datenbank auszulesen
und darüber den zu wählenden Algorithmus bestimmen lassen. Zusätzlich werden
noch einige andere Operationen entsprechend der eingestellten Optionen durchgeführt.
So werden z.B. wahlweise alle abgeleiteten Fakten vor der Fixpunktiteration gelöscht.
Dadurch müssen solche allgemeinen Einstellungen nicht in jeder einzelnen Fixpunktmethode überprüft und ggf. ausgeführt werden.
Die Duplikateliminierung geschieht immer direkt während der unterschiedlichen Operationen mit den in Abschnitt 5.1 vorgestellten Methoden der Klasse Tools.
5.4.1 Naive Fixpunktberechnung
Die naive Fixpunktberechnung zeichnet sich durch ihren sehr simplen und kurzen Algorithmus aus. Der in Kapitel 3.1.1 vorgestellte Pseudocode ist direkt in Java übertragbar. Der große Nachteil ist die schlechte Laufzeit der Methode. Realisiert wird die
naive Fixpunktberechnung in der Methode naiveFPI(…) mit den oben bereits erwähnten Parametern. Besonders interessant ist die aufgerufene Methode zum Finden der
neuen Fakten (computeNewFacts(…)). Sie wird für alle implementierten Fixpunktberechnungen benötigt. Als Parameter erhält sie zwei Argumente. Das erste Argument ist
68
die Regel, die aktuell betrachtet werden soll. Das zweite Argument ist die gesamte
(positive) Faktenmenge, die als Auswertungsgrundlage dient.
Als erstes muss die computeNewFacts(…)-Methode sicherstellen, dass keine alten Variablenbindungen von zurückliegenden Operationen mehr existieren. Die Bindung einer Variablen wird intern nur durch die Zuweisung eines Wertes vollzogen. Daher
müssen zu Beginn alle alten Variablenwerte gelöscht werden.
Der eigentliche Auswertungsprozess wird dann durch Aufruf der Methode findRuleBodySubs(…) gestartet. Als Parameter erhält sie neben der Faktenmenge und dem zu
untersuchenden Regelrumpf auch einen Parameter substitions, über den die Rückgabe
der Antwort in Form aller möglicher Variablensubstitutionen erfolgt. Der letzte Parameter der Methode findRuleBodySubs(…) gibt die Position des aktuell zu untersuchenden Literals an. Dieser Parameter ist nötig, da es sich um eine rekursive Methode
handelt.
Für das angegebene Literal des Regelrumpfs werden nun alle möglichen Substitutionen bestimmt. Dazu wird die Methode getSubstitutions(…) aufgerufen, die jedes Literal implementiert hat. Dies wird durch die Ableitung aller Literaltypen von der abstrakten Klasse Literal sichergestellt. Der erste Parameter beinhaltet die zugrunde liegende Faktenmenge. Der zweite Parameter dient zur Unterscheidung von positiven
und negativen Literalen, wenn es sich um ein Objekt vom Typ RelationalLiteral handelt. Für alle anderen Literaltypen kann zwar beim Aufruf der Methode auf den Booleschen Parameter verzichtet werden, allerdings ist die Implementierung trotzdem mit
Booleschem Parameter nötig. Die kürzere Aufrufmöglichkeit wird durch eine weitere
getSubstitutions(…)-Methode ermöglicht, die die eigentliche Methode immer mit dem
Parameter true aufruft. Das Ergebnis der Methode ist die Menge aller möglichen Literale mit unterschiedlich gebundenen Variablen. Bei den neuen Bindungen werden natürlich alte Bindungen nicht überschrieben, sondern auf Erfüllbarkeit getestet und ggf.
wird die aktuelle Substitution komplett verworfen. Ungebundene Variablen können
dabei am Ende nicht mehr auftreten. Eine genaue Beschreibung der Methode getSubstitutions(…) aus der Klasse RelationalLiteral erfolgt in Abschnitt 5.4.4.
Für jedes der gefundenen Literale wird dann ein neuer Regelrumpf mit den entsprechenden Variablenbindungen erstellt und die Methode findRuleBodySubs(…) mit den
ggf. bisher gefundenen Substitutionen und dem neuen Regelrumpf aufgerufen. Die
Position des zu untersuchenden Literals wird dabei um eins erhöht.
Die Rekursion terminiert, wenn die Position des zu untersuchenden Literals größer
gleich (da die Indizierung bei Java mit der Null beginnt) der Anzahl an Literalen ist.
Dann werden alle gefunden Substitutionen dem Parameter substitutions hinzugefügt
und stehen damit der aufrufenden Methode zur Verfügung (spezielle Art des Call-byReferenz Prinzips in Java).
Die computeNewFacts(…)-Methode kann nun alle gefunden Variablensubstitutionen
dafür nutzen, den Variablen im Regelkopf die entsprechenden Werte zuzuweisen und
somit neue Fakten generieren.
69
Da die Literalreihenfolge intern durch die Klasse LiteralContainer entsprechend der
gespeicherten SIPS zurückgegeben wird, müssen die Methoden der Fixpunktiteration
keine besondere Reihenfolge beachten und können immer zum „nächsten“ Literal
springen.
In der Methode naiveFPI(…) wird für jede Regel der übergebenen Regelmenge nun
die Methode computeNewFacts(…) aufgerufen und die Resultate temporär gespeichert. Am Ende wird die temporäre neue Faktenmenge zur Datenbank hinzugefügt.
Wenn mindestens ein neues Fakt in die Datenbank aufgenommen wurde, liefert die
entsprechende Methode „Wahr“ zurück. Anschließend beginnt der Algorithmus mit
der aktualisierten Faktenmenge von vorne. Wurde kein wirklich neues Fakt generiert,
terminiert der Algorithmus.
Neben dem nötigen Code, um die in Kapitel 3 beschriebenen Algorithmen zu realisieren, befinden sich im Code der unterschiedlichen Methoden noch zusätzliche Anweisungen. Es wird beispielsweise mitgezählt, wie viele unnötige Fakten generiert wurden, obwohl sie schon in der Datenbank oder der neuen Faktenmenge enthalten waren.
Diese Informationen werden dann über ein spezielles Objekt der Klasse Statistic verwaltet. Die genaue Aufgabe der Klasse Statistic und deren Funktionsweise werden in
Abschnitt 5.4 erläutert. Sie spielt keine Rolle für die Fixpunktberechnung, sondern
speichert, nur beliebige statistische Informationen ab. Die Klasse Statistic wird auch
bei den anderen Methoden benutzt, um statistische Werte zu sammeln.
5.4.2 Semi-naive Fixpunktberechnung
Die semi-naive Fixpunktberechnung ist in der Methode semiNaiveFPI (…) realisiert.
Die Iteration unterscheidet sich im Prinzip kaum von der naiven Fixpunktiteration.
Allerdings werden zum einen nur von einer speziellen Teilmenge der Regeln neue
Fakten abgeleitet, zum anderen müssen die neuen Fakten auch immer in die ursprünglichen Relationen umkopiert werden. Das Besondere an der semi-naiven Fixpunktberechnung ist vor allem die im Vorfeld durchzuführende Regeltransformation und das
Zurückkopieren der Ergebnisfakten in die eigentlichen Relationen.
Bei der Regeltransformation wird zu jeder regelbasierten Relation von der mindestens
eine Regel in der angegebenen Regelmenge enthalten ist, eine neue korrespondierende
Deltarelation in der Datenbank angelegt. Diese werden temporär in einem ArrayList<RelationID>-Objekt gespeichert, um während der Fixpunktiteration schneller auf
sie zugreifen zu können. Dann werden zu jeder Regel aus der zu untersuchenden Regelmenge alle nötigen Deltaregeln erstellt und in die Datenbank aufgenommen. Dazu
wird die Methode createDeltaRules(…) aufgerufen. Sie hat zwei Parameter: zum einen die zu transformierende Regel, zum anderen die Menge aller relevanten Deltarelationen.
Die Methode createDeltaRules(…) erstellt entsprechend der Definition 3.1 die neuen
Deltaregeln. Dazu wird eine Kopie der ursprünglichen Regel angelegt. Der Kopf der
neuen Regel erhält den Wert der Konstanten DELTA als Präfix. Die Konstante DEL70
TA wird in der Klasse Constants definiert und enthält den String „dELTA“ (Der
Kleinbuchstabe am Anfang der Konstanten ist nötig, da es sich sonst nach der Transformation nicht mehr um einen korrekten Regelbezeichner handeln würde).
Anschließend wird für jedes positive Literal der Regel überprüft, ob eine entsprechende Deltarelation in der übergebenen Deltarelationsliste enthalten ist. Wird eine solche
Relation gefunden, wird wieder eine Kopie der temporären Regel angelegt, bei der das
aktuell untersuchte Literal ebenfall mit dem Präfix DELTA versehen wird. Die neue
Regel wird dann in die Menge der transformierten Antwortregeln eingefügt. Sollte im
Laufe der gesamten Iteration keine einzige Regel in die Ergebnismenge eingefügt
worden sein, so wird am Ende der Methode nur die erste Kopie, die nur einen veränderten Kopf aufweist, eingefügt. Die Vorgehensweise kann an der Beispieldatenbank
„bsp3_2“ des DatalogLabs und des zugehörigen Beispiels 3.2 in dieser Arbeit nachvollzogen werden.
Dann beginnt der eigentliche Iterationsprozess in der semiNaiveFPI (…)-Methode. Die
Vorgehensweise ist genau die Gleiche wie in Abschnitt 3.1.2 erklärt. Allerdings kommen auch hier, wie bei der naiven FPI, noch einige Anweisungen zur Sammlung statistischer Daten hinzu. Nachdem der Iterationsprozess abgeschlossen ist, werden entsprechend der eingestellten Datenbankoptionen noch die erzeugten Deltarelationen und die
zugehörigen Regeln gelöscht oder in der Datenbank belassen. Dies bietet den Vorteil,
dass bei einer erneuten semi-naiven FPI nicht wieder alle Deltaregeln erschaffen werden müssen. Es birgt aber auch die Gefahr, dass wenn zwischen beiden Fixpunktiterationen Regeln geändert wurden, falsche Deltaregeln im Schema verbleiben. Ebenso
können falsche Fakten in den alten Deltarelationen gespeichert sein, die dann zu einem
falschen Ergebnis der neue FPI führen. Zur Veranschaulichung kann das oben genannte Beispiel 3.2 verwendet werden, indem einfach einige Fakten in die Datenbank eingegeben werden und anschließend die Fixpunktiteration gestartet wird und das Ergebnis betrachtet wird. Alternativ kann das Verhalten auch mit Beispiel 3.3 und der zugehörigen Testdatenbank „bsp3_3“ genau nachvollzogen werden.
5.4.3 Iterierte Fixpunktberechnung
Wie bereits in Kapitel 3 erwähnt, ist die iterierte Fixpunktberechnung in der Lage, jegliche stratifizierbare Regelmengen auszuwerten. Dazu benötigt sie aber eine Stratifikation der gegebenen Datenbank bzw. der Regelmenge. Eine solche Stratifikation kann
durch den Methodenaufruf stratify() aus der Klasse DatabaseAccess neu erstellt werden. Mit getStratification() erhält man die letzte berechnete Stratifikation. Um ein
Stratification-Objekt zu erhalten, bedarf es des Abhängigkeitsgraphs der Datenbank.
Dieser wird durch ein Objekt vom Typ DependencyGraph dargestellt. Man könnte
zwar auch direkt aus der Datenbank eine Stratifikation herleiten, aber dazu müssten
immer viel größere Objekte im Speicher durchmustert werden, was die Laufzeit der
Stratifikationsbildungen verschlechtern würde. Dazu kann der Abhängigkeitsgraph bei
Schemaänderungen einfach konsistent gehalten werden und muss nicht jedes Mal neu
berechnet werden. Dies geschieht bei der jetzigen Implementierung aber nicht. Außer71
dem kann bei einer Erweiterung mit Hilfe des DependencyGraph-Objekts der Abhängigkeitsgraph ebenfalls schneller berechnet und gezeichnet werden.
Im Abhängigkeitsgraph werden als Knoten RelationID-Objekte gespeichert. Die Kanten geben immer den Start- und den Zielknoten, sowie die Art der Verbindung an.
Knoten können entweder positiv oder negativ miteinander verbunden sein. Der Abhängigkeitsgraph wird einfach durch sequentielles Durchlaufen der Regelmenge erstellt. Dabei wird für jede neue Deklaration ein neuer Knoten angelegt und für jede
neue Kopf-Rumpfliteral-Kombination eine neue Kante. Die Kante wird mit „Wahr“
markiert, wenn es sich um ein positives Rumpfliteral handelt, sonst mit „Falsch“.
Das aus dem Abhängigkeitsgraphen erstellte Stratification-Objekt beinhaltet zum einen eine Boolesche Variable, die angibt, ob die Datenbank überhaupt stratifizierbar ist.
Zum anderen enthält sie eine Liste mit einer Liste von RelationID-Objekten. Die äußere Liste stellt dabei die Liste der Strata dar. Um nun eine Stratifikation zu bilden, werden zuerst alle Relationen in die Schicht Null eingeordnet. Dann wird für jede positive
Kante geprüft, ob sich die Relation des ausgehenden Knoten mindestens in der gleichen Schicht befindet wie die Relation des Zielknotens. Wenn das nicht der Fall ist,
wird die Relation des Startknotens in die gleiche Schicht wie die Relation des Zielknotens gesetzt. Für negative Knoten wird ähnlich verfahren, allerdings muss sich hier der
Startknoten mindestens eine Schicht über dem Zielknoten befinden. Sollten nun zwei
Relation wechselseitig negativ von einander abhängen, würde die einzuteilende
Schicht immer wieder um eins erhöht. Da aber eine stratifizierbare Datenbank nicht
mehr Strata als Relationen aufweisen kann, bricht der Prozess ab, sobald eine Relation
in ein Stratum mit einer höheren Nummer als der Anzahl der Relationen eingeteilt
werden soll. Der Algorithmus liefert dann den Wert „Falsch“ zurück, da die Datenbank nichtstratifizierbar ist. Ansonsten wird die Prüfung der Kanten solange wiederholt, bis keine Veränderung mehr an der Schichtenzuteilung vorgenommen wurde.
Um die Methode iteratedFPI(…) aufzurufen, muss die zu betrachtende Regelmenge
nicht als normale Regelliste übergeben werden, sondern als Stratification-Objekt. Der
Algorithmus ruft dann für jede Schicht die semi-naive Fixpunktberechnung auf. Dazu
bestimmt er alle zur aktuellen Schicht gehörenden Regeln und übergibt sie an die Methode seminaiveFPI(…). Allerdings muss vorher die Option zum Löschen der Deltarelationen gesetzt sein, welches die Methode dann automatisch macht. Würde diese nicht
nach jedem inneren Iterationsprozess gelöscht werden, würde sich die semi-naive Fixpunktberechnung auf eine zu große Menge von Deltarelationen beziehen, was zu einer
schlechteren Laufzeit führen kann. Durch die zu große Menge an Deltarelationen würde der Algorithmus nicht mehr bestimmen können, welche Regeln nur in der ersten
inneren Iteration ausgewertet werden müssen. Zur Veranschaulichung kann für die
iterierte Fixpunktberechnung wieder das Beispiel 3.4 mit der zugehörigen Testdatenbank genutzt werden.
72
5.5 Datenbankschnittstelle
Der Sinn einer zusätzlichen Implementierung einer eigenständigen Datenbankschnittstelle neben den bereits erwähnten Datenbankmanagern liegt, wie schon in Kapitel 4
gesagt, in einer sauberen Trennung zwischen dem DBMS und den Benutzerschnittstellen. Die Datenbankschnittstelle wurde in der Klasse DatabaseAccess realisiert. In dieser Klasse werden alle zu einer Datenbank gehörenden Objekte, wie die SchemaManager-Instanz und die DatabaseManager-Instanz, gespeichert. Auch eine zugehörige
Instanz der DependencyGraph-Klasse, sowie der Klasse Stratification werden in der
Klasse DatabaseAccess verwaltet. Dies hat den großen Vorteil, dass in ihr Methoden
zum gleichzeitigen Speichern und Laden aller zu einer Datenbank gehörenden Objekte
einfach implementiert werden können. Die Benutzerschnittstelle ist also nicht gezwungen die einzelnen Komponenten einzeln zu verwalten. Weiter kann man dadurch
eine gesamte Datenbank durch ein Objekt als Parameter übergeben. Dies ist besonders
für die verschiedenen statischen Module wie z.B. den Fixpunktmanager von Vorteil.
Der größte Vorteil liegt aber in der sicheren Verwaltung der internen Daten. So werden bei allen Methoden, auf die öffentlich (d.h. durch Methoden, die nicht im Datalog
DBMS-Paket liegen) zugegriffen werden kann, nur tiefe Kopien der internen Objekte
zurückgegeben. Dadurch ist sichergestellt, dass von außen keine Inkonsistenz durch
unsichere Datenmanipulation stattfinden kann. Innerhalb des DBMS-Pakets kann aber
mit den sehr viel schnelleren flachen Kopien gearbeitet werden. Dadurch müssen beispielsweise Änderungen nicht als Ergebnis zurückgegeben und dann in die entsprechenden Mengen eingefügt werden, sondern können direkt als eine Art von Call-ByReference durch Objektparameter behandelt werden.
Die grundsätzliche Aufgabe der Klasse DatabaseAccess ist also einfach nur das Delegieren der Anfragen der Benutzerschnittstelle an die entsprechenden Komponenten
und das Erstellen von tiefen Kopien der erhaltenen Antworten. Dazu nutzt die Klasse
DatabaseAccess die in jeder Klasse implementierten Kopier-Konstruktoren. In diesen
Konstruktoren wird sichergestellt, dass auch alle untergeordneten Objekte durch eine
tiefe Kopie ersetzt werden. Eine andere Möglichkeit wäre die Nutzung der vordefinierten Methoden clone(). Allerdings erzeugt clone() standardmäßig nur flache Kopien.
Die untergeordneten Objekte verweisen dann trotzdem noch auf die gleichen Instanzen. Dies ist aber gerade nicht erwünscht. Natürlich könnte man nun ebenfalls alle
clone()-Methoden aller zugehörigen Klassen entsprechend implementieren, aber spätestens beim Kopieren vorgefertigter Strukturen (wie z.B. einer ArrayList) könnten
Probleme auftreten. Weiter würde das Verhalten der implementierten clone()Methoden vom Java-Standard abweichen, was den Einstieg und die Erweiterung durch
andere Entwickler erschweren würde [19].
Zusätzlich verwaltet die Klasse DatabaseAccess noch ein Objekt zum Speichern von
verschiedenen Optionen. Dies ist in der Klasse Options realisiert. Sie enthält verschiedene Boolesche Variablen, mit denen das Verhalten der verschiedenen Fixpunktiterationsverfahren gesteuert werden kann, sowie zwei Ganzzahlvariablen. Mit diesen Variablen kann man festlegen, welche Art von FPI bzw. Anfragebeantwortung im Default-Fall aufgerufen werden soll. Weiter enthält auch die Klasse Options alle nötigen
73
„Getter“- und „Setter“-Methoden, um die Einstellungen zu ändern. Sie beinhaltet natürlich auch eine load(…)- und eine save(…)-Methode, wie sie schon von den anderen
Klassen bekannt sind. Durch das Speichern dieser Optionen gehen dann nach dem
Verlassen des Programms keine Datenbankeinstellungen verloren.
Als letztes enthält die Klasse DatabaseAccess noch ein Objekt vom Typ Statistic, welches die Statistiken der zuletzt ausgeführten Operation speichert.
Die Statistic-Klasse wurde entworfen, um möglichst einfach und flexibel Daten über
unterschiedlichste Operationen verwalten zu können. Damit der Benutzer die gesammelten Daten auch mit einer Operation in Verbindung bringen kann, existiert eine Variable vom Typ String, die den Namen bzw. die Beschreibung der protokollierten Operation speichert. Weiter muss die Klasse eine beliebige Anzahl von Informationen aufnehmen können. Dazu enthält sie einfach zwei zueinander gehörende Arrays. Das eine
Array hat den Typ String und das andere Array den Zahlentyp Long. Das Array vom
Typ Long kann nun zum Speichern einer Zählvariable genutzt werden oder auch, um
die aktuelle Zeit (in Millisekunden) zu speichern. Damit lassen sich dann die vergangene Zeit, die Anzahl an Iterationen oder in Kombination verschiedener Feldwerte
auch Durchschnittswerte auf einfache Weise berechnen und speichern. Das Array vom
Typ String dient dabei wieder nur zur Beschreibung der im entsprechenden Feld des
Long-Arrays gespeicherten Werts. Das String-Array ermöglicht den Zugriff durch Angabe eines String-Objekts, was die Fehleranfälligkeit beim Programmieren und Verwalten des Statistic-Objekts deutlich verringert und gleichzeitig das dynamische Einfügen in ein bereits vorhandenes Statistic-Objekt ermöglicht (allerdings müssten dann
direkte Zugriffe über die Position verboten werden und als Datenstruktur kein Array
sondern eine ArrayList gewählt werden). Außerdem können die String-Objekte auch
direkt bei der Ausgabe der Statistik benutzt werden, ohne dass die ausgebende Methode Kenntnis über die gesammelten Werte und deren Bedeutung haben muss. Dafür
müssen die String-Objekte aber für den Anwender verständliche Beschreibungen der
Werte enthalten.
5.6 Anfragemanager
Der Anfragemanager stellt ähnlich wie der Fixpunktmanager eine allgemeine, statische
answerQuery()-Methode zur Verfügung. Diese wählt dann entsprechend der gespeicherten Einstellungen im Options-Objekt der Datenbank den gewünschten Algorithmus zur Anfragebeantwortung aus. Aktuell ist nur ein naiver Algorithmus zur Anfragebeantwortung implementiert. Alle internen Methoden sind wieder statisch implementiert worden. Man muss also keine neue Instanz der Klasse anlegen, um Anfragen
zu beantworten. Zurückgegeben wird ein Objekt vom Typ RelationFacts, welches alle
Antwortfakten enthält. Durch diese Gesamtstruktur ist eine leichte Erweiterung um
andere Algorithmen (wie z.B. der Magic Sets-Methode) möglich.
Als Parameter erhält die Methode zum einen die Datenbank selbst, zum anderen die
Anfrage. Die Anfrage wurde wieder als eigenständige Klasse implementiert, da dies
74
weit effizienter als die Darstellung durch ein String-Objekt ist. Auch die Weiterverarbeitung und die Umformung in eine Regel sind damit sehr leicht und schnell möglich.
Die Klasse Query, die die Anfrage darstellt, beinhaltet eine Liste von Variablen der so
genannten Zielliste. Diese ist wieder durch eine ArrayList vom Typ Variable implementiert. Weiter enthält die Klasse ein LiteralContainer-Objekt, was der Qualifikation
entspricht. Das Query-Objekt ist also eine genaue Umsetzung der in Kapitel 2 vorgestellten Definition einer Anfrage. Wie schon bei der Definition gesehen, besteht eine
starke Gemeinsamkeit zu den Datalog-Regeln. Dadurch ist es möglich, direkt eine Anfrage in eine Regel zu überführen.
Die naive Methode naiveQueryAnswer(…) aus der Klasse QueryManager, die den
Anfragemanager darstellt, erhält als Argumente die gleichen Parameter wie die answerQuery()-Methode. Der Rückgabewert ist ebenfalls vom Typ RelationFacts. Der
Algorithmus versucht nun eine neue Relationsdeklaration in der Datenbank anzulegen.
Dazu legt der Algorithmus ein neues Declaration-Objekt für eine abgeleitete Relation
an. Die Stelligkeit entspricht der Anzahl an Variablen in der Zielliste der Anfrage. Der
Name der Relation wird aus der Konkatenation der Konstanten TEMPQUERYNAME
und der statischen Variablen queryNumber gebildet. Dabei wird die Variable queryNumber solange erhöht, bis keine solche Deklaration mehr existiert oder eine Deklaration gefunden wird, die die gleiche Anfrage darstellt. Hierbei darf nicht die normale
equals(…)-Methode zum Testen auf Gleichheit genutzt werden, sondern es muss die
Methode equalsStrict(…) verwendet werden. Dadurch ist es möglich in der Datenbank
alte Anfragen zu belassen und ihr erneutes Auftreten schneller zu beantworten. Das
Verhalten, ob beantwortete Anfragen wieder gelöscht werden sollen, kann wieder im
Options-Objekt der Datenbank hinterlegt werden.
Nachdem die neue Deklaration in die Datenbank eingefügt wurde, wird ebenfalls eine
der Anfrage entsprechende Regel in die Datenbank eingefügt. Diese neue Regel erhält
als Kopf ein neues RelationalLiteral, das als Name den Bezeichner der eingefügten
Deklaration und als Parameterliste die Zielliste der Anfrage erhält. Der Regelrumpf
wird einfach durch die Qualifikation der Anfrage festgelegt. Nun wird auf der gesamten Datenbank eine Fixpunktoperation durchgeführt. Dadurch erhält man die Antwort
auf die gegebene Anfrage und kann diese zwischenspeichern. Dann wird ggf. die Anfragedeklaration wieder aus der Datenbank gelöscht und die gespeicherte Antwort zurückgegeben. Abbildung 5.4 verdeutlicht noch einmal die Vorgehensweise des Algorithmus.
75
Schemadeklarationen
Q?
R
F
R+
erweiterte
Regelmenge
Fixpunktiteration
Antworten und
Zwischenergebnisse
Abbildung 5.4: Aufbau der Anfragebeantwortung
Der große Nachteil des Algorithmus ist der Umstand, dass für jede Anfrage die Bedeutung der gesamten Datenbank bestimmt wird. Meist würde dies allerdings schon für
eine kleine Teilmenge der Regeln genügen. Daher lassen sich deutlich effizientere Algorithmen implementieren (z.B. Magic Sets-Algorithmus).
5.7 Integritäts- und Transaktionsmanager
Wie bereits einleitend gesagt wurde, sind der Integritäts- und Transaktionsmanager nur
sehr spärlich bzw. fast gar nicht implementiert worden. Dennoch ist eine kurze Erklärung der implementierten Klassen hilfreich, um eine möglichst einfache Erweiterung
zu gewährleisten. Außerdem bringt zumindest der implementierte Integritätsmanager
für den Benutzer schon einen gewissen Nutzen mit sich. Zwar ist eine ständige Überwachung der Integritätsbedingungen mit der implementierten Vorgehensweise nicht
effizient möglich, dennoch lässt er sich für die manuelle angestoßene Überprüfung
aller Integritätsbedingungen einsetzen.
Der Integritätsmanager ist ebenfalls durch statische Methoden einer gesonderten Klasse (ICManager) realisiert worden. In ihr werden verschiedene Methoden bereitgestellt
um Integritätsbedingungen zu prüfen oder um Integritätsbedingungen zurückzugeben,
gegen die verstoßen wurde. Eine ständige Überprüfung von Integritätsbedingungen ist
nicht möglich. Die naiv implementierten Methoden zur Integritätsprüfung würden eine
extrem langsame Laufzeit zur Folge haben. Dies liegt vor allem daran, dass nach jeder
Änderung an der Datenbank immer erst eine vollständige FPI durchgeführt werden
muss. Die Methoden dienen also nur zur nachträglichen Prüfung, die extra durch den
Benutzer ausgeführt werden muss. Es findet bei den implementierten Methoden auch
keinerlei Fehlerbehandlung statt, z.B. werden illegale Fakten nicht entfernt, was
grundsätzlich auch höchstens für Basisrelationen eine vernünftige Lösung ist. Eine
Anpassung der Faktenmenge muss immer zusätzlich durch den Benutzer durchgeführt
werden.
76
Zu den im Integritätsmanager implementierten Methoden gehören checkConstraint(…), getWrongConstraints(…) und checkConstraints(…). Die Methode
checkConstraint(…) muss zwei Parameter übergeben bekommen: die zu überprüfende
Integritätsbedingung als Objekt vom Typ Constraint und einen Booleschen Parameter,
der festlegt, ob es sich um eine positive oder negative Integritätsbedingung handelt.
Der Boolesche Parameter wird benötigt, da in der Klasse Constraint keine Möglichkeit
besteht, zwischen positiven und negativen Integritätsbedingungen zu unterscheiden. In
der Methode wird dann nur geprüft, ob ein zur Integritätsbedingung passendes Fakt
existiert oder nicht. Im Fall einer positiven Integritätsbedingung wird beim Auffinden
eines solchen Fakts „Wahr“ zurückgegeben, sonst ist das Ergebnis „Falsch“. Bei negativen Integritätsbedingungen wird genau andersherum verfahren. Die Methode führt
allerdings vor dem Test keine Fixpunktberechnung auf der Datenbank durch und kann
damit scheinbar falsche Antworten liefern.
Die Methode checkConstraints(…) gibt „Wahr“ zurück, wenn gegen keine Integritätsbedingung verstoßen wird, sonst ist der Rückgabewert „Falsch“. Sie dient nur zum
Testen der gesamten Datenbank. Je nach Einstellung in den Datalog-Optionen startet
die Methode checkConstraints(…) vor der Bestimmung des Wahrheitswerts eine neue
Fixpunktberechnung, um definitiv eine korrekte Antwort geben zu können. Die Methode getWrongConstraints(…) startet nie eine Fixpunktberechnung. Sie gibt einen
Text mit allen Integritätsbedingungen zurück, gegen die verstoßen wird. Dies geschieht durch hinzufügen von HTML-Tags, um den Rückgabewert schon für eine
Ausgabe zu formatieren. Beide erhalten als Parameter die zu prüfende Datenbank als
Objekt vom Typ DatabaseAccess.
Der im Rahmen dieser Arbeit implementierte Transaktionsmanager beherrscht im eigentlichen Sinn keine Transaktionen. Es existiert lediglich eine Methode, die Elementaränderungen verarbeiten kann. Ebenso wurde keine Methode zur Behandlung von
bedingten Änderungen implementiert. Richtige Transaktionen würden ohne einen
vollständigen und effizienten Integritätsmanager ohnehin keinen Sinn machen. Auf die
anderen Komponenten wurde verzichtet, da im Rahmen dieser Arbeit nicht das gesamte DBMS mit allen unterschiedlichen Ansätzen implementiert werden konnte.
Grundsätzlich sollte der Transaktionsmanager, ähnlich wie der Anfrage- und der Fixpunktmanager, durch statische Methoden einer gesonderten Klasse implementiert werden. Weiter sollten Hilfsklassen wie ElementalModification, die Elementareänderungen Elementaränderungen darstellt, eigenständig realisiert werden. Im Falle der Klasse
ElementalModification ist dies auch exemplarisch geschehen. Aber auch für bedingte
Änderungen und die komplexen Transaktionen sollte für eine einfache Bearbeitung so
verfahren werden. Dabei sollte wieder auf eine gute Kompatibilität der verschiedenen
Klassen geachtet und auf bereits implementierte Objekte zurückgegriffen werden.
Die Klasse ElementalModification besteht so z.B. nur aus einer Booleschen Variablen
und einem RelationalLiteral-Objekt. Eigentlich würde zwar für eine Elementaränderung ein Fact-Objekt reichen, aber so lässt sich eine Klasse zur Repräsentation von
bedingten Änderungen direkt von der Klasse ElementalModification ableiten und alle
enthaltenen Elemente weiter benutzen. Man spart sich also wieder die Arbeit einige
77
Methoden und Objekte mehrfach zu programmieren bzw. zu deklarieren. Allerdings
muss so beim Erstellen eines ElementalModification-Objekts immer erst überprüft
werden, dass keine Variablen im RelationalLiteral-Objekt enthalten sind (es sich also
um ein Fakt handelt).
Der eigentliche Transaktionsmanager besteht dann aus verschiedenen commitModification(…)-Methoden. In der Methode commitModification(…) für elementare Änderungen wird entsprechend der Booleschen Variable des ElementalModificationObjekts ein addFact(…), bzw. removeFact(…) mit dem enthaltenen Fakt als Parameter durchgeführt. Die Methode commitModification(...) muss mehrfach implementiert
werden, und zwar für jeden Änderungs- bzw. Transaktionstyp. Unterschieden werden
die Methoden dann nur über ihre Parametertypen. Alternativ könnte man aber auch die
commitModification(…)-Methode in den unterschiedlichen Klassen zur Darstellung
der Änderungen implementieren. Allerdings wäre dann der Transaktionsmanager nicht
mehr in einer Klasse gekapselt. Eine mögliche Lösung wäre alle zugehörigen Klassen
in einem eigenen Paket zu implementieren. Dies hätte aber zur Folge, dass Zugriffe
aus diesem neuen Paket auf geschützte Methoden des DBMS-Pakets nicht mehr so einfach möglich wären.
5.8 Sonstige Klassen (und wichtige Methoden)
In diesem Abschnitt werden einige Hilfsklassen und die Methode getSubstitutions(…),
die alle von der abstrakten Klasse Literal abgeleiteten Klassen implementieren müssen, erklärt. Die allgemeine Aufgabe der getSubstitutions(…)-Methode wurde schon in
den vorherigen Abschnitten beschrieben. Hier soll nun am Beispiel der Klasse RelationalLiteral die Umsetzung und die interne Vorgehensweise etwas genauer betrachtet
werden. Zu den beschriebenen Hilfsklassen gehören die bereits erwähnten Klassen
Parser, Tools und Constants. Alle drei sind statische Klassen und eine Instanziierung
kann nicht stattfinden, da sie nur über private Konstruktoren verfügen und es keine
internen Methoden gibt, die diese aufrufen.
5.8.1 Die Methoden zur Variablensubstitution
Wie bereits in Abschnitt 5.3.1 beschrieben, besteht die Aufgabe der Methode getSubstitutions(…) darin, alle möglichen legalen Variablensubstitutionen für das als Parameter übergebene Literal zu bestimmen. Dazu wird zuerst geprüft, ob es sich um das
immer wahre Literal true handelt. Wenn das der Fall ist wird es direkt zur Antwort
hinzugefügt und diese zurückgegeben. Ansonsten werden alle zu diesem Literal passenden Fakten aus der Datenbank ausgelesen. Sollte diese Menge nicht leer sein, wird
für jedes gefundene Fakt geprüft, ob es zur aktuellen Variablenbelegung passt. Dazu
wird für positive Literale (der zweite Methodenparameter hat den Wert true) geprüft,
ob die Methode equals(…) aus der Klasse Term den Wert „Wahr“ zurückgibt. Dann
wird das gefundene Fakt benutzt, um die entsprechenden Variablenbindungen auf eine
Kopie des aktuellen Literals zu übertragen. Diese Kopie enthält für alle Variablen des
78
aktuellen Literals eine Bindung und damit einen Wert und wird in die Ergebnismenge
eingefügt.
Die Methode equals(…) der Klasse RelationalLiteral erhält dabei als Parameter das
Fakt als ConstantList-Objekt. Daher weiß die equals(…)-Methode, dass sie nur die
Parameterliste der aktuellen Instanz mit dem gegebenen Objekt vergleichen soll. Dabei
wird zuerst überprüft, ob die Länge beider Listen gleich ist. Sind sie nicht gleich lang,
wird sofort „Falsch“ zurückgegeben. Ansonsten wird jede Position der einen Liste mit
jeder Position der anderen Liste direkt verglichen.
Dazu wird die equals(…)-Methode der Klasse Term aufgerufen. Diese dient zum Vergleich von Variablen und Konstanten. Sie gibt „Wahr“ zurück, wenn beide Variablen
den gleichen Bezeichner haben und beide auch den gleichen Wert haben, oder eine der
beiden nicht gebunden ist (sie also noch keinen Wert zugewiesen bekommen hat).
Konstanten sind nur dann gleich, wenn sie den exakt gleichen Wert darstellen. Das
Besondere ist der Vergleich von einer Konstanten mit einer Variablen oder umgekehrt.
Dann gibt die equals(…)-Methode der Klasse Term „Wahr“ zurück, wenn die Variable
den gleichen Wert wie die Konstante hat oder wenn die Variable ungebunden ist.
Durch diese besondere Art des Vergleichs können also die verschiedenen Parameter
direkt mit einem Befehl verglichen werden. Sind also die Ausdrücke der zueinander
gehörenden Positionen gleich und der Ausdruck in der Parameterliste des Literals eine
Variable, so muss geprüft werden, dass gleiche Variablen an anderen Stellen im gleichen Literal mit dem gleichen Wert versehen werden, d.h. es muss geprüft werden, ob
das Fakt für die anderen Variablenvorkommen die gleichen Werte an den entsprechenden Stellen hat. Wenn dies nicht der Fall ist wird ebenfalls sofort „Falsch“ zurückgegeben.
Handelt es sich hingegen um ein negatives Literal, muss jede enthaltene Variable bereits eine Bindung aufweisen. Dies ist durch die Sicherheitsanforderung an Regeln
bzw. die sichere SIPS gewährleistet. Es wird also lediglich geprüft, ob eines der in der
Datenbank enthaltenen Fakten zum negativen Literal passt. Dazu wird wieder die Methode equals(…) aufgerufen. Sobald sie aber diesmal „Wahr“ zurückgibt, wird als Ergebnis direkt eine leere Liste zurückgegeben. Die aufrufende Methode weiß dann, dass
es keine legale Variablensubstitution für den gesamten Ausdruck geben kann. Wurden
hingegen alle Fakten getestet, so wird einfach das untersuchte Literal (wie im Fall des
positiven Literals) in die Ergebnisliste eingefügt. Damit ist klar, dass die aktuell untersuchte Variablensubstitution, bezogen auf das aktuelle Literal, gültig ist. Am Ende der
Methode wird die Ergebnisliste mit allen gefundenen Substitutionen zurückgegeben.
5.8.2 Die Vergleichesmethoden der Datenstruktur
Da nun beide Methoden kurz vorgestellt wurden und auch ihre unterschiedlichen Anwendungsbereiche, ist es nötig, sie noch einmal genau zu untersuchen und ihre unterschiedliche Implementierung zu begründen.
79
Die equals(…)-Methode prüft, ob zwei Objekte wie z.B. Konstanten und Variablen
zueinander passen. Aber damit gilt dies natürlich auch für alle Objekte, die Konstanten
und Variablen enthalten und diese Methode heranziehen, um ihre eigene Ähnlichkeit
mit anderen Objekten zu prüfen. Dies macht auch für die meisten Anwendungen Sinn
und es darf gar nicht auf exakte Gleichheit getestet werden. So sollen ja z.B. bei der
Variablensubstitution genau die Fakten gefunden werden, welche zu den Literalen
passen. Ebenso könnte man diese Methode auch für die Duplikateliminierung beim
Einfügen von neuen Regeln ins Datenbankschema nutzen. Denn es macht keinen Sinn
für eine Relation R die bereits die Regel r1: r(X) <- edge(X, Y). enthält, eine weiter
Regel r2: r(X) <- edge(X, 2). hinzuzufügen. Die Regel r2 könnte keine neuen Fakten
generieren. Allerdings müsste dann dafür gesorgt werden, dass die mächtigere Regel
im Schema enthalten bleibt. Wenn man zuerst r1 einfügt und dann r2 ist dies auch ohne
weiteres möglich. Geschieht das Einfügen aber umgekehrt, müsste der Schemamanager erst die alte Regel entfernen und dann die neue einfügen. Dafür benötigt der
Schemamanager eine Operation, die die Mächtigkeit der einzelnen Regeln definiert.
Für einfache Regeln ist dies auch sicher schnell möglich. Für komplexere Regelmengen und spätestens wenn mehrere Regeln gemeinsam einer neuen Regel entsprechen
ist die Aufgabe nicht mehr trivial und vor allem auch nicht mehr durch die Methode
equals(…) lösbar, da die equals(…)-Methode immer nur zwei Objekte miteinander
vergleicht. Außerdem wäre das Verhalten der Datenbank nicht unbedingt immer für
den Anwender sofort zu verstehen, wenn diese automatisch ganze Regeln entfernen
würde.
Eine solche Optimierung sollte besser durch eine eigenständige Methode, die ggf. sogar separat vom Benutzer ausgeführt werden sollte, realisiert werden.
Ein weiterer Grund, warum für den Vergleich von Regeln nicht die Methode equals(…) benutzt werden sollte, liegt an der Implementierung des Anfragemanagers.
Dieser sucht vor dem Erstellen einer zur Anfrage passenden Regel, ob diese bereits im
Schema enthalten ist. Würde man hierbei auf die Methode equals(…) zurückgreifen,
würden falsche Antworten erzeugt werden. Würde der Benutzer zuerst die Anfrage q1:
{(X): edge(X, Y)}? stellen und hätte er dabei die Option gewählt, alte Anfragen in der
Datenbank zu behalten (um ggf. schnellere Antworten zu erhalten), würde anschließend bei der Anfrage q2:{(X): edge(X, 2)}? die Antwort von q1 ausgegeben. Bei dieser
Anwendung macht es also definitiv keinen Sinn mehr die mächtigere Anfrage bzw.
Regel dominieren zu lassen. Es ist also unumgänglich eine weitere Methode, die die
exakte Gleichheit zweier Objekte bestimmt, zu realisieren.
Aus diesem Grund wurde zusätzlich noch die Methode equalsStrict(…) eingeführt und
implementiert. Damit werden dann zwar ggf. überflüssige Regeln ins Datenbankschema mit aufgenommen, aber die Verarbeitung funktioniert intuitiv ohne zusätzliche Sicherheitsmaßnahmen.
80
5.8.3 Datalog-Parser
Die Klasse DatalogParser hat die Aufgabe, für jeden Datalog-Ausdruck eine Methode
zum Parsen bereitzustellen. Sie wurde eingeführt, um bei einer Erweiterung des Programms nur eine kleine wohl definierte Menge von Klassen und Methoden anpassen
zu müssen. So ist zum Beispiel bei einer Hinzunahme eines weiteren Built-InOperators für zweistellige Funktionen (wie z.B. den Modulo-Operator) nur die Klasse
BinaryTerm und das entsprechende Konstrukt in der Klasse DatalogParser (in diesem
Fall die Konstante FUNCTIONALOPERATOR) anzupassen. Alle Datalog-Strukturen
wie Regeln, Anfragen oder Änderungen sind dann ohne Veränderungen an ihren Repräsentationsklassen in der Lage, die Erweiterungen auszuwerten. Im zugehörigen
Quellcode sind die für diese Erweiterung nötigen Zeilen bereits als Kommentare eingefügt worden. Sie befinden sich in den Dateien BinaryTerm.java und DatalogParser.java und sind mit dem zusätzlichen Kommentar „Beispielerweiterung Modulo“
versehen.
Auch komplexere Erweiterungen lassen sich meist ohne Änderungen der abhängigen
(also indirekt betroffenen) Klassen durchführen. Selbst wenn dadurch beispielsweise
neue Klassen und Veränderungen an den Methoden aus der Klasse DatalogParser nötig sind.
Weiter ermöglicht die Kapselung der Parsingkomponenten ein einfaches Umstellen
auf andere Vorgehensweisen beim Parsen. Auch hierfür gilt selbstverständlich, dass
dann nur die Klasse DatalogParser verändert werden muss. Die Kapselung wird ermöglicht, indem für jeden Datalog-Ausdruck der als Text dargestellt werden kann und
für den eine eigene Java-Klasse zur internen Repräsentation existiert, eine parseXXX(…)-Methode in der Klasse DatalogParser bereitgestellt wird. „XXX“ ist dabei
durch den Klassenamen zu ersetzen. Als Parameter erhält die Methode ein StringObjekt mit der (potentiellen) Textdarstellung des Ausdrucks. Diese Methode versucht
den übergebenen Ausdruck entsprechend der Datalog-Syntax zu parsen und gibt bei
Erfolg eine neue Instanz der entsprechenden Klasse zurück. Im Fehlerfall wird eine
IllegalArgumentException-Ausnahme ausgelöst. Die aufrufende Methode muss den
Fehler dann abfangen und behandeln.
Alle implementierten Klassen rufen also immer ihre zugehörige Parsing-Methode auf,
wenn sie ein String-Objekt verarbeiten müssen. Die Klasse Rule ruft zum Beispiel die
Methode parseRule(…) auf. Die Methode parseRule(…) ruft natürlich ihrerseits auch
wieder andere parseXXX(…)-Methoden auf. Dadurch muss derselbe Code nicht
mehrmals programmiert werden und Veränderungen übertragen sich automatisch auf
komplexere Strukturen, die diese Methoden nutzen.
Eine der einfachsten Möglichkeiten beliebige Texte zu parsen ist die Verwendung von
regulären Ausdrücken. Dazu bietet die Programmiersprache Java die Klassen Pattern
und Matcher an. Sie dienen zum Speichern und Auswerten von regulären Ausdrücken.
Weiter ermöglichen sie es, einfache reguläre Ausdrücke zu immer komplexeren Ausdrücken zusammenzusetzen. Dies geschieht allerdings über eine Rückkonvertierung in
String-Objekte. Leider ist es nur bis zu einer bestimmten Komplexität möglich, den
81
gesamten Ausdruck auf einmal auszuwerten bzw. zu prüfen. Die Pattern-Klasse ist ab
einer bestimmten Schachtelungstiefe nicht mehr in der Lage, zu einem gegebenen regulären Ausdruck eine Pattern-Instanz anzulegen. Dieses Problem lässt sich aber
leicht durch das sukzessive Parsen der Teilausdrücke lösen. Somit sind keine zu großen Pattern-Objekte nötig.
Um nicht immer wiederkehrende reguläre Ausdrücke neu definieren zu müssen, werden für alle Datalog-Ausdrücke, sowohl für die elementaren als auch die strukturierten, eigene reguläre Ausdrücke definiert. Diese können dann bequem zum Erstellen
der komplexeren Ausdrücke benutzt werden. Dies fördert zusätzlich die Lesbarkeit
dieser komplexen Ausdrücke. Weiter vereinfacht es den Test von regulären Teilausdrücken. Diese vordefinierten Konstanten sollten direkt als Pattern-Objekte definiert
werden und nicht als String-Objekte. Dadurch können diese vorkompiliert werden und
müssen nicht bei jeder Verwendung erst zu einem Pattern-Objekt transferiert werden.
Dies erhöht gerade beim Einlesen großer Textdateien als Datenquelle die Performance
des Parsers sehr.
Da auch die Klasse String bei den von ihr bereitgestellten Suchmethoden immer erst
ein neues Pattern-Objekt anlegt, ist der direkte Weg über die regulären Ausdrücke
schneller als das manuelle Zerlegen der String-Objekte durch direkte Zugriffsmethoden der Klasse String [19].
Grundsätzlich wurden alle Datalog-Ausdrücke mit den ihrer Syntax entsprechenden
regulären Ausdrücken dargestellt. Eine kleine Ausnahme bildet der reguläre Ausdruck
für Konstanten. Dabei ist es bei einer in Hochkommata eingefassten Konstante zwingend nötig, enthaltene Hochkommata mit einem Backslash anzukündigen. Der Backslash fungiert dabei als Escapezeichen. Um auch das Escapezeichen in einer Konstanten verwenden zu können, muss der Backslash als doppelter Backslash (\\) eingegeben
werden. Das macht allerdings die Eingabe in einem Java-String-Objekt nicht sehr übersichtlich, da sowohl für den Java-String als auch für den regulären Ausdruck selbst
der Backslash als Escapezeichen dient. Dadurch wird der Java-String für den regulären
Ausdruck, der das oben erklärte Escapeverhalten von Datalog beschreibt, durch eine
sehr hohe Zahl von Backslashs recht unübersichtlich. Da es sich aber um die gängige
Vorgehensweise handelt, wurde sie hier auch beibehalten. Beispiel 5.1 soll noch einmal die Problematik der Escapezeichen verdeutlicht werden.
Beispiel 5.1 (String-Objekt für den regulärer Ausdruck für in Hochkommata eingefasste Konstanten in Datalog)
“ \\’ ( [^\\’\\\\] | (\\ \\’) | ([\\\\][\\\\]) )* \\’ “
(Im eigentlichen String-Objekt dürfen keine Leerzeichen enthalten sein,
sie sind aber der Übersicht wegen eingefügt worden.)
Dabei steht \\’ für das führende bzw. endende Hochkomma. Der Ausdruck
[^\\’\\\\] steht für ein beliebiges Zeichen ohne das Hochkommata und ohne den
einfachen Backslash. Diese beiden Zeichen dürfen nur in bestimmten Kombinationen auftreten: entweder als Backslash direkt gefolgt von einem Hochkommata (\\ \\’) oder als doppelter Backslash ([\\\\][\\\\]). Dies darf dann beliebig oft
82
(auch kein Mal) zwischen den umschließenden Hochkommata vorkommen. Dafür ist der gesamte Ausdruck geklammert und mit dem Sternchen versehen
(…)*. Für eine genaue Beschreibung der regulären Ausdrücke und die Bedeutung aller Steuerzeichen in Java sei auf die umfangreiche Java-Dokumentation
verwiesen [8].
In ihrer jetzigen Form ist die Klasse nicht in der Lage die in Kapitel 2 definierten lokalen Regeln zu parsen. Ausdrücke mit einer solchen lokalen Regel werden von den entsprechenden Methoden immer als fehlerhaft zurückgewiesen (auch wenn diese durchaus der Datalog-Syntax und Semantik entsprechen). Dies gilt sowohl für normale Regeln als auch für Anfragen oder Integritätsbedingungen. Der Grund liegt in der deutlich höheren Komplexität und Länge der entsprechenden regulären Ausdrücke und den
damit bereits oben beschriebenen Problemen. Da man aber die lokalen Regeln einfach
vor einer Anfrage bzw. vor dem Nutzen in einer anderen Regel einfach dem Schema
hinzufügen kann, stellt es keine Einschränkung an die Ausdrucksstärke der formulierten Ausdrücke dar.
In Beispiel 5.2 ist das Ergebnis des Parsens einer Regel durch die Klasse DatalogParser veranschaulicht. Die gewählte Darstellungsform soll dabei die erstellten Objekte
und die Belegungen ihrer Argumente verdeutlichen. Dabei sind in der Textdarstellung
und der internen Darstellung analoge Teile durch gleichartige Rechtecke markiert. Objekte ohne Inhalt sind in der internen Darstellung nicht extra markiert worden. Zu ihnen gehört die Liste der Zuweisungsliterale, da kein Zuweisungsliteral im Beispiel
enthalten ist. Der interne Aufbau der Zuweisungsliterale ist aber ähnlich wie der Aufbau der Vergleichsliterale.
Textteile, die in der Textdarstellung der Regel nicht extra durch ein gepunktetes
Rechteck markiert sind, werden in der internen Darstellung nicht gespeichert. Dies ist
möglich, da ihr Vorkommen und ihre Position an festen Stellen des Ausdrucks erfolgen müssen. Beim Parsen der Eingabe wird natürlich geprüft, ob sie im gegebenen
String-Objekt wirklich vorhanden sind. Ebenso werden sie bei der Textausgabe automatisch eingefügt. Zu den nicht gespeicherten Zeichen und Symbolen gehören alle
Kommata, die Klammern, der Implikationspfeil (<=), der Punkt am Ende der Regel,
das „not“ der negativen Literale aber auch das Zuweisungssymbol (:=). Bei Anfragen
werden auch nicht der Doppelpunkt, das abschließende Fragezeichen und die geschweiften Klammern gespeichert.
83
Beispiel 5.2: (interne Darstellung einer Regel)
Regel: p(X, Y) <- r(X, Y, Z), not s(Y), X <= Z + 4.
Rule {
}
head (RelationalLiteral) = {
identifier (String) = “p”;
parameters (ArrayList<Term>) = {
parameters[0] (Variable) = {
value (String) = ””;
identifier (String) = "X";
}
parameters[1] (Variable) = {
value (String) = ””;
identifier (String) = "Y";
}
}
}
body (LiteralContainer) = {
positivLiterals (ArrayList<RelationalLiteral>) = {
positivLiterals[0] (RelationalLiteral) = {
identifier (String) = “r”;
parameters (ArrayList<Term>) = {
parameters[0] (Variable) = {
value (String) = ””;
identifier (String) = "X";
}
parameters[1] (Variable) = {
value (String) = ””;
identifier (String) = "Y";
}
parameters[2] (Variable) = {
value (String) = ””;
identifier (String) = "Z";
}
}
negativLiterals (ArrayList<RelationalLiteral>) = {
negativLiterals[0] (RelationalLiteral) = {
identifier (String) = “s”;
parameters (ArrayList<Term>) = {
parameters[0] (Variable) = {
value (String) = ””;
identifier (String) = "Y";
}
}
assignLiterals (ArrayList<AssignmentLiteral>) = {}
compareLiterals (ArrayList<CompareLiteral>) = {
compareLiterals [0] (CompareLiteral) = {
arg1 (Variable) = {
value (String) = ””;
identifier (String) = "X";
}
op (String) = “<=”;
arg2 (BuiltInTerm) = {
arg1 (Variable) = {
value (String) = ””;
identifier (String) = "Z";
}
op (String) = “+”;
arg2 (Constant) = {
value (String) = ”4”;
}
}
}
}
}
84
5.8.4 Hilfsklassen und deren Methoden
Die Klasse Tools bietet neben den bereits erwähnten generischen Methoden zum Verwalten von Mengen bzw. mengenähnlichen Strukturen noch die Methoden arrayListToString(…) und getBundleString(…). Die erste Methode dient, wie der Name
schon vermuten lässt, nur zum formatierten Konvertieren eines ArrayList-Objekts in
ein String-Objekt. Dies wird vor allem zur Ausgabe benutzt. Der erste Parameter ist
dabei die Liste selbst, der zweite Parameter bestimmt das zu verwendende Trennzeichen zwischen den einzelnen Listenelementen. Da die Methode arrayListToString(…)
typunabhängig implementiert wurde, kann sie für jede Instanziierung. der Klasse ArrayList benutzt werden und ist damit ein gutes Beispiel für die Wiederverwendung von
Code.
Die Methode getBundleString(…) hingegen hat eine ganz andere Aufgabe. Durch sie
soll die Sprachunabhängigkeit des gesamten Paktes gewährleistet werden. Dies kann
man z.B. durch das Anlegen einer Sprachklasse gewährleisten, in der man alle Zeichenketten in einer Konstanten hinterlegt. Dies hat aber den Nachteil, dass man nicht
einfach während der Laufzeit einen neuen Sprachsatz laden kann. Weiter befinden sich
immer alle Zeichenketten im Hauptspeicher, was bei sehr umfangreichen Programmen
zu Problemen führen kann.
Die Programmiersprache Java bietet aus diesen Gründen die Klasse ResourceBundle
an. Diese enthält eine Methode mit deren Hilfe man aus einer externen Datei den gespeicherten Wert zu einem Schlüssel erhält. Der Dateiname setzt sich dabei aus einem
freiwählbaren Namen (Namen des „ResourceBundle“), einem Unterstrich (_), einer
Landeskennung und der Dateiendung „.properties“ zusammen. Die Methode getBundle(…) zu einem gegebenen ResourceBundle-Objekt sucht zur Laufzeit automatisch
aufgrund der eingestellten Landessprache die passende Datei. Da aber beim Suchen
nach der passenden Datei und dem gesuchten Schlüssel die bereitgestellten Methoden
bei einem Misserfolg eine Ausnahme auslöst, wurde die Methode getBundleString(…)
in der Klasse Tools implementiert. Diese fängt bei einem Fehler nicht einfach nur die
Ausnahme ab, sondern versucht auch noch auf eine andere Sprache umzustellen. Sollte
dies immer noch nicht erfolgreich sein, wird keine Fehlermeldung zurückgegeben,
sondern nur der Text „no text found“. Dadurch muss nicht an jeder Stelle, an der eine
Zeichenkette ausgegeben bzw. benutzt werden soll, auch eine Ausnahmebehandlung
implementiert werden. Außerdem ist so das Programm auch noch mit einer unvollständigen oder fehlerhaften Sprachdatei lauffähig. Grundsätzlich sind sogar zur Laufzeit noch Änderungen und Korrekturen an den Sprachdateien möglich, sodass man die
Veränderungen direkt nachvollziehen kann. Allerdings muss die graphische Oberfläche durch den Benutzer zum Neuzeichnen aufgefordert werden. Dazu muss er den
veränderten Sprachsatz im Menü File -> Options -> Languages (erneut) auswählen.
Beispiel 5.3 veranschaulicht die Abläufe beim Aufruf der Methode getBundleString(…).
Um nun das gesamte implementierte Programm in eine andere Sprache zu übersetzen,
müssen nur die beiliegenden ResourceBundle-Dateien übersetzt werden. Eine Neu85
kompilieren oder gar eine Veränderung des Quellcodes ist nicht nötig. Für die verschiedenen Programmteile wie die graphische Oberfläche und das DBMS wurden gesonderte ResourceBundle-Dateien angelegt. Damit ist auch hier eine strikte Trennung
der einzelnen Komponenten garantiert. Dies ermöglicht es, die komplette Benutzerschnittstelle durch eine neue zu ersetzen, ohne dabei überflüssigen Code aus mit dem
DBMS gemeinsam genutzten Klassen oder Dateien zu entfernen [19].
Beispiel 5.3 (Einlesen einer sprachsatzabhängigen Zeichenkette)
Als Beispiel soll der Tooltipptext eingelesen werden, der angezeigt werden soll, wenn
der Benutzer mit der Maus auf der entsprechenden Schaltfläche einige Zeit verweilt.
Um den sprachsatzabhängigen Text zu finden, wird folgender Befehl aufgerufen:
Tools.getBundleString("gui", "MinimizeAllToolTip");
Der erste Parameter spezifiziert den Dateinamen der ResourceBundle-Datei. Der zweite Parameter legt den zu suchenden Schlüssel fest. Es wird in diesem Beispiel auf folgendes Schlüssel-Werte-Paar zugegriffen:
MinimizeAllToolTip=minimize all windows
Dies gibt den Text „minimize all windows“ zurück.
Um auch variable Elemente im Rückgabetext einzufügen, wird folgender Weg benutzt:
Tools.getBundleString("gui", "FileNotFoundError").replace("%f", filename);
Im zurückgegebenen Ausdruck wird dann der Teilstring „%f“ automatisch durch beliebige Werte ersetzt. Dies ist vor allem dann interessant, wenn gewisse Teile des Textes erst zur Laufzeit bekannt sind. Für diese Art der Parametrisierung wurde das Prozentzeichen (%) als Steuerzeichen im Rahmen dieses Programms festgelegt. Das
Schlüssel-Werte-Paar könnte so aussehen:
FileNotFoundError=Unable to find %f.
Das Ergebnis ist dann der gefundene Text mit dem Wert der Variablen filename an der
Stelle des Teilstrings „%f“ (das Ergebnis könnte z.B. „Unable to find test.db.“ sein).
Findet keine Ersetzung statt oder existiert kein entsprechender Teilausdruck im ResourceBundle-Wert, wird der unveränderte Wert zurückgegeben. Dies kann zu unleserlichen Ausgaben führen („Unable to find %f.“), verursacht aber keinen Fehler und
auch keinen Programmabsturz.
Als letztes soll noch die Klasse Constants beschrieben werden. Auch hierfür liegt der
Grund in einer zentralen Wartung und Verwaltung des Programms. In der Klasse
Constants sollen vor allem solche global gültigen Werte, die in verschiedenen Bereichen des Programms genutzt werden, zur Verfügung gestellt werden. Die Aufgabe der
Konstanten ist es also, eine Art der Kommunikation zwischen eigentlich getrennten
86
Komponenten zu ermöglichen, ohne direkt auf Variablen oder Methoden der anderen
Klassen zuzugreifen. Dadurch sind wieder Änderungen speziell an diesen Konstanten
selbst möglich, ohne den gesamten Quellcode zu durchsuchen.
Vor allem werden Konstanten definiert, die die relativen Pfade zu weiteren Ressourcen
oder die zur Regeltransformation nötigen Präfixe enthalten. Diese Präfixe werden beispielsweise an mehreren Stellen benötigt. Dabei ist es sehr wichtig, dass an allen Stellen wirklich mit exakt den gleichen Werten gearbeitet wird. Zum einen werden die
Präfixe beim automatischen Erstellen transformierter Regel benötigt. Zum anderen
beim nachträglichen Löschen dieser Regeln. Da je nach eingestellten Optionen diese
Regeln nicht automatisch wieder gelöscht werden, stellt der Schemamanager eine Methode bereit, die alle transformierten Regeln nachträglich entfernt. Durch die Definition der Konstanten kann man also sicher sein, dass wirklich in beiden Fällen die gleichen Regeln behandelt werden.
Weiter sind so genannte Steuerkonstanten definiert. Sie werden benutzt, um z.B. über
Methodenparameter bestimmte Algorithmen auszuwählen. Ihre Verwendung erhöht
die Lesbarkeit des Codes und macht somit den Einstieg für andere Programmierer
deutlich leichter. Allerdings müssen sich dazu die Entwickler an den Grundsatz halten,
nur die vorgesehenen Konstanten als Parameter zu übergeben und nicht ihre interne
Zahlendarstellung. Dabei handelt es sich allerdings um eine gängige Java-Konvention.
87
Kapitel 6
Implementierung der graphischen Oberfläche
Im folgenden Kapitel wird die Vorgehensweise bei der Implementierung der graphischen Oberfläche zur Nutzung des realisierten DBMS beschrieben. Dazu werden der
Fensteraufbau und die da hinter liegenden Funktionalitäten, sowie die Umsetzung mit
Java Schrittweise erläutert. Bei der Erstellung der graphischen Oberfläche wurden die
in Abschnitt 4.1.3 beschriebenen Entwurfskriterien berücksichtigt und auch umgesetzt.
Bei dem grundsätzlichen Aufbau und der Darstellungsform der Ausgabe wurde aus
den in Abschnitt 4.1.3 genannten Gründen die Einzelfenstervariante gewählt. Dazu ist
das Hauptfenster in fünf verschiedene Bereiche unterteilt worden. Die Implementierung des Hauptfensters ist in der Klasse MainWindow im Paket datalogGUI zu finden.
In diesem Paket befinden sich auch alle weiteren Klassen, die zur graphischen Anzeige
notwendig sind. Das Verwalten (Laden und Speichern) der Einstellungen, die sich nur
auf die graphische Oberfläche beziehen, wurde durch die Klasse GUIOptions realisiert. Sie dient vor allem der Wiederherstellung der alten Fensterpositionen und den
alten Fenstergrößen.
Im Folgenden wird der Fensteraufbau sukzessiv erklärt:
Der oberste der fünf Bereiche ist der Steuerungsbereich, der aus der Menü- und der
Symbolleiste besteht. Dieser ist in Abbildung 6.1 mit einem roten Rechteck umrandet.
Über diesem Bereich befindet sich noch die Kopfleiste des Hauptfensters. In ihr werden der Programmname und die aktuelle Versionsnummer der Datenbank angegeben.
Die angezeigte Versionsnummer ist die, in der die Datenbank gespeichert wird. Es
handelt sich nicht um die Version, in der die Datenbank ggf. geladen wurde. Beim Laden können für die Faktendatei und die Schemadatei sogar unterschiedliche Versionen
benutzt werden. Diese Versionsnummern sind über das Menü „Help“ darstellbar.
Die Menüleiste im Steuerungsbereich bietet dem Benutzer immer den vollen Umfang
an ausführbaren Befehlen in der aktuellen Ansicht. Spezielle Befehle in den unterschiedlichen Ansichten werden entsprechend ein- und ausgeblendet. Weiter ist zu jedem Menüeintrag auch immer eine Tastenkombination hinterlegt, über die der entsprechende Befehl direkt gestartet werden kann.
88
Abbildung 6.1: Hauptansicht der graphischen Oberfläche
Die Symbolleiste lässt sich durch den Benutzer in ein kleines Fenster umwandeln und
an jede beliebige Position verschieben. Damit kann die Anzeigefläche minimal erweitert werden und der Benutzer kann die Symbolleiste an eine für ihn schneller erreichbare Stelle verschieben. Es ist also auch bei diesem Modell gut möglich, durch kleine
Individualeinstellungen die Arbeitsgeschwindigkeit zu erhöhen und den unterschiedlichen Gewohnheiten der Benutzer gerecht zu werden.
Die Symbolleiste bietet die Möglichkeit, eine neue leere Datenbank zu erstellen, eine
bereits gespeicherte Datenbank aus dem Dateisystem zu laden, bzw. die aktuelle Datenbank an einer beliebigen Stelle im Dateisystem zu speichern. Weiter kann man mit
ihr eine Fixpunktiteration starten oder alle temporären bzw. alle materialisierten Fakten löschen, das Optionsfenster öffnen, oder sich alle Integrationsbedingungen anzeigen lassen, gegen die verstoßen wird. Ebenso können über eine Schaltfläche alle offenen Ergebnisfenster vergangener Fixpunktberechnungen wieder geschlossen werden.
Weiter kann über die Symbolleiste auch ein Dialogfenster (DatalogExpressionDialog)
für einen beliebigen Datalog-Ausdruck geöffnet werden. Es existiert auch ein Symbol
für das Starten eines Transaktions-Managers, der allerdings nicht implementiert ist.
Daher ist das Symbol auch nur grau dargestellt und führt keine Aktion aus. In
Abbildung 6.2 ist die Symbolleiste als eigenständiges Fenster dargestellt und mit den
entsprechenden Bedeutungen der Schaltflächen versehen.
89
Neue
Datenbank
erstellen
Datenbank
speichern
Datenbank
laden
Temporäre
Fakten
löschen
FPI
starten
Optionsfenster
anzeigen
Mat. Fakten
löschen
Alle FPI
Ergebnisfenster
schließen
Integritätsbedingungen
Transaktionsfenster
DatalogAusdrücke
Abbildung 6.2: Symbolleiste mit Erklärungen als eigenständiges Fenster
Die Implementierung der beiden Steuerleisten wird durch die zu den JavaKlassenbibliotheken gehörenden Klassen JToolbar (für die Symbolleiste) und JMenuBar, sowie JMenu und JMenuItem (für die Menüleiste) direkt in den beschriebenen
Funktionen unterstützt. Sie sind im Java-Swing-Paket enthalten, welches zu den JavaFoundation-Classes (JFC) gehört. Für eine genaue Beschreibung dieser Objekte und
aller aus den Standardbibliotheken verwendeten Klassen sei hiermit einmal auf die
Java-API-Dokumentation verwiesen [8]. Weitere Informationen zu den meisten JavaKlassen und deren Nutzung sind auch in [19] und [15] zu finden.
Zu allen Symbolen auf der Toolleiste sind Tooltipps hinterlegt worden, so dass sich
der unerfahrene Benutzer diese leicht erklären lassen kann. Tooltipps lassen sich in
Java für fast jede graphische Komponente der Standardbibliothek einfach durch die
Methode setToolTipText(…) setzen. Dazu gehören alle Schaltflächen, aber auch Teilbereiche eines Fensters, oder das gesamte Fenster.
Bei der Darstellung von Texten an der graphischen Oberfläche wird die bereits beim
DBMS-Paket erklärte Methode getBundleString(…) aus der Klasse Tools benutzt.
Damit ist auch bei der graphischen Oberfläche eine Übersetzung der angezeigten Texte einfach möglich. Zu den wenigen Ausnahmen zählen einige komplexe, im HTMLFormat ausgegebene Texte. Diese Texte sind daher in englischer Sprache verfasst,
damit ein größtmöglicher Benutzerkreis sie verstehen kann. Auf die betroffenen Ausgaben wird später noch genau verwiesen. Alle Tooltipps, Menünamen und Menüeinträge werden aber ausnahmslos mit der Methode getBundleString(…) geladen.
Der zweite Bereich, der ebenfalls der Steuerung dient, enthält das Hauptmenü zur
Wahl der gewünschten Ansicht. Es ist in Abbildung 6.1 orange umrandet. Dieses kann
allerdings anders als die Symbolleiste oder die Menüleiste ausgeblendet werden. Dazu
muss der kleine Pfeil auf dem Fensterteiler unter bzw. rechts vom Hauptmenü, der auf
das Hauptmenü zeigt, gedrückt werden. Zur Wiederherstellung wird der je zugehörige
Pfeil in die andere Richtung benutzt. Mit den Fensterteilern, auch Splittern genannt,
kann auch die Größe benachbarter Anzeigebereiche verändert werden. Auch hierzu
bietet die umfangreiche Standardbibliothek von Java schon eine Klasse zur einfachen
Realisierung an. Sie gehört ebenfalls, wie die meisten für die graphische Oberfläche
genutzten Klassen, zum Swing-Paket und trägt den Namen JSplitPane.
90
Die Schaltflächen des Hauptmenüs (durch JButtons umgesetzt) kann sich der Benutzer
ebenfalls durch Tooltipps erklären lassen. Weiter wird bei der zuletzt betätigten
Schaltfläche der Hintergrund ausgegraut. Daran kann der Benutzer schnell erkennen in
welcher Ansicht er sich befindet.
Die Schaltfläche, die eigentlich die Anzeige des Abhängigkeitsgraphen veranlassen
soll, ist als funktionsunfähig markiert worden. Die bereitgestellte Java-Methode graut
das gewählte Objekt dann vollständig aus (auch den Text der Schaltfläche) und verhindert das Ausführen der für das Objekt hinterlegten Aktionen. Durch die inaktiven
Elemente ist es möglich, die Veränderung an den implementierten Komponenten möglichst gering zu halten, wenn eine entsprechende Erweiterung durchgeführt werden
soll. Im Fall des Abhängigkeitsgraphen muss nur die leere Klasse GraphWindow mit
dem entsprechenden Code gefüllt werden und die Schaltfläche im Hauptprogramm
aktiviert werden. Die restlichen Anweisungen befinden sich schon im Code.
Solche vorbereitenden Maßnahmen sind auch in anderen Fällen, z.B. für die Erweiterung um zusätzliche Toolleisten in einigen Ansichten, bereits im Code getroffen worden. Allerdings sind diese nicht immer wie im Beispiel des Abhängigkeitsgraphen an
der Oberfläche sichtbar. So existieren z.B. für einige Fenster schon die Gerüstmethoden und Zugriffsvariablen für Symbolleisten oder Popup-Menüs im Quellcode.
Je nach gewählter Ansicht werden in der Menüleiste immer nur die nötigen Menüeinträge angezeigt. Damit kann der Benutzer nicht nur über die gerade gezeigten Daten,
die Menü- und die Statusleiste erkennen in welcher Ansicht er sich befindet, sondern
auch das Hauptmenü selbst zeigt es ihm deutlich an.
Der dritte Bereich des Hauptfensters ist ein unterstützender Anzeigebereich, im Folgenden auch als Nebenanzeigebereich bezeichnet. Er ist in Abbildung 6.1 grün schraffiert und direkt unter dem Hauptmenü angeordnet. Die Aufgabe des Nebenanzeigebereichs ist das Anzeigen bzw. die Auswahl von Daten, welche die im Hauptbereich angezeigten Informationen weiter erläutern oder spezifizieren. Allerdings können hier
niemals Daten verändert werden. Dazu wird ausschließlich der Hauptanzeigebereich
genutzt.
Das Kernstück der graphischen Oberfläche stellt der Hauptanzeigebereich dar. In ihm
werden immer die gerade zu bearbeitenden bzw. anzuzeigenden Daten visualisiert. Der
Hauptbereich ist in Abbildung 6.1 grün umrandet. Die Darstellungsform und die Art
und Menge der Daten hängt dabei von der gewählten Ansicht und ggf. von dem zusätzlich gewählten Datensatz im Nebenanzeigebereich ab.
Im unteren Bereich des Fensters befindet sich schließlich der letzte Bereich. Er dient
nur der schnellen Informationsweitergabe an den Benutzer. Dafür werden zwei Statusleisten benutzt. Sie sind in Abbildung 6.1 blau umrandet. Die obere Statusleiste zeigt
Informationen für die aktuell gezeigten Daten im Hauptbereich des Fensters an, z.B.
die Anzahl der Datensätze. Die angezeigten Informationen sind also abhängig von der
gewählten Ansicht.
91
Die untere Statusleiste (im Folgendem auch Hauptstatusleiste genannt) gibt hingegen
an, welche Datenbank gerade im Zugriff ist. Dabei wird der letzte Speicherort angegeben. Bei einer neuen Datenbank, die noch nicht gespeichert wurde, steht nur „Database: new“ bzw. die entsprechende Übersetzung. Im Weiteren wird immer die englische
Sprachversion der graphischen Oberfläche beschrieben. Bei abweichenden Bezeichnungen sollte überprüft werden, ob das genutzte DatalogLab mit einem anderen
Sprachsatz arbeitet.
Weiter ist in der Mitte der Hauptstatusleiste der Name der aktiven Ansicht vermerkt.
In Abbildung 6.1 ist dies die Schemaansicht (Anzeige: „Schema“).
Nachdem nun das Hauptfenster mit seinen unterschiedlichen Bereichen erklärt wurde,
werden im Folgenden die verschiedenen Ansichten und ihre unterschiedlichen Darstellungen im Haupt- bzw. Nebenanzeigebereich erklärt.
6.1 Darstellung von Anwendungskomponenten
In den folgenden Abschnitten werden die einzelnen Anwendungsbereiche des DatalogLabs, welche über die graphische Schnittstelle gesteuert werden können, beschrieben und ihre Aufgaben erklärt.
6.1.1 Schema
In Abbildung 6.1 ist bereits die Ansicht zur Bearbeitung des Schemas zu sehen, wie es
bei jedem neuen Programmstart der Fall ist. In dieser Ansicht wird der zusätzliche Anzeigebereich nicht benötigt und bleibt leer. Im Hauptbereich werden alle Relationsdeklarationen der aktuellen Datenbank angezeigt. Dies geschieht in Tabellenform.
In der Tabelle werden alle nötigen Daten einer Datalog-Deklaration angezeigt, können
aber nicht editiert werden. Die Informationen, ob die Fakten der Relation hergeleitet
werden und ob die Fakten gespeichert werden, werden durch Kontrollfelder (auch
Checkboxen genannt) angezeigt. Die Stelligkeit, sowie der Bezeichner der Relation
werden in normalen Textfeldern dargestellt. Um eine Deklaration zu ändern, muss sie
zuerst entfernt und dann die geänderte Deklaration hinzugefügt werden.
Tabellen lassen sich in Java mit Hilfe der vordefinierten Klasse JTable definieren. Um
auch Tabellen anzeigen zu können, die mehr Daten enthalten als auf einmal angezeigt
werden können, wurden die Anzeigebereiche noch um Rollbalken (Scrollbars) erweitert. Hierzu wurde die Klasse JScrollPane benutzt, die z.B. auch direkt erweiterte
Funktionen, wie Mausradunterstützung zum Auf- und Abblättern, unterstützt. Die
Rollbalken werden nur dann eingeblendet, wenn sie benötigt werden. Also dann, wenn
nicht mehr alle Daten auf einmal visualisiert werden können. Diese Java-Klassen wurden ebenso bei der Realisierung der anderen Ansichten benutzt.
92
Tooltipps wurden auch für die Tabellespalten hinterlegt, so dass der unerfahrene Anwender sofort eine Hilfe angeboten bekommt. Weiter ist es möglich, die Tabelle nach
Belieben umzusortieren. Dies kann einmal durch einen Doppelklick mit der linken
Maustaste auf den entsprechende Spaltenkopf geschehen oder aber über die zusätzliche Toolleiste dieser Ansicht. Die Symbolleiste befindet sich am oberen Rand des Ansichtsbereichs und ist, im Gegensatz zur Hauptsymbolleiste, nicht frei bewegbar.
Die zusätzliche Symbolleiste bietet nur Befehle an, die sich einzig auf die Deklarationen bzw. deren Darstellung auswirken. Die Befehle und Aktionen sind ebenso über die
Menüleiste und dort über den kontextabhängigen Menüpunkt „Schema“ ausführbar.
Zu den zusätzlichen Befehlen zählt das bereits oben erwähnte Sortieren. Beim Sortieren existiert die Möglichkeit entweder absteigend oder aufsteigend zu sortieren. Weiter
kann man die markierten Deklarationen aus der Datenbank löschen. Dabei werden
auch alle Fakten und Regeln der Relation automatisch mitgelöscht. Außerdem können
auch alle Delta-Relationen, die ggf. im Schema gespeichert wurden, automatisch gelöscht werden. Dazu müssen sie nicht gesondert durch den Benutzer selektiert werden,
sondern es muss lediglich der entsprechende Befehl aufgerufen werden.
Der letzte und vielleicht wichtigste Befehl, der direkt über die zusätzliche Symbolleiste ausführbar ist, dient zum Anlegen einer neuen Deklaration. Dafür wird ein neues
Dialogfenster (siehe Abbildung 6.3) geöffnet. Hier kann analog zur Tabelle ein neuer
Eintrag erstellt werden. Beim Bestätigen durch die „Add“-Schaltfläche überprüft das
Datenbanksystem automatisch, ob eine solche Deklaration angelegt werden kann. Sollte dies nicht der Fall sein, wird der Dialog nicht geschlossen und der Anwender wird
durch ein Nachrichtenfenster über den Fehler informiert. Dann kann er sich entscheiden, ob er die angegebene Deklaration verbessern oder verwerfen („Cancel“Schaltfläche) will.
Abbildung 6.3: Deklarationsdialog
Das Dialogfenster zum Erstellen von neuen Deklarationen ist kein eigenständiges
Fenster sondern ein internes Fenster (JInternalFrame), das nur in der Schemaansicht
sichtbar ist und auch nicht aus dem Hauptanzeigebereich herausbewegt werden kann.
Beim Wechsel zu einer anderen Ansicht wird es für den Benutzer versteckt, bei Rückkehr zur Schemaansicht wird es ggf. wieder automatisch aufgebaut. Dadurch gehen
keine ungespeicherten Daten verloren.
93
Die Befehle und die zugehörigen Symbole auf der Symbolleiste der Schemaansicht
sind noch einmal in Abbildung 6.4 erläutert.
Deklaration
hinzufügen
Absteigend
sortieren
Aufsteigend
sortieren
DeltaRelationen
löschen
Selektierte
Relationen
löschen
Abbildung 6.4: Symbolleiste der Schemaansicht
Weiter ist noch ein Popup-Menü für die Schemaansicht realisiert worden, über das die
Befehle „Löschen der markierten Deklarationen“ und „Hinzufügen einer neuen Deklaration“ ausgeführt werden können.
In der Informationsleiste wird die Anzahl aller Deklarationen, die zu der geladenen
Datenbank gehören, angezeigt. Dies ist besonders dann von Interesse, wenn nicht mehr
alle Deklarationen gleichzeitig auf dem Bildschirm dargestellt werden können.
6.1.2 Regeln
Wechselt der Benutzer in die Regelansicht hat er die Möglichkeit die Regeln zu bearbeiten, dazu wird im zusätzlichen Anzeigebereich unter dem Hauptmenü eine Liste
aller im Schema enthaltenen Relationen angezeigt. Mit Hilfe dieser Liste spezifiziert
der Benutzer, welcher Regelsatz angezeigt werden soll. Dazu muss die entsprechende
Relation mit der Maus ausgewählt werden. In der Hauptanzeigefläche werden dann
nur die Regeln der gewählten Relation angezeigt. Diese können editiert, erweitert oder
gelöscht werden. Der Wechsel zu einer anderen Relation ist jederzeit durch das Auswählen einer neuen Relation im Nebenanzeigebereich möglich.
Die Visualisierung von Listen wird in Java durch die Klasse JList und die abstrakte
Klasse AbstractListModel unterstützt. Der Inhalt des Hilfsfensters wird durch die im
Rahmen dieser Diplomarbeit implementierten Klasse RelationsList angezeigt, die auf
einem JList-Objekt basiert. Die Klasse RelationsList wird nicht nur in der Regelansicht
genutzt um eine Liste von Relationen anzuzeigen und ist daher noch durch einige intern zu setzende Parameter für die verschiedenen Aufgaben konfigurierbar. So kann
beispielsweise angegeben werden, welche Art von Relationen angezeigt werden sollen
und ob eine Auswahl möglich ist bzw. Auswirkungen auf andere Anzeigeflächen hat.
Im Hauptfenster werden die verschiedenen Regeln dann einfach in normalen Textfeldern dargestellt. Zusätzlich wird vor jeder Regel noch ein Kontrollkasten angezeigt,
der ausschließlich zum Selektieren einiger oder aller Regeln dient. Anschließend kann
man dann die selektierten Regeln entfernen.
94
Das Hinzufügen einer neuen Regel kann über die Tastenkombination „Strg + A“ oder
den entsprechenden Menüeintrag der Menüleiste erfolgen. Es wird dann eine neue
Musterregel am Ende der Liste eingefügt. Diese Musterregel besteht aus dem Relationsbezeichner und einer Parameterliste, gefolgt von dem Zuweisungspfeil „<-“ und
einem leeren Regelrumpf. In der Parameterliste wird an jeder Stelle eine neue, durch
eine fortlaufende Nummer beschriebene, Variable gesetzt. Das Textfeld der neuen Regel wird aktiviert und der Cursor ans Ende der Musterregel gestellt. Der Anwender
muss nun die Regel vervollständigen und ggf. die Variablennamen im Regelkopf
durch neue „sprechende“ Namen ersetzen.
Beim Verlassen eines Regelfeldes wird die aktuelle Eingabe an das DBMS übergeben.
Dieses hat dann die Aufgabe zu prüfen, ob es sich um eine gültige Regel handelt und
ob diese in das Datenbankschema geschrieben werden kann. Ist dies nicht der Fall,
kann der Benutzer entscheiden, ob die gemachten Änderungen verworfen werden oder
er die fehlerhafte Regel weiter editieren und verbessern will. Unter „dem Verlassen
eines Feldes“ versteht man den Verlust des Fokus der graphischen Anwendung. Liegt
der Fokus einer Anwendung auf einem Textfeld, so ist dort der Cursor sichtbar.
Alle bereits erwähnten zusätzlichen Befehle der Regelansicht sind wieder über eine
eigene Symbolleiste auswählbar. Die genaue Bedeutung der Symbole ist in Abbildung
6.5 erklärt.
Regel
hinzufügen
Alle Regeln
selektieren
Keine Regel
selektieren
Selektierte
Regeln
entfernen
Abbildung 6.5: Symbolleiste der Regelansicht
Die Informationsleiste der Regelansicht zeigt die Anzahl der zur gewählten Relation
gehörenden Regeln an. Um auch hier mehr Regeln effizient anzeigen zu können als
auf einmal auf dem Bildschirm gezeigt werden können, findet die Ausgabe im Hauptanzeigefenster wieder mithilfe eines JScrollPane-Objekts statt. Also verfügt auch diese Ansicht über Rollbalken.
6.1.3 Fakten
In der Faktenansicht wird im Hilfsbereich, genau wie bei der Regelansicht, wieder eine
Liste von Relationen angezeigt. Allerdings stehen nun nur Basisrelationen zur Auswahl. Anders als bei der Regeldarstellung ist es aber möglich, mehre Faktenmengen
von unterschiedlichen Relationen gleichzeitig anzuzeigen. Dies geschieht durch das
Auswählen neuer Relationen im Nebenanzeigebereich. Für die Anzeige werden im
95
Haupansichtsbereich zu jeder gewählten Relation interne Fenster geöffnet, die an diese
spezielle Ansicht gebunden sind. Die Implementierung und das Verhalten entsprechen
dem oben vorgestellten Deklarationsdialogfenster. Damit kann der Benutzer, anders
als in der Regelansicht, die Fakten mehrerer Relationen gleichzeitig auf dem Bildschirm anzeigen (siehe Abbildung 6.7). Es ist ihm aber nicht möglich, zweimal die
Faktenmenge der gleichen Relation auf einmal zu visualisieren. Dies würde zum einen
zu einer unübersichtlichen Darstellung für den Benutzer führen, zum anderen wäre ein
Datenaustausch bzw. eine Änderungsweitergabe zwischen den internen Fenstern nötig.
Dies würde die graphische Oberfläche unnötig verlangsamen und ihre Struktur deutlich komplexer machen, was eine potentielle Erweiterung ggf. erschweren könnte.
Die zur Faktenansicht gehörende Symbolleiste ist in Abbildung 6.6 erklärt und dient
nur zur Verwaltung der internen Fenster. Es sind also keine Veränderungen an den
Daten der Datenbank über sie möglich.
Alle Fenster
minimieren
Alle Fenster
maximieren
Alle Fenster
wiederherstellen
Alle Fenster
schließen
Abbildung 6.6: Symbolleiste der Faktenansicht
Für die Darstellung der Fakten in den internen Faktenfenstern wird eine Tabelle genutzt. Diese besteht nur aus den eingetragenen Konstanten. Der Relationsname wird
nicht extra in der Tabelle aufgeführt und muss somit auch nicht durch den Benutzer
eingegeben werden. Diese Vorgehensweise ist möglich, da in jedem internen Fenster
nur die Fakten einer Relation, die im Fenstertitel spezifiziert wird, angezeigt werden.
Für die Implementierung wurden wieder die Klasse JTable und alle zugehören Klassen
verwendet. Das standardmäßige Umgestallten der Tabelle (Verschieben der Spaltenpositionen) musste dabei natürlich deaktiviert werden. Dies ist nötig, da in Datalog
keine Spaltenbezeichner existieren und somit anderenfalls keine eindeutige Darstellung mehr möglich wäre.
Als zusätzliche Information wird als Spaltenüberschrift der vermutete Datentyp der
entsprechenden Spalte angezeigt (in Abbildung 6.7 ist es z.B. der Zahlentyp „Double“). Dazu wird geprüft, ob alle Konstanten einer Spalte zum selben Datentyp gehören.
Diese interne Bestimmung des Datentyps durch die Datenbank ist für die Nutzung der
Built-In-Operationen zwingend nötig (siehe Abschnitt 2.1.2.2 und Abschnitt 5.1.1).
Wie bereits beschrieben wird die zusätzliche Information aber nicht dazu benutzt, um
die Eingabe eines solchen Typs zu erzwingen. Sie dient lediglich als Hinweis für den
Benutzer.
Weiter besteht, wie bei der Deklarationstabelle der Schemaansicht, die Möglichkeit,
die Fakten nach den verschiedenen Spalten der Tabelle umzusortieren. Außerdem ist
96
natürlich auch das Löschen, Einfügen und Editieren von Fakten möglich. Beim Einfügen wird allerdings kein Eingabedialog geöffnet, sondern der Tabelle eine leere Zeile
hinzugefügt. Allerdings darf immer nur eine leere Zeile in der Tabelle enthalten sein
(wegen der Mengenorientiertheit von Datalog). Wird am Ende der Tabelle das Editieren einer Zelle durch die „Enter“-Taste abgeschlossen, wird automatisch eine neue
Zeile angefügt und der Cursor in diese gesetzt. Der Benutzer kann also bequem viele
neue Fakten nacheinander eingeben.
In der Statusleiste der internen Fenster werden der Relationstyp und die Anzahl der
Fakten angezeigt. Der Relationstyp wird angegeben, um ggf. auch Fakten anderer Relationen in dieser Ansicht anzeigen zu können.
Auch in den internen Faktenfenstern gibt es zur schnellen Befehlsauswahl ein PopupMenü, das durch Drücken der rechten Maustaste angezeigt wird.
Abbildung 6.7: Faktenansicht mit geöffneten Faktenfenstern
Zwar sind die Tabellen der internen Fenster auch wieder mit Rollbalken versehen
(vergleiche Abbildung 6.7), allerdings ist dies beim Hauptanzeigebereich nicht möglich. Dies hat mit dem potentiellen Maximieren der Fenster zu tun. Dies ist dann zwar
immer noch möglich, aber würde nicht immer zum gewünschten Effekt führen. Durch
die Rollbalken würde der potentielle Anzeigebereich unter Umständen größer als der
aktuell angezeigte Bereich. Das Maximieren der Fenster würde zu große Fenster erzeugen, die nicht mehr richtig bzw. vollständig angezeigt werden könnten. Um dieses,
97
für den Benutzer unerwartete, Verhalten nicht zu erzeugen, ist auf das Hinzufügen von
Rollbalken im Hauptanzeigebereich verzichtet worden.
Die Toolleiste der internen Faktenfenster ist mit all ihren Symbolen in Abbildung 6.8
noch einmal vergrößert dargestellt und mit Erklärungen versehen worden.
Aufsteigend
sortieren
Fakt
hinzufügen
Absteigend
sortieren
Zum Anfang
scrollen
Selektierte
Fakten
löschen
Zum Ende
scrollen
Abbildung 6.8: Symbolleiste der Faktenfenster
6.1.4 Integritätsbedingungen
In der Ansicht zur Darstellung der im Schema definierten Integritätsbedingungen hat
der Hauptdarstellungsbereich den gleichen Aufbau wie in der Regelansicht. Allerdings
werden immer alle Integritätsbedingungen auf einmal angezeigt. Die Liste der im
Schema definierten Relationen im Nebenbereich dient daher auch nicht zur Auswahl
einer Teilmenge von Integritätsbedingungen. Sie soll dem Anwender nur einen Überblick über die Stelligkeit der Relationen geben. Damit ist er nicht gezwungen diese
Informationen in einer anderen Ansicht zu suchen.
Auch die zusätzliche Symbolleiste dieser Ansicht besitzt die gleichen Symbole wie die
der Regelansicht. Sie führen auch die äquivalenten Befehle auf den Integritätsbedingungen durch. Der Vollständigkeit wegen ist sie noch einmal in Abbildung 6.9 dargestellt und erklärt.
IB
hinzufügen
Alle IB
selektieren
Keine IB
selektieren
Selektierte IB
entfernen
Abbildung 6.9: Symbolleiste der Integritätsansicht
Wichtig ist, dass wie in allen anderen Ansichten die graphische Oberfläche keine Plausibilitätsprüfungen durchführt, sondern diese an das Datenbankmanagementsystem
übergibt. In diesem ist aber, wie in Abschnitt 5.4.3 erläutert, kein Automatismus implementiert, der die Einhaltung von Integritätsbedingungen überwacht. Somit haben
98
auch Veränderungen an ihnen keinen direkten Einfluss auf die Fakten und Regeln der
Datenbank. Bei einer Erweiterung des zugrunde gelegten Datenbankmanagementsystems um eine solche Funktionalität sind aber keine Änderungen an der Implementierung der graphischen Oberfläche nötig. Dies liegt daran, dass die graphische Oberfläche bereits jetzt einen Befehl zum Prüfen der Korrektheit der Integritätsbedingung an das
DBMS schickt, der zurzeit nur eine syntaktische Prüfung enthält, aber auf Seiten des DBMS
beliebig erweitert werden kann.
Je nach Einstellungen der Datenbank ist es sogar jetzt schon möglich, das Einfügen
von Integritätsbedingungen zu verhindern, gegen die im aktuellen Datenbankzustand
verstoßen werden würde.
6.1.5 Stratifikation
Die Stratifikationsansicht unterscheidet sich im Vergleich zu den bisher vorgestellten
Ansichten im Wesentlichen um zwei Punkte. Zum einen hat der Benutzer keine Möglichkeit, die gezeigten Daten dieser Ansicht zu verändern, zum anderen werden keine
Daten im Nebenanzeigebereich dargestellt. Bei der Visualisierung im Hauptanzeigebereich wird für alle Relationen eine normale Java-Schaltfläche (Klasse JButton) benutzt. Für jede Schicht wird im Anzeigebereich ein eigenes Feld erstellt, welches
durch einen Rahmen gekennzeichnet ist. Die unterste Schicht wird dabei ganz unten
angezeigt. Anschließend werden die Relationen in den entsprechenden Schichten
durch die erzeugten Schaltflächen dargestellt. In Abbildung 6.10 ist die Darstellung
einer zweischichtigen Stratifikation sichtbar.
In ihren jetzigen Implementierungen sind sowohl das DBMS, als auch die graphische
Oberfläche in Bezug auf Stratifikationen stark eingeschränkt. Dabei sind die Einschränkungen der graphischen Oberfläche vor allem in der eingeschränkten Funktionalität des Datenbankmanagementsystems begründet. Sollte dieses um Algorithmen zur
Bestimmung unterschiedlicher Stratifikationen und/oder um einen effizienten Algorithmus zur Validierung einer gegebenen Stratifikation erweitert werden, müssten diese beispielsweise über eine zusätzliche Symbolleiste für den Nutzer der graphischen
Oberfläche nutzbar gemacht werden. Das Codegerüst für eine solche Symbolleiste ist
auch schon Teil der im Rahmen dieser Arbeit entwickelten Java-Klassen.
Weiter könnte man dem Benutzer erlauben, individuelle Schichtungen zu erzeugen.
Daher wurde auch die obige Darstellungsform gewählt. Das Programm könnte leicht
so erweitert werden, dass die Schaltflächen, welche eine Relation repräsentieren, einfach per Drag&Drop in eine andere Schicht gezogen werden können. Anschließend
müsste natürlich eine Validierung stattfinden.
99
Abbildung 6.10: Hauptbereich mit zweischichtiger Stratifikation
Als Tooltipptext der einzelnen Schaltflächen werden alle zu dieser Relation gehörenden Regeln angezeigt. Dies ermöglicht es dem Benutzer, diese Informationen zu erhalten, ohne die Ansicht wechseln zu müssen. Besonders wichtig ist diese Hilfestellung
der graphischen Oberfläche für die potentielle Erweiterung um die oben erwähnte
Drag&Drop-Funktionalität.
6.2 Anfrageeditor
Im Anfrageeditor hat der Benutzer die Möglichkeit, eine Anfrage an das System zu
stellen. Dazu wird wieder ein internes Dialogfenster über einen der Schaltflächen der
Symbolleiste geöffnet. Dabei wird als mögliche Anfrage immer die zuletzt gestellte
Anfrage vorgegeben. Damit kann der Anwender deutlich schneller kleinere Veränderungen vornehmen oder Fehler in seinen Anfragen beheben. Wurde in der aktuellen
Sitzung noch keine Anfrage gestellt, wird die leere Anfrage „{( ): }?“ eingefügt, um
dem Benutzer wenigstens die Klammerung vorzugeben.
Das Dialogfenster bietet bei der jetzigen Implementierung im Gegensatz zum Deklarationsfenster keine Unterstützung bei der Eingabe einer Anfrage. Einzig die Anzeige
aller Relationen und deren Stelligkeit im Hilfsanzeigebereich stehen dem Benutzer als
Hilfsmittel wieder zur Verfügung. Die Anfrage ist in gewöhnlicher Datalog-Syntax zu
formulieren und komplett über die Tastatur einzugeben. Zurzeit sind nur Mengenanfragen an das System möglich. Anschließend wird durch Drücken der Entertaste oder
Betätigen der „Compute“-Schaltfläche die Anfragebeantwortung gestartet. Alternativ
kann der Benutzer die Eingabe auch über die „Cancel“-Schaltfläche abbrechen oder
die bereits eingetragne Anfrage mit der „Clear“-Schaltfläche durch die leere Anfrage
„{( ): }?“ ersetzen.
Kann die Anfrage durch das DBMS beantwortet werden, d.h. handelt es sich um eine
syntaktisch korrekte Anfrage, wird im Hauptanzeigebereich, ähnlich wie bei der Faktenanzeige, ein neues inneres Fenster geöffnet und darin das Ergebnis visualisiert. Der
Fenstertitel ist nicht wie bei den internen Faktenfenstern der Relationsname, sondern
einfach die Anfrage selbst. In der Ergebnistabelle werden dann alle Substitutionen für
die Zielliste angezeigt. Die Statusleiste des internen Fensters gibt Aufschluss über die
Anzahl der gefundenen Ergebnisse. Bei einer leeren Zielliste, also einer entarteten
Mengenanfrage, die erfolgreich war, wird immer das Ergebnis Leeremenge („{ }“)
zurückgegeben. Die Anzahl der Ergebnisse ist dann eins. Konnte keine gültige Antwort gefunden werden, ist die Tabelle komplett leer und es wird angezeigt, dass null
100
Ergebnisse gefunden werden konnten. In Abbildung 6.11 sind einige unterschiedliche
Antworten zu verschiedenen Anfragen abgebildet.
Abbildung 6.11: Anfrageeditor mit einigen Beispielen
Die Symbolleiste hat die gleichen Befehle zur Steuerung der internen Fenster wie die
Symbolleiste der Faktenansicht. Zusätzlich existiert noch ein Symbol zum Öffnen des
Dialogfensters. Das Symbol ist in Abbildung 6.11 markiert.
6.3 Statistische Informationen
Die Statistikansicht dient nur der Informationsausgabe, d.h. in dieser Ansicht können
keine Änderungen vorgenommen werden. Die Aufgabe der Statistikansicht besteht vor
allem darin, die Leistungsfähigkeit der unterschiedlichen Algorithmen zu verdeutlichen. In der Statistikansicht kann sich der Anwender Informationen über die Größe der
Datenbank bzw. die Anzahl der unterschiedlichen in der Datenbank gespeicherten Objekte anzeigen lassen. Die Werte beziehen sich dabei auf die gerade im Speicher befindlichen Daten und entsprechen nicht unbedingt den Werten nach einer Fixpunktiteration. Angezeigt werden diese Werte im Nebenanzeigebereich. Im Hauptanzeigebereich hingegen werden statistische Informationen über die letzte ausgeführte Operation
angegeben, sowie die gerade in der Datenbank enthaltenen Fakten. Diese müssen wieder nicht vollständig oder korrekt sein, da keine Fixpunktoperation vor der Visualisierung angestoßen wird. Bei den statistischen Werten der letzten Operation handelt es
101
sich zurzeit allerdings nur um Informationen über die letzte Fixpunktiteration. Dabei
ist es egal, ob die FPI im Rahmen einer Anfragebeantwortung oder der Integritätsprüfung durchgeführt worden ist, oder ob sie direkt vom Benutzer angestoßen wurde.
Wird die FPI direkt vom Benutzer gestartet, so wird zusätzlich ein neues Fenster geöffnet, um die Ergebnisse der Fixpunktiteration anzuzeigen. Dazu gehören dann auch
die statistischen Werte. Jede Ausgabe findet dabei in einem neuen Fenster statt. Dies
ermöglicht den direkten Vergleich zwischen verschiedenen Algorithmen und deren
Resultaten. Diese Resultatfenster werden auch dann geöffnet, wenn sich der Benutzer
nicht in der Statistikansicht befindet.
Um die Ausgabe in den Textfeldern zu formatieren, werden die Texte automatisch als
HTML-Text vom Programm erzeugt. Diese Generierung könnte man auch zum Erstellen einer Ausgabedatei nutzen. In dieser könnte dann beispielsweise der Verlauf und
die Nutzung der verschiedenen Operationen einer Sitzung protokolliert werden. Diese
Daten könnten bei späteren Erweiterungen herangezogen werden, um zu bestimmen,
welche Operationen optimiert werden müssen, da sie statistisch gesehen besonders oft
von den verschiedenen Anwendern ausgeführt werden. Außerdem ließen sich so empirische Daten über die Effizienz der unterschiedlichen Algorithmen gewinnen.
6.4 Statusfenster
Im einleitenden Abschnitt dieses Kapitels wurden bereits die Funktionalitäten der
Symbolleiste erklärt. Dazu gehören auch das Öffnen des Options- und des Integritätsfensters. Beide werden im Folgenden erläutert. Das besondere an ihnen ist, dass sie
zum einen keine internen Fenster sind und zum anderen auch nicht an eine bestimmte
Ansicht gebunden sind. Es ist also möglich, sie jederzeit zu öffnen und zu benutzen.
Vor allem bleiben sie auch bei Sichtwechseln aktiv und sichtbar.
Das Integritätsfenster hat, anderes als der Hauptansichtsbereich der Integritätsansicht,
nicht die Aufgabe, die Integritätsbedingungen zu verwalten, sondern soll alle Integritätsbedingungen anzeigen, gegen die in der aktuellen Datenbankausprägung verstoßen
wird. Diese Ausgabe geschieht wieder mit Hilfe eines in HTML formatierten Textes.
In Abbildung 6.12 ist ein Integritätsfenster abgebildet. In ihm ist zu erkennen, dass die
Datenbank das Fakt edge(1, 1) nicht enthält, obwohl dies eigentlich durch eine Integritätsbedingung vorgeschrieben ist. Ebenso ist zu sehen, dass das Fakt edge(2, 1) in der
Datenbank enthalten ist, obwohl es eine Integritätsbedingung gibt, die das verbietet.
102
Abbildung 6.12: Fenster mit Integritätsbedingungen, gegen die verstoßen wird
Obwohl das Integritätsfenster permanent geöffnet bleibt und unabhängig von der restlichen Oberfläche ist, aktualisieren sich die angezeigten Daten bei Datenbankänderungen nicht von alleine. Dies ist aus Effizienzgründen, wie in Abschnitt 5.4.3 erklärt, mit
den aktuell implementierten Methoden nicht möglich. Daher existiert die Möglichkeit
für den Benutzer, die Daten manuell aktualisieren zu lassen. Dazu existiert ein PopupMenü mit zwei Einträgen. Der erste startet erst eine neue Fixpunkberechnung und
prüft dann die Integritätsbedingungen. Der zweite Aufruf startet keine erneute Fixpunktberechnung. Er sollte dann benutzt werden, wenn der Anwender selbst eine Fixpunktberechnung durchgeführt hat oder er nur Basisfakten geändert, gelöscht oder
hinzugefügt hat und nur Integritätsbedingungen existieren, die Basisrelationen prüfen.
Über das Optionsfenster (siehe Abbildung 6.13) kann der Anwender alle möglichen
Datalog-Einstellungen vornehmen. Dazu zählt das Festlegen des zu wählenden Algorithmus zur Fixpunktbestimmung. Außerdem kann bestimmt werden, ob ggf. erstellte
Delta-Relationen nach der Operation gelöscht werden sollen. Weiter kann festgelegt
werden, ob alle temporären und materialisierten Fakten vor einer Fixpunktberechnung
gelöscht werden oder nicht. Wenn dies nicht geschieht, ist die Berechnung ggf. schneller. Es kann aber auch zu einem falschen Ergebnis führen. Genauso kostet das erneute
Berechnen einer gültigen Stratifikation zwar Zeit, ist je nachdem, ob es Veränderungen an der Datenbank seit der letzten Berechnung gab, zwingend nötig, um eine korrektes Ergebnis zu erhalten.
103
Abbildung 6.13: Optionsfenster
Einige der bereits im Optionsfenster enthaltenen Schaltflächen sind deaktiviert (grau
dargestellt), da ihre Funktionalität seitens des DBMS nicht implementiert wurde. Sie
sollen wieder eine möglichst unkomplizierte Erweiterbarkeit ermöglichen bzw. aufzeigen.
Optionen, die logisch zu einem bestimmten Aufgabenfeld gehören, sind mit einem
zusätzlichen Rahmen zusammengefasst. Außerdem existieren auch in diesem Fenster
die üblichen Tooltipps.
104
Kapitel 7
Beispielanwendung des DatalogLabs
In diesem Kapitel der Umgang mit dem DatalogLab an einem kleinen Beispiel verdeutlicht. Dazu wird erklärt, wie eine neue Datenbank erstellt und schrittweise mit Informationen gefüllt wird.
Zum Erstellen einer neuen Datenbank kann entweder die entsprechende Schaltfläche
der Symbolleiste oder der Eintrag „New DB“ im Menü „File“ benutzt werden. Alternativ kann der Befehl auch über das Tastenkürzel „Strg + N“ aufgerufen werden. Das
System erstellt daraufhin eine neue, komplett leere Datenbank. Anschließend sollte der
Benutzer in die Schemaansicht wechseln, wenn diese nicht schon aktiv ist. Hier können neue Relationen definiert werden.
Als Beispiel soll nun eine Datenbank realisiert werden, die die Darstellung eines Graphen repräsentiert. Dazu muss zuerst eine zweistellige Basisrelation mit dem Namen
„edge“ anlegt werden. Dies kann entweder über die zusätzliche Menüleiste der Schemaansicht, über den Eintrag „Add declaration“ im Menü „Schema“, oder über die entsprechende Tastenkombination „Strg + A“ erfolgen.
Im erscheinenden Dialogfenster muss der Haken „stored“ gesetzt sein, während der
Haken nach „derived“ nicht gesetzt sein darf. Im Feld „Relation name“ muss der Relationsname „edge“ und im Feld „Arity“ die Stelligkeit („2“) eingetragen werden. Das
Betätigen der Schaltfläche „Add“ führt bei einer syntaktisch korrekten Eingabe zum
Einfügen der spezifizierten Relation.
Nach dem gleichen Schema sollen nun noch die einstellige, abgeleitete Relation „nodes“ und die zweistellige, abgeleitete Relation „path“ eingefügt werden. Für abgeleitete Relationen muss im Dialogfenster der Haken „derived“ gesetzt sein. Die Relation
„nodes“ soll später alle Knoten des Graphen ausgeben. Die Relation „path“ soll hingegen alle möglichen Knotenpaare angeben, für die ein Pfad existiert, über den der zweite Knoten vom ersten Knoten aus erreicht werden kann.
Das Ergebnis der drei Deklarationseinfügungen ist in Abbildung 7.1 dargestellt.
105
Abbildung 7.1: Neueingefügte Deklarationen
Um die neu eingegebenen Daten nicht zu verlieren, sollen sie nun gespeichert werden.
Dazu kann z.B. der Eintrag „Save DB“ des Menüs „File“ benutzt werden. Es öffnet
sich ein Dateibrowser. In ihm lassen sich entweder alle Dateien im Dateisystem anzeigen oder nur solche, mit den für das Programm bekannten Datalog-Dateiendungen
(„.sc“, „.db“ und „.txt“). Endet die Datei mit „.txt“ wird die normale DatalogTextausgabe benutzt, sonst wird die erweiterte interne Speicherung gewählt. Die Endung wird dann auch automatisch auf „.sc“ und „.db“ angepasst. Als Dateiname kann
nun beispielsweise „BSP6“ eingegeben werden. Der Speicherort ist beliebig. War die
Operation erfolgreich, wird nun in der Statusleiste des Hauptfensters nicht mehr „Database: new“ angegeben, sondern der Pfad und der Dateiname, unter dem die Datenbank gespeichert wurde. In Abbildung 7.2. ist eine mögliche Anzeige der Statusleite
abgebildet.
Abbildung 7.2: Statusleiste Hauptfenster
Nachdem mindestens eine Basisrelation angelegt wurde, kann man neue Fakten zu
dieser Relation eingeben und in der Datenbank speichern. Dazu muss in die Faktenansicht gewechselt werden. Am einfachsten geschieht dies über die Schaltfläche „Facts“
des Hauptmenüs. Im unteren Anzeigebereich befindet sich eine Liste aller Basisrelationen. Um nun Fakten zu einer Basisrelation eingeben zu können, muss durch einen
Doppelklick auf die Relation im unteren Anzeigebereich ein entsprechendes Fenster
geöffnet werden. In dem neuen internen Fenster im Hauptanzeigebereich kann dann
die Eingabe erfolgen. Wenn noch kein Fakt in der entsprechenden Basisrelation eingegeben wurde, muss ein neues Fakt über die Schaltfläche „Add Fact“ (grünes Pluszeichen) der Symbolleiste des internen Fensters eingefügt werden. Nun können in der
Tabelle zellenweise neue Werte eingegeben werden. Wird in der letzten Zelle der letz106
ten Zeile die Eingabe mit „Enter“ bestätigt, wird automatisch eine neue Zeile angefügt
und zum Editieren ausgewählt.
Der durch die Datenbank darzustellende Graph ist in Abbildung 7.3 visualisiert. In die
Datenbank müssen also die Fakten edge(1, 2), edge(2, 3), edge(3, 4), edge(4, 2) und
edge(5, 5) eingefügt werden.
1
2
3
4
5
Abbildung 7.3: graphische Darstellung des Beispiels
Die Navigation in der Faktentabelle ist mit der Maus oder über die Tastatur (Pfeiltasten) möglich. Nachdem die Fakten erfolgreich eingefügt wurden, sollte die graphische
Oberfläche wie in Abbildung 7.4 aussehen. Es dürfen sich keine leeren Fakten mehr in
der Tabelle befinden, da diese sonst auch zur Faktenableitung herangezogen würden.
Die Reihenfolge der Fakten spielt selbstverständlich keine Rolle.
Abbildung 7.4: Eingefügte Fakten
Nun kann die Faktenansicht wieder verlassen werden. Dazu muss dass interne Fenster
nicht geschlossen werden. Da die Daten schon während der Eingabe, genauer gesagt
jedes Mal dann, wenn eine Zelle verlassen wird, geprüft und an die Datenbank weitergegeben werden. Aber sie werden natürlich nicht automatisch in der ausgewählten Datenbankdatei gesichert. Eine solche Sicherung würde nur beim Beenden des Programms bei aktivierter automatischer Speicherung erfolgen. Ansonsten wird der Be107
nutzer beim Beenden gefragt, ob dies geschehen soll. Verneint er diese Anfrage oder
bricht den ggf. folgenden Dateiauswahldialog ab, werden die Daten verworfen und
sind verloren.
Als nächstes sollen die Regeln der abgeleiteten Relationen erstellt werden. Dazu muss
in die Regelansicht gewechselt werden. Hier muss nun analog zur Auswahl einer Relation in der Faktenansicht eine Relation gewählt werden, um für diese neue Regeln zu
erstellen oder die vorhandenen zu editieren. Zuerst sollen die Regeln der Relation „nodes“ dem Schema hinzugefügt werden. Um das oben beschriebene Verhalten der Relation „nodes“ zu erhalten, müssen die Regeln
nodes(X) <- edge(X, _).
nodes(X) <- edge(_, X).
eingegeben werden. Die Wahl der Variablennamen ist dabei unwichtig. Sie müssen
nur an den entsprechenden Stellen gleich sein. Bei den Regeln handelt es sich also nur
um die Projektion des ersten bzw. des zweiten Parameters aus der Relation „edge“ auf
die Relation „nodes“.
Etwas interessanter sind die nötigen Regeln für die abgeleitete Relation „path“. Die
einzufügenden Regeln sind in Abbildung 7.5 zu sehen. Sie sind im Screenshot rot umrandet.
Abbildung 7.5: Eingefügte Regeln
Nach der Eingabe von Fakten und einer Regelmenge kann eine Fixpunktberechnung
gestartet werden, um die ableitbaren Fakten zu bestimmen. Grundsätzlich sollte dazu
jedoch erst die Stratifikationsansicht betrachtet werden. Beim Wechsel in diese An108
sicht fordert die graphische Oberfläche automatisch eine Stratifikation beim DBMS an
bzw. lässt eine neue Stratifikation erstellen. Da das DBMS nur über Algorithmen zur
Auswertung von stratifizierbaren Regelmengen verfügt, sollte die Stratifizierbarkeit
auf diesem Wege durch den Anwender geprüft werden. Grundsätzlich ist diese Prüfung nach jeder Regeländerung sinnvoll. Existiert keine gültige Stratifikation, so steht
in der Statusleiste des Haupanzeigebereichs der Stratifikationsansicht „Stratification
typ: not stratifiable“.
Diese Informationen helfen dem Anwender bei der Entscheidung, ob eine Fixpunktiteration überhaupt Sinn macht (nur wenn es eine gültige Stratifikation gibt) und welche
Methoden zur Berechnung genutzt werden können. Bevor nun also endlich die Fixpunktberechnung angestoßen werden sollte, empfiehlt es sich noch die Einstellungen
zur Fixpunktberechnung zu prüfen. Dazu muss in einer beliebigen Ansicht beispielsweise über die Hauptsymbolleiste das Optionsfenster geöffnet werden.
Für die erstellte Beispieldatenbank kann jeder der aufgeführten Algorithmen benutzt
werden. Es sollten aber die Standardeinstellungen, die in Abbildung 6.13 zu sehen
sind, benutzt werden. Nun kann die Fixpunktiteration gestartet werden. Als Ergebnis
erhält man ein Resultatfenster wie es in Abbildung 7.6 dargestellt ist.
Abbildung 7.6: Ergebnis der Fixpunktiteration
Alternativ kann man natürlich auch beliebige Anfragen an die Datenbank stellen. Dazu
muss zum Anfrageeditor gewechselt werden und ein Anfragedialogfenster geöffnet
werden. Beispielsweise könnte man wissen wollen, von welchen Knoten der Knoten
„4“ erreichbar ist. Die zu stellende Anfrage lautet dann etwa „{(X) : path(X, 4)}?“.
Das Ergebnis sind die Knoten „3“, „2“, „1“ und „4“ selbst.
109
Hat man die Veränderungen an der Datenbank beendet, sollte man abschließend die
Datenbank wieder speichern. Dazu muss nicht erneut der Name der Datei angeben
werden, der zuletzt benutzte Name wird bei der Wahl des „Save“-Befehls automatisch
verwendet. Durch die Auswahl des „Save DB as…“-Befehls ist es aber durchaus möglich einen neuen Dateinamen anzugeben.
Die erstellte Beispiel-Datenbank ist unter dem Namen „Bsp6.sc“ beim zugehörigen
Programm zu finden. Dort sind auch weitere Beispiele aus dieser Arbeit sowie zusätzliche Datenbanken hinterlegt, die die Möglichkeiten des Programms verdeutlichen.
110
Kapitel 8
Zusammenfassung und Ausblick
Im Rahmen dieser Diplomarbeit wurde ein mit Basisfunktionalitäten ausgestattetes
deduktives Datenbanksystem mit der zugehörigen Datenstruktur entworfen und realisiert. Dieses basiert auf der Datenbanksprache Datalog. Weiter wurde eine zugehörige
graphische Benutzerschnittstelle entwickelt und implementiert. Beides wurde weitgehend unabhängig von einander umgesetzt. Durch den modularen Aufbau des gesamt
Programms ist eine Erweiterung oder sogar ein Austausch der einzelnen Programmteile möglich. Die sichere und effiziente Kommunikation zwischen diesen beiden Komponenten erfolgt über eine eigens entworfene Schnittstelle, die als Teil der Datenbankkomponente realisiert wurde.
Für die Entwicklung des deduktiven DBMS musste zuerst die Anfragesprache Datalog
formal definiert werden. Dies ist in Kapitel 2 geschehen. Hier wurden zuerst alle wichtigen Datalog-Ausdrücke syntaktisch beschrieben. Dabei ist durchgehend die Notation
von [17] benutzt worden. Anschließend wurde die Semantik der definierten DatalogAusdrücke festgelegt. Dazu wurde auf die gängige Fixpunktsemantik zurückgegriffen.
Eine modeltheoretische Semantik wurde nicht betrachtet.
Im dritten Kapitel wurden einige Methoden zur Berechnung des Fixpunkts vorgestellt.
Dies war nötig, da die Fixpunktberechnung das Kernstück eines deduktiven Datenbanksystems ausmacht. Sie wird für die Bestimmung aller abgeleiteten Fakten verwendet und damit auch für die (naive) Beantwortung einer Anfrage oder die (naive)
Überprüfung der Integritätsbedingungen. Um die gängigen Algorithmen möglichst
effizient ausführen zu können, war es wichtig, schon vor dem Erstellen der Datenstruktur die genaue Vorgehensweise der Fixpunktoperationen zu betrachten. Dabei
wurden auch Verfahren berücksichtigt, die im Rahmen dieser Arbeit nicht implementiert werden sollten. Jedoch ohne eine genaue Betrachtung dieser Verfahren wäre die
Vorbereitung einer einfachen und effizienten Erweiterung nicht machbar gewesen.
In Kapitel 4 wurde schließlich die Anforderungsanalyse für das DatalogLab erstellt.
Mithilfe der Anforderungsanalyse wurde die Wahl der Programmiersprache Java begründet und anschließend die Systemarchitektur des gesamten Programms, sowie die
Aufgaben der einzelnen Komponenten beschrieben.
111
Im fünften Kapitel ging es um die Darstellung und Implementierung von deduktiven
Regeln. Zur Realisierung wurde dabei die Programmiersprache Java verwendet und
anhand der ersten Kapitel eine Datenstruktur auf der Basis von Datalog entworfen.
Das Ergebnis ist eine kompakte Datenstruktur, die keine redundanten Daten enthält
und die gespeicherten Daten strukturiert zur Verfügung stellt. Ebenso wurde die Umsetzung einiger der in Kapitel 3 vorgestellten Fixpunktverfahren realisiert. Außerdem
wurden weitere wichtige Bestandteile des deduktiven DBMS besprochen. Dazu zählten z.B. der Anfragemanager und der Integritätsmanager, aber auch die Datenbankschnittstelle zur Anbindung an eine Benutzerschnittstelle.
In Kapitel 6 wurde dann eine passende graphische Benutzerschnittstelle entworfen und
ihre Implementierung beschrieben. Die entworfene graphische Oberfläche greift dabei
nur auf die in Kapitel 5 beschriebene Datenbankschnittstelle zu. Die Benutzerschnittstelle ermöglicht alle im Rahmen von Kapitel 5 realisierten DBMS Komponenten zu
steuern und zu nutzen. Die graphische Oberfläche ist beim Entwurf so konstruiert
worden, dass sie möglichst leicht erweiterbar ist.
Abschließend lässt sich sagen, dass es sich bei dem implementierten Programm keineswegs um ein vollständiges deduktives Datenbankmanagementsystem mit einer völlig ausgereiften graphischen Oberfläche handelt. Dennoch ist es gelungen alle grundlegenden Funktionalitäten von Datalog zu realisieren und ein zu Lehrzwecken einsetzbares deduktives DBMS zu erstellen. Zu den wichtigsten Eigenschaften des entstandenen Programms zählen:
-
-
-
die strukturierte Architektur, welche sich im Paketaufbau von Java widerspiegelt und Erweiterungen leicht ermöglicht,
die organisierten Klassenhierarchien, wodurch es gelungen ist keine redundanten Daten mehrfach zu speichern. Ebenso wurde kein Quellcode doppelt
implementiert,
der etwas komplexere Aufbau der Datenstruktur im Vergleich zu den Realisierungen von S. Lobko [16] und V. Bill [2], der es bei Nutzung gleicher
Technologien gestattet, die Zugriffe auf Elemente der Datenstruktur (beispielsweise der Zugriff auf die Rumpfliterale einer Regel) zu verbessern,
die Speicherung der Datenbank im internen Format der Datenstruktur, welches ein deutlich schnelleres Laden erlaubt,
der zentral implementierte Parser, der durch seine Realisierung mit regulären
Ausdrücken leicht verbessert bzw. um weitere Ausdrücke ergänzt werden
kann,
die benutzerfreundliche graphische Oberfläche, welche sich durch ein flexibles Design auszeichnet und es ermöglicht, die vom Benutzer geänderten Einstellungen bei einem Neustart des Programms automatisch wiederherzustellen,
die intuitive und flexible Steuerung des Gesamtprogramms, welche die Bedienung durch Tastenkombinationen, Menü- und Symbolleisten, PopupMenüs und normale Schaltflächen ermöglicht,
und die grundsätzliche Erweiterbarkeit aller realisierten Komponenten durch
die konsequente Umsetzung objektorientierter Programmierrichtlinien.
112
Für die Weiterentwicklung des DatalogLabs existieren eine Vielzahl von Möglichkeiten. Für die Erweiterung der eigentlichen Datenbankkomponenten bietet sich die Implementierung solcher Algorithmen an, die nicht stratifizierbare Regelmengen verarbeiten können. Interessant wäre ebenso eine Erweiterung um Methode zur effizienten Anfragebearbeitung. Auch die Erweiterung des Programms um Transaktionen, lokale Regeln und im Hintergrund prüfbare Integritätsbedingungen stellt eine sinnvolle Ergänzung dar. All diese Erweiterungen sind in den entsprechenden Klassen einzubinden
bzw. zu realisieren.
Außerdem bietet die graphische Oberfläche eine Menge von potentiellen Erweiterungen und Optimierungen. Dabei kann es sich um triviale Anpassungen handeln, wie
beispielsweise die Aufnahme neuer Tastenkombinationen oder neuer Symbole, oder
um komplexe funktionale Ergänzungen, wie die Visualisierung des Abhängigkeitsgraphen. Auch bei der graphischen Oberfläche wurde, wie bei der Entwicklung und Realisierung des gesamten Pakets, darauf geachtet, eine möglichst einfache Erweiterbarkeit zu ermöglichen. Grundsätzlich sind die meisten Erweiterungen der graphischen
Oberfläche aber nur dann möglich, wenn entsprechende Methoden vom Datenbankmanagementsystem bereitgestellt werden. Dazu wurde, wenn möglich, auch schon die
nötige Vorarbeit, wie in Kapitel 5 beschrieben, geleistet.
Im Gegensatz zu den Vorbereitungen im Datenbankmanagementsystem sind die Vorbereitungen für eine potentielle Weiterentwicklung der graphischen Oberfläche teilweise direkt für den Anwender sichtbar. Denn nur so ist sicherzustellen, dass bei einer
Erweiterung die bereits implementierten Komponenten nicht vollständig überarbeitet
werden müssen. Dazu zählen vor allem auch die Anordnung und die Position der verschiedenen Schaltflächen und Elemente in den realisierten Fenstern. Durch die Einfügung von funktionslosen Grafikelementen ist eine spätere Umgestaltung nicht nötig.
Viele der potentiellen Erweiterungen wurden bereits kurz beschrieben. Einige Weiterentwicklungen für die graphische Oberfläche sollen aber hier noch einmal bzw. erstmals vorgestellt werden.
Die grundlegendste Erweiterung, die bereits angesprochen wurde, ist die Darstellung
des Abhängigkeitsgraphen. Sie bedarf zwar nur weniger Änderungen an den bereits
implementierten Klassen, der Umfang des neu zu generierenden Codes ist aber recht
groß. Dabei kann sich grundsätzlich an die Implementierung von Svetlana Krivorok
[14] orientiert werden.
Eine völlig andere Art der Erweiterung würde keine neuen Funktionalitäten mit sich
bringen, sondern ist die bessere Unterstützung des Benutzers beim Erstellen von Datalog-Ausdrücken. Hier wären ähnliche Vereinfachungen wie im Rahmen der Deklarationen für Regeln und Anfragen möglich. Beispielsweise könnten in Regeln durch
Drag&Drop weitere Literale aus der gegebenen Menge der Relationen hinzugefügt
werden. Noch komfortabler wäre ein zusätzliches Dialogfenster, welches das Erstellen
von relationalen Ausdrücken wie z.B. dem „natural join“, dem Durchschnitt oder der
Differenz unterstützt. Um mit diesem Dialogfenster Regeln in Datalog zu generieren,
wäre somit kaum noch die Kenntnis der eigentlichen Datalog-Syntax nötig. Der Be-
113
nutzer würde im neuen Dialogfenster nur zwei Relationen auswählen und den gewünschten Ausdruck, z.B. die Differenz, erzeugen.
Grundsätzlich könnte die gesamte graphische Oberfläche durch Drag&DropFunktionen, die Daten nicht nur innerhalb des Programms, sondern auch zu anderen
Programmen verschieben, erweitert werden.
Weiterhin nützlich wären die bereits erwähnten Erweiterungen zum manuellen Optimieren von Stratifikationen. Diese bedürfen aber, anders als die Ergänzung um ein
weiteres Dialogfenster, zuerst einmal eine Erweiterung des zugrunde liegenden Datenbankmanagementsystems (wie in Abschnitt 6.5 erklärt).
Für eine Weiterentwicklung um eine Transaktionsmanagementkomponente ist, wie
oben erwähnt, eine Erweiterung des Datenbankmanagementsystems nötig. Diese Erweiterung sollte durch eine intuitive Eingabe der graphischen Oberfläche unterstützt
werden. Von der reinen Texteingabe, wie es problemlos über das Dialogfenster für
beliebige Datalog-Ausdrücke möglich wäre, ist abzusehen. Der Grund dafür liegt in
der Fehleranfälligkeit der reine Texteingabe und des hohen Zeitaufwands. Alternativ
sollten Befehle bereitgestellt werden, mit denen der Benutzer stückweise eine Transaktion zusammenstellen kann. Diese sollte ähnlich wie eine potentielle Unterstützung
zum Erstellen neuer Regeln aufgebaut sein.
Um dem Anwender besser gezielte Hilfestellungen geben zu können, wäre auch die
Implementierung einer kontextabhängigen Hilfefunktion vorteilhaft. Diese könnte
dann beispielsweise über Popup-Menüs aufgerufen werden. Außerdem kann die statische Hilfe beliebig erweitert werden. Zurzeit beinhaltet sie nur kurze Auszüge und
Verweise aus dieser bzw. auf diese Arbeit. Denkbar wären aber ebenso eine voll
HTML-fähige Hilfe mit Links und Suchfunktionen, so wie es heute im Rahmen gängiger Programme der Fall ist. Die Grundlagen dafür sind bereits im Programm gelegt
worden.
Nützlich könnte außerdem die Erweiterung um eine vollständige Protokollierung der
ausgeführten Befehle sein. Diese Protokollierung könnte entweder auf der Datenbankebene durchgeführt werden und wäre dann wieder unabhängig von der jeweiligen Benutzerschnittstelle, andererseits könnte sie aber ebenfalls auf der Ebene der Benutzerschnittstelle selbst implementiert werden. Dies hätte den Vorteil, dass auch solche Operationen, die vom Benutzer abgebrochen oder verworfen wurden, protokolliert werden könnten.
Weiter könnte natürlich die Sammlung der statistischen Daten erweitert werden. Es
wäre z.B. denkbar, dass die Zeiten der inneren Iterationsläufe bei einer Fixpunktiteration bestimmt werden. Ferner könnten die Anzahl der zu erstellenden Relationen oder
der zu transformierenden Regeln für einige Algorithmen auch interessant sein.
Grundsätzlich könnte die graphische Oberfläche noch um einen Befehl erweitert werden, der zur aktuellen Datenbank bzw. dem aktuellen Datenbankschema nur einen
neuen Faktensatz lädt. Eine Veränderung des Schemas würde dann nicht stattfinden.
114
Der umgekehrte Fall wäre zwar ebenfalls denkbar, aber nicht unbedingt sinnvoll, da
dann fast immer auch die Faktenmenge verändert werden müsste.
Eine andere Erweiterung, die sich eigentlich nur auf die Dateiausgabe bezieht, ist die
Speicherung der zusätzlichen Informationen in Kommentaren der DatalogTextdateien. Damit wäre es möglich, auch die in der internen Darstellung zusätzlich
gespeicherten Daten im Textmodus zu sichern. Die Kompatibilität mit anderen Datalog-Programmen wäre dann immer noch gegeben, aber die zusätzlichen Informationen
würden nicht verloren gehen. Diese Änderung bezieht sich auf die Speicherarten des
Datenbankmanagementsystems. Bei der graphischen Oberfläche muss dann nur die
Möglichkeit bestehen, diese Funktion zu aktivieren.
115
Literaturverzeichnis
[1]
Behrend A.: Effiziente Materialisierung regeldefenierter Daten in PROLOG.
Diplomarbeit, Rheinische Friedrich-Wilhelms-Universität Bonn, Institut für Informatik III, 1999.
[2]
Bill V.: Entwurf und Implementierung eines Moduls zur Materialisierung regeldefinierter Daten in Datalog. Diplomarbeit, Rheinische Friedrich-WilhelmsUniversität Bonn, Institut für Informatik III, 2004.
[3]
Bry F.: Logic Programming as Constructivism: A Formalization and its Application to Databases. In: Proceedings of the 8thACM SIGACT-SIGMODSIGART Symposium on Principles of Database Systems, pages 34-50. ACM,
March 1989.
[4]
Ceri S., Gottlob G., Tanca L.: What You Always Wanted to Know About Datalog (And Never Dared to Ask). IEEE Transactions on knowledge and data engineering, vol. I, no. I, March 1989. 146-166.
[5]
Friedl J. E. F.: Reguläre Ausdrücke. 2. Auflage. Deutsche Übersetzung von
Andreas Karrer. Oreilly Verlag. April 2003.
[6]
Grant J., Minker J.: Deductive database theories. The Knowledge Engineering
Review 4(4), 1989, 267-304.
[7]
Engesser H. [Hrsg.], Claus, V. [Bearb.]: Duden Informatik: ein Sachlexikon für
Studium und Praxis. Bibliographisches Institut & F.A. Brockhaus AG. 1993.
[8]
JavaTM 2 Platform Standard Edition 5.0 API Specification. Sun Microsystems,
Inc., 2004.
[9]
JavaTM Tutorials. Sun Microsystems, Inc., 2006.
Online: http://java.sun.com/docs/books/tutorial.
[10]
JavaTM SE 6 Release Notes. Features and Enhancements. Sun Microsystems,
Inc., 1994-2007.
Online: http://java.sun.com/javase/6/webnotes/features.html
[11]
Kelter U.: Lehrmodul „Architektur von DBMS“, WS’ 2005/2006, Universität
Siegen.
116
Online: http://pi.informatik.uni-siegen.de/kelter/lehre/05w/lm/lm_dbsa_
20051021_a5.ps.gz
[12]
Kemp D. B., Srivastava D., Stuckey P. J.: Magic Sets and Bottom-Up Evaluation of Well-Founded Models. ISLP 1991.
[13]
Kemper A., Eickler A.: Datenbanksysteme 5. Auflage. Oldenbourg Wissenschaftsverlag GmbH, 2004.
[14]
Krivoruk V.: Entwurf und Implementierung einer graphischen Schnittstelle für
das DatalogLab. Diplomarbeit, Rheinische Friedrich-Wilhelms-Universität
Bonn, Institut für Informatik III, 2003.
[15]
Krüger G.: Handbuch der Java-Programmierung. 4., aktualisierte Auflage 2006.
Addison-Wesley, 2006.
HTML-Version: http://www.javabuch.de
[16]
Lobko S.: Entwurf und Implementierung einer Komponente zur Anfragebearbeitung in einem deduktiven Datenbanksystem. Diplomarbeit, Rheinische
Friedrich-Wilhelms-Universität Bonn, Institut für Informatik III, 2004.
[17]
Manthey R.: Vorlesung „Deduktive Datenbanken“, SS’ 2006, Universität Bonn.
[18]
Ramakrishnan R., Srivastava D., Sudarshan S.: Rule Ordering in Bottom-Up
Fixpoint Evaluation of Logic Programs, IEEE Transactions on Knowledge and
Data Engeniering, Vol. 6, No. 4. August 1994.
[19]
Ullenboom C.: Java ist eine Insel. Fünfte Auflage, Programmieren für die Java
2-Plattform in der Version 5, 2006.
HTML-Version: http://www.galileocomputing.de/openbook/javainsel5/.
117
Anhang
Der Anhang dieser Arbeit besteht aus einer CD mit allen zu dieser Arbeit erstellten
Dateien und Programmen.
Die CD beinhaltet:
-
die Quellcode-Dateien des DatalogLabs im Verzeichnis „\DatalogLab\src“,
die kompilierten Dateien des DatalogLabs im Verzeichnis „\DatalogLab\bin“,
die API-Dokumentation in HTML-Format im Verzeichnis „\DatalogLab\doc“,
alle erstellten Beispieldatenbanken im Verzeichnis „\examples“,
das gesamte Programm in einem lauffähigen Java-Paket (datalogLab.jar) und
einer zugehörigen Installationsanweisung im Stammverzeichnis und
außerdem noch diese Arbeit im Verzeichnis „Diplomarbeitstext“ als PDFDokument.
118
Erklärung
Hiermit erkläre ich, dass ich diese Diplomarbeit selbstständig durchgeführt und keine anderen
als die angegebenen Quellen und Hilfsmittel verwendet habe.
Bonn, 29.03.2007
Herunterladen