Entwurf und Implementierung eines SQL-DDL

Werbung
Entwurf und Implementierung eines
SQL-DDL-Präprozessors zur
Unterstützung von
Datenbankentwurfsmustern
Diplomarbeit im Fach Informatik
vorgelegt von
Olivier Wirz
Basel, Schweiz
Matrikelnummer 99-051-815
Supervisor:
Prof. Dr. Carl-Christian Kanne
Database Technology Research Group
Betreuer:
Boris Glavic
Abgabe der Arbeit: 1. März 2009
Angefertigt am
Institut für Informatik
der Universität Zürich
Zusammenfassung
In der Software-Entwicklung sind wiederkehrende Entwurfsstrukturen, sog.
Entwurfsmuster, unentbehrlich geworden. Sie stehen für gutes Design und
definieren ein Vokabular unter Entwicklern. Auch beim Entwurf von Datenbankschemata existieren wiederkehrende Muster. Allerdings sind diese
Muster weit weniger gut dokumentiert als Muster aus anderen Bereichen,
wie bspw. der objektorientierten Programmierung. Unter dem Begriff Datenbankentwurfsmuster liefert diese Diplomarbeit Beispiele von bekannten
Entwurfsmustern, die bei der Erzeugung von Schemata auf relationalen Datenbanksystemen verwendet werden. Durch Spezifikation, Entwurf und Implementierung von SQLPP (Structured Query Language Preprocessor), eines
datenbankunabhängigen SQL-Präprozessors für die Verarbeitung von Makros (sog. Makroprozessor), wird eine Möglichkeit beschrieben, wie Datenbankentwurfsmuster gespeichert und wiederverwendet werden können. Dabei
werden Datenbankentwurfsmuster als Makros definiert.
Klassifikationen gemäss ACM Computing Classification System
(http://www.acm.org/class/1998/):
D.3.3, H.2.1, H.2.3, D.3.4, D.3.2
Abstract
Design patterns have become essential in software development. They identify
good design and define a vocabulary among developers. Recurring patterns
also exist in database schema design but are not as well documented as design
patterns of other areas in software development such as e. g. design patterns
of object-oriented development. This diploma thesis provides examples of
design patterns for schema creation for relational databases. We refer to this
type of patterns as Database Design Patterns. We present SQLPP, a SQL
preprocessor for macro processing. SQLPP enables users to store and reuse
design patterns for schema creation, whereas the design patterns are defined
in macros.
Classifications according to the ACM Computing Classification System
(http://www.acm.org/class/1998/):
D.3.3, H.2.1, H.2.3, D.3.4, D.3.2
Categories and Subject Descriptors
Matching classifications according to the ACM Computing Classification System (http://www.acm.org/class/1998/)
D.3.3
[Programming Languages]: Languages Constructs and Features — Patterns
H.2.1
[Database Management]: Logical Design — Schema and Subschema
H.2.3
[Database Management]: Languages — Data Description Languages
D.3.4
[Programming Languages]: Processors — Preprocessors
D.3.2
[Programming Languages]: Language Classification — Macro
and Assembly Languages
Danksagung
Ich möchte allen herzlich danken, die in irgend einer Form zum Gelingen
dieser Diplomarbeit beigetragen haben.
Ein grosses Dankeschön geht an meinen Betreuer Boris Glavic, der immer für
mich Zeit hatte und mir stets mit guten Ratschlägen zur Seite stand. Seine
Kompetenz und konstruktiven Anregungen haben massgeblich zum Resultat
dieser Arbeit beigetragen.
Für die Gelegenheit, dieses packende Thema überhaupt bearbeiten zu dürfen, möchte ich mich bei Prof. Carl-Christian Kanne bedanken.
Ganz besonders möchte ich meinen Eltern danken, die mich nicht nur während dieser Arbeit, sondern während meines gesamten Studiums in jeder
erdenklichen Form unterstützt haben.
Des Weiteren danke ich Andri Steinmann und Werner Winkelmann sowie
meinem gesamten Umfeld.
Inhaltsverzeichnis
Inhaltsverzeichnis
I
I
Inhalt
1
1 Einleitung
3
1.1
Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3
1.2
Problemstellung und Ziel dieser Arbeit . . . . . . . . . . . . .
5
1.3
Aufbau . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
2 Terminologie und Definitionen
2.1
Entwurfsmuster . . . . . . . . . . . . . . . . . . . . . . . . . .
2.1.1
9
9
Ziele von Entwurfsmustern . . . . . . . . . . . . . . . . 10
2.2
Datenbank-Terminologie . . . . . . . . . . . . . . . . . . . . . 11
2.3
Formale Sprachen . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.3.1
2.4
Kontextfreie Grammatik . . . . . . . . . . . . . . . . . 15
Compiler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.4.1
Lexer . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.4.2
Parser . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.5
Präprozessor . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.6
Weitere Begriffe . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.6.1
Datenbank-Smells . . . . . . . . . . . . . . . . . . . . . 25
3 Datenbankentwurfsmuster
3.1
27
Musterkategorien . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.1.1
Muster zur Abbildung anderer Datenmodelle . . . . . . 28
I
INHALTSVERZEICHNIS
3.2
3.3
3.4
3.5
3.1.2
Anfrage- und Speicherplatzoptimierungsmuster . . . . . 29
3.1.3
Muster der Datenbankunabhängigkeit
Temporale Daten . . . . . . . . . . . . . . . . . . . . . . . . . 31
3.2.1
Das temporale Datenmodell . . . . . . . . . . . . . . . 31
3.2.2
Temporalrelationale Abbildung . . . . . . . . . . . . . 32
Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
3.3.1
Vererbung in SQL:1999 . . . . . . . . . . . . . . . . . . 37
3.3.2
Vererbung in PostgreSQL . . . . . . . . . . . . . . . . 39
Multidimensionale Daten . . . . . . . . . . . . . . . . . . . . . 41
3.4.1
OLAP versus OLTP . . . . . . . . . . . . . . . . . . . 41
3.4.2
Das multidimensionale Datenmodell . . . . . . . . . . . 42
3.4.3
Multidimensionalrelationale Abbildung . . . . . . . . . 43
Partitionierung . . . . . . . . . . . . . . . . . . . . . . . . . . 47
3.5.1
Horizontale Partitionierung . . . . . . . . . . . . . . . 47
3.5.2
Vertikale Partitionierung . . . . . . . . . . . . . . . . . 48
3.6
Denormalisierung . . . . . . . . . . . . . . . . . . . . . . . . . 48
3.7
Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . 49
4 Anforderungsspezifikation
4.1
51
Anforderungen . . . . . . . . . . . . . . . . . . . . . . . . . . 51
4.1.1
Akteure . . . . . . . . . . . . . . . . . . . . . . . . . . 51
4.1.2
Hauptfunktionen des Systems . . . . . . . . . . . . . . 51
4.1.3
Anwendungsfälle . . . . . . . . . . . . . . . . . . . . . 52
4.2
Nicht-funktionale Anforderungen . . . . . . . . . . . . . . . . 54
4.3
Sprachen und Plattform . . . . . . . . . . . . . . . . . . . . . 55
4.4
Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . 56
5 Lösungskonzept
57
5.1
Vorgehen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
5.2
Lösungsvorschläge . . . . . . . . . . . . . . . . . . . . . . . . . 58
5.3
II
. . . . . . . . . 30
5.2.1
Die Lösungsvorschläge im Vergleich . . . . . . . . . . . 59
5.2.2
Wahl des Lösungskonzepts . . . . . . . . . . . . . . . . 61
Funktionalität des Makroprozessors . . . . . . . . . . . . . . . 62
INHALTSVERZEICHNIS
5.4
5.5
5.6
5.3.1
SQL-Code . . . . . . . . . . . . . . . . . . . . . . . . . 63
5.3.2
Definition von Makros . . . . . . . . . . . . . . . . . . 64
5.3.3
Makroaufrufe . . . . . . . . . . . . . . . . . . . . . . . 64
5.3.4
Vermischung von zwei Sprachen . . . . . . . . . . . . . 65
5.3.5
Makros, die Makros aufrufen . . . . . . . . . . . . . . . 65
5.3.6
Einkopieren von Dateien . . . . . . . . . . . . . . . . . 66
5.3.7
Bedingte Übersetzung . . . . . . . . . . . . . . . . . . 67
5.3.8
Weitere Anforderungen . . . . . . . . . . . . . . . . . . 67
Detailentwurf . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
5.4.1
Hauptaufgaben des Makroprozessors . . . . . . . . . . 68
5.4.2
Ein 3-Phasen-Modell . . . . . . . . . . . . . . . . . . . 68
Implementierung . . . . . . . . . . . . . . . . . . . . . . . . . 75
5.5.1
Modularisierung . . . . . . . . . . . . . . . . . . . . . . 75
5.5.2
Makrosprache . . . . . . . . . . . . . . . . . . . . . . . 75
5.5.3
Statement Model . . . . . . . . . . . . . . . . . . . . . 81
5.5.4
Zyklensuche . . . . . . . . . . . . . . . . . . . . . . . . 86
Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . 87
6 Der SQLPP in Aktion
6.1
6.2
6.3
6.4
89
Beispiel 1: Archivierungstabelle . . . . . . . . . . . . . . . . . 89
6.1.1
Implementierung in SQL . . . . . . . . . . . . . . . . . 89
6.1.2
Makro . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
6.1.3
Fazit . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Beispiel 2: Star-Schema . . . . . . . . . . . . . . . . . . . . . . 98
6.2.1
Implementierung in SQL . . . . . . . . . . . . . . . . . 98
6.2.2
Makro . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
6.2.3
Fazit . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
Beispiel 3: Vererbungstabellen . . . . . . . . . . . . . . . . . . 103
6.3.1
Implementierung in SQL . . . . . . . . . . . . . . . . . 104
6.3.2
Makros
. . . . . . . . . . . . . . . . . . . . . . . . . . 109
Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . 112
III
INHALTSVERZEICHNIS
7 Diskussion und Ausblick
7.1 Ergebnisse und Zusammenfassung . . . . . . . .
7.2 Weitere Forschung . . . . . . . . . . . . . . . .
7.2.1 Katalog von Entwurfsmustern . . . . . .
7.2.2 Makro-Bibliothek . . . . . . . . . . . . .
7.2.3 Schemainformationen und Java-Makros .
7.2.4 Zusätzliche semantische Überprüfungen .
7.2.5 Integrierte Entwicklungsumgebung (IDE)
7.2.6 Graphische Entwicklungshilfen . . . . . .
7.2.7 Datenbankunabhängigkeit . . . . . . . .
II
Anhang
. .
. .
. .
. .
. .
. .
für
. .
. .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
Makros
. . . . .
. . . . .
113
. 113
. 115
. 115
. 116
. 116
. 116
. 117
. 117
. 118
119
Quellcode
121
Glossar
121
Aufgabenstellung
123
Abbildungsverzeichnis
123
Liste von Codebeispielen
125
Literaturverzeichnis
127
IV
Teil I
Inhalt
1
Kapitel 1
Einleitung
1.1
Motivation
Seit dem Erscheinen des erfolgreichen Buchs von Erich Gamma, Richard
Helm, Ralph Johnson und John Vlissides [GHJV04], welche als „Gang of
Four“ einen ersten umfassenden Katalog von Entwurfsmustern für objektorientierte Programmierung geschrieben haben, ist man sich bei der SoftwareEntwicklung der Bedeutung von Entwurfsmustern für wiederkehrende Entwurfsprobleme bewusst. Die Vorteile solcher Muster liegen auf der Hand: Sie
definieren eine Sprache unter Entwicklern, das heisst, sie steigern die Verständlichkeit und damit die Qualität von Quellcode, sie lösen Probleme, die
immer wieder auftauchen und reduzieren somit den Aufwand, sowohl beim
Forward als auch beim Re- und Reverse Engineering.
Wiederkehrende Muster gibt es aber nicht nur in der objektorientierten Programmierung, auch bei der Implementierung von Datenbankschemata
existieren Muster. Meist werden heute Schemata auf ein relationales Datenmodell abgebildet. Die verwendeten Muster sind meist abhängig vom Modell,
auf dem sie verwendet werden. Deshalb konzentriert sich diese Arbeit auf
Muster für die Umsetzung von Schemata in relationalen Datenbanken. Mir
ist bisher keine Publikation bekannt, die solche Muster umfassend katalogisiert. Auch die Verfasser von [MKB05] stellen fest, dass sich viele Arbeiten
damit beschäftigt haben, wie Entwurfsmuster in objektorientierten Daten3
1.1. Motivation
banken angewendet werden können, aber es gäbe nur wenige, die von der
Anwendung in relationalen Datenbanken berichten.
Einige Arbeiten beschäftigen sich damit, bekannte Muster aus der objektorientierten Programmierung auf relationale Datenbanken anzuwenden
(z. B. [Amb06]). In der relationalen Welt gibt es keine Objekte, Klassen und
Instanzen. Ebenso wenig sind Konzepte wie die Vererbung, Kapselung oder
Polymorphie inhärent vorhanden. Durch die weite Verbreitung von relationalen Datenbanken und objektorientierten Programmiersprachen ist es sinnvoll,
solche Abbildungskonzepte (sog. objektrelationales Mapping) zu dokumentieren und als wiederverwendbare Muster zur Verfügung zu stellen. Dies trifft
auch auf die Umsetzung von Datawarehouse-Systemen und temporalen Datenbanken zu. In Datawarehouse-Systemen werden Daten meist multidimensional modelliert, in temporalen Datenbanken stützt man sich auf temporale
Konzepte. Um diese Daten in relationalen Datenbanken zu speichern, müssen
Abbildungskonzepte verwendet werden. [Jen00] stellt fest, dass bei der Wiederverwendung von temporalen Konzepten durchaus Nachholbedarf besteht:
Accommodating the time-varying nature of the enterprise is
largely left to the developers of database applications, leading
to ineffective and inefficient ad-hoc solutions that must be reinvented each time a new application is developed. The result is
that data management is currently an excessively involved and
error-prone activity.
Die bisher erwähnten Muster haben alle den gleichen Verwendungszweck,
nämlich ein Rezept für die Abbildung von unterschiedlichen Konzepten zu
liefern (3.1.1). Es gibt aber auch andere Arten von Mustern, die bspw. zwecks
Performancesteigerung oder wegen Platzmangel Daten reorganisieren (3.1.2)
oder Muster, die die Datenbankunabhängigkeit fördern (3.1.3).
Diese Diplomarbeit wird durch den Entwurf eines Makroprozessors für
DDL-Befehle zeigen, wie Muster für die Implementierung von Datenbankschemata wiederverwendet werden können. Dadurch wird nicht nur das Verständnis des Datenbankentwurfs erhöht, sondern es wird auch der Aufwand
bei der Erstellung und Erweiterung solcher Entwürfe verkleinert. Das Schrei4
1.2. Problemstellung und Ziel dieser Arbeit
ben von SQL-Skripten zur Erstellung von Tabellen, Indexen, Schlüssel- und
Fremdschlüsselbeziehungen sowie anderen Constraints, Triggern und Funktionen kann sehr zeitaufwändig sein. Oft muss der gleiche Code in leicht
abgeänderter Form wieder und wieder neu geschrieben werden. Zum Beispiel
kann innerhalb eines Schemas zur Versionierung von Datensätzen jeweils pro
Tabelle eine Archivierungstabelle erstellt werden. Die Archivierungstabelle
speichert alle Daten, die aufgrund von Änderungen nicht mehr gültig sind.
Bei der Erzeugung eines solchen Schemas werden die Tabellen meist mit Triggern ausgestattet, um gelöschte und veränderte Daten in die Archivierungstabellen zu transferieren. Diese Trigger müssen für alle Tabellen definiert
werden, unterscheiden sich aber kaum voneinander. Bei der Implementierung
dieses Musters muss also
1. die Erzeugung der Archivierungstabelle,
2. die Erzeugung der Trigger für jede Tabelle im Schema wiederholt werden.
Deshalb besteht bei wiederkehrenden Mustern – wie in diesem Beispiel – die
Gefahr, dass Code kopiert wird, was neben dem manuellen Zusatzaufwand
auch zu Fehlern oder schlechter Namensgebung führt. Letzteres resultiert in
einem Entwurf, der nur schwer verständlich ist und somit ein wesentliches
Qualitätsmerkmal nicht erfüllt. Ausserdem erschwert duplizierter Code die
Refaktorierung von Datenbankschemata [AS06]. Aus allen bisher genannten
Gründen sollten Codeduplikate prinzipiell vermieden werden.
1.2
Problemstellung und Ziel dieser Arbeit
In dieser Diplomarbeit soll ein Werkzeug entwickelt werden, welches es ermöglicht, Entwurfsmuster für die Erzeugung von Datenbankschemata zu
speichern und wiederzuverwenden. Die Entwurfsmuster müssen in einem ersten Schritt gesammelt und analysiert werden. Diese Arbeit hat jedoch nicht
den Anspruch, einen vollständigen Katalog von Entwurfsmustern zu liefern.
Vielmehr soll mit bekannten Beispielmustern gezeigt werden, dass es solche
5
1.3. Aufbau
Entwurfsmuster gibt und dass durch ihre einmalige Definition und Speicherung mit einem Werkzeug Zeit bei der Umsetzung von Schemata eingespart
werden kann. Die analysierten Beispielmuster können in der letzten Phase
des Projekts dazu verwendet werden, das erstellte Werkzeug zu evaluieren.
Der zeitliche Rahmen des Projektes umfasst 6 Monate.
1.3
Aufbau
Die Ausarbeitung dieser Diplomarbeit ist in 7 Kapitel gegliedert:
1. Einleitung
2. Terminologie und Definitionen
3. Datenbankentwurfsmuster
4. Anforderungsspezifikation
5. Lösungskonzept
6. Präprozessor in Aktion
7. Diskussion und Ausblick
In Kapitel 2 werden die Grundlagen und Konzepte beschrieben, die für diese
Diplomarbeit relevant sind. Dieses Kapitel führt die in dieser Arbeit verwendete Terminologie ein.
In Kapitel 3 wird der Begriff Datenbankentwurfsmuster definiert. Es werden verschiedene Entwurfsmuster beschrieben und aufgezeigt, in welchem
Problemkontext sie verwendet werden. Auf einer abstrakteren Ebene der
zahlreichen Problemkontexte werden die Muster in verschiedene Kategorien
eingeteilt. Dieses Kapitel zeigt nicht im Detail, wie die jeweiligen Muster umgesetzt werden. Die Implementierung einiger Beispielmuster findet der Leser
6
1.3. Aufbau
in Kapitel 6.
In Kapitel 4 folgt eine lösungsneutrale und informale Beschreibung der Anforderungen, welche das zu erstellende Werkzeug erfüllen muss. Die Anforderungen werden dabei in funktionale und nicht-funktionale Anforderungen
unterteilt. Die Anforderungsspezifikation bildet die Grundlage des Lösungskonzepts, welches in Kapitel 5 beschrieben wird.
Kapitel 5 zeigt verschiedene Möglichkeiten auf, wie ein Werkzeug umgesetzt
werden kann, das die Anforderungen aus Kapitel 4 erfüllt. Aufgrund der Vorund Nachteile der verschiedenen Varianten wird dann ein Lösungskonzept
gewählt. Die Detaillierungstiefe dieses Konzepts wird im Detailentwurf verfeinert. Danach folgt eine Beschreibung der Implementierung der wichtigsten
Komponenten des Werkzeugs. In diesem Implementierungsteil wird auch die
Syntax der Sprache beschrieben, die notwendig ist, um Entwurfsmuster zu
definieren.
Kapitel 6 zeigt Beispielmuster in Anwendung mit dem erstellten Werkzeug.
Es wird aufgezeigt, wie Muster in der Praxis ohne Werkzeug umgesetzt werden und welche Vorteile die Einführung eines Werkzeugs bringt. Die Umsetzung der Muster mit dem Werkzeug soll dem Leser aber auch an Beispielen
veranschaulichen, weshalb für den Entwurf des Werkzeugs einige der in Kapitel 5 genannten Lösungsansätze gewählt wurden.
Kapitel 7 führt eine Diskussion über die Ergebnisse dieser Diplomarbeit und
macht Vorschläge, in welche Richtung das behandelte Thema weiter erforscht
werden könnte.
7
1.3. Aufbau
8
Kapitel 2
Terminologie und Definitionen
In diesem Kapitel werden wichtige Begriffe und Konzepte beschrieben, die
in Zusammenhang mit dieser Diplomarbeit stehen. Spezifische Begriffe und
Konzepte der Diplomarbeit werden auf den Grundlagen dieses Kapitels aufbauen und in den nachfolgenden Kapiteln beschrieben.
2.1
Entwurfsmuster
Ein Entwurfsmuster identifiziert und beschreibt ein wiederkehrendes Problem in der Softwareentwicklung. Es bietet für das wiederkehrende Problem
einen abstrahierten Lösungsvorschlag, ohne eine konkrete Implementierung
zu definieren. Das Entwurfsmuster kann auf seinen Problembereich beliebig
oft angewendet werden. Dabei kann die Implementierung jedes Mal anders
aussehen.
In [GHJV04] wurde von der „Gang of Four“ 1 um E. Gamma erstmals ein
umfassender Katalog von Entwurfsmustern für objektorientierte Programmierung zusammengestellt. Dieses Buch gilt heute als Klassiker der objektorientierten Softwareentwicklung. Die Autoren von [GHJV04] definieren vier
Grundelemente eines Entwurfsmusters:
1. Mustername: Der Name sollte das Entwurfsproblem, seine Lösungen
1
Mit „Gang of Four“ sind die vier Autoren Erich Gamma, Richard Helm, Ralph Johnson
und John Vlissides gemeint.
9
2.1. Entwurfsmuster
und Auswirkungen beschreiben. Der Name des Entwurfsmusters wird
so zum Vokabular unter Entwicklern und hilft ihnen, sich besser zu
verständigen.
2. Der Problemabschnitt beschreibt den Problemkontext, in dem das Muster angewendet werden kann.
3. Der Lösungsabschnitt beschreibt die Elemente, aus denen der Entwurf
besteht, sowie deren Beziehungen, Zuständigkeiten und Interaktionen.
In der objektorientierten Programmierung sind die Elemente Klassen,
in der relationalen Welt Tabellen (auch Entitätstypen oder Relationen
genannt) und andere Schemaobjekte.
4. Ein Konsequenzenabschnitt zeigt die Vor- und Nachteile des Musters
auf. Flexibilität, Erweiterbarkeit und Portabilität durch Einfluss des
Musters können hier ebenfalls aufgezeigt werden.
Wie bereits erwähnt, befassen sich Entwurfsmuster nicht mit der konkreten
Implementierung, um ein Problem zu lösen. Sie geben lediglich die Struktur
einer Problemlösung vor, die oft und auf viele verschiedene Arten umsetzbar
ist. Dies trifft auch auf die Muster zu, die in dieser Arbeit verwendet und
umgesetzt wurden.
2.1.1
Ziele von Entwurfsmustern
Entwurfsmuster sollen helfen, Software zu entwickeln, die wiederverwendbar,
wartungsarm und leicht verständlich ist. Durch die oben aufgeführten Elemente werden dem Programmierer Entwurfsstrukturen und ihre Absichten
vermittelt, sodass er gutes Design von erfahrenen Entwicklern wiederverwenden kann, „ohne das Rad neu zu erfinden“.
Angewandte Entwurfsmuster erleichtern die Verständlichkeit von Quellcode, weil sie durch Abstraktionen die Systemkomplexität reduzieren. Der
Entwickler versteht sofort „was hier geschieht“, wenn er ein Muster erkannt
hat. Deshalb dienen Entwurfsmuster auch als Dokumentationsmittel. Z. B.
zeigt der Kommentar „/*This is a Singelton Class*/“ mit der dazugehörigen
Singelton-Struktur auf den ersten Blick eine Entwurfsentscheidung.
10
2.2. Datenbank-Terminologie
Die Kenntnis von Entwurfsmustern kann auch das Verständnis von Klassen einer Klassenbibliothek erleichtern. Java-Entwickler, die beispielsweise
bereits Bekanntschaft mit dem Decorator-Muster gemacht haben, werden
Zweck und Anwendung der unzähligen Ein- und Ausgabestrom-Klassen in
der Java-Standardbibliothek besser verstehen, weil dort genau dieses Muster
umgesetzt wurde.
Es kommt hinzu, dass die Kommunikation durch einen Katalog von Entwurfsmustern, wie ihn die „Gang of Four“ [GHJV04] für objektorientierte Entwurfsmuster erstellt hat, unter Entwicklern erleichtert wird. Sie entwickeln
ein gemeinsames Vokabular, welches hilft, sich bspw. bei Review-Sitzungen
auf das Wesentliche zu konzentrieren, ohne sich in Details zu verlieren.
2.2
Datenbank-Terminologie
Bei der Entwicklung von Datenbanken wird ein Problembereich, also ein Ausschnitt der Umwelt (auch Miniwelt genannt), durch einen Datenbankentwurf
modelliert. Das Ergebnis des Datenbankentwurfs ist das Datenbankschema.
Das Datenbankschema abstrahiert die in der Realität vorkommenden Entitäten mit ihren Eigenschaften, Beziehungen und Verhalten [Gep02]. Das
Datenmodell beschreibt die Struktur und Semantik der Daten. Das Modell,
welches in der Praxis am häufigsten verwendet wird, ist das relationale Modell
mit mathematisch wohldefinierten Relationen als Entitäten. Diese Relationen können als Tabellen (Tables) dargestellt werden. Tabellen beschreiben
eine Abstraktion der konkreten Gegenstände aus der Miniwelt, ähnlich wie
Klassen oder Typen in einem objektorientierten Modell. Um ein Datenbankschema zu definieren wird eine Datendefinitionssprache (DDL: data definition
language) benötigt. Diese Sprache modelliert die Elemente eines bestimmten
Datenmodells.
Datenbankentwurf
Datenbankschema
Datenmodell
Datendefinitionssprache
Für die Manipulation von Daten wird eine Datenmanipulationssprache
(DML: data manipulating language) verwendet. Diese Sprache definiert die
Art und Weise, wie Datenelemente erzeugt, gelöscht, verändert und gefunden
werden können.
Zur Verwaltung einer Datenbank wird ein Datenbankverwaltungssystem Datenbankverwaltungssystem
11
2.2. Datenbank-Terminologie
(DBMS: database management system) verwendet. Es regelt den Zugriff auf
die Datenbank, indem es die DDL- und DML-Operationen verarbeitet. Das
DBMS und die Datenbank werden zusammen als Datenbanksystem (DBS:
database system) bezeichnet [Gep02].
Wie bereits erwähnt werden in der Praxis am häufigsten relationale Datenbanksysteme verwendet. Die meisten kommerziellen DBS sind relationale
Datenbanksysteme und auch in dieser Arbeit wurde mit PostgreSQL ein relationales DBS genutzt. In der Folge dieses Dokumentes wird nur noch Bezug
auf das relationale Datenmodell und diverse Erweiterungen dieses Modells
genommen, wohl wissend, dass es auch noch andere Datenmodelle gibt. Diese Einschränkung ist wichtig, da die Entwurfsmuster der Schemadefinition
meist modellspezifisch sind. Z. B. wären Abbildungskonzepte für die Vererbung von Typen für ein objektorientierten DBS überflüssig, weil die Vererbung Teil des objektorientierten Modells ist und somit von einem objektorientierten DBS unterstützt wird. Auf einer relationalen Datenbank sind solche
Abbildungskonzepte aber sinnvoll, um Daten, die durch objektorientierte Applikationen erzeugt werden, in relationalen Datenbanken zu speichern. Solche
Abbildungskonzepte werden Abschnitt 3.1.1 erläutert.
SQL
Die am häufigsten verwendete Sprache kommerzieller relationale Datenbanksysteme ist (SQL: Structured Query Language), welche sowohl Operatoren zur Definition (DDL) als auch Manipulation (DML) von Daten in relationalen Datenbanken besitzt. In SQL wird bspw. als Konstruktor für Tabellen
der DDL-Befehl CREATE TABLE verwendet. Für die Manipulation von
Daten verwendet SQL die Operatoren INSERT, UPDATE, DELETE und
SELECT.
Die theoretischen Grundlagen von SQL stammen aus der relationalen
Algebra und dem Tupelkalkül. Neben SQL existieren aber auch noch andere Sprachen für relationale Datenmodelle wie QBE (Query by Example),
QUEL (Query Language) oder Datalog. Ausser QBE, welches bspw. im DBS
Microsoft Access verwendet wird, finden diese Sprachen jedoch kaum Verwendung in kommerziellen Datenbanksystemen. SQL hat sich wohl auch deshalb
durchgesetzt, weil es im Gegensatz zu Ausdrücken in der relationalen Algebra
relativ einfach verständlich ist. Auch PostgreSQL verwendet SQL.
12
2.3. Formale Sprachen
2.3
Formale Sprachen
Ein Werkzeug, welches in der Lage ist, Entwurfsmuster auf eine Sprache abzubilden, muss diese Sprache „verstehen“, d.h. es muss die Sprache in eine
Datenstruktur übersetzen können. Um eine Sprache wie SQL zu übersetzen,
muss das Werkzeug über eine formale Sprachspezifikation dieser Sprache verfügen. Eine solche Sprachspezifikation bezeichnet man auch als Grammatik
[Par07].
Eine formale Sprache ist eine Menge von Wörtern [JEH00]. Diese Wör- formale Sprache
ter werden auch als Zeichenketten bezeichnet. Zeichenketten werden aus ei- Zeichenketten
ner endlichen Folge von Symbolen, die nicht unterbrochen ist, gebildet. Die
Menge der Symbole wird durch ein Alphabet bestimmt.
Eine Grammatik definiert Syntax und Semantik einer Sprache. Die Syntax Syntax
definiert Regeln, wie Symbole und Zeichenketten einer Sprache angeordnet
werden dürfen. Eine Sprache definiert sich aber nicht nur durch die Anordnung von Symbolen und Zeichenketten, sondern auch durch die Bedeutung,
die eine bestimmte Anordnung hat. Diese Bedeutung wird als Semantik be- Semantik
zeichnet.
Grundsätzlich ist es möglich, eine Folge von Zeichenketten der meisten
in der Informatik eingesetzten Sprachen als Sequenzen von Zuständen eines
Zustandsautomaten darzustellen. Unter Zustandsautomat versteht man in
diesem Kontext meist einen deterministischen endlichen Automaten (DFA: DFA
Deterministic Finite Automaton). Ein solcher Automat kennt eine endliche
Menge von Zuständen und ist deterministisch, weil er für jedes Eingabesym!
bol aus einem Alphabet
genau einen eindeutig festgelegten Folgezustand
besitzt [JEH06, JEH00]. Zustandsübergänge werden als Transitionen bezeichnet. Für jedes Eingabesymbol gibt es genau eine Transition, die das Symbol
bearbeitet.
Endliche Automaten können als gerichteter Graf in einem sog. Transitionsdiagramm dargestellt werden. Dabei bilden die Zustände die Knoten des
Grafen, wobei es genau einen Anfangszustand S0 und eine Menge von Endzuständen (auch akzeptierende Zustände genannt) SE gibt. Die Zustandsübergänge werden als gerichtete Kanten des Grafen dargestellt. Sie zeigen
13
2.3. Formale Sprachen
die Richtung der Zustandsänderung bei der Bearbeitung eines spezifischen
Eingabesymbols.
!
Ein DFA akzeptiert alle Zeichenketten aus dem Alphabet
, die den
Automaten von seinem Anfangszustand in einen Endzustand überführen.
Dargestellt im Transitionsdiagramm gibt es für einen DFA genau einen Pfad
pro Eingabe.
regulärer
Ausdruck
DFAs sind aber nicht mächtig genug, um jede beliebige Sprache zu modellieren. In [JEH00] wird gezeigt, dass DFAs nur Sprachen akzeptieren, die als
reguläre Ausdrücke gebildet werden können. Reguläre Ausdrücke verknüpfen
Symbole aus einem gegebenen Alphabet mit genau drei Operationen: Alternative, Verkettung und Kleene‘sche Hülle. Sind a und b Zeichenketten aus
!
dem Alphabet
, so bildet die Alternative (a|b) die Menge der Zeichenketten aus a oder b. Die Verkettung bildet die Zeichenkette (ab). Die Kleene‘sche Hülle (a∗ ) bildet die Menge aller Zeichenketten aus einer beliebigen
Folge von a. Daraus lässt sich nun die Syntax von regulären Ausdrücken
definieren[JEH00, JEH06]:
• ∅ ist ein regulärer Ausdruck und bezeichnet die leere Menge
• ε ist ein regulärer Ausdruck und bezeichnet das leere Wort
!
!
• ∀ai "
(für alle Zeichen eines Alphabets ) ist ai ein regulärer Ausdruck
• Sind a und b reguläre Ausdrücke, so sind auch (a|b), (ab) und (a∗ )
reguläre Ausdrücke
Es gibt Sprachen, die man syntaktisch korrekt als regulären Ausdruck beschreiben kann und die somit von einem DFA als Eingabe erkannt und akzeptiert werden. Solche Eingaben können dennoch semantisch falsch sein, d.h.
keinen Sinn ergeben, weil ein regulärer Ausdruck nicht unbedingt genügt, um
auch die Semantik der Sprache vollständig zu beschreiben.
Sprachen können aber auch andere Eigenschaften haben, die dazu führen, dass sie nicht als reguläre Ausdrücke dargestellt werden können. Bspw.
treten geklammerte Ausdrücke immer paarweise auf, d.h. nach n öffnenden
14
2.3. Formale Sprachen
Klammern müssten n schliessende Klammern folgen. Da der DFA ein endlicher Automat ist, kann n keine beliebige Grösse erreichen. Ein DFA hat ein
beschränktes Gedächtnis, weil er eine endliche Anzahl an Zuständen hat.
Um Programmiersprachen zu analysieren, reichen aus den genannten Gründen endliche Automaten nicht aus. Andere Mittel sind notwendig, um solche Sprachen zu beschreiben. Kontextfreie Grammatiken, die in Abschnitt
2.3.1 beschrieben werden, sind in der Lage, verschachtelte, geklammerte Ausdrücke korrekt darzustellen. Für die Analyse von kontextfreien Grammatiken
kann ein Kellerautomat (Push-Down-Automat oder Stackmaschine) verwen- Kellerautomat
det werden. Ein Kellerautomat ist ein endlicher Automat, der über einen
Kellerspeicher (auch Stack genannt) verfügt. Der Kellerspeicher ist ein „first
in - last out (FILO)“-Speicher. Werden Eingabezeichen verarbeitet, werden
sie entweder sofort verarbeitet oder in den Kellerspeicher gelegt. Aus dem
Kellerspeicher werden, wie bei einem Tellerstapler in einer Cafeteria [JEH00],
die Zeichen (Teller) ausgelesen, die zuletzt in den Speicher gelegt wurden. Ein
verschachtelter Klammerausdruck kann verarbeitet werden, weil der Kellerautomat durch den Kellerspeicher über ein Gedächtnis verfügt: Werden öffnende Klammern eingelesen, werden diese im Kellerspeicher abgelegt. Folgt
eine schliessende Klammer, wird die oberste öffnende Klammer aus dem Kellerspeicher gelöscht. Bleibt nach Abarbeitung des Eingabestroms eine Klammer im Kellerspeicher, weiss der Automat, dass die Eingabe nicht korrekt
ist.
2.3.1
Kontextfreie Grammatik
Wie wird eine Sprache, die sich mit einem Kellerautomaten analysieren lässt,
spezifiziert? Kellerautomaten sind in der Lage, kontextfreie Grammatiken zu
analysieren [JEH00, JEH06]. Eine kontextfreie Grammatik besteht aus vier
Elementen [ASU99]:
1. Einer Menge von Symbolen, auch Terminale oder Tokens genannt. Ein Symbole
Lexem ist die Zeichenfolge, aus der ein Symbol besteht.
2. Einer Menge von Nichtterminalen (diese Nichtterminale kann man sich Nichtterminale
15
2.3. Formale Sprachen
Abbildung 2.1: Vereinfachte Produktionsregel des Nichtterminals „create_stmt“
in der SQL-Sprache.
als Methodenaufrufe eines Kellerautomaten vorstellen).
3. Einer Menge von Produktionen (auch Regeln genannt), bei denen genau
ein Nichtterminal (links) aus einer Folge von Terminalen und Nichtterminal (rechts) abgeleitet wird.
Produktionen
4. Ein Nichtterminal, welches als Startsymbol gekennzeichnet ist.
Abbildung 2.1 zeigt eine Produktionsregel der SQL-Sprache. Um die Produktion in dieser Abbildung durchzuführen, sind weitere Produktionsregeln
notwendig, die die Nichtterminale qualified_named und optTableElementList
ableiten. Um das Beispiel so einfach wie möglich zu gestalten, wurde diese
Produktionsregeln der SQL-Grammatik stark vereinfacht.
Das Beispiel kann wie folgt erweitert werden:
• create_stmt ist das Startsymbol
• qualified_name → identifier ;
• identifier → (’a’..’z’ | ’A’..’Z’|)+;
• optTableElementList → tableElementList | /*EMPTY*/
• tableElementList → identifier type (’,’ identifier type)*
• type → (’INTEGER’ | ’TEXT’)
Die obigen Regeln in Worten ausgedrückt:
• Das Nichtterminal qualified_name besteht aus dem Terminal identifier
• Das Nichtterminal identifier ist ein regulärer Ausdruck aus Klein- und
Grossbuchstaben
16
2.3. Formale Sprachen
create_stmt
CREATE
TABLE
qualified_name
identifier
myTable
optTableElementList
(
)
tableElementList
identifier
id
type
INTEGER
,
identifier
type
name
TEXT
Abbildung 2.2: Vereinfachter Syntaxbaum für die Ableitung eines SQL-Befehls
• Das Nichtterminal optTableElementList besteht entweder aus dem Nichtterminal tableElementList oder ist leer (d.h. tableElementList ist optional)
• Das Nichtterminal tableElementList besteht aus einem Nichtterminal
identifier, gefolgt vom Nichtterminal type und einer beliebigen Folge
des Verkettung (’,’ identifier type)
• Das Nichtterminal type besteht entweder aus dem Terminal ’INTEGER’ oder dem Terminal ’TEXT’
Die Ableitung von type zeigt, dass diese Spezifikation der SQL-Sprache stark
vereinfacht ist. Sie kennt bspw. nur die zwei Datentypen „Integer“ und „Text“.
Ein Satz wird gebildet, indem man mit der Produktion des Startsymbols Satz
beginnt und nach und nach die Produktionen abwärts in der Hierarchie ausführt, bis eine Produktion nur noch aus Terminalen besteht. Danach kann
das nächste Nichtterminal der darüberliegenden Hierarchiestufe bearbeitet
werden. Ein Satz wird in der Literatur [ASU99] auch als Wort der Sprache
bezeichnet.
Die Menge der Ableitungen, die notwendig sind, um einen Satz zu bilden,
können als Bäume dargestellt werden. Solche Bäume nennt man Ableitungsbäume oder Syntaxbäume (auch Parsebäume genannt). Die Abbildung 2.2 Syntaxbaum
17
2.3. Formale Sprachen
zeigt den Syntaxbaum für den Befehl
CREATE TABLE myTable ( i d INTEGER, name TEXT)
nach den oben aufgeführten Produktionsregeln.
Ein Kellerautomat, der die Eingaben einer kontextfreien Grammatik analysiert, kann z. B. so implementiert werden, dass er die noch nicht verarbeiteten Grammatikregeln (die Nichtterminals) als Methodenaufrufe im Kellerspeicher ablegt und die abgeglichenen Symbole sofort verarbeitet. Methoden
können rekursiv sein, das heisst, sie können sich selbst wieder aufrufen. Dies
geschieht bspw. bei verschachtelten Klammerausdrücken. Jede Verarbeitung
einer Methode kann wieder neue Methodenaufrufe im Kellerspeicher ablegen.
Eine Methode ist vollständig verarbeitet, wenn ihre Symbole und alle von ihr
erzeugten Aufrufe aus dem Kellerspeicher entfernt sind. Dann folgt die Verarbeitung der nächsten Methode im Kellerspeicher. Diese Schritte wiederholen
sich, bis der Kellerspeicher leer ist. Eine solche Art der Implementierung wird
häufig für die Syntaxanalyse mit Parsern (siehe Abschnitt 2.4) verwendet.
Eine Grammatik, wie sie oben beschrieben wurde, heisst kontextfrei, weil
das Nichtterminal auf der linken Seite frei steht und nicht von einem Kontext von anderen Symbolen oder Nichtterminalen umgeben sein kann. Wenn
z. B. das Nichtterminal a durch die Produktionsregel a → De spezifiziert ist,
dann wird in jeder Eingabe, welche a enthält, diese Produktion angewendet.
Eine Produktionsregel der Art baE → Dc wäre demgegenüber kontextsensitiv, da die Produktionsregel für a nur eine Produktion im Kontext mit b
(links von a) und E (rechts von a) erlauben würde. Leider werden für viele Sprachen kontextsensitive Informationen benötigt, um die Semantik der
Sprache zu beschreiben. Um syntaktisch korrekte, aber semantisch falsche
Sätze einer kontextfreien Grammatik auszuschliessen, werden zusätzliche semantische Informationen benötigt [JEH00].
Ein weiteres Problem mit kontextfreien Grammatiken entsteht, wenn ein
Wort auf verschiedene Weisen erzeugt werden kann. Eine solche Grammatik
Mehrdeutigkeiten nennt man mehrdeutig. Mehrdeutigkeiten existieren dann, wenn es für einen
Satz mehr als einen Parsebaum gibt. Zur Veranschaulichung einer Mehrdeutigkeit wurde ein Beispiel aus [ASU99] übernommen.
18
2.3. Formale Sprachen
sting
sting
sting
3
+
sting
4
-
sting
2
3
+
-
4
sting
2
Abbildung 2.3: Syntaxbäume einer mehrdeutigen kontextfreien Grammatik für
den Ausdruck 3 + 4 − 2 [ASU99]
Die folgende Grammatikregel soll einen mathematischen Ausdruck mit beliebig vielen ganzzahligen Additionen und Subtraktionen ausdrücken:
string → string + string|string − string|0|1|2|3|4|5|6|7]8]9]0
Diese Produktionsregel angewendet auf den Ausdruck 3 + 4 − 2 erzeugt mehrere Syntaxbäume, wie Abbildung 2.3 zeigt.
Manchmal ist es möglich, mehrdeutige Grammatiken so umzuformulieren, dass die Grammatik eindeutig wird. Mehrdeutigkeiten können aber auch
dann entstehen, wenn die Syntax der Sprache nicht mehr ausreicht, um Ausdrücke korrekt zu interpretieren. Z. B. kann in der Programmiersprache C der
Ausdruck i*j sowohl für eine Multiplikation als auch für eine Typenzuweisung
mit einem Pointer auf i stehen [Par07]. Semantische Informationen können
in solchen Fällen dazu verwendet werden, das Spezifizieren von Mehrdeutigkeiten zu verhindern.
Im Abschnitt 2.4.2 werden Parser für die Analyse von kontextfreien Grammatiken und der Parsergenerator ANTLR vorgestellt. ANTLR verwendet
semantische Prädikate, um Mehrdeutigkeiten zu beseitigen und semantische
Bedingungen zu prüfen. Semantische Prädikate sind Boole‘sche Ausdrücke, Semantische
die Aussagen über die semantische Gültigkeit von Alternativen machen kön- Prädikate
nen [Par07].
19
2.4. Compiler
2.4
Kompilieren
Lexer
Symboltabelle
Compiler
Der Compiler übernimmt die Aufgabe, Programme einer bestimmten Quellsprache in eine bestimmte Zielsprache zu übersetzen. Diesen Prozess nennt
man Kompilieren (oder Übersetzen). Beim Kompilieren findet eine syntaktische und semantische Gültigkeitsprüfung der Quelle statt. Die Sätze der
Quellsprache werden in der Regel als Eingabestrom in mehreren aufeinanderfolgenden Phasen analysiert. Diese Zerlegung ist nicht notwendig, denn einzelne Phasen können auch zusammengefasst werden [ASU99]. Dennoch werden sie hier einzeln beschrieben:
Bei der lexikalischen Analyse werden die Zeichen von einem sog. Lexer
auf das Vokabular der Sprache abgebildet, sodass ein Strom aus Symbolen
(ein sog. Tokenstrom) an den eigentlichen Parser weitergegeben werden kann.
Der Lexer erstellt eine Symboltabelle, welche während des ganzen Analyseprozesses verwendet werden wird. Eine Symboltabelle ist eine Datenstruktur,
welche Informationen über die einzelnen Ausdrücke der Quellsprache enthält.
Während der lexikalischen Analyse werden die einzelnen Lexeme (siehe Abschnitt 2.4.1) als Eintrag in die Symboltabelle geschrieben. Spätere Phasen
können diesen Eintrag um weitere Informationen ergänzen. Eine Symboltabelle kennt in der Regel zwei Operationen:
• insert(s, t) : Erzeugt einen Eintrag für die Zeichenfolge s und das Symbol t
• lookup(s) : Gibt den Index einer Zeichenfolge s zurück
Parser
In einer zweiten Phase übernimmt ein sog. Parser eine hierarchische Analyse, wobei die einzelnen Symbole zu grammatikalisch (korrekten) Sätzen
zusammengefasst werden. Die Ausgabe des Parsers ist ein Syntaxbaum. Der
Syntaxbaum liefert eine hierarchische Struktur der Sätze aus den einzelnen
Ausdrücken, weshalb der Begriff hierarchische Analyse für das Parsen verwendet wird. Der Syntaxbaum wird für die semantische Analyse weiterverwendet. Die Trennung von lexikalischer und syntaktischer Analyse bringt
einige Vorteile [Par07]:
20
2.4. Compiler
• Separation of Concerns: Der Parser muss sich nicht um die Bedeutung
von einzelnen Zeichen oder Wörter kümmern, sondern nur um Symbole, die Klassen von Zeichen oder Wörtern ausdrücken. Für den Parser
macht es bspw. keinen Unterschied, ob er die Zahl 1 oder 2 behandelt,
denn beide sind Instanzen des Symbols „Integer“.
• Modularität: Bei einer strikten Trennung kann sowohl der Lexer als
auch der Parser wiederverwendet oder ausgetauscht werden. So ist es
bspw. denkbar, dass dieselbe Symboltabelle, welche während der lexikalischen Analyse erstellt wurde, mehrfach mit verschiedenen Parsern
analysiert wird.
• Filter: Nicht jedes Symbol eines Eingabestroms ist interessant. Der
Lexer übernimmt die Aufgabe, bspw. Leerzeichen und Zeilenumbrüche
zu filtern und nur die relevanten Symbole an den Parser weiterzugeben.
Es können weitere Phasen des Compilers folgen, die für dieses Projekt aber
eine untergeordnete Rolle spielen. Der Vollständigkeit halber seien sie hier
trotzdem noch kurz aufgeführt [ASU99]:
• Die Semantische Analyse überprüft, ob der syntaktisch korrekte Quellcode auch eine sinnvolle Bedeutung hat. Bspw. werden in dieser Phase
Typüberprüfungen für Zuweisungen und Operationen durchgeführt.
• Zwischencode-Erzeugung: Nach der Syntax- und Semantikanalyse erzeugen manche Compiler eine Zwischendarstellung des Quellprogramms,
die die weitere Verarbeitung erleichtern soll. Auch der Syntaxbaum
kann als Zwischencode betrachtet werden.
• Code-Optimierung: Diese Phase soll den Zwischencode in effizienten
Zielcode umschreiben, um die Performance zu optimieren.
• Code-Erzeugung: Die letzte Phase erzeugt den Zielcode. Zielcode kann
bspw. Maschinencode, ein Assemblerprogramm oder Bytecode (Java)
sein.
21
2.4. Compiler
2.4.1
Lexer
Der Lexer übernimmt eine lineare Analyse des Quellprogramms. Dabei werden die gefundenen Ausdrücke auf die in der Grammatik definierten Symbole
abgebildet. Die lexikalische Analyse für den SQL-Ausdruck
CREATE TABLE CURRENT_USERS (USERID INTEGER, FNAME TEXT,
LNAME TEXT) ;
könnte durch den Lexer z. B. auf folgende Symbole abgebildet werden:
1. Das SQL Schlüsselwort CREATE
2. Das SQL Schlüsselwort TABLE
3. Den Identifikator CURRENT_USERS
4. Das Symbol „(“
5. Das Symbol „)“
6. Den Identifikator USERID
7. Den Typ INTEGER
8. Das Symbol „ ,“
9. Den Identifikator FNAME
10. Den Typ TEXT
11. Das Symbol „ ,“
12. Den Identifikator LNAME und
13. Den Typ TEXT
Schlüsselwort
Ein Schlüsselwort ist in dieser Grammatik ein Symbol mit einer speziellen
Bedeutung innerhalb der Sprache. So steht in SQL das Schlüsselwort TABLE für Tabellen oder Relationen der Datenbank. Findet der Lexer das
Lexem TABLE, wird es sofort auf das Symbol „Table“ abgebildet. Gleiches
22
2.4. Compiler
geschieht in diesem Beispiel mit den Lexemen „(„ , „)“ und „ ,“. „Identifikator“
und „Typ“ sind Symbole, die mehrere verschiedene Lexeme abbilden. Der
Lexer übergibt die gefundenen Symbole als Tokenstrom an den Parser. Der
Lexer kann dabei bereits gewisse Symbole, die in der jeweiligen Sprache keine Bedeutung haben, herausfiltern (vgl. Seite 21 „Filtern von unbrauchbaren
Symbolen“). So werden bei der Analyse von SQL-Code Leerschläge, Tabulatoren und Zeilenumbrüche während der lexikalischen Analyse entfernt.
2.4.2
Parser
Ein Parser erkennt grammatikalische Strukturen. Für die Syntaxanalyse wird
ein Parser verwendet. Der Parser ordnet die während der lexikalischen Analyse gefundenen Symbole hierarchisch in einem Parsebaum an.
Parsergenerator
Parsergeneratoren erzeugen Parser, üblicherweise aufbauend auf einer kontextfreien Grammatik. Heutige Parser-Generatoren arbeiten mit komplexen,
leistungsfähigen Algorithmen. Für dieses Projekt wurde ANTLR (ANother ANTLR
Tool for Language Recognition) als Parsergenerator verwendet [FP07, Par07].
ANTLR generiert aufgrund einer formalen Spezifikation einer kontextfreien
Grammatik sowohl einen Lexer, als auch einen Parser.
Der Parser verwendet sowohl endliche deterministische Automaten (DFAs),
als auch Kellerautomaten. DFAs werde von ANTLR innerhalb von einzelnen Produktionsregeln verwendet, um zwischen verschiedenen Alternativen
zu entscheiden. Ist der DFA nicht in der Lage, eine Entscheidung zu fällen,
kann ein Kellerautomat verwendet werden. Wie in Abschnitt 2.3.1 beschrieben, legt der Kellerautomat die noch nicht verarbeiteten Regeln im Kellerspeicher ab, um nach und nach alle möglichen Pfade abzusuchen. Wenn der
Kellerspeicher leer ist, befindet sich der Kellerautomat in einem akzeptierten
Zustand. Da die Methode mit dem Kellerautomaten – durch das Absuchen
von vielen möglichen Pfaden – „teuer“ ist, versucht man zunächst, mit einem
DFA zum Ziel zu kommen.
23
2.5. Präprozessor
2.5
Präprozessor
Präprozessoren sind Programme, die eine Eingabe zu einer Ausgabe verarbeiten, welche von einem anderen Programm (dem Hauptprogramm, -prozess)
wiederum als Eingabe verwendet wird. Im Compilerbau werden Präprozessoren oft dazu verwendet, den Quellcode von Programmen vor der eigentlichen
Übersetzung zusammenzusetzen, indem Quellcode von externen Dateien eingebunden wird. Ein solcher Präprozessor kann u. U. auch dazu verwendet
werden, Makros zu definieren und zu expandieren [ASU99].
Makros
Makros sind Kürzel, die durch den Präprozessor durch ein Stück Programmcode ersetzt werden. Sie dienen dazu, wiederkehrende Programmstrukturen vereinfacht darzustellen, sodass diese Strukturen wiederverwendet werden können, ohne dass Quellcode kopiert werden muss. Programme,
die Zeichenfolgen durch andere Zeichenfolgen ersetzen, werden Makroprozessoren genannt. Solche Makroprozessoren werden bspw. beim Textsatzsystem
LATEX, aber auch in Programmiersprachen wie C (C++ Preprocessor) oder
Skriptsprachen wie PHP (Hypertext Preprocessor) verwendet.
Rationaler
Präprozessor
Unter einem rationalen Präprozessor versteht man Präprozessoren, die
eine Programmiersprache um neue Sprachkonstruktionen erweitern. Dabei
kann es sich um Konstruktionen handeln, die die Sprache bspw. um neue
Kontrollflussmöglichkeiten (Schleifen, bedingte Anweisungen) oder um neue
Datenstrukturen ergänzen. In ähnlicher Weise existieren auch Spracherweiterungen, die vom Präprozessor in Prozeduraufrufe übersetzt werden. Rationale
Präprozessoren und Spracherweiterungen werden meist in Form von eingebauten Makros umgesetzt. Die Definitionen dieser Makros werden durch ein
eindeutiges Schlüsselwort gekennzeichnet und bestehen in der Regel aus einem Namen und einer Parameterliste (in der Literatur [ASU99] auch als
Schablone bezeichnet) sowie einem Rumpf. Wenn die Makrosprache kein
Überladen von Makroaufrufen erlaubt, müssen die Makronamen eindeutig
gewählt werden. Können die Aufrufe überladen werden, müssen Makroname
und Struktur der Parameterliste eindeutig gewählt werden. Da der Präprozessor die Definitionen in der Regel sequenziell durchläuft, würden gleichnamige Definitionen entweder einfach überschrieben werden oder während
24
2.6. Weitere Begriffe
der Vorverarbeitung zu Fehlern und ungewollten Nebeneffekten führen. Die
Parameterliste besteht aus einer Liste von formalen Parametern. Der Rumpf
enthält die eigentliche Definition des Makros. Darin enthalten sind Referenzen auf die formalen Parameter, die in der Parameterliste definiert wurden.
Wenn ein Makro aufgerufen wird, geschieht dies meist über ein Schlüsselwort (z. B. #EXPAND) und den Makronamen, sowie die gewählte Parameterbelegung. Der Präprozessor fügt dann die Parameterbelegung an der Stelle
ein, wo im Rumpf der Makrodefinition die formalen Parameter referenziert
wurden und ersetzt den Makroaufruf mit diesem expandierten Rumpf. Im
Abschnitt 5.5.2 wird anhand des SQL-Makroprozessors veranschaulicht, wie
eine solche Makrosprache spezifiziert und umgesetzt werden kann.
2.6
2.6.1
Weitere Begriffe
Datenbank-Smells
Datenbank-Smells sind Probleme beim Entwurf einer Datenbank, die gegebenenfalls durch Refaktorierungsmassnahmen behoben werden sollten. Die
Idee von Datenbank-Smells lehnt sich eng an das Konzept von Code Smells
[DDN03] an, welches die Probleme in Quellcodes von Programmen bezeichnet. In der Literatur findet man typische Code Smells wie long methods,
switch statements, duplicated code. Demgegenüber beschreibt Ambler [AS06]
die sieben Datenbank-Smells tables with too many rows, multipurpose column,
multipurpose table, redundant data, tables with to many columns, smart columns und fear of change. Drei dieser Datenbank-Smells können durch das
Muster Partitionierung (siehe Abschnitt 3.5) refaktoriert werden:
• Tables with too many rows: Hier handelt es sich um Tabellen, die aufgrund von zu grossen Datenmengen eine schlechte Anfrageperformance
aufweisen. Solche Tabellen können mit horizontaler oder vertikaler Partitionierung refaktoriert werden.
• Multipurpose table/Tables with too many columns: Diese Tabellen sind
Speicher für mehrere verschiedene Gegenstände, die eigentlich nicht zu25
2.6. Weitere Begriffe
sammengehören. Solche Tabellen weisen viele Nullwerte auf, da viele
Attribute gegenstandsspezifisch sind. Die verschiedenen Gegenstände
sollten auf eigene kohäsive Tabellen verteilt werden. Durch horizontale
Partitionierung und Normalisierung kann man solche Tabellen refaktorieren.
26
Kapitel 3
Datenbankentwurfsmuster
In dieser Diplomarbeit beschäftigen wir uns damit, wie Muster der Schemadefinition in relationalen Datenbanken wiederverwendet werden können. Diese
Muster werden hier als Datenbankentwurfsmuster bezeichnet. Andere Arten
von Mustern, die in der Praxis oftmals unter demselben Begriff zusammengefasst werden, werden hier nicht behandelt und bei der weiteren Verwendung
des Begriffs Datenbankentwurfsmuster nicht miteinbezogen. Nicht im Fokus
dieser Diplomarbeit sind Transaktionsmuster (Transaction Design Patterns)
und Datenmodellierungsmuster (Data Model Patterns, Data Analysis Patterns).
Im ersten Teil dieses Kapitels werden mögliche Kategorien von Datenbankentwurfsmustern vorgeschlagen. Für die Kategorisierung werden dazugehörige Beispielmuster aufgezählt. Diese Muster, ihre Absichten und die dazugehörigen Konzepte werden in den nachfolgenden Abschnitten eingehender
beschrieben. Diese Diplomarbeit hat nicht zum Ziel, eine vollständige Aufzählung von Datenbankmustern und ihren Kategorien zu liefern. Die Beipiele
sollen lediglich zeigen, dass solche Muster existieren. Einige der Beispielmuster werden in Kapitel 6 dazu verwendet, das in dieser Diplomarbeit erstellte
Werkzeug zu testen. Da Entwurfsmuster auf einer abstrakteren Ebene angesiedelt sind als ihre konkrete Implementierung, ist es möglich, dass es zu den
präsentierten Vorschlägen Alternativimplementierungen gibt.
27
3.1. Musterkategorien
3.1
Musterkategorien
Bei der Suche nach Datenbankentwurfsmustern, die bei der Schemadefinition auftauchen, bin ich auf verschiedene Klassen von Mustern gestossen, die
sich meist in weitere Unterklassen aufteilen lassen. Die vorgeschlagenen Kategorien fassen gemeinsame Absichten der jeweiligen Muster zusammen. Zum
Zweck der Kategorisierung werden in diesem Abschnitt bereits Muster, Konzepte und Datenmodelle kurz erwähnt, die dann im späteren Verlauf dieses
Kapitels eingehender erläutert werden.
3.1.1
Muster zur Abbildung anderer Datenmodelle
Diese Muster besitzen die gemeinsame Eigenschaft, andere Datenmodelle in
das relationale Modell abzubilden oder das Modell zu erweitern.
Entwurfsmuster bei objektrelationalen Abbildungen
Bei Software-Projekten der gegenwärtigen Zeit werden vorwiegend objektorientierte Sprachen und Konzepte zur Entwicklung von modernen BusinessApplikationen verwendet. Während für den Quellcode Sprachen wie Java
oder C++ verwendet werden, werden Daten weiterhin in relationalen Datenbanksystemen gespeichert. Das relationale Datenmodell ist trotz seiner
Einfachheit sehr flexibel, weshalb solche Datenbanken weit verbreitet sind
und durch führende Datenbankhersteller wie DB2 und Oracle immer noch
weiterentwickelt werden. Um die Brücke von objektorientierten zu relationalen Konzepten zu schlagen, gibt es Abbildungsmethoden, welche gemäss
[Amb06] als objektrelationales Mapping bezeichnet werden. Da die Vererbung
ein weit verbreitetes und „simuliertes“ Konzept aus dieser Sparte ist, wurde
es in dieser Arbeit als Beispielmuster umgesetzt (Abschnitt 3.3).
Datawarehouse-Entwurfsmuster
In analyseorientierten Systemen, sog. OLAP-Systemen (OLAP: online analytic processing) existiert ebenfalls der Bedarf, verschiedenartige Konzepte
28
3.1. Musterkategorien
aufeinander abzubilden. Das in diesem Kontext verwendete, multidimensionale Datenmodell wird auf das relationale Datenmodell abgebildet, um bestehende relationale Datenbanksysteme einsetzen zu können (Abschnitt 3.4.3).
Dabei entstehen Entwurfsmuster wie das Snowflake- und das Star-Schema.
Ein weiteres Muster, welches bei OLAP-Systemen immer wieder anzutreffen ist, ist das Aggregationsmuster. Dieses Muster wird dazu benötigt,
feingranulare Daten aus dem transaktionalen Betrieb in eine gröbere Form
zu bringen, die den Anforderungen der Analyse entspricht.
Entwurfsmuster für temporale Daten
Zeitbezogene Daten ermöglichen die Versionierung von gelöschten und veränderten Daten und schaffen damit die Möglichkeit, Veränderungen zurückverfolgen zu können. Als Entwurfsmuster lassen sich hier Uni- und bitemporale Tabellen sowie Erweiterungen der Anfragesprache um temporale Aspekte
identifizieren.
In diese Kategorie von Mustern fallen auch Methoden zur Speicherung
von Änderungen in Dimensionstabellen eines Datenwürfels, sog. Slowly Changing Dimensions (SCD). Es gibt davon drei Muster, die mit SCD Typ 1 3 bezeichnet sind. Wie wir in Abschnitt 3.2 sehen werden, sind SCD eine
Mischung aus Datawarehouse-Entwurfsmuster und Entwurfsmuster für temporale Daten.
3.1.2
Anfrage- und Speicherplatzoptimierungsmuster
Aufgrund von Speicherplatz- oder Performanceproblemen entsteht in der
Praxis oftmals der Bedarf, grosse Tabellen durch Partitionierung zu verkleinern. Platzprobleme treten dann auf, wenn die Plattenkapazität beschränkt
ist. Durch Partitionen kann eine Tabelle auf mehrere physische Platten verteilt werden. Bei Performanceproblemen werden Tabellen durch Partitionieren so verkleinert, dass, je nach Art der Anfrage, nur eine Teilmenge der
Partitionen analysiert werden muss, was zu schnelleren Antwortzeiten führt.
Auch in verteilten Datenbanksystemen kann das Muster der Partitionierung
zur Beschleunigung von Anfragen verwendet werden. Bspw. wird der Ge29
3.1. Musterkategorien
samtdatenbestand in standortspezifische Partitionen unterteilt. Die Partitionen werden auf die Datenbankserver vor Ort verteilt, sodass pro Standort
vorwiegend Anfragen gegen den lokalen Datenbankserver getätigt werden
können. Bekannte Muster der Partitionierung sind horizontale und vertikale
Partitionierung.
Ein weiteres Muster, welches oft im Zusammenhang mit Query-Optimierung
Verwendung findet, ist die Denormalisierung. Werden Tabellen denormalisiert, sind bei gewissen Anfragen weniger Verbundoperationen nötig, womit
die Antwortzeiten deutlich gesenkt werden können. Beim DatawarehouseMuster Star-Schema (Abschnitt 3.4.3) wird Denormalisierung verwendet, um
die Performance von Analyseanfragen zu erhöhen.
3.1.3
Muster der Datenbankunabhängigkeit
Um die Datenbankunabhängigkeit von Applikationen zu erhöhen, kann die
Schnittstelle zur Datenbank an produktspezifischen Stellen angepasst werden. So können bspw. sprach- oder datenbankspezifische Datentypen durch
benutzerdefinierte Datentypen ersetzt werden, die denselben Wertebereich
abbilden. Bei unterschiedlicher Namensgebung für Schlüsselwörter oder Funktionen können z. B. benutzerdefinierte Funktionen eingesetzt werden. Benutzerdefinierte Funktionen und Datentypen bilden dann eine neue Schnittstelle
für Applikationen, die auf die Datenbank zugreifen. Wird die Datenbank
ersetzt, so müssen nur die benutzerdefinierten Funktionen und Typen angepasst werden. Diese Art von Muster kennen wir aus der objektorientierten
Programmierung als Adapter [GHJV04]. Adapter konvertieren Schnittstellen
von Klassen in Schnittstellen, die von den Klienten akzeptiert werden.
In den nächsten Abschnitten folgen nun einige Beispiele von Datenbankentwurfsmustern und ihre Konzepte:
• Muster zur Abbildung anderer Datenbanken
– Temporalrelationale Abbildungen (3.2)
– Objektrelationale Abbildungen am Beispiel der Vererbung (3.3)
30
3.2. Temporale Daten
– Multidimensionalrelationale Abbildungen (3.4)
• Anfrage und Speicheroptimierungsmuster
– Partitionierung (3.5)
– Denormalisierung (3.6)
3.2
Temporale Daten
In diesem Abschnitt werden temporale Konzepte, das temporale Datenmodell
und Muster beschrieben, die im Zusammenhang mit temporalrelationalen
Abbildung verwendet werden.
Wie später bei den multidimensionalrelationalen Abbildungsmustern gezeigt wird, werden temporale Muster auch in Kombination mit anderen Mustern verwendet. Z. B. werden im Zusammenhang mit analyseorientierten Systemen Daten historisiert, um fehlerhafte Anaysen zu vermeiden (Abschnitt
3.4.3). Unter Historisierung verteht man das Festhalten von Änderungen in
einer Datenbank, damit diese Änderungen zurückverfolgt werden können.
3.2.1
Das temporale Datenmodell
In der Terminologie von temporalen Datenbanken sind Fakten logische Aus- Fakten
sagen, die entweder wahr oder falsch sind. Die Entitäten einer Datenbank
zeichnen Fakten auf. Diese Fakten können mit verschiedenen zeitlichen Aspekten in Verbindung stehen [Jen00]. In relationalen Datenbanken sind Entitäten Tupel und die Fakten sind Aussagen über diese Tupel, die entweder wahr
oder falsch sind. Die meisten Beispiele in den folgenden Abschnitten basieren
auf der Annahme, dass ein relationales Datenmodell für die Abbildung der
temporalen Daten verwendet wird. Einige dieser Abbildungskonzepte werden
im nächsten Abschnitt (3.2.2) beschrieben.
Temporale Datenbanken zeichnen die zeitliche Veränderung von Daten in
einer Datenbank auf:
A temporal database supports some aspect of time, not counting
user-defined time.
31
3.2. Temporale Daten
Gemäss dieser Definition aus [JCD+ 93] genügt ein zeitlicher Bezug von Daten, wie beispielsweise Geburtsdatum in einer Personen-Relation, nicht, um
von einer temporalen Tabelle zu sprechen. Daraus folgt, dass nicht jede Tabelle, welche zeitliche Attribute besitzt, auch eine temporale Tabelle ist. Temporale Datenbanken werden oftmals auch als Zeit-orientierte (time oriented )
oder als historische (historical ) Datenbanken bezeichnet. In der temporalen
Datenhaltung gibt es zwei Arten von Zeitbezug:
Gültigkeitszeit
Die Gültigkeitszeit (VT: Valid Time) eines Faktes ist die Zeit, in der
dieser Fakt in der modellierten Realität wahr ist. [JCD+ 93] Als Beispiel kann
man sich einen Videoverleih vorstellen, bei dem Filme für genau einen Tag
ausgeliehen werden können. Ab dem Zeitpunkt der Ausleihe befindet sich
der Fakt „ist innerhalb Leihfrist“ für genau 24 Stunden in einem wahren
Gültigkeitszustand.
Transaktionszeit
Die Transaktionszeit (tt: transaction time) ist die Zeit, in der ein Fakt
in der Datenbank gültig ist. Die Transaktionszeit kann nicht in der Zukunft
liegen und kann nicht nachträglich geändert werden. [JCD+ 93] Im Beispiel
des Videoverleihs befindet sich der Fakt „ist_Kunde“ solange in einem wahren
Transaktionszustand, bis der Kunde aus der Datenbank gelöscht wurde.
Zeitintervall
Ein Zeitintervall ist die Zeit zwischen zwei Ereignissen, wobei ein Ereignis zum Zeitpunkt t auf der Zeitachse auftritt. Somit ist ein Intervall zeitlich
fixiert, das heisst, es hat einen Anfangs- und einen Endpunkt auf der Zeitachse.
3.2.2
Temporalrelationale Abbildung
VT-Relationen (VT State Tables)
Zeitstempel
Eine VT-Relation ist eine Relation, die genau eine Gültigkeitszeit unterstützt [JCD+ 93]. Um Relationen mit einer Gültigkeitszeit auszustatten, ist
es notwendig, einen zeitlichen Bezug einzubauen. Dieser zeitliche Bezug wird
in [JCD+ 93] als Zeitstempel (Timestamp) definiert. Die Definition lässt offen, wie der Zeitstempel in relationalen Datenbank implementiert werden
soll. Meistens wird der Zeitbezug jedoch als Intervall abgebildet. Der Zeitbezug kann sich dabei auf einzelne Attribute (Attribut-Zeitstempelung) oder
32
3.2. Temporale Daten
auf das gesamte Tupel (Tupel-Zeitstempelung) beziehen. Da in den meisten
relationalen Datenbanken keine Datentypen für Zeitintervalle (wie oben definiert) existieren, sieht eine mögliche Implementierung der Tupel-Zeitstempelung so aus, dass jeweils ein zusätzliches Attribut für den Beginn und ein
zusätzliches Attribut für das Ende des Intervalls verwendet wird.
TT-Relationen (TT State Tables)
Eine TT-Relation ist eine Relation, die genau eine Transaktionszeit unterstützt [JCD+ 93]. Äquivalent zu VT-Relationen verfügen TT-Relationen über
einen Zeitstempel. Auch hier kann eine Implementierung gewählt werden, die
durch zwei zusätzliche Zeitstempelattribute die Relation mit einem Intervall
für die Transaktionszeit ausstattet. Der Beginn der Transaktionszeit eines Tupels wird mit dem aktuellen Zeitpunkt als Zeitstempel der Einfügeoperation
des Tupels gesetzt. Wird ein Tupel gelöscht, so wird anstelle einer tatsächlichen Löschung das Ende der Transaktionszeit gesetzt. Gleiches geschieht mit
Tupeln, die modifiziert werden, wobei in diesem Fall noch ein neues Tupel
mit den modifizierten Werten und offenem Ende für die Transaktionszeit in
die Relation eingefügt wird.
Archivierungstabellen
Manchmal ist es sinnvoll, Daten in TT-Relationen, die sich nicht mehr im
Transaktionszustand befinden, von den restlichen Daten zu trennen und in
einer separaten Tabelle zu platzieren. Diese zusätzlichen Tabellen bezeichnen
wir in dieser Arbeit als Archivierungstabellen. Sie speichern alle Versionen
eines Objekts1 , die nicht mehr gültig sind.
Um dies umzusetzen unterstützen sowohl Basistabelle als auch Archivierungstabelle genau eine gemeinsame Transaktionszeit. Beide Tabellen verfügen über die gleichen Attribute. Wenn ein Datensatz gelöscht oder verändert
wird, verliert der alte Datensatz seine Gültigkeit und wird in die Archivierungstabelle transferiert, wo der Transaktionsendzeitpunkt gesetzt wird. Die
1
Wir verwenden hier den Begriff Objekt für einen Fakt aus der Realität, der durch die
Aufzeichnung von Transaktionszeit aus mehreren Versionen und somit mehreren Tupeln
bestehen kann.
33
3.2. Temporale Daten
Basistabelle besitzt für ihre Tupel nie einen Transaktionsendzeitpunkt. Bei
einer Änderung erhält die Basistabelle ein neues Tupel, womit bei dieser
Operation die Anzahl Tupel innerhalb dieser Tabelle die gleiche bleibt.
Wenn Daten oft manipuliert werden, kann die Grösse des gesamten Datenbestands stark ansteigen, da nie etwas tatsächlich gelöscht wird und viele
verschiedene Versionen von einem Objekt existieren können. Deshalb kann es
von Vorteil sein, wenn archivierte Daten in einer anderen Tabelle gespeichert
werden. Dies vereinfacht und beschleunigt Anfragen gegen aktuelle Versionen.
Ein weiterer Vorteil entsteht dadurch, dass die Trennung den Primärschlüssel auf der Basistabelle vereinfacht und damit Fremdschlüssel, die die Basistabelle referenzieren, viel einfacher definiert werden können (siehe nächster
Abschnitt).
Primärschlüssel
In temporalen Relationen muss der Primärschlüssel stets die Zeitdimension
enthalten. Eine TT-Relation, die mit zwei Zeitstempelattributen implementiert wurde, muss eines der beiden Zeitstempelattribute im Primärschlüssel
haben. Beim Beispiel der Archivierungstabelle kann auf der Basistabelle ein
Primärschlüssel ohne zeitlichen Bezug verwendet werden, da die Basistabelle
einen, und zwar den aktuellen, Objektzustand speichert. In der Archivierungstabelle, wo eine Evolution von Objektzuständen gespeichert wird, muss
der Primärschlüssel mindestens eines der beiden Zeitattribute erfassen. Dies
gilt auch für alle Unique Constraints, die auf einer solchen Tabelle definiert
werden. Enthält der Primärschlüssel einen zeitlichen Bezug, kann Objektidentität nicht mehr durch den Primärschlüssel ausgedrückt werden. Der Primärschlüssel identifiziert dann lediglich einen Objektzustand. Dies wiederum
erschwert die Referenzierung von Objekten, da eine herkömmlichen Fremdschlüsselbeziehung nur Attribute referenzieren kann, die als Primärschlüssel
und Unique Constraints spezifiziert sind und somit nur einen Objektzustand
referenzieren kann. Jedes Mal, wenn sich der Zustand ändert, müsste diese
Referenz angepasst werden. Mit dem Muster der Archivierungstabelle ist sichergestellt, dass Referenzen auf den Primärschlüssel der Basistabelle immer
34
3.2. Temporale Daten
den aktuellen Objektzustand referenzieren.
Bitemporale Relationen
Eine bitemporale Relation ist eine Relation, die genau eine Gültigkeitszeit
und genau eine Transaktionszeit unterstützt [JCD+ 93]. Bitemporale Tabellen sind nützlich, weil Ereignisse in der modellierten Miniwelt nicht immer
zum gleichen Zeitpunkt in die Datenbank geschrieben werden, wie sie in der
Realität eintreten. Bitemporale Relationen ermöglichen es, Differenzen zwischen Gültigkeitszeit (z. B. „Wann war es geplant?“) und Transaktionszeit
(z. B. „Wann ist es tatsächlich eingetroffen und in die Datenbank eingetragen
worden?“) zu finden. Würde anstelle einer bitemporalen Relation eine TTRelation modelliert werden, dann könnten keine Aussagen über die Zukunft
(z. B. „Wann ist es geplant?“) gemacht werden und man würde immer nur
den jeweiligen Zustand der Relation aufzeichnen. Würde man nur eine VTRelation modellieren, dann hätte man keine Rollback-Möglichkeit mehr und
man könnte Änderungen nicht mehr zurückverfolgen. Ein Beispiel dazu:
Student Meier immatrikuliert sich am 1. Oktober 2004 an der Universität Zürich in Wirtschaftswissenschaften.
Ein neues Tupel repräsentiert Student Meier als eingeschriebenen Student
an der Universität Zürich. Transaktionszeit und Gültigkeitszeit beginnen zum
Zeitpunkt 1.10.2004 und sind somit (noch) übereinstimmend.
Nach einigen Wahlveranstaltungen in Datenbanken und Software Engineering bemerkt Student Meier, dass er eigentlich Informatik studieren möchte und lässt sich am 15. Juni 2005 ins Hauptfach Wirtschaftsinformatik umschreiben. Diese Umschreibung gilt ab Beginn
des Herbstsemesters am 20. Oktober 2005.
Ein neues Tupel mit der Fachrichtung „Informatik“ muss erstellt werden.
Das alte Tupel erhält als Ende der Transaktionszeit den 15.6.2005 und als
35
3.2. Temporale Daten
Ende der Gültigkeitszeit den 19.10.2005. Beim neuen Tupel unterscheiden
sich die Zeitpunkte von TT- und VT-Beginn: TT beginnt sofort nach der
Umschreibung am 16.6.2005, VT erst bei Semesterbeginn am 20.10.2005.
Vom 1. Januar 2006 bis 7. Juli 2006 verbringt Student Meier ein
Auslandssemester an der Universität von Göteborg. Für die Zeit in
Göteborg darf er nach Absprache mit dem Dekanat in Zürich eingeschrieben bleiben. Er meldet sich in Göteborg am 10. Februar 2006
an. Nach seiner Rückkehr promoviert Meier im Frühjahr 2009 und
exmatrikuliert sich am 15. März per 8. Juli 2009.
Auch hier wird es Unterschiede zwischen TT und VT geben, die durch die
bitemporale Relation sichtbar gemacht werden. Das Beispiel wird unten mit
den nötigen Operationen in SQL Schritt für Schritt durchgeführt. In diesem Beispiel wurden die Zeitstempel so implementiert, dass für TT und VT
jeweils zwei zusätzliche Attribute in die Relation eingebaut wurden. Dies entspricht nur einer möglichen Implementierung.
Matrikelnr.
Name
Vorname
Universität
Fachrichtung
T TB
T TE
V TB
V TE
Die Datenbankoperationen in SQL für das obige Beispiel und diese bitemporale Relationen würden so aussehen:
1. Immatrikulation:
INSERT INTO T_Students VALUES ( ’ 04−000−000 ’ , ’ Meier ’ , ’
Hans ’ , ’ Uni ␣ Z ü r i c h ’ , ’ I n f o r m a t i k ’ 0 1 . 1 0 . 2 0 0 4 , ∞
, 0 1 . 1 0 . 2 0 0 4 , ∞)
2. Umschreibung von Wirtschaft zur Informatik:
(a) Neuen Datensatz erzeugen:
INSERT INTO T_Students VALUES ( ’ 04−000−000 ’ , ’ Meier ’
, ’ Hans ’ , ’ Uni ␣ Z ü r i c h ’ , ’ I n f o r m a t i k ’ ,
16.07.2005 , ∞ , 20.10.2005 , ∞)
36
3.3. Vererbung
(b) Alten Datensatz modifizieren:
UPDATE T_STUDENTS SET T TE = 1 5 . 0 7 . 2 0 0 5 . . . ;
UPDATE T_STUDENTS SET V TE = 1 9 . 1 0 . 2 0 0 5 . . .
3. Einschreiben in Göteborg für genau ein Semester:
INSERT INTO T_STUDENTS VALUES ( ’ 04−000−000 ’ , ’ Meier ’ , ’
Hans ’ , ’ Uni ␣ Göteburg ’ , ’ I n f o r m a t i k ’ , 1 0 . 0 2 . 2 0 0 6 ,
∞ , 01.01.2006 , 07.07.2006)
4. Promotion, Datensatz modifizieren, da Meier kein Student mehr ist:
UPDATE T_STUDENTS SET T TE = 1 5 . 0 3 . 3 0 0 9 . . . ;
UPDATE T_STUDENTS SET V TE = 0 8 . 0 7 . 2 0 0 9 . . .
3.3
Vererbung
In diesem Abschnitt folgt die Beschreibung des Verberbungsmusters als Beispiel einer objektrelationalen Abbildung. Es wird gezeigt, wie Vererbung im
SQL-Standard und im DBS PostgreSQL umgesetzt ist.
3.3.1
Vererbung in SQL:1999
Vererbungskonzepte existieren in SQL seit dem SQL:1999-Standard [Ame99a,
Ame99b, Gep02]. Dieser Standard definierte erstmals das Konzept der Vererbung von Typen, die als benutzerdefinierte Typen (UDT: User-defined types)
bezeichnet werden. UDTs werden unterteilt in distinct- und strukturierte Typen (structured type).
Distinct Types werden definiert, um Datentypen mit dem gleichen Basistyp unterscheiden zu können.
Strukturierte Typen sind „zusammengesetzte Datentypen“, bestehend aus
einer Menge von Attributen und Methoden. Durch Struktur und Wertebereich der Attribute definieren strukturierte Typen einen Zustand, durch ihre
Methoden ein typspezifisches Verhalten.
37
3.3. Vererbung
Strukturierte Typen können sowohl weitervererbt, als auch referenziert
oder als Wertebereich von Attributen instanziert2 werden. Auch das Definieren von abstrakten Typen, die nicht instanziert werden können, ist möglich.
Für jeden Typ, der nicht abstrakt ist, wird ein Konstruktor definiert, der
für die Instanzierung des Typs verwendet wird. Werden Typen als „final“
deklariert, können sie nicht weitervererbt werden.
Benutzerdefinierte Methoden, welche dem strukturierten Typen ein typspezifisches Verhalten geben, können als Klassenmethoden oder als Instanzmethoden definiert werden. Instanzmethoden dürfen in Untertypen überschrieben werden.
Durch die Verwendung von systemdefinierten Methoden für Lese- und
Schreibzugriff auf Attribute eines strukturierten Typs definiert SQL:1999
auch das Konzept der Kapselung für strukturierte Typen. Diese Methoden
werden automatisch erzeugt. Leider wird das Konzept des Information Hidings durch den Standard nur unvollständig umgesetzt, weil keine eingeschränkten Zugriffsrechte innerhalb und ausserhalb von Vererbungshierarchien, z. B. durch die Schlüsselworte private/protected/public, möglich sind.
Verschiedene Instanzen von Typen sollten unterscheidbar sein, selbst wenn
sie über den gleichen Zustand verfügen. Instanzen können durch Objektidentität unterschieden werden, welche in SQL:1999 durch Objektidentifikatoren
(OID) erreicht wird:
Ein OID ist eine Eigenschaft einer Instanz, die sie eindeutig identifiziert und sie von allen anderen Instanzen des gleichen Typs
oder anderer Typen unterscheidet [Gep02].
Zustand, typspezifisches Verhalten, Vererbung und Kapselung machen strukturierte Typen dem Klassenbegriff eines Objektmodells sehr ähnlich. Strukturierte Typen erlauben die Repräsentation von Klassen in Datenbanken,
doch für die Persistenz werden Tabellen benötigt, in denen Tupel abgelegt
werden können. Der SQL:1999-Standard löst dies, indem er den Tabellenbegriff so definiert, dass Tabellen Instanzen von strukturierten Typen verwalten
2
Instanzierte Typen sind Unterobjekte eines Typs. Referenzierte Typen werden eigenständig gespeichert und können von mehreren Typen referenziert werden.
38
3.3. Vererbung
ST1
ST2
T1
is type of
ST3
T2
T3
is type of
is type of
Abbildung 3.1: Vererbung typisierter Tabellen in SQL:99 [Gep02]
können. Solche Tabellen werden auch als typisierte Tabellen bezeichnet. Sowohl strukturierte Typen als auch typisierte Tabellen können weitervererbt
werden, wobei darauf zu achten ist, dass die Tabellenhierarchie ein Abbild
der Typenhierarchie ist. Wenn die Tabelle T1 durch den strukturierten Typ
ST1 typisiert wird und Supertabelle von T2 und T3 ist, dann müssen die Typen ST2 und ST3 von T2 bzw. T3 Subtypen von ST1 sein [Gep02]. Dies ist in
Abbildung 3.1 veranschaulicht.
Bei Anfragen gegen die Wurzel einer Klassenhierarchie werden alle Tupel
der Subtabellen ebenfalls zurückgegeben. Diese Anfragen können nur Attribute zurückliefern, welche in der Wurzel definiert sind. Mit dem ONLY Operator können Tupel aus den Subtypen von der Ergebnismenge ausgeschlossen werden.
3.3.2
Vererbung in PostgreSQL
In PostgreSQL (Version 8.3) existiert keine Vererbung von benutzerdefinierten Datentypen, wie sie in SQL:1999 spezifiziert ist3 . Trotzdem können Tabellen von Supertabellen erben, es ist sogar Mehrfachvererbung möglich. Diese
Tabellenvererbung wird aber wiederum nicht vom SQL-Standard unterstützt,
weshalb bei ihrer Verwendung immer mögliche Portierungsprobleme bedacht
3
PostgreSQL unterstützt benutzerdefinierte Datentypen; diese können aber nicht weitervererbt werden und weichen auch in anderen Bereichen vom Standard ab [Kli04, Pos08].
39
3.3. Vererbung
werden müssen.
Eine Tabelle erbt sämtliche Attribute ihrer Supertabelle. Wenn man nun
Anfragen gegen die Wurzel einer solche Tabellenhierarchie tätigt, werden die
Tupel der gesamten Hierarchie abwärts zurückgegeben. Projiziert wird allerdings nur auf die Attribute der Wurzel. Es ist möglich, mit dem Schlüsselwort
ONLY nur die Tupel einer bestimmten Tabelle anzufordern, ohne dass die
Tupel der Subtabellen in der Ergebnismenge ebenfalls enthalten sind. Damit funktionieren Anfragen gegen Tabellenhierarchien genau gleich wie im
SQL:1999-Standard definiert.
Ein Problem bei der Vererbung in PostgreSQL ist die Vererbung von
Constraints, denn Subtabellen erben nur die Check-Constraints ihrer Eltern.
Unique-Key-, Primärschlüssel- und Fremdschlüssel-Vererbung wird nicht unterstützt. Auch Benutzerberechtigungen werden nicht automatisch weitervererbt: Ein Benutzer, der keine Berechtigung für Subtabellen hat, erhält nur
Zugriff auf die Supertabelle und muss mit Hilfe des ONLY -Schlüsselwortes
Daten dieser Tabelle selektieren und manipulieren. Um diese Nachteile bei
der Erstellung neuer Subtabellen zu beheben, sind eine Reihe von Massnahmen notwendig:
1. Für jede Subtabelle muss der Primärschlüssel weitergereicht werden,
sodass die Vereinigung des Primärschlüssel-Attributs über die Hierarchie eindeutig ist. Das Gleiche gilt für Unique Constraints.
2. Eingehende Fremdschlüssel der Wurzel müssen so erweitert werden,
dass sie auch auf den Primärschlüssel jedes neuen Subtyps zeigen.
3. Ausgehende Fremdschlüssel der Typenhierarchie müssen für die Subtypen erneut definiert werden.
4. Benutzerberechtigungen müssen auch auf die Subtabelle übertragen
werden oder der SELECT -Operator muss für Benutzer ohne Berechtigung für die Subtypen umgeschrieben werden.
Die Art, wie PostgreSQL die Vererbung unterstützt, ist vor allem dann nützlich, wenn man grosse Tabellen partitionieren möchte (siehe Abschnitt 3.5).
40
3.4. Multidimensionale Daten
3.4
Multidimensionale Daten
In diesem Abschnitt werden analyseorientierte Systeme, das multidimensionale Datenmodell und Muster beschrieben, die zur Abbildung von multidimensionalen Daten in relationalen Datenbanken verwendet werden.
3.4.1
OLAP versus OLTP
OLAP steht für Online Analytic Processing. OLAP Systeme sind als analyseorientierte Systeme im Hinblick auf Queryprozessierung optimiert, das heisst,
dass analyseorientierte Anfragen möglichst performant und einfach durchgeführt werden können. Im Gegensatz zu transaktionalen Systemen, sog. OLTP
(Online Transactional Processing), sind in OLAP-Systemen denormalisierte
Daten eher verbreitet (siehe Abschnitt 3.4.3). Transaktionale Systeme sind in
Produktionsumgebungen eingebunden, in welchen Anwendungen permanent
Datensätze erstellen, löschen oder verändern. Ihr Hauptaugenmerk liegt auf
Konsistenzsicherung und kurzen Antwortzeiten bei vielen parallelen Anfragen. Normalisierung verhindert Redundanzen, womit weniger Aufwand für
die Wartung der Datenbank anfällt.
OLAP-Systeme sind ein Hilfsmittel zur Entscheidungsfindung (decision
support system) in Unternehmen. Anfragen werden meist ad-hoc von BusinessAnalysten oder dem höheren Management durchgeführt. Diese Anfragen sind
oftmals sehr komplex und erfolgen über grosse Datenmengen. Deshalb ist die
Realisierung von kurzen Antwortzeiten im Sekunden- bis Minutenbereich für
OLAP eine Herausforderung. In transaktionalen Systemen liegen die Antwortzeiten meist viel tiefer (im Millisekunden- bis Sekundenbereich).
Um die Performance von Anfragen in OLAP-Systemen zu verbessern,
können in aufwändigen Datenintegrationsprozessen die Resultate von komplexen Analysen in vorberechneten Sichten (Materialized Views) gespeichert
werden. Für die Datenintegration wird meist ein ETL-Prozess (ETL: Extract Transform Load) verwendet, bei dem die Daten aus verschiedenen Datenquellen extrahiert, in einem Arbeitsbereich (Staging Area) transformiert
und dann ins Data Warehouse geladen werden. Für die Erzeugung von solchen Sichten wird meist das Aggregationsmuster angewandt. Weitere Perfor41
3.4. Multidimensionale Daten
mancesteigerung kann durch Indexieren von Daten erreicht werden. Indexe
sind Zugriffspfade auf Datenelemente. Sie dienen zur Leistungsverbesserung
von Anfragen gegen Teilmengen der Daten. In relationalen System werden
Indexe für Unique Constraints und Primärschlüssel angelegt. Die bessere
Performance von Anfragen muss allerdings mit mehr Aufwand bei der Verwaltung von Indexen erkauft werden. Das Gleiche trifft auf die Benutzung
und Verwaltung von vorberechneten Sichten zu. Dies ist jedoch kein Problem,
weil die Datenintegration meist in regelmässigen Abständen und ausserhalb
von transaktionalen Spitzenzeiten erfolgt, um den Transaktionsbetrieb der
OLTP-Systeme nicht zu stören. Sind die Daten im OLAP-System integriert,
werden sie weder modifiziert noch verändert.
3.4.2
Fakten
Dimensionen
Klassifikationshierarchien
Data Cube
Das multidimensionale Datenmodell
Für die Modellierung von OLAP-Systemen wird meist ein multidimensionales Datenmodell verwendet, welches die Analysedaten in Fakten und Dimensionen wiedergibt. Bspw. kann man sich als Fakten die Verkäufe oder
Umsätze eines Warenhauses vorstellen. Als typische Dimensionen in einem
solchen Datenschema sind bspw. eine zeitliche, eine geographische und eine produktspezifische Dimension denkbar. Diese Dimensionen lassen sich in
Klassifikationshierarchien unterteilen. So besteht eine zeitliche Dimension
bspw. aus den Hierarchiestufen „Tag“, „Woche“, „Monat“ und „Jahr“. Zwischen den einzelnen Dimensionsstufen einer Hierarchie besteht meistens eine
1:n-Beziehung. Es ist möglich, dass auch Parallelhierarchien existieren (z. B.
bildet die Klassifikationsstufe „Woche“ eine Parallelhierarchie zur Klassifikationsstufe „Monat“). Ein wichtiger Bestandteil von multidimensionalen Daten
bilden die Kenngrössen oder Masse, welche qualifizierende Informationen der
Fakten repräsentieren (z. B. Verkäufe in SFr.).
Multidimensionale Daten können als Würfel (Data Cubes, Hyper Cubes)
dargestellt werden. Die Dimensionen bilden dabei die Kanten des Würfels
und beinhalten qualifizierende Informationen. Aus den einzelnen Klassifikationsstufen können Teilwürfel gebildet werden. Die Würfelzellen selbst sind
das Abbild der Fakten, welche somit die Dimensionselemente (qualifizierende
42
3.4. Multidimensionale Daten
Informationen) mit den Kenngrössen (quantifizierende Informationen) assoziieren.
Wie bereits erwähnt, werden in der Praxis vorwiegend relationale Datenbanksysteme verwendet, weshalb meist ein relationaler Ansatz für die
Umsetzung von OLAP-Systemen gewählt wird. Als weitere Gründe kommen hinzu, dass es keine einheitliche Anfragesprache, keine standardisierte
Modellierungssprache und keine einheitliche Formalisierung des multidimensionalen Datenmodells gibt [Gep07]. Im folgenden Abschnitt werden zwei
mögliche Muster beschrieben, mit denen ein Datenwürfel in einer relationalen Datenbank simuliert werden kann.
3.4.3
Multidimensionalrelationale Abbildung
Snowflake-Schema
Das Snowflake-Schema ist ein Muster für die Abbildung eines multidimensionalen Datenmodells auf ein relationales Datenmodell. Dabei wird für jede
Klassifikationsstufe innerhalb einer Dimension eine Relation erstellt. Die Tabellen erhalten einen Primärschlüssel, die beschreibenden Attribute der jeweiligen Hierarchiestufe und einen Fremdschlüssel für die 1:n-Beziehung mit
der direkt darüberliegenden Klassifikationsstufe. Die Tabelle „Produkt“ in
Abbildung 3.2 besitzt bspw. einen Fremdschlüssel auf die Tabelle „Produktfamilie“.
Die Fakten des Datenwürfels werden in einer separaten Faktentabelle gespeichert, die für die Kenngrössen (z. B. Verkäufe, Umsatz) Fremdschlüssel auf die erste Hierarchiestufe sämtlicher Dimensionen besitzt. Der Primärschlüssel der Faktentabelle ist ein zusammengesetzter Schlüssel aus den
Fremdschlüsseln, enthält also die gleiche Anzahl Attribute wie der Würfel
Dimensionen.
Das Snowflake-Schema normalisiert die Dimensionstabellen. Dies bringt
den Vorteil, dass Änderungsanomalien bei der Aktualisierung von Dimensionstabellen leichter zu vermeiden sind. Ausserdem werden Redundanzen vermieden. Der grosse Nachteil von Snowflake-Schemata ist ihre schlechte Performance, da bei Anfragen über mehrere Klassifikationsstufen zwischen jeder
43
3.4. Multidimensionale Daten
PGruppeID
Gruppe
...
Produktdimension
PFamilieID
Familie
PGruppeID
...
ProduktID
Name
Marke
Preis
PFamilieID
...
ShopID
Filiale
StrasseID
...
StrasseID
Strasse
StadtID
...
StadtID
Stadt
RegionID
...
RegionID
Region
LandID
...
LandID
Land
...
Ortsdimension
ProduktID
ShopID
Tag
Verkäufe
Jahr
...
Monat
Jahr
...
Tag
Monat
...
Verkaufsfakten
Zeitdimension
Abbildung 3.2: Snowflake-Schema mit den Dimensionen Zeit, Ort und Produkt
für die Verkaufsfakten
Stufe eine (teure) Verbundoperation nötig ist. Z. B. sind für eine Analyse
der Summe der Verkäufe pro Land, Jahr und Produktgruppe des SnowflakeSchema in Abbildung 3.4.3 elf Verbundoperationen notwendig. Davon werden
acht Verbundoperation benötigt, um Klassifikationstufen zusammenzufassen.
Besser ist es, eine andere Form der relationalen Abbildung zu wählen, in der
Dimensionen nicht mehr stufenweise unterteilt sind, sondern denormalisiert
in einer einzigen Tabelle (pro Dimension) gespeichert werden. Diese Form
der Abbildung wird im Star-Schema realisiert.
Star-Schema
Das Star-Schema ist das meistverbreitetste Muster zur Darstellung von multidimensionalen Daten in OLAP-Systemen. Star-Schemata bestehen aus einer
Faktentabelle, die über Fremdschlüsselbeziehung mit den verschiedenen Dimensionstabellen verknüpft ist (siehe Abbildung 3.3). Äquivalent zum Snowflake-Schema bilden die Fremdschlüssel einen zusammengesetzten Primär44
3.4. Multidimensionale Daten
Produktdimension
ProduktID
Name
Marke
Preis
ProduktGruppe
ProduktFamilie
...
Ortsdimension
Verkaufsfakten
ShopID
Filiale
Strasse
Stadt
Region
Land
...
ProduktID
ShopID
Tag
Verkäufe
Zeitdimension
Tag
Woche
Monat
Jahr
...
Abbildung 3.3: Star-Schema mit den Dimensionen Zeit, Ort und Produkt für die
Verkaufsfakten
schlüssel der Faktentabelle. Das Abbild gleicht einem Stern, deshalb der Name.
Star-Schemata sind Snowflake-Schemata mit denormalisierten Dimensionen. Durch die Denormalisierung können die vielen Verbundoperationen zwischen den Klassifikationsstufen vermieden werden, was die Performance von
Anfragen verbessert – im Tausch gegen mehr Speicherplatzbedarf und mehr
Aufwand für die Vermeidung von Änderungsanomalien.
Slowly Changing Dimensions
Änderungen von Tupeln in Dimensionstabellen stellen ein Problem dar, denn
die Analysen in OLAP-Systemen befassen sich mit Daten, die sich über
Zeiträume erstrecken. In OLTP-Systemen ist meist nur der aktuelle Zustand
relevant. Durch Änderungen werden alte Zustände überschrieben und gehen
verloren. Dies kann zu fehlerhaften Analysen führen. Damit die Historie in
OLAP-Systemen nicht verloren geht, müssen die Dimensionsdaten versioniert
werden. Die Dimensionstabellen werden um einen temporalen Aspekt erweitert. Die Verfahren, die dazu angewendet werden, werden Slowly Changing
45
3.4. Multidimensionale Daten
Dimensions genannt. Man unterscheidet drei Typen von Slowly Changing
Dimensions [Gep07]:
• SCD Typ 1: Dieser Typ bezeichnet eine Dimensionstabelle ohne temporale Modellierung. Werden Datensätze modifiziert, dann werden die
alten Datensätze überschrieben und die Historie geht verloren. Dies
kann zu falschen Ergebnissen bei den Analysen führen.
• SCD Typ 2: Hierbei werden die Dimensionstabellen als TT-Relation
modelliert. Diese können auf die gleiche Weise implementiert werden,
wie die TT-Relationen in Abschnitt 3.2.2 mit den äquivalenten Vorund Nachteilen. So müssen bspw. die Primärschlüssel um mindestens
ein Zeitstempelattribut erweitert werden, womit Objektidentität nicht
mehr über den Schlüssel ausgedrückt wird (siehe Primärschlüssel von
TT-Relationen in Abschnitt 3.2.2 auf Seite 34). Dies wiederum macht
Anfragen und die Unterhaltung von referenzieller Integrität komplexer.
• SCD Typ 3: Die Dimensionstabelle wird um ein weiteres Zustandsattribut erweitert. So kann für ein Tupel immer der aktuelle und der
vorherige Zustand gespeichert werden. Eine solche Implementierung ergibt offenbar nur dann Sinn, wenn während der Zeitspanne, über die
sich die Analysen erstrecken, die Daten nicht mehr als einmal verändert wurden. Ansonsten führt auch dieses Verfahren zu fehlerhaften
Analysen.
Dimensionstabellen, die als SCD Typ 2 modelliert werden, sind eine Mischung
aus zwei Mustern: Star/Snowflake-Schema und TT-Relation.
Aggregation
Meist werden in einem Datawarehouse die Daten nicht auf feinster Granularitätsstufe gebraucht, weswegen feingranulare Daten nach analyserelevanten
Kriterien aggregiert und gespeichert werden. Dies steigert die Performance
von Anfragen und reduziert u. U. den benötigten Speicherplatz.
In Aggregationen werden mehrere Klassifikationsstufen der Klassifikationshierarchie zusammengefasst. Im Datawarehouse-Jargon nennt man diese
46
3.5. Partitionierung
Operation Roll-up. Typische Aggregationsfunktionen sind Summe, Mittelwert, Median, Maximum, Minimum. Im Beispiel aus Abbildung 3.3 könnte
bspw. eine vorberechnete Sicht für die Summe der Verkäufe pro Monat gebildet werden.
3.5
Partitionierung
Wie zu Beginn dieses Kapitels bereits beschrieben, wird das Partitionierungsmuster entweder zur Verbesserung von Anfrageperformance oder zur Behebung von Speicherplatzproblemen angewendet. Das Muster kann aber auch
angewendet werden, um Zugriffsberechtigungen auf Tabellen einzuschränken
oder um Tabellen zu refaktorieren.
In der objektorientierten Programmierung können durch schlechtes evolutionäres Design sog. Code Smells wie God Classes oder Long Methods
[DDN03] entstehen. God Classes kann man durch Refaktorierungsmassnahmen in mehrere kohäsive Klassen aufteilen. Long Methods werden in mehrere
Methoden aufgeteilt, die in sich funktional ähnlich sind. Ähnliche Probleme
und Massnahmen gibt es auch bei Entwürfen von Datenbankschemata: Ambler benennt in [AS06] tables with too many rows, multipurpose table und
tables with to many columns drei Datenbank-Smells (siehe Abschnitt 2.6.1),
bei denen man durch Partitionierung die Daten umverteilen und so das Design verbessern kann. Durch vertikale Partitionierung können denormalisierte
Tabellen normalisiert werden und eine grosse Tabelle, die zu viele Attribute
aufweist, kann in mehrere kohäsive Tabellen aufgeteilt werden. Horizontale
Partitionierung wird vorwiegend zur Performance-Verbesserung – u. a. auch
in verteilten Datenbanksystemen – verwendet.
3.5.1
Horizontale Partitionierung
Bei der horizontalen Partitionierung wird eine Tabelle auf mehrere Tabellen
abgebildet, die genau die gleichen Attribute haben, aber jeweils eine meist
disjunkte Teilmenge der Tupel der ursprünglichen Tabelle speichern. Partitionen können nach anfragerelevanten Kriterien gebildet werden. Wenn z. B.
47
3.6. Denormalisierung
ein zeitlicher Aspekt in den Daten vorhanden ist, können monatliche Partitionen gebildet werden. Anfragen gegen einen Monat müssen dann nur noch
eine Partition absuchen. Je mehr Partitionen eine Anfrage erfasst, desto kleiner wird der Performancevorteil, da immer mehr Partitionen vereinigt werden
müssen (Union-Operation). Der Performanceverlust, der durch eine UnionOperation entsteht, ist aber im Gegensatz zu der Verbundoperation, die bei
der Vereinigung von vertikalen Partitionen notwendig ist, vernachlässigbar
klein.
Auch bei verteilten Datenbanken wird horizontale Partitionierung angewendet. Dabei wird der gesamte Datenbestand auf standortspezifische Partitionen verteilt und auf den lokalen Datenbankservern gespeichert, um die
Performance zu verbessern.
3.5.2
Vertikale Partitionierung
Bei der vertikalen Partitionierung wird eine Tabelle auf mehrere Tabellen
abgebildet, indem jede Tabelle eine disjunkte Teilmenge der Attribute der
ursprünglichen Tabelle speichert. Lediglich die Schlüsselattribute werden dupliziert. Werden durch das vertikale Partitionieren ausschliesslich redundante Daten innerhalb einer Tabelle entfernt, spricht man von „Normalisieren“.
Da bei Anfragen auf vertikal partitionierte Daten u. U. mehr Verbundoperationen zwischen den einzelnen Partitionen ausgeführt werden müssen, kann
vertikale Partitionierung die Performance stark negativ beeinflussen.
3.6
Denormalisierung
Die Denormalisierung ist quasi die Umkehrabbildung der vertikalen Partitionierung. Bei diesem Entwurfsmuster werden Tabellen der 2. oder 3. Normalform, die über Fremdschlüsselbeziehungen miteinander verknüpft sind,
in einer denormalisierten Tabelle zusammengefasst. Dadurch kann verhindert werden, dass bei Anfragen teure Verbundoperationen zwischen mehreren Tabellen durchgeführt werden müssen, was die Performance der Anfragen
beschleunigt. Das Denormalisierungsmuster wird bspw. bei der Umwandlung
48
3.7. Zusammenfassung
von Snowflake-Schemata in Star-Schemata angewendet.
3.7
Zusammenfassung
Datenbankentwurfsmuster sind Entwurfsmuster für die Erzeugung von Schemata. In dieser Diplomarbeit wurden Entwurfsmuster im Kontext von relationalen Datenbanksystemen analysiert. Diese Muster konnten in drei verschiedene Kategorien eingeteilt werden:
1. Muster zur Abbildung anderer Datenmodelle: Diese Muster bilden nichtrelationale Konzepte auf relationale Schemata ab.
2. Anfrage- und Speicheroptimierungsmuster: Diese Muster werden verwendet, um Anfragen von neuen oder bereits bestehenden Schemata
zu beschleunigen oder Speicherplatzprobleme zu beheben.
3. Muster der Datenunabhängigkeit: Datenbankspezifische Implementierungen vermindern die Portabilität von Applikationen. Diese Muster
fördern die Datenbankunabhängigkeit von Datenbankapplikationen.
Das temporale Datenmodell bildet Daten mit einem Zeitbezug ab, der nicht
benutzerdefiniert ist. Bei der Abbildung dieses Datenmodells auf eine relationale Datenbank unterscheidet man drei Arten von Relationen:
1. VT-Relationen erfassen die Gültigkeitszeit von Relationen. Die Gültigkeitszeit ist die Zeit, in der ein Fakt in der Realität wahr ist.
2. TT-Relationen erfassen die Transaktionszeit von Relationen. Die Transaktionszeit entspricht der Zeit, in der ein Fakt in der Datenbank gültig
ist. Die Archivierungstabelle ist eine mögliche Implementierung einer
TT-Relation. Sie vereinfacht Fremdschlüsselbeziehungen mit einer TTRelation, weil die Objekte in der Basistabelle immer im aktuellen Zustand sind.
3. Bitemporale Relationen sind Tabellen mit genau einer Gültigkeits- (VT)
und einer Transaktionszeit (TT).
49
3.7. Zusammenfassung
Für komplexe Analysen in OLAP-Systemen werden die Daten meist multidimensional als Cube modelliert und auf relationale Datenbanken abgebildet.
Muster dieser Abbildung sind Snowflake- und Star-Schema. Sie bestehen aus
einer Faktentabelle und mehreren Dimensionstabellen. Star-Schemata sind
denormalisierte Snowflake-Schemata. Um auch über grössere Zeitspannen
korrekte Analysen durchführen zu können, darf die Historie von Daten in den
Dimensionstabellen nicht verloren gehen, weshalb diese Tabellen als Slowly
Changing Dimensions Typ 2 modelliert werden können.
Bei der Partitionierung von Tabellen kennt man zwei Verfahren: Horizontale und vertikale Partitionierung. Beide Verfahren bilden die Daten von
einer Tabelle auf mehrere Tabellen ab. Ungünstiges Partitionieren kann die
Performance von Anfragen auch negativ beeinflussen. Dies trifft ganz besonders stark auf vertikale Partitionierung zu, weil das Verknüpfen von solchen
Partitionen eine teure Verbundoperation erfordert.
Mit dem Denormalisierungsmuster werden mehrere normalisierte Tabellen miteinander verknüpft, um bei Anfragen Verbundoperationen zwischen
diesen Tabellen zu vermeiden. Die Denormalisierung wird bspw. für die Umwandlung von Snowflake-Schemata in in Star-Schemata verwendet.
50
Kapitel 4
Anforderungsspezifikation
In diesem Kapitel werden die Anforderungen an eine Applikation beschrieben, welche Entwurfsmuster der Schemadefinition zur Wiederverwendung
bereitstellen kann. Auf die funktionalen Hauptanforderungen folgt eine informale Beschreibung von nicht-funktionalen Anforderungen. Das Kapitel
wird mit den Randbedingungen abgeschlossen, die die verwendete Datenbankplattform und andere Produkte für die Implementierung spezifizieren.
4.1
4.1.1
Anforderungen
Akteure
Einziger Akteur ist der Benutzer, der DDL-Operationen ausführen möchte,
um ein Datenbankschema zu erzeugen oder zu verändern.
4.1.2
Hauptfunktionen des Systems
Datenbankanfragen entgegennehmen und verarbeiten
Das System sollte in der Lage sein, Datenbankanfragen, insbesondere DDLAnfragen, entgegenzunehmen und weiterzuverarbeiten.
51
4.1. Anforderungen
Definieren und Speichern von Entwurfsmustern
Der Benutzer kann Entwurfsmuster definieren und für die Wiederverwendung
abspeichern.
Wiederverwendung von Entwurfsmustern
Das gespeicherte Entwurfsmuster kann für konkrete Implementierungen dieses Musters geladen und somit wiederverwendet werden, ohne dass der Benutzer Anpassungen am Muster selbst vornehmen muss. Das gespeicherte
Muster ist somit generisch für diese Klasse von Entwurfsproblemen. Einund Ausgabecode kann in Skriptdateien gespeichert werden. Der Eingabecode umfasst dabei sowohl Muster als auch Quellcode der Datenbanksprache,
die Ausgabe enthält Anfragen in der gewählten Zielsprache.
Erzeugen von Datenbankobjekten
Sowohl die Wiederverwendung von Entwurfsmustern als auch die normalen
DDL-Operationen erzeugen oder verändern Objekte eines Datenbankschemas.
Fehlerbenachrichtigung
Die Applikation sollte in der Lage sein, die Benutzeranfragen auf Fehler zu
prüfen und den Benutzer zu benachrichtigen, falls Fehler gefunden werden,
die nicht von der Applikation behoben werden können.
4.1.3
Anwendungsfälle
Da es nur einen Akteur gibt, wird dieser in den folgenden Anwendungsfällen
nicht mehr explizit erwähnt, auch wenn dies der üblichen Notation eines
informalen Anwendungsfalls widerspricht.
DDL und Mustereingabe
Auslöser
52
Anfragen des Benutzers
4.1. Anforderungen
Beschreibung Anfragen des Benutzers werden entgegengenommen und verarbeitet. Die Eingabe kann direkt über die Kommandozeile oder
durch eine Datei erfolgen, die eingelesen werden soll. Dieser
Anwendungsfall besteht aus den Anwendungsfällen Konkretisieren von Entwurfsmustern, Fehlerprüfung und Erzeugung
einer Ausgabe.
Speichern von Entwurfsmustern
Auslöser
Entwurfsmusterdefinition
Beschreibung Wenn der Benutzer ein Muster definiert, soll das System das
Muster unter dem definierten Namen speichern.
Konkretisierung von Entwurfsmustern
Auslöser
Musteranwendungsanfrage
Beschreibung Alle Aufrufe von vordefinierten Mustern innerhalb von Benutzeranfragen müssen konkretisiert werden. Dies bedeutet, dass
das System das gespeicherte Muster findet, lädt und für die
konkrete Anfrage anwendet.
Fehlerprüfung
Auslöser
Eingabe verarbeitet
Beschreibung Bevor eine Ausgabe erzeugt wird, muss eine Fehlerprüfung
durchgeführt werden. Falls das System gefundene Fehler nicht
selbst beheben kann, wird der Benutzer benachrichtigt.
Erzeugung einer Ausgabe
Auslöser
Eingabe verarbeitet und Fehlerprüfung abgeschlossen
Beschreibung Als letzter Schritt der Anfrageverarbeitung wird eine Ausgabe
erzeugt. Der Benutzer spezifiziert, in welcher Form (z. B. Name
53
4.2. Nicht-funktionale Anforderungen
System
DDL/MusterEingabe
<include>
<include>
<include>
Konkretisierung
von
Entwurfsmustern
Fehlerprüfung
Akteur
Speichern von
Entwurfsmuster
Erzeugung einer
Ausgabe
Abbildung 4.1: UML Anwendungsfalldiagramm
und Pfad einer Ausgabedatei) diese Ausgabe erzeugt werden
soll.
4.2
Nicht-funktionale Anforderungen
Wiederverwendbarkeit
Mit dieser Applikation soll ein Weg aufgezeigt werden, wie Entwurfsmuster für die Schemadefinition gespeichert und wiederverwendet werden können. Unter Verwendung von Musterbeispielen sollen die Möglichkeiten dieses Ansatzes untersucht werden. Dafür muss ein konkretes Datenbanksystem
verwendet werden, auf das die Schemadefinitionen der Beispiele abgebildet
werden können. Die Applikation soll dennoch so entworfen werden, dass das
Datenbanksystem austauschbar ist.
54
4.3. Sprachen und Plattform
Erweiterbarkeit
Erweiterbarkeit und Wiederverwendbarkeit sind eng miteinander verknüpft.
Meist bedeutet Wiederverwendung, dass die Applikation auch erweitert werden muss. Erweiterbarkeit wird hier aber nicht nur in Bezug auf die Wiederverwendung für andere Datenbanksysteme angestrebt, sondern auch für alle
anderen Teile der Applikation, bei denen in Zukunft möglicherweise Bedarf
besteht, aufgrund von Spezifikationsänderungen Anpassungen vorzunehmen.
Weniger Code-Duplikate
In Datenbankschemadefinitionen wird oft Code dupliziert. Dies trifft besonders dann zu, wenn dieselben Entwurfsmuster wieder und wieder auf neue Objekte angewendet werden. Das Refaktorieren von solchen Schemata erschwert
sich durch viele Duplikate, weil sämtliche Duplikate gefunden und angepasst
werden müssen. Deshalb sollte ein guter Datenbankentwurf das Duplizieren
von Code vermeiden [AS06]. Durch diese Applikation soll der Bedarf sinken,
bestehenden Code zu kopieren und manuell anzupassen. Zusätzlich soll die
Fehlerrate und der Aufwand für die Schemadefinition verkleinert werden.
4.3
Sprachen und Plattform
Um die Umsetzung von Entwurfsmustern und den daraus entstehenden Nutzen zu messen, soll für Testzwecke ein relationales Datenbanksystem verwendet werden. Für dieses Projekt wurde PostgreSQL 8.3 [Pos08] gewählt.
Es handelt sich dabei um ein Open-Source-Produkt, welches auf dem Legacy
DBS POSTGRES 4.2 der Universität von Berkeley basiert. Der SQL-Dialekt
von PostgreSQL richtet sich mehrheitlich nach dem SQL:2003-Standard und
heisst pgSQL bzw. PL/pgSQL (Procedural Language PostgreSQL). Abwei- pgSQL
chungen vom Standard können in [Kli04] oder in [Pos08] nachgeschlagen
werden.
In der Folge wird SQL als Synonym für Datenbanksprachen1 allgemein
1
Es existieren neben SQL noch andere Datenbanksprachen, siehe Kapitel 2.
55
4.4. Zusammenfassung
und die Abkürzung pgSQL für die Anfragesprache von PostgreSQL verwendet.
4.4
Zusammenfassung
Hier werden nochmals alle Anforderungen nummeriert aufgelistet. Die Querverweise zeigen auf spätere Kapitel, wo sich Designentscheidungen und Implementierung befinden, die durch die jeweilige Anforderung begründet sind.
1. Hauptfunktionen
(a) Datenbankanfragen entgegennehmen und verarbeiten (5.3.1, 5.4.2,
5.4.2 und 5.5.3)
(b) Definieren und Speichern von Entwurfsmustern (5.3.2, 5.4.2, 5.5.2
und 5.5.3)
(c) Wiederverwendung von Entwurfsmustern (5.3.3, 5.4.2, 5.5.2 und
5.5.3)
(d) Erzeugung von Datenbankobjekten (5.5.3)
(e) Fehlerbenachrichtigung (5.5.4)
2. Nichtfunktional
(a) Wiederverwendbarkeit (5.4.2, 5.2 und 5.5.1)
(b) Erweiterbarkeit (5.5.3)
(c) Weniger Code-Duplikate
56
Kapitel 5
Lösungskonzept
Basierend auf den Anforderungen von Kapitel 4 wird nun ein Lösungskonzept
für ein Werkzeug ausgearbeitet, welches die genannten Anforderungen erfüllt.
Zunächst wird beschrieben, welches Prozessmodell für den Entwicklungsprozess verwendet wurde. Danach werden verschiedene Lösungsvorschläge aufgezeigt und miteinander verglichen. Die genannten Lösungsvorschläge sind als
grobe Vorschläge auf einem abstrakten Niveau zu verstehen. Anhand des Vergleichs wird eine begründete Entscheidung gefällt. Am Ende dieses Kapitels
folgt die Verfeinerung des gewählten Lösungsvorschlags in den Abschnitten
5.4 Detailentwurf und 5.5 Implementierung. Diese beiden Abschnitte behandeln nur die wichtigsten Aspekte des zu erstellenden Systems, da Umfang und
Komplexität auf dieser Detaillierungsstufe den Rahmen dieser Ausarbeitung
sprengen würden und dadurch kaum das Verständnis für das System erhöht
wird. So wurde bspw. bewusst darauf verzichtet, ein vollständiges Klassendiagramm im Implementierungsabschnitt zu platzieren. Sämtliche Diagramme
sind abstrahiert und zeigen nur Ausschnitte. Gesamtstruktur und sämtliche
Details der Lösung können dem dokumentierte Quellcode entnommen werden, welcher sich auf der beiliegenden CD befindet.
57
5.1. Vorgehen
5.1
evolutionäres
Prototyping
Vorgehen
Bei der Konzeption des Entwurfs und der Implementierung wurde ein modularer Ansatz gewählt. Zusammen mit dem Betreuer wurden die Kernfunktionen
des Systems identifiziert, um daraus einen evolutionären Prototypen [Gli03]
zu entwickeln. Auf diese Weise sollte ein modulares, leicht erweiterbares System entstehen und sichergestellt werden, dass möglichst bald ein lauffähiges
Subsystem existiert, welches nach und nach mit zusätzlicher Funktionalität
ergänzt werden kann. Das Softwareentwicklungs-Prozessmodell, welches für
dieses Projekt gewählt wurde, orientierte sich stark am Prozessmodell der
agilen Softwareentwicklung, welches wiederum Parallelen zum Wachstumsmodell aufweist [Gli03]. Der Betreuer übernahm dabei die Rolle des Kunden, der zusammen mit dem Diplomanden die Anforderung für die nächste
Teillieferung der inkrementell wachsenden Software spezifizierte und die Lösungsvorschläge des Diplomanden prüfte und genehmigte.
5.2
Lösungsvorschläge
Für die Umsetzung eines Systems, das den Anforderungen aus Kapitel 4
gerecht wird, haben sich drei Alternativen angeboten:
1. Vorverarbeitung von SQL-Code: Der Datenbankentwickler verfasst seine Schemadefinitionen in SQL. Wenn er ein Muster implementieren
möchte, dann kann er dieses Muster mit einem Kürzel (sog. Makro)
konkretisieren. Natürlich muss das jeweilige Muster zunächst als Makro definiert werden. Makros werden von einem Makroprozessor in SQL
übersetzt. Der übersetzte Code ersetzt dann die Kürzel im ursprünglichen Code.
2. SQL-Spracherweiterung: Damit Entwurfsmuster durch die Datenbank
intern umgesetzt werden, kann die Datenbanksprache erweitert werden. Der Benutzer hat bspw. die Möglichkeit, neben herkömmlichem
SQL-Code auch vordefinierte Entwurfsmuster mit Hilfe von zusätzlichen SQL-Operatoren aufzurufen. Im Gegensatz zu Makros wird die
58
5.2. Lösungsvorschläge
Anfrage nicht in einem zusätzlichen Schritt vorverarbeitet, sondern eine angepasste Version des Datenbank-Compilers übernimmt die Übersetzung der zusätzlichen Operatoren in angewandte Entwurfsmuster.
3. Verwendung von benutzerdefinierten Funktionen: Entwurfsmuster werden auf einer Programmierschnittstelle (API: Application Programming
Interface), welche vom DBS zur Verfügung gestellt wird, als benutzerdefinierte Funktionen erzeugt. Diese Funktionen können mit SQL aufgerufen werden, wenn ein Muster wiederverwendet werden soll.
Eine genaue Grenze zwischen Spracherweiterungen und Makroprozessoren zu
ziehen ist schwierig, denn die Übergänge sind fliessend. Dies wird offensichtlich, wenn man rationale Präprozessoren betrachtet, die eine Programmiersprache mit eingebauten Makros um neue Sprachkonstruktionen erweitern.
Solche Makroprozessoren werden in der Literatur [ASU99] durchaus auch als
Spracherweiterungen bezeichnet.
Die alternative Spracherweiterung, die hier gemeint ist, soll sich vom
Makrobegriff abgrenzen, indem sie eine Erweiterung der Datenbanksprache
durch ein Reengineering des Compilers und der Implementierung des DBMS
meint. Diese Erweiterung versteht die neue Sprache, die den Aufruf von vordefinierten Entwurfsmustern erlaubt.
5.2.1
Die Lösungsvorschläge im Vergleich
Erweiterbarkeit, Modularität
Kapitel 4 spezifiziert die Anforderung, dass die Applikation in der Lage sein
soll, Entwurfsmuster auf verschiedene Datenbanksysteme abzubilden. Dazu
ist es notwendig, dass Möglichkeiten geschaffen werden, verschiedene Datenbanksprachen und Dialekte zu verstehen. Um dies zu ermöglichen, ist eine
Schnittstelle notwendig, an der neue Sprachen oder Dialekte als Plug-Ins
„eingesteckt“ werden können.
Modifiziert man ein DBMS, um die Sprache zu erweitern (Vorschlag 2),
so ist die Applikation an die Neu-Implementierung des Datenbanksystems gebunden. Es ist dann – falls überhaupt – nur unter grossem Aufwand möglich,
59
5.2. Lösungsvorschläge
Entwurfsmuster auf ein anderes DBS zu übertragen und wiederzuverwenden.
Selbst das Portieren auf neue Versionen des zuvor angepassten Datenbanksystems könnte problematisch und aufwändig werden.
Auch bei der Verwendung von benutzerdefinierten Funktionen (Vorschlag
3) entsteht eine starke Abhängigkeit zum verwendeten DBS. Falls überhaupt Programmierschnittstellen zur Erzeugung solcher Funktionen vorhanden sind, handelt es sich dabei oft um datenbankspezifische Programmiersprachen und Dialekte, was die Portierung auf andere Datenbanksysteme
erschwert. Immerhin verfügen die meisten DBS über eine eigene prozedurale
Sprache. Manche Datenbanksysteme akzeptieren sogar externe Prozeduren,
die sich in SQL aufrufen lassen. In Oracle ist es bspw. möglich, solche Prozeduren in sämtlichen Programmiersprachen zu implementieren, die sich auf der
jeweiligen Plattform als dynamische Bibliothek kompilieren lassen [FP03].
Auch PostgreSQL bietet Möglichkeiten, externe Programmiersprachen für
benutzerdefinierte Funktionen zu verwenden. Leider gibt es auch bei der Art
und Weise, wie externen Programmiersprachen eingebunden sind, grosse Unterschiede. Somit ist nicht garantiert, dass solche Funktionen wiederverwendet werden können, selbst wenn das DBS die gleichen Programmiersprachen
unterstützt.
Ein Makroprozessor (Vorschlag 1) kann so entworfen werden, dass er
als datenbankunabhängige Komponente für verschiedene Datenbanksysteme
wiederverwendet werden kann. Darin liegt der Hauptvorteil eines Makroprozessors.
Aufwand
Für dieses Projekt stehen sechs Mann-Monate zur Verfügung. In dieser kurzen Zeit ein komplettes Datenbanksystem umzubauen (Vorschlag 2), ist mit
den verfügbaren Ressourcen kaum zu realisieren. Steht ein API zur Verfügung, welches es erlaubt, neue Bibliotheken in die bestehende Sprache (Vorschlag 3) einzubauen, kann mit deutlich weniger Aufwand gerechnet werden.
Etwas mehr Aufwand muss für die Entwicklung eines Makroprozessors (Vorschlag 1) einkalkuliert werden, da zusätzliche Übersetzungsphasen für die
60
5.2. Lösungsvorschläge
Vorverarbeitung implementiert werden müssen, was für eine Sprache wie SQL
einen nicht unerheblichen Aufwand bedeutet. Dennoch sollte diese Aufgabe
in sechs Mann-Monaten zu bewältigen sein.
Mächtigkeit
Die Mächtigkeit von benutzerdefinierten Funktionen (Vorschlag 3) ist eingeschränkt. Sie beschränkt sich auf die Mächtigkeit der Sprache, in der die
Funktionen definiert werden. Demgegenüber bieten sich beinahe unbeschränkte Möglichkeiten bei einem aufwändigen Reengineering eines Datenbanksystems (Vorschlag 2).
Da ein Makroprozessor (Vorschlag 1) SQL-Code generiert, ist er auf die
Funktionalität des DBMS angewiesen. welches diesen Code entgegennimmt
und verarbeitet. Damit beschränkt sich die Mächtigkeit des Makroprozessors
auf die Mächtigkeit der SQL-Sprache des DBMS. Damit lassen sich dennoch
alle geforderten Funktionalitäten umsetzen. Natürlich gilt auch für den Makroprozessor: Je umfangreicher die Funktionalität, desto aufwändiger wird
die Lösung. Da sich der Makroprozessor aber sehr gut dafür eignet, inkrementell aufgebaut zu werden, kann der Funktionsumfang an die zeitlichen
Ressourcen angepasst werden. Der Aufwand für die Entwicklung eines Systems mit Kernfunktionalitäten ist für den Makroprozessor wesentlich weniger
aufwändig als für eine Spracherweiterung.
5.2.2
Wahl des Lösungskonzepts
Der Makroprozessor hebt sich deutlich von den beiden anderen Lösungsvorschlägen ab, weil er als einzige Variante das wichtige Kriterium der Wiederverwendbarkeit erfüllt. Auch in den anderen Bereichen erfüllt er die Anforderungen an ein Werkzeug zur Speicherung und Wiederverwendung von Datenbankentwurfsmustern innerhalb der Rahmenbedingungen dieses Projekts.
Dies soll in Abbildung 5.1 veranschaulicht werden. Die in dieser Grafik verwendete absolute Bewertung basiert auf der subjektiven Wahrnehmung des
Verfassers, weshalb die Abstände zwischen den Lösungsvarianten auf den
drei Achsen nicht unbedingt interpretierbar sind. Die Grafik soll lediglich
61
5.3. Funktionalität des Makroprozessors
Wiederverwendbarkeit
Makroprozessor
Spracherweiterung
Benutzerdefinierte
Funktionen
Mächtigkeit
Aufwand
Abbildung 5.1: Die drei Lösungsvarianten im Vergleich mit den Dimensionen
Wiederverwendbarkeit, Mächtigkeit und Aufwand. Je weiter die Punkte vom Zentrum entfernt liegen, desto besser. Punkte im grünen Bereich erfüllen die Anforderungen.
veranschaulichen, dass der Makroprozessor als einziger Lösungsvorschlag die
geforderten Eckpunkte im grünen Bereich der Dimensionen „Wiederverwendbarkeit“, „Mächtigkeit“ und „Aufwand“ erreicht.
SQLPP
Aus diesem Grund wurde entschieden, für dieses Projekt einen Makroprozessor zu entwerfen. Im Folgenden wird für die zu erstellende Applikation
auch das Kürzel SQLPP (SQL Pre Processor) verwendet. Der Name ist
möglicherweise zu wenig generisch gewählt, da es auch denkbar wäre, den
Makroprozessor für andere Sprachen zu verwenden. Wie wir im Abschnitt
5.4 sehen werden, basiert diese Wiederverwendbarkeit (oder Sprachunabhängigkeit) auf einer geschickten Trennung von SQL und der Sprache für die
Definition und den Aufruf von Makros.
5.3
Funktionalität des Makroprozessors
In diesem Abschnitt werden die Anforderungen an die Funktionalität eines
solchen Makroprozessors spezifiziert. Diese Anforderungen sind keine Wieder62
5.3. Funktionalität des Makroprozessors
holung der Anforderungsspezifikation aus Kapitel 4. Sie stehen in direktem
Zusammenhang mit dem gewählten Lösungskonzept und sind deshalb auf
einem weniger abstrakten Level angesiedelt als die ursprüngliche, lösungsneutrale Anforderungsspezifikation.
5.3.1
SQL-Code
Ein Makroprozessor soll durch die Verwendung von Makros SQL-Code generieren. Makros definieren SQL-Codeabschnitte, die vom SQLPP mit zusätzlichen, anwendungsspezifischen Informationen ergänzt werden. Auf diese
Weise erzeugt der SQLPP vollständige SQL-Befehle. In den wenigsten Fällen
jedoch wird ein Datenbankentwickler das ganze Schema mit Makros generieren. Idealerweise definiert er nur für wiederkehrende Muster Makros, die er
dann bei der Schemadefinition wiederverwendet. Der restliche Code wird als
„normaler“ SQL-Code verfasst. Der Makroprozessor soll also SQL-Code mit
Code anreichern, den er durch die Verwendung von Makros generiert.
Damit der Makroprozessor mit SQL arbeiten kann, muss er SQL-Code
„verstehen“. Das wiederum bedeutet, dass ein Makroprozessor über eine Sprachspezifikation verfügen muss, die zumindest die Grammatik von SQL abdeckt.
Um Makros zu verwenden, muss die Grammatik erweitert werden. Diese Erweiterungen werden in den nächsten Abschnitten beschrieben.
Mit Hilfe der Grammatik kann die Syntax der Sprache geprüft werden.
Es wäre auch denkbar, neben der syntaktischen Analyse eine semantischen
Analyse durchzuführen. Um die Semantik zu prüfen, benötigt der Makroprozessor Schemainformation oder eine Anbindung zur Datenbank, um diese Informationen zu extrahieren. Der Mehraufwand für die Implementierung und
die Anwendung von semantischen Analysen wäre beträchtlich, der Mehrwert,
der daraus entstünde, zu gering, um den zusätzlichen Aufwand zu rechtfertigen. Deshalb wird in diesem Projekt darauf verzichtet, eine semantische
Analyse zu implementieren.
63
5.3. Funktionalität des Makroprozessors
5.3.2
Definition von Makros
Will man Makros verwenden, müssen diese natürlich irgendwann1 definiert
werden. Gemäss der Beschreibung von Makroprozessoren in Abschnitt 2.5
kann man Makros mit einem Namen, formalen Parametern und einem Rumpf
spezifizieren. In Kapitel 3 haben wir gesehen, dass Datenbankentwurfsmuster immer Codeabschnitte beinhalten, die bei jeder Anwendung des Musters
gleich bleiben. So haben z. B. alle Dimensionstabellen des SCD-Typs 2 dieselben zwei Zeitstempelattribute für die Transaktionszeit, wenn man die in 3.4.3
auf Seite 45 vorgeschlagene Implementierung für das Muster wählt. Codeabschnitte, die immer gleich bleiben, werden bei der Definition von Makros im
Rumpf platziert.
Neben gleichbleibenden Codeabschnitten enthalten die meisten Implementierungen von Mustern Code, der sich für jede Anwendung des Musters
verändert. Eine Archivierungstabelle hat bspw. bei jeder Anwendung des
Musters eine andere Basistabelle mit anderen Attributen. Solche Codeabschnitte müssen bei jeder Anwendung des Musters neu spezifiziert werden.
Für Makros bietet sich dafür eine Parameterübergabe an. In einer Makrodefinition sollten deshalb formale Parameter spezifiziert werden können. Referenzen auf diese formalen Parameter innerhalb des Rumpfes ermöglichen es,
den Rumpf mit den anwendungsspezifischen Teilen des Codes zu ergänzen,
wenn ein Makro aufgerufen wird. In Kapitel 6 wird das Archivierungsmuster mit dem SQLPP umgesetzt. Die Basistabelle und ihre Attribute werden
dabei als Parameter übergeben (vgl. Codebeispiel 6.4 auf Seite 93).
5.3.3
Makroaufrufe
Makroaufrufe müssen so spezifiziert werden, dass ein Makro, welches definiert
wurde, gefunden und mit den anwendungsspezifischen Informationen gefüttert wird. Ein Makroaufruf sollte also ein referenzierendes Element, z. B. den
Namen des Makros, und eine konkrete Parameterbelegung beinhalten. Die
1
Makros müssen nicht unbedingt vor ihrer Verwendung definiert werden. Wie wir später
sehen werden, wurde der SQLPP so entworfen, dass Makros auch erst definiert werden
können, nachdem sie verwendet wurden.
64
5.3. Funktionalität des Makroprozessors
konkrete Parameterbelegung beinhaltet die anwendungsspezifischen Informationen eines Musters.
5.3.4
Vermischung von zwei Sprachen
Damit Makros bestehenden Code anreichern können (vgl. Abschnitt 5.3.1),
muss es möglich sein, Makros innerhalb von SQL-Code aufzurufen. Dies bedeutet, dass Makrocode mit SQL-Code vermischt werden darf.
Durch die Vermischung von zwei Sprachen, entsteht eine neue Sprache, die
viel komplexer ist als die Zielsprache SQL. Da Makros auch nur Teilmengen
des Codes eines SQL-Befehls sein können, müsste man für jedes Terminal,
jedes Nichtterminal und jede Teilmenge von Terminalen und Nichtterminalen einer Regel als Alternative auch einen Makroaufruf in die Produktionen der kontextfreien Grammatik einbauen. Selbst bei einer einzigen simplen
Produktion, wie der vereinfachten Form des „CREATE TABLE“-Befehls aus
Abbildung 2.1 auf Seite 16, ist die Umsetzung von gemischtem Code ein
extrem schwieriges Unterfangen: Die Menge der Produktion und die Komplexität nimmt mit jeder Teilmenge von Terminalen und Nichtterminalen
stark zu. Durch die vielen zusätzlichen Produktionen und die erhöhte Komplexität steigt die Gefahr, dass Mehrdeutigkeiten spezifiziert werden. Es ist
aber nicht nur schwierig, eine solche Grammatik zu spezifizieren, die Komplexität der Grammatik kann auch zu Performanceproblemen führen, weil
der Aufwand für das Parsen der Grammatik ansteigt. Wie kann man eine
komplexe Grammatik vermeiden und dennoch Makros innerhalb von SQLBefehlen verwenden? Der Detailentwurf (Abschnitt 5.4 auf Seite 68) wird
darauf eine Antwort geben.
5.3.5
Makros, die Makros aufrufen
Eines der Hauptmotive aus Kapitel 1, ein Werkzeug für die Wiederverwendung von Datenbankentwurfsmustern zu entwickeln, war, Codeduplikate zu
reduzieren (siehe auch Anforderung 2c). Auch bei Makrodefinitionen sollten
Codeduplikate vermieden werden. Um dies zu tun, muss die Wiederverwendbarkeit von Makros möglich sein. Wiederverwendbarkeit von Makrodefini65
5.3. Funktionalität des Makroprozessors
tionen bedeutet, dass die wiederverwendbaren Codeabschnitte der Definitionen in eigene Makrodefinitionen ausgelagert werden können. Bspw. könnte
der Code für die Erzeugung von zwei Zeitstempelattributen auf temporalen
Relationen als eigenes Makro ausgelagert werden und von Makros für die
Erzeugung von TT-Relationen, bitemporalen Relationen oder SCD-Typ-2Dimensionen wiederverwendet werden. Um diese ausgelagerten Makros wiederverwenden zu können, müssen Makros von anderen Makros aufgerufen
werden können. Die Möglichkeit, dass sich Makros gegenseitig aufrufen, verursacht aber Probleme: So können Makros, die sich wechselseitig immer wieder aufrufen, einen endlosen Zyklus von Aufrufen verursachen. Wie dieses
Problem behoben wurde, wird in Abschnitt 5.4.2 und Abschnitt 5.5.4 erläutert.
In vielen Situationen ist es von Vorteil, ein grosses Problem in viele kleine Probleme zu unterteilen. Auch bei der Verwendung von Makros ist es
wünschenswert, die Umsetzung eines Datenbankentwurfsmusters auf mehrere
Makrodefinitionen zu verteilen. Dies verringert die Komplexität von Makros
und erhöht dadurch die Verständlichkeit, was als weiteres Motiv für die Aufteilung von Makros in Makros, die sich gegenseitig aufrufen, erwähnt werden
darf.
5.3.6
Einkopieren von Dateien
Eine weitere Möglichkeit für die Reduktion von Komplexität und die Wiederverwendung von Makros und anderem Code wäre die Möglichkeit, Dateien
einzukopieren. Solche Dateien könnten nicht nur Makrodefinitionen, sondern
auch Makroaufrufe und SQL-Code enthalten. Dadurch könnte man bspw.
mehrere Makroaufrufe, die für dieselbe Aufgabe verwendet werden, in eine
eigene Datei auslagern. Wenn diese Datei einkopiert wird, kann verhindert
werden, dass die gleiche Abfolge von Befehlen immer wieder neu geschrieben
werden muss.
66
5.3. Funktionalität des Makroprozessors
5.3.7
Bedingte Übersetzung
Da der SQLPP für verschiedene Zielsprachen verwendet werden soll, wäre es
gut, wenn man Makros so spezifizieren könnte, dass sie für andere Sprachen
ebenfalls wiederverwendet werden können. Es sollte eine Möglichkeit geben,
dem Präprozessor alternative Implementierungen von Makros zur Verfügung
zu stellen, sodass der SQLPP zur Laufzeit entscheiden kann, welche Implementierung er für die Übersetzung verwendet. Der SQLPP wird diese Entscheidung aufgrund von Bedingungen treffen, die bspw. die Existenz einer
Zielsprache testen.
5.3.8
Weitere Anforderungen
Wie wir durch die Umsetzung von Beispielmustern in Kapitel 6 sehen werden, ergeben sich je nach Komplexität des Datenbankentwurfsmusters weitere
Anforderungen.
Schemainformationen
So wäre es bspw. von Vorteil, wenn der Präprozessor Informationen über
den Zustand eines Datenbankschemas besitzt würde. Diese Informationen
bezeichnen wir in der Folge dieses Dokuments als Schemainformationen. SchemaNur mit solchen Informationen ist möglich, DDL-Code zu generieren, der informationen
abhängig von bestehenden Schemaobjekten ist. Ausserdem könnten solche
Information dafür verwendet werden, zusätzliche semantische Analysen (vgl.
Abschnitt 5.3.1) durchzuführen.
Schemainformationen können entweder vom SQLPP durch eine Datenbankanbindung extrahiert oder aus Skripts eingelesen werden und müssen
dann auf eine interne Datenstruktur abgebildet werden. Wie diese interne
Datenstruktur implementiert werden kann, wird in Abschnitt 5.5.3 auf Seite 84 erläutert.
67
5.4. Detailentwurf
Kontrollstrukturen
Kontrollstrukturen
Eine weitere Anforderung, die in Kapitel 6 offensichtlich wird, ist die Notwendigkeit von zuätzlichen Kontrollstrukturen innerhalb der Makrosprache, wie
sie in fast jeder Programmiersprache vorhanden sind. Wird der Makroprozessor gemäss den Abschnitten 5.3.1 - 5.3.7 spezifiziert, so kann er nur einfache
Textersetzung durch Einsetzen der Parameterbelegung durchführen. In Abschnitt 6.1 auf Seite 89 wird an einem Beispiel gezeigt, wie der SQLPP ein
Muster auf diese Weise umsetzen kann. Die Textersetzung genügt für Datenbankentwurfsmuster, die nicht besonders komplex sind. Sie genügt jedoch
nicht für komplexere Muster. Z. B. sollte ein Makro, welches Subtabellen einer
Vererbungshierarchie erzeugt, über alle Benutzerberechtigungen der Wurzel
iterieren können, um diese Benutzerberechtigungen auch für die Subtabelle
zu definieren.
5.4
5.4.1
Detailentwurf
Hauptaufgaben des Makroprozessors
Der Präprozessor, der für dieses Projekt erstellt wurde, erfüllt drei Hauptaufgaben:
1. Definieren von Makros
2. Expandieren von Makros
3. Einkopieren von Dateien
5.4.2
Ein 3-Phasen-Modell
Eine der Hauptanforderungen aus Kapitel 4 ist die Anforderung 2a auf Seite 56, „Wiederverwendbarkeit“: Die Applikation soll modular erweiterbar entworfen werden, sodass der Makroprozessor für verschiedene Datenbanksysteme verwendet werden kann. Um dies zu erreichen, wurde ein Steckersystem
als Architekturmetapher verwendet. Glinz definiert in [Gli03] eine Architekturmetapher als
68
5.4. Detailentwurf
SQLPP
Sprachspezifikationen
(Grammatiken)
D
2
B
Oracle
PostgreSQL
Schnittstelle für die Anbindung von Sprachen
Abbildung 5.2: Steckersystem für die Anbindung von Datenbanken. In diesem
Beispiel werden die Grammatiken der SQL-Dialekte von PostgreSQL, Oracle und
DB2 „eingesteckt“.
... Modell, das eine Architektur über analoge, vertraute Bilder erschliesst. Die Metapher ermöglicht ein besseres Verständnis
der Systemstruktur und ist das Leitbild für die Gestaltung der
Architektur.
Architekturmetapher
Ein Steckersystem besteht aus einer Grundkomponente mit einer Schnittstelle, an welcher Zusatzkomponenten bei Bedarf „eingesteckt“ werden. Die
Architektur des SQLPP wurde nach diesem Prinzip aufgebaut, indem die
Applikation mit einer Schnittstelle entworfen wurde, an der beliebige Sprachen von Datenbanksystemen eingebunden werden können, ohne dass dafür
Anpassungen an der Implementierung des SQLPP notwendig sind (Abbildung 5.2).
Die Forderung nach Austauschbarkeit der Sprachen verlangt, dass die
Spracherweiterung, die für die Definition und den Aufruf von Makros notwendig ist, vollständig von der Datenbanksprache entkoppelt wird. Dadurch
kann für die Zielsprache und für die Makrosprache jeweils eine eigene Grammatik spezifiziert werden. Um dies zu erreichen, wurde die Übersetzung in
drei Phasen aufgeteilt:
1. Phase: Mit Hilfe einer Sprachspezifikation (Grammatik) für Makros
69
5.4. Detailentwurf
erkennt der SQLPP Makroaufrufe und Makrodefinition
2. Phase: Die gefundenen Makroaufrufe werden geprüft und expandiert.
3. Phase: Eine zweite Grammatik spezifiziert die verwendete Datenbanksprache. Mit dieser Sprachspezifikation wird der Zielcode validiert.
Die Trennung dieser Phasen in Teilphasen und die damit verbundene Trennung der Grammatiken behebt ein zweites Problem, welches aus den funktionalen Anforderungen an einen Makroprozessor hervorgeht (vgl. Abschnitt 5.3
auf Seite 62): Sie führt dazu, dass die Komplexität der Grammatiken sehr
stark reduziert werden kann. Um Makroaufrufe innerhalb von SQL-Code
zu erlauben, wäre eine komplexe Erweiterung der ohnehin schon komplexen
SQL-Grammatik notwendig (vgl. Abschnitt 5.3.4). Indem das Problem in
Teilprobleme gliedert wird und eigene Grammatiken für Quell- und Zielsprache spezifiziert werden, wird die Umsetzung der Grammatiken vereinfacht
und die Performance bei der Verarbeitung – durch die reduzierte Komplexität der Grammatiken – verbessert.
Phase 1 und 3 können gemäss den in Abschnitt 2.4 beschriebenen Phasen
des Compilerbaus in weitere Teilphasen gegliedert werden. Eine Abbildung
der Architektur, die auf diesem Phasenmodell basiert, befindet sich am Ende
dieses Abschnitts (Abbildung 5.4). Sie kann als Referenz für die einzelnen
Komponenten in dieser Beschreibung verwendet werden.
Phase 1: Erkennen von Makroaufrufen und -definitionen
In der ersten Phase übersetzt der Präprozessor eine Eingabe aus Makroaufrufen, Makrodefinitionen, Dateiimporten (sog. File-Includes) und SQL-Code
Statement
in eine interne Datenstruktur. Der Statement Recognizer erkennt diese unterRecognizer
schiedlichen Befehle aus der Eingabe. Für die Erkennung verwendet er eine
MeSQL
Spezifikation der Makrosprache MeSQL (Macro extended SQL) und bildet die
Statement Model Befehle auf die interne Datenstruktur, das Statement Model, ab. Das Statement Model dient als Zwischencodeerzeugnis, welches während der weiteren
Phasen der Übersetzung verwendet und manipuliert wird. Die Implementierung des Statement Models wird in Abschnitt 5.5.3 erläutert.
70
5.4. Detailentwurf
Bei der Abbildung von Befehlen im Statement Model unterscheiden wir
zwischen Aufrufen (Calls) und Definitionen (Definitions). Aufrufe können Aufrufe
z. B. SQL-Befehle, File-Include-Befehle (File Include Calls), Makroaufrufe
(Macro Calls) oder eine Mischung aus beidem sein2 . Reiner SQL-Code wird
von SQL-Code, welcher Makroaufrufe enthält, getrennt, da er in der nächsten
Phase nicht behandelt werden muss und somit direkt an Phase 3 weitergegeben werden kann. Dadurch wird die Performance verbessert. Auch Dateien,
die nur SQL-Code enthalten, können auf die gleiche Weise behandelt werden.
Das Weiterreichen dieser Aufrufe wird in Abbildung 5.3 dargestellt.
Definitionen sind Makrodefinitionen oder Dateien (File Include Data). Definitionen
Dateien enthalten Code, der an der Aufrufstelle des File-Includes-Befehls
einkopiert werden soll. Dateien definieren also den Inhalt einer Datei, ein
File-Include-Befehl spezifiziert den Aufruf, diesen Inhalt einzukopieren. Eine
Datei wird nur einmal definiert, kann aber beliebig oft einkopiert werden.
Definitionen enthalten die Daten, die für die Expandierung bzw. für das
Einkopieren benötigt werden. Erst in Phase 2 werden Makros expandiert
und Dateien einkopiert. Da das Einkopieren im Prinzip nichts anderes ist als
das Expandieren einer Dateireferenz, wird in der Folge auch das Einkopieren
von Dateien als „Expandieren“ bezeichnet.
Das Expandieren wird aus zwei Gründen auf eine später Phase verschoben: Zum einen ermöglicht ein derartiges Design, dass Makros verwendet
werden können, bevor sie definiert wurden. Dies verringert bspw. die Fehleranfälligkeit, wenn bestehender Code umstrukturiert wird. Zum anderen
ergeben sich aus dieser Phasentrennung auch Performancevorteile: Wenn Makroaufrufe und File-Includes selbst wieder Makroaufrufe erzeugen, muss sichergestellt werden, dass keine Zyklen beim Aufruf von Makros entstehen.
Die Makroaufrufe können als Graf (Aufrufgraf) dargestellt werden. Dieser
Aufrufgraf wird nach Zyklen durchsucht, was später in Abschnitt 5.5.4 erläutert wird. Wahlweise hat man nun die Möglichkeit, den Grafen entweder
bei jedem Makroaufruf oder ein einziges Mal, nachdem alle Makroaufrufe bekannt sind, zu prüfen. Wird eine Zyklensuche bei jedem der n Makroaufrufe
durchgeführt anstatt am Ende einmal, so steigt der Aufwand für die Suche
2
Eine abschliessende Liste aller Aufrufe finden Sie in Abschnitt 5.5.
71
5.4. Detailentwurf
Statement
Model
SQL
Statement Recognizer
File
Incl.
Call
SQL Validator
SQL Code
Expander
SQL +
Macro
Calls
Calls
Macro
Calls
MeSQL
Code
...
Macro
Defs
Definitions
File
Incl.
Data
Phase 1
Phase 2
Phase 3
Abbildung 5.3: In Phase 1 bildet der Statement Recognizer den MeSQL-Code in
eine interne Datenstruktur, das „Statement Model“, ab. SQL-Code, der nicht mit
Makros angereichert ist, wird direkt an Phase 3 weitergereicht.
im schlimmsten Fall (worst case) um den Faktor n an. Der schlimmste Fall
tritt ein, wenn der SQLPP alle Makroaufrufe prüfen muss, d. h., wenn ein
Zyklus durch den letzten Makroaufruf verursacht wird, oder gar kein Zyklus
existiert.
Durch das Verschieben des Expandierens auf eine spätere Phase kann also
der Aufwand für die Zyklensuche reduziert werden und Makros dürfen vor
ihrer Definition aufgerufen werden. Das Zwischencodeerzeugnis dieser Phase,
das Statement Model, dient zur Eingabe für die nächste Phase. Sie beginnt
mit der Zyklensuche und expandiert danach alle Makroaufrufe.
Phase 2: Makro-Expandierung und Einkopieren von Dateien
Nachdem der erste Teil des MeSQL-Codes analysiert ist, kann der PräproMacro Expander zessor mit der Makro-Expandierung (Macro Expander) und dem Einkopieren
File Includer
von Dateien (File Includer) beginnen. Der SQLPP erhält alle notwendigen
Information aus dem Statement Model. Aus diesen Informationen werden
nun die Makroaufrufe und der Inhalt der einkopierten Dateien analysiert und
72
5.4. Detailentwurf
der Aufrufgraf nach Zyklen durchsucht. Ist er zyklenfrei, können die Makros
expandiert werden.
Die Expandierung funktioniert so wie in Abschnitt 2.5 für Makroprozessoren beschrieben: Die Parameterbelegung des Makroaufrufs wird an den referenzierenden Stellen des Rumpfs eingesetzt. Dieser modifizierte Rumpf mit
den eingesetzten Parameterwerten ersetzt den Makroaufruf im ursprünglichen Code des Statement Models.
Für das Einkopieren der Dateien wird für jede Datei Phase 1 und 2 wiederholt, bis sämtliche Dateien einkopiert wurden und die Makros expandiert
sind. Die einkopierten Dateien ersetzen die File-Include-Befehle im Statement
Model. Phase 2 kann als eigentliche Übersetzungsphase betrachtet werden,
denn von nun an verfügt das Statement Model über Sätze der Zielsprache,
die in der letzten Phase lediglich noch validiert werden müssen.
Phase 3: Gültigkeitsprüfung
Diese Phase soll überprüfen, ob der übersetzte Code syntaktisch und semantisch korrekt ist. Aus den in 5.3.1 auf Seite 63 erläuterten Gründen wird
in diesem Projekt auf die Umsetzung einer semantischen Analyse verzichtet. Weiterführende Arbeiten mit dem SQLPP könnten sich aber durchaus
mit der Umsetzung dieser Art der Analyse beschäftigen (siehe auch 7.2.4 auf
Seite 116).
Für die syntaktische Analyse wird eine Komponente benötigt, die die Zielsprache versteht. Diese Komponente wird hier als SQL Validator bezeichnet. SQL Validator
Als Eingabe dient das Statement Model aus Phase 2. Die Gültigkeitsprüfung
ist eine bedingte Übersetzung, da sie für verschiedene Sprachen durchgeführt
werden kann. Mehr dazu folgt in Abschnitt 5.5.
73
5.4. Detailentwurf
Input
Script
File
start
Command
Line Input
Statement Recognizer
Script
File(s)
MeSQL Lexer
MeSQL Parser
File Includer
Feed Data
Structure
Statement
Model
Statement
StatementModel
Model
Statement
Collection
Macro
Definitions
File
Includes
SQL Statements
(not validated)
SQL Validator
Macro
Calls
Expanded
Statements
Macro Definition
Lookup
Macro Body
SQL Lexer
SQL Parser
Macro Expander
Macro Body
Lexer
Expand
Controller
validated SQL
Statements
Macro Body
Parser
SQL Output
Recursive Macro
Expansion
SQL
Script
File
Command
Line Output
end
Abbildung 5.4: Gesamte Architektur des SQLPP im Überblick
74
5.5. Implementierung
5.5
Implementierung
Für die Umsetzung des SQLPP wurde die Java Standard Edition 5 als Programmiersprache verwendet [Ull06, JDK04]3 . In den folgenden Abschnitten
werden die wichtigsten Ausschnitte dieser Implementierung präsentiert.
5.5.1
Modularisierung
Damit neue Datenbanksprachen im Steckersystem eingegliedert werden können, muss zunächst eine Sprachspezifikation erstellt und daraus ein Parser
generiert werden. Spezifiziert wird ein „Sprachstecker“ (Plug-In) mit Hilfe des Parsergenerators ANTLR (vgl. Abschnitt 2.4.2 auf Seite 23). Der
Parser muss dabei das Interface der abstrakten Klasse SQLParser, welche
das Startsymbol der kontextfreien SQL-Grammatik festlegt, implementieren. ANTLR-Lexer und -Parser können zur Laufzeit eingebunden werden,
indem die Lexer- und Parser-Klassen in einer Java-Property-Datei registriert
werden. Durch Java-Reflection [Ref02] (auch als Introspektion bezeichnet)
werden diese Klassen dann dynamisch zur Laufzeit geladen und verwendet.
5.5.2
Makrosprache
In Phase 1 erkennt der Makroprozessor Makroaufrufe und -definitionen. Um
Makros zu verwenden, wurde eine eigene Sprache spezifiziert. Wie im Detailentwurf (Abschnitt 5.4 auf Seite 68) beschrieben ist, wurde diese Makrosprache strikt von der Zielsprache getrennt, um mit verschiedenen Zielsprachen
arbeiten zu können und die Komplexität der zu spezifizierenden Grammatik zu reduzieren. In diesem Abschnitt wird die Syntax der Makrosprache
beschrieben.
Lexikalische Struktur
Bezeichner Der Bezeichner ist ein regulärer Ausdruck (siehe 2.3 auf Seite 14), der mit einem Gross-, Kleinbuchstaben oder einem Unter3
Anstelle der neusten, 8. Auflage von [Ull06] wurde die 5. Auflage verwendet, da diese
die in dieser Arbeit verwendete Programmiersprache Java 1.5 dokumentiert.
75
5.5. Implementierung
strich (_) beginnt, gefolgt von einer beliebig langen Zeichenkette
aus Gross- und Kleinbuchstaben, Ziffern und Unterstrichen.
;
Semikolons terminieren einen MeSQL-Befehl.
\
Ein Backslash wird in MeSQL als Maskierungszeichen (auch „Escape-Zeichen“ genannt) verwendet. Es bewirkt, dass das nachfolgende Zeichen maskiert und somit nicht als Funktionszeichen
erkannt wird. Für das Verb „maskieren“ wird in der Folge der aus
dem Angelsächsischen stammende Begriff „escapen“ verwendet.
Makrodefinition
Makrodefinition
Die Makrodefinition spezifiziert Name und Verhalten eines Makroaufrufs. Mit
dem Befehl #DEFINE MACRO wird eine Makrodefinition erzeugt:
#DEFINE MACRO <Makroname> ( <P a r a m e t e r l i s t e > ) { <Rumpf> } ;
Makronamen
Ein Makroname ist ein eindeutiger Bezeichner.
Parameterliste
Eine Parameterliste besteht aus einer kommaseparierten Liste von Parameternamen.
Parametername
Parameternamen sind Bezeichner für die formalen Parameter eines Makros.
Da Parameternamen nur innerhalb einer Makrodefinition sichtbar sind, dürfen die gleichen Namen in verschiedenen Makrodefinition verwendet werden.
Rumpf
Der Rumpf bildet die eigentliche Implementierung eines Makros. Er besteht
aus einer Folge von Makroaufrufen, Parameterreferenzen oder Wörtern. Diese Wörter werden aus einer Folge von Unicode-Basic-Latin-Zeichen gebildet
und sind durch Leerschläge getrennt. Die Zeichen „Raute“ (#), „Semikolon“
(;) und „Backslash“ (\) sind geschützt und müssen bei Verwendung innerhalb des Rumpfes mit Backslash (\) escaped werden. Der Rumpf wird durch
geschweifte Klammern begrenzt. Eine Begrenzung ist notwendig, damit der
Wörter
76
5.5. Implementierung
Präprozessor das Ende des Rumpfes erkennen kann.
Paramaterreferenzen sind die Referenzen auf die formalen Parameter, die in Parameterder Parameterliste spezifiziert wurden. Sie bestehen aus dem Raute-Symbol referenzen
(#), gefolgt vom Parameternamen. Diese Notation kann verwendet werden,
wenn der einzusetzende Parameterwert ein Wort bildet, welches die lexikalischen Eigenschaften eines Bezeichners erfüllt. Für die Erzeugung von SQLCode ist es manchmal aber auch notwendig, dass der Makroprozessor Teile
von Wörtern bilden kann, die keine Bezeichner sind. Z. B. ist der Ausdruck
<schemaname>.<t a b e l l e n n a m e >
kein Bezeichner, weil in der Mitte ein Punkt steht. Um Teile von solchen
Wörtern zu bilden, muss die Referenz mit einem Startsymbol und einem
Endsymbol begrenzt werden. Dies kann man machen, indem die Parameterreferenz mit der Zeichenfolge #< (links) und der Zeichenfolge #> (rechts)
begrenzt wird. Z. B. kann man mit dem Ausdruck
Codebeispiel 5.1: Abgeschlossene Parameterreferenz
p u b l i c . #< t a b e l l e n n a m e #>
den Parameter „tabellenname“ referenzieren und einsetzen, was ohne Begrenzung nicht möglich wäre. Abgeschlossene Referenzen können auch verwendet
werden, wenn Parameternamen mit einer Zeichenfolge beginnen, die für andere Parameternamen stehen (z. B. „tabelle“ und „tabellennamen“).
Für die Makrodefinitionen wurde kein „Überladen“ implementiert. Makronamen dürfen nur einmal vergeben werden. Der Benutzer wird aber nicht
daran gehindert, denselben Makronamen mehrfach zu definieren. Tut er dies,
wird die bisherige Definition mit der neuen Definition überschrieben.
Codebeispiel 5.2 zeigt eine simple Makrodefinition, bei der table_name
als formaler Parameter einen Tabellennamen spezifiziert, der beim Aufruf
des Makros im Rumpf eingesetzt wird (siehe Codebeispiel 5.3).
Codebeispiel 5.2: Makrodefinition
#DEFINE MACRO m _ s e l e c t _ a l l ( table_name ) {
SELECT ∗ FROM #table_name } ;
77
5.5. Implementierung
Makroaufrufe
Makroaufruf
Ein Makroaufruf expandiert an einer beliebigen Stelle im Code ein Makro
mit einer bestimmten Parameterbelegung. Bei einem Makroaufruf muss die
Parameterbelegung festgelegt werden. Ein Makro kann beliebig oft – und
bevor es definiert wurde – aufgerufen werden. Der Aufruf expandiert die Makrodefinition, die den gleichen Namen trägt.
#EXEC Makroname (< P a r a m a t e r w e r t e l i s t e >)
Parameterwerte- Die Parameterwerteliste besteht aus einer Liste von Parameterwerten. Jeder
liste
Parameterwert in der Liste ist durch einfache Anführungszeichen begrenzt.
Die Anführungszeichen von zwei verschiedenen Parameterwerten sind jeweils
durch ein Komma getrennt.
Parameterwerten Die Begrenzung der Parameterwerte mit Anführungszeichen wurde gewählt,
um Parameterwerte zuzulassen, die aus mehreren Wörtern bestehen. Wenn
ein einfaches Anführungszeichen innerhalb eines Parameterwerts als normales, einfaches Anführungszeichen verwendet werden soll, dann muss dieses mit
„Backslash“ escaped werden. Dieses Design ist notwendig, damit der Benutzer SQL-Befehle oder Teile von SQL-Befehlen als Parameter mitgeben und
auf diese Weise zusammenhängende SQL-Codeabschnitte einsetzen kann.
Das Codebeispiel 5.3 zeigt wie die Makrodefinition aus Codebeispiel 5.2
aufgerufen werden kann. Der Aufruf
Codebeispiel 5.3: Makroaufruf
#EXEC m _ s e l e c t _ a l l ( ’ T_Students ’ ) ;
erzeug die Ausgabe:
SELECT ∗ FROM T_Students ;
Einkopieren von Dateien
Um Skript-Dateien mit MeSQL-Code einzukopieren, wird der #INCLUDEBefehl verwendet. Das Einkopieren von Dateien ist ein eigenständiger Befehl.
78
5.5. Implementierung
Es ist nicht möglich, Dateien innerhalb einer Makrodefinition oder eines SQLBefehls einzukopieren.
#INCLUDE <Dateiname >;
Der Dateiname spezifiziert den relativen Pfad eines MeSQL-Skripts, ausge- Dateiname
hend vom Wurzelverzeichnis der Applikation. MeSQL-Skripte sind gewöhnliche Textdateien in UTF8-Kodierung. Das Codebeispiel 5.4 kopiert den Inhlt
der Datei beispiel.txt, welches sich im Wurzelverzeichnis der Applikation befinden, an der Aufrufstelle ein.
Codebeispiel 5.4: Einkopieren einer MeSQL-Datei
#INCLUDE b e i s p i e l . t x t ;
Einkopieren von SQL-Dateien
Wie bereits auf Seite 71 erläutert, ist es besser, SQL-Code ohne MeSQLSprachelemente nicht auf die gleiche Weise wie MeSQL-Code zu behandeln.
Es ist nicht nötig, dass der Inhalt dieser Dateien in den ersten zwei Phasen
übersetzt wird, da es sich ohnehin schon um Code der Zielsprache handelt
(vgl. Abschnitt 5.4.2 auf Seite 68). Wenn ein Benutzer SQL-Skriptdateien
verfasst, kann er diese mit dem #SQLINCLUDE-Befehl einkopieren. Diese SQL Include
Art des Einkopierens birgt zwei Vorteile gegenüber dem „normalen“ Einkopieren von Dateien:
1. Bessere Performance: Dateien, die mit diesem Befehl einkopiert werden, werden erst in der letzten Phase vom SQL Validator geprüft. Dadurch kann Rechenaufwand eingespart werden und die Performance der
Bearbeitung verbessert sich.
2. SQL-Code muss nicht angepasst werden: Der vollständiger Verzicht auf
Elemente der MeSQL-Sprache führt dazu, dass nur die Syntax von SQL
beachtet werden muss. So kann bspw. auf das Escapen von Zeichen
verzichtet werden, die in MeSQL geschützt sind. Der SQL-Code kann
so verfasst werden, wie es der Datenbankentwickler gewohnt ist.
79
5.5. Implementierung
#SQLINCLUDE Dateiname ;
Der Dateiname spezifiziert auch hier den relativen Pfad. Es handelt sich
um eine Textdatei, die in der Zielsprache (SQL) verfasst wurde. In Codebeispiel 5.5 wird das SQL-Skript sqlscript.sql im Verzeichnis scripts, welches
sich unter dem Wurzelverzeichnis der Applikation befinden, einkopiert.
Codebeispiel 5.5: Einkopieren einer SQL-Datei
#INCLUDE / s c r i p t s / s q l s c r i p t . s q l ;
Bedingte Übersetzung
Bedingte
setzung
Über- Die bedingte Übersetzung ermöglicht es, Code nur dann zu übersetzen, wenn
eine Bedingung, welche die Existenz von Makrodefinitionen prüft, erfüllt ist.
Auch hier gilt, dass das Makro auch nach der Bedingung definiert werden
darf.
#IFDEF |#IFNDEF <Makroname>
<A u f r u f l i s t e >
[#ELSDEF <Makroname>
<A u f r u f l i s t e >]
[#ELSE ]
#ENDIF ;
Der Befehl #IFDEF kann verwendet werden, um zu prüfen, ob eine Makrodefinition existiert. #IFNDEF macht das Gegenteil; es prüft, ob eine Makrodefinition nicht existiert. Innerhalb der Bedingungsanweisungen kann optional
mit dem Befehl #ELSDEF die Existenz einer zweiten Makrodefinition getestet werden. Der Befehl #ELSDEF kann beliebig oft wiederholt werden.
Optional kann ein Standardverhalten mit dem Befehl #ELSE definiert werden, welches dann ausgeführt wird, wenn keine Bedingung wahr ist.
Aufrufliste
Die Aufrufliste ist eine Folge von beliebig vielen MeSQL-Befehlen, die ausgeführt werden, wenn die Bedingung zutrifft. Diese Befehle werden mit der
80
5.5. Implementierung
bisher beschriebenen Syntax in die Bedingung eingesetzt, d. h., dass jeder
Befehl mit einem Semikolon terminiert wird.
Bedingtes Übersetzen ermöglicht es, Makros zu schreiben, die auf mehrere
verschiedene Zielsprachen anwendbar sind (vgl. Abschnitt 5.3.7 auf Seite 67).
Um die Zielsprache zu spezifizieren, kann diese bspw. als Makro spezifiziert
werden, welches in der bedingten Anweisung geprüft wird (siehe Codebeispiel 5.6) .
Codebeispiel 5.6: Bedingte Übersetzung
#IFDEF PostgreSQL
/∗ PostgreSQL Code Block ∗/
#ELSDEF O r a c l e
/∗ O r a c l e SQL Code Block ∗/
#ELSE
/∗ Standard SQL Code Block ∗/
#ENDIF
5.5.3
Statement Model
Das Statement Model dient als Zwischencodeerzeugnis für die einzelnen Phasen der Übersetzung. Als interne Datenstruktur speichert es Aufrufe der
Quellsprache, die in der Zielsprache abgebildet werden sollen, sowie Definitionen von Makros und File-Includes. Aufrufe sind Objekte des Typs IStatement, gemäss Abbildung 5.5. IStatement ist eine Schnittstelle, welche von
allen Aufrufen der Makrosprache implementiert wird. Das Statement Model
verfügt über eine Collection, die nur Typen dieser Schnittstelle akzeptieren.
Die Reihenfolge der in der Collection gespeicherten Objekte des Typs IStatement entspricht der Reihenfolge der Aufrufe im Zielcode.
Für die Expandierung in Phase 2 wird Polymorphie verwendet. Die Schnittstelle IStatement definiert die Methode zur Expandierung, die Subklassen
implementieren sie auf unterschiedliche Weise. Der Leser sei an dieser Stelle nochmals daran erinnert, dass die Expandierung erst durchgeführt wird,
nachdem der Makroaufrufgraf auf Zyklen überprüft wurde. Die Zyklensuche,
die dies sicherstellt, wird in Abschnitt 5.5.4 auf Seite 86 beschrieben.
81
5.5. Implementierung
<<interface>>
IStatement
getText(): String
expand(): void
SQLStatement
text:String
...
MacroCall
text:String
...
...
...
HybridStmt
text:String
tokens:Queue
...
IncludeStmt
text:String
definition:IncludeDefinition
...
...
...
SQLIncludeStmt
text:String
...
ConditionStmt
text:String
conditions:Vector<condition>
...
...
...
Abbildung 5.5: Ausschnitt aus dem „Statement Model“ mit einer vereinfachten
Abbildung des Klassendiagramms der MeSQL-Befehlstypen
Zukünftige Erweiterungen des SQLPP um zusätzliche Befehle können als
Typen definiert werden, die die Schnittstelle IStatement implementieren.
Definitionen
Weitere fundamentale Klassen des Statement Models, die für eine bessere
Übersicht nicht auf der Abbildung zu finden sind, sind Klassen, die Definitionen repräsentieren. Dazu gehören die Klassen MacroDefinition und IncludeDefinition. In Abbildung 5.3 des Detailentwurfs sind diese Klassen als Macro
Definitions bzw. File Include Data im Statement Model dargestellt. Beide
Klassen erben von der abstrakten Klasse StatementDefinition, die gemeinsames Verhalten der Unterklassen implementiert. Objekte vom Typ MacroDefinition werden durch den Befehl #DEFINE MACRO <Macroname> erzeugt
und speichern den Namen des Makros, die formalen Parameter sowie Wörter,
Makroaufrufe und Parameterrefernzen im Rumpf. Wird die Klasse IncludeDefinition durch den Befehl #INCLUDE <Datei> instanziert, so speichert
das erzeugte Objekte eine Collection von Objekten des Typs IStatements.
Diese Objekte repräsentieren die MeSQL-Befehle innerhalb der einzukopierenden Datei. Natürlich wird die Klasse IncludeDefinition nur einmal pro
Datei instanziert, auch wenn der File-Include-Befehl, der durch #INCLUDE
<Datei> ausgelöst wird, beliebig oft wiederholt wird. Wie wir in Abschnitt
5.5.4 sehen werden, bilden die Definitionen den Ausgangspunkt einer sequentielle Suche nach Zyklen, die durch die unvorsichtige Verwendung von
82
5.5. Implementierung
Makroaufrufen und File-Include-Befehlen entstehen können.
SQLStatement
SQL-Befehle, die nicht mit MeSQL-Code angereichert sind, werden als Objekte des Typs SQLStatements im Statement Model abgespeichert. Eine Expandierung von SQL-Befehlen ist nicht notwendig, weshalb die Methode expand() eine leere Implementierung verwendet.
MacroCall
Ein Objekt vom Typ MacroCall ist ein Makroaufruf-Befehl (#EXEC <Makroname>;), der nicht von anderem Code umgeben ist. Die Implementierung
der expand()-Methode sucht die zum Aufruf gehörende Makrodefinition des
Typs MacroDefinition und führt eine Textersetzung im Rumpf durch, indem sie die Parameterbelegung an den spezifizierten Stellen einfügt. Für
Makroaufrufe im Rumpf wird die Expandierung und somit der Aufruf der
expand()-Methode rekursiv durchgeführt.
HybridStatement
Dies sind Makroaufrufe, die mit SQL-Code oder anderen Makroaufrufen angereichert sind. Die Trennung zwischen diesem Typ und dem SQLStatementTyp wurde aus Performancegründen gewählt (vgl. Abschnitt 5.4.2). Objekte
vom Typ HybridStatement verfügen über eine First-In-First-Out-Queue (FIFO) aller Wörter4 , aus denen ihr Aufruf zusammengesetzt ist. Beim Expandieren werden diese Codeelemente in der Reihenfolge der Queue ausgelesen
und eingesetzt. Codeelemente des Typs MacroCall werden wie oben beschrieben rekursiv expandiert, bevor sie eingesetzt werden.
IncludeStatement
Bei Aufrufen mit dem Befehl #INCLUDE <Dateiname> wird sowohl ein
Objekt vom Typ IncludeStatement als auch ein Objekt des Typs IncludeDe4
Wörter wurden in Abschnitt 5.5.2 auf Seite 76 spezifiziert
83
5.5. Implementierung
finition erzeugt. Die Klasse IncludeStatement repräsentiert den Aufruf des
Einkopierens und hält eine Referenz auf IncludeDefinition. Aufrufe innerhalb
der Datei werden als Objekte des Typs IStatement in einer Collection von
IncludeDefinition gespeichert. Bei der Expandierung werden diese Objekte in
der Collection durch den Aufruf von expand() gemäss ihrer Typspezifikation
expandiert.
SQLIncludeStatement
Objekte dieses Typs sind analog zu den Objekten des Typs IncludeStatement einzukopierende Dateien. Diese Dateien enthalten nur SQL-Code. Ein
solches Objekt wird durch den Befehl #SQLINCLUDE erzeugt. Die Trennung zwischen Objekten des Typs IncludeStatement und SQLIncludeStatement wurde aus denselben Performance-Überlegungen durchgeführt wie bei
der Trennung von HybridStatement und SQLStatement. Ausserdem kann,
wie bereits in Abschnitt 5.5.2 erläutert wurde, in Skripten, die diesen Typ
erzeugen, SQL-Syntax verwendet werden, ohne dass geschützte Zeichen der
Makrosprache escaped werden müssen. Der Inhalt der Datei wird einkopiert,
wenn das Objekt erzeugt wird. Die Implementierung der expand()-Methode
ist wie beim Typ SQLStatement leer.
ConditionStatement
Bedingte Übersetzung wird auf Objekte des Typs ConditionStatement abgebildet. Diese Objekte verfügen für jede Bedingung (Condition) über eine
Collection von IStatement-Objekten. Wenn eine Bedingung erfüllt ist, werden
diese Objekte expandiert.
Schemaobjekte
Will man die Anforderungen aus Abschnitt 5.3.8 auf Seite 67 umsetzen und
den Makroprozessor mit Schemainformationen versorgen, muss ein Modell
zur Abbildung von Schemaobjekten implementiert werden. Ein solches Modell könnte so umgesetzt werden, dass man pro Schemaobjekttyp eine eigene
Klasse bildet, die entweder eine Schnittstelle oder eine abstrakte Klasse des
84
5.5. Implementierung
Typs SchemaObject implementiert. Konkrete Subklassen dieses Typs sind
bspw. TableObject, TriggerObject, FunctionObject, ViewObject, etc.
Auf eine Implementierung von Schemaobjekten wurde verzichtet, weil
mit den in Kapitel 6 auf Seite 89 beschriebenen Methoden ebenfalls Schemaobjekte verwendet werden können und die Umsetzung eines Modells für
Schemaobjekte den zeitlichen Rahmen dieser Diplomarbeit sprengen würde.
Java-Makros
Kontrollstrukturen, wie sie in Abschnitt 5.3.8 auf Seite 67 beschrieben sind,
können vom Benutzer als Makros mit Java-Code spezifiziert werden. Die
Definition von Java-Makros umfasst, analog zu den herkömmlichen Makrodefinitionen, einen Namen, Parameter und einen Rumpf. Der Rumpf wird
mit Java-Code spezifiziert, wobei die Parameterreferenzen der Syntax von
Javacode folgen. Der SQLPP verfügt über eine Template-Klasse mit einer
statischen Template-Methode. Für jede Definition generiert der SQLPP eine
Java-Quellcodedatei, indem er den Makronamen als Klassennamen verwendet, den Methodenrumpf der Klassenmethode durch den Makrorumpf ersetzt
und die Parameterspezifikation aus der Makrodefinition für die Parameterspezifikation der Klassenmethode verwendet. Die so entstehende Quellcodedatei wird mit Hilfe des Java-Compilers javac übersetzt. Wird ein JavaMakro gespeichert und verwendet, so wird mit Hilfe von Java Reflection
[Ref02] die Klasse zur Laufzeit geladen und die Klassenmethode aufgerufen.
Es wäre auch denkbar, anstelle von Java-Makros eigene MeSQL-Befehle
für die Umsetzung von Kontrollstrukturen zu definieren. Die Umsetzung von
internen Kontrollstrukturen wäre allerdings mit wesentlich mehr Aufwand
verbunden, weil dafür die Sprache der Kontrollstrukturen neu spezifiziert
und in die MeSQL-Grammatik eingebaut werden müsste. Dies würde die
Komplexität der bestehende Grammatik erhöhen.
Zum Zeitpunkt des Verfassens dieses Dokuments wird an der Umsetzung
von Java-Makros in Form eines Proof of Concept gearbeitet. Es ist noch offen, inwieweit diese zusätzliche Funktionalität bis zum Ende des Projekts
verfügbar ist. Für die Definition von Java-Makros ist der Befehl #DEFI85
5.5. Implementierung
NE JMACRO <Makroname> und für den Aufruf #JEXEC <Makroname>
reserviert.
5.5.4
Aufrufgraf
Zyklensuche
Da Makros sich gegenseitig aufrufen können, ist es möglich, dass die Aufrufe einen Zyklus verursachen. Wenn z. B. das Makro M1 definiert wurde
um M2 aufzurufen und M2 wiederum M1 aufruft, so würde der SQLPP in
einen endlosen Zyklus geraten, wenn dieser den Aufruf eines dieser beiden
Makros erlauben würde. Aus diesem Grund werden sämtliche Makrodefinitionen nach Zyklen durchsucht. Für die Suche wird aus den gegenseitigen
Makroaufrufen in den Makrodefinitionen ein Aufrufgraf erzeugt. Dieser Aufrufgraf umfasst nicht nur Makrodefinitionen, sondern Definitionen (vgl. Definitionen 5.4.2) allgemein, also auch Dateien (File Include Data), die den
Inhalt von File-Include-Befehlen spezifizieren. Die Suche nach Zyklen ist als
Tiefensuche implementiert. Bei der Tiefensuche wird der Graph als Baum
aufgespannt und zunächst alle Knoten und Pfade links abwärts des Baumes
traversiert. Sobald ein Knoten in Bearbeitung ist, wird er markiert. Wenn
von einem Knoten sämtliche Nachfolgepfade vollständig abgesucht sind, ist
die Bearbeitung des Knotens abgeschlossen und der Knoten damit validiert.
Die Markierung kann entfernt werden. Führt aber einer der Nachfolgepfade
zu einem Knoten, der markiert ist, so bildet dieser Pfad einen Zyklus und
die Expandierung wird nicht ausgeführt.
Bei der Suche nach Zyklen wird sequentiell jede Definition in der Collection des Statement Models evaluiert (siehe auch Codebeispiel 5.7). Das sequentielle Auslesen ist notwendig, weil es Grafen mit mehreren Komponenten
geben kann. Knoten werden durch die Zyklensuche in Codebeispiel 5.8 mit
ist_validiert markiert, wenn alle nachfolgenden Pfade eines Knotens traversiert wurden. Knoten mit dieser Markierung werden beim sequentiellen
Auslesen aus den Definitionen kein weiteres Mal geprüft.
In der Implementierung der Zyklensuche in Codebeispiel 5.8 wird ein Zyklus gefunden, wenn ein Knoten bearbeitet werden soll, der bereits unter Bearbeitung steht und mit in_Beearbeitung markiert ist (Zeile 2). Andernfalls
86
5.6. Zusammenfassung
wird der Knoten markiert und seine Nachfolgeknoten werden rekursiv aufgerufen (Zeile 6-7). Wenn alle Nachfolger durchsucht sind, wird die Markierung
in_Bearbeitung aufgehoben und der Knoten mit ist_validiert markiert.
Codebeispiel 5.7: Pseudocode der Erzeugung eines Aufrufgrafen
methode e r s t e l l e _ G r a f
1
für a l l e d e f i n i t i o n in Definitionen
2
f a l l s d e f i n i t i o n n i c h t i s t _ v a l i d i e r t dann
3
mache z y k l e n s u c h e ( d e f i n i t i o n ) ;
4
Codebeispiel 5.8: Pseudocode der Zyklensuche
methode z y k l e n s u c h e ( D e f i n i t i o n d e f )
1
f a l l s d e f in_Bearbeitung dann
2
Zyk l u s gefunden ;
3
sonst
4
d e f . in_Bearbeitung=wahr ;
5
für a l l e nachfolgerknoten in def
6
mache Z y k l e n s u c h e ( n a c h f o l g e r k n o t e n ) ;
5.6
7
d e f . in_Bearbeitung=f a l s c h ;
8
d e f . i s t _ v a l i d i e r t=wahr ;
9
Zusammenfassung
Aufgrund der Anforderungen aus Kapitel 4 haben sich drei mögliche Lösungsvarianten angeboten: Spracherweiterung, Makroprozessor oder benutzerdefinierte Funktionen. Nach Abwägung der Vor- und Nachteile der drei
Dimension Wiederverwendbarkeit, Aufwand und Mächtigkeit wurde als Lösungskonzept die Umsetzung eines Makroprozessors gewählt. Dieser Makroprozessor wird als SQLPP bezeichnet.
Die Aufgabe des SQLPP ist die Speicherung von Datenbankentwurfsmustern als Makros. Damit Makros definiert und gespeichert werden können, wurde eine eigene Sprache spezifiziert. Gleichzeitig wurde die Grammatik der Zielsprache spezifiziert, um die Ausgabe des SQLPP syntaktisch
87
5.6. Zusammenfassung
zu analysieren. Eine Trennung der beiden Sprachspezifikationen reduziert die
Komplexität der zu erstellenden Spezifikationen, verbessert die Performance
der Analyen und erhöht die Modularität des Makroprozessors. Aus diesem
Grund wurde die Vorverabeitung in drei Phasen unterteilt:
1. Phase: Analyse der Makrosprache
2. Phase: Expandierung der Makros und Einkopieren der Dateien
3. Phase: Syntaktische Analyse der Zielsprache
Die Zielsprache, die in der 3. Phase verwendet wird, kann durch Verwendung eines Steckersystems ausgetauscht werden. Durch bedingte Übersetzung
ist sichergestellt, dass auch die Implementierung der Makros zur Laufzeit
festgelegt werden kann.
Das Statement Model bildet als Zwischencodeerzeugnis und interne Datenstruktur die Objekte der Befehle, die durch die Vorverarbeitung vom SQLPP in die Zielsprache übersetzt werden. Dieses Modell ist so aufgebaut, dass
Erweiterungen des SQLPP um zusätzliche Befehle leicht implementiert werden können. Java-Makros sind eine mögliche Erweiterung, um den Makroprozessor mit Kontrollstrukturen zu ergänzen. Zukünftige Erweiterungen des
SQLPP könnten Schemainformationen auf eine interne Datenstruktur abbilden. Wie wir in Kapitel 6 sehen werden, könnten diese Informationen in eine
für Datenbankentwurfsmuster geeignete Form gebracht werden.
Da die Wiederverwendbarkeit von Makros erfordert, dass Makros andere
Makros aufrufen können, muss sichergestellt werden, dass solche Makroaufrufe nicht zu einem endlosen Zyklus von Aufrufen führen. Die Zyklensuche
erzeugt einen Aufrufgrafen, der mit Tiefensuche traversiert wird. Damit wird
sichergestellt, dass keine Zyklen existieren, bevor Makros expandiert werden.
88
Kapitel 6
Der SQLPP in Aktion
Dieser Abschnitt zeigt, wie der Makroprozessor zur Umsetzung und Wiederverwendung von Entwurfsmuster verwendet werden kann. Dazu wurden
drei Beispielmuster aus Kapitel 6 ausgewählt. Für jedes Muster wird zunächst eine mögliche Implementierung in SQL präsentiert. Der für die SQLBeispiele verwendete Dialekt ist pgSQL von PostgreSQL. Nach den SQLImplementierungsbeispielen folgt eine Beschreibung der Umsetzung des jeweiligen Musters in MeSQL. Am Ende von jedem der drei Beispiele werden
die Ergebnisse diskutiert.
6.1
Beispiel 1: Archivierungstabelle
In Kapitel 3 wurden Archivierungstabellen als eine Möglichkeit der Implementierung von TT-Relationen beschrieben. Wir möchten in diesem Abschnitt das TT-Relationen-Muster auf diese Art implementieren, um zu zeigen, wie dieses Muster als Makro gespeichert werden kann und welche Vorteile
daraus gezogen werden können.
6.1.1
Implementierung in SQL
Für die vorgeschlagene Implementierung benötigen wir zwei identische Tabellen. Die Basistabelle t_rel_now speichert alle momentan gültigen Tupelversionen und die Archivierungstabelle t_rel_old speichert Tupelversionen,
89
6.1. Beispiel 1: Archivierungstabelle
die nicht mehr gültig sind. Die Tabelle t_rel_now verfügt in diesem Beispiel
über ein Schlüsselattribut id und ein zusätzliches Attribut name. Für die Erfassung der Transaktionszeit erhalten beide Tabellen noch zusätzlich einen
Zeitstempel für den Beginn (tt_begin) und einen Zeitstempel für das Ende
(tt_end ) der Transaktionszeit.
Codebeispiel 6.1: Erzeugung einer Basis- und einer Archivierungstabelle
−−Tabelle für die aktuellen Datensätze
CREATE TABLE t e s t . t_rel_now (
t t _ b e g i n TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT
1
2
3
current_timestamp ,
tt_end TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT ’
4
i n f i n i t y ’ : : TIMESTAMP WITHOUT TIME ZONE,
name TEXT,
5
CONSTRAINT pkey_t_rel_now PRIMARY KEY ( i d )
6
);
7
−−Tabelle für die historisierten Datensätze
8
CREATE TABLE t e s t . t _ r e l _ o l d (
t t _ b e g i n TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT
9
10
current_timestamp ,
tt_end TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT ’
11
i n f i n i t y ’ : : TIMESTAMP WITHOUT TIME ZONE,
name TEXT,
12
CONSTRAINT pkey_t_rel_old PRIMARY KEY ( id , t t _ b e g i n )
13
);
Da in t_rel_now nur die aktuellen Versionen der Daten gespeichert sind,
genügt id als Primärschlüssel für diese Tabelle (Zeile 6). In der Archivierungstabelle definiert sich der Primärschlüssel hingegen als Kombination der
Attribute id und der Transaktionszeit tt (Zeile 13). Für die Transaktionszeit
kann eines der beiden Zeitstempelattribute verwendet werden.
Trigger
Damit bei Änderungen modifizierte und gelöschte Tupel in die Archivierungstabelle transferiert werden, wird ein sog. Trigger benötigt. Ein Trigger
reagiert auf ein Ereignis, indem er eine Aktion ausführt. Ereignisse sind Modifikationsoperationen (INSERT, UPDATE und DELETE) von Daten. Die
90
14
6.1. Beispiel 1: Archivierungstabelle
Ausführung der Aktion kann noch mit zusätzlichen Bedingungen spezifiziert
werden. In pgSQL definiert man dafür eine Triggerfunktion, die vor oder nach
einer INSERT-, UPDATE- oder DELETE-Operation ausgeführt wird. Diese Funktionen müssen den Typ „Trigger“ zurückgeben. In unserem Beispiel
wurde die Triggerfunktion in pl/pgSQL [Pos08] implementiert.
Codebeispiel 6.2: Triggerfunktion für die Archivierung
CREATE OR REPLACE FUNCTION t e s t . tfunc_t_rel_now ( ) RETURNS "
1
t r i g g e r " AS $BODY$
DECLARE
2
i d _ e x i s t s BOOLEAN;
3
BEGIN
4
IF (TG_OP = ’INSERT ’ ) THEN
5
IF (NEW. tt_end != ’ i n f i n i t y ’ : : timestamp OR NEW.
6
t t _ b e g i n != current_timestamp ) THEN
RAISE EXCEPTION ’ cannot ␣ i n s e r t ␣ data ␣ t h a t ␣ i s ␣ not ␣
7
v a l i d ␣ from ␣ c u r r e n t ␣ time ␣ t o ␣now␣ ( i n f i n i t y ) ’ ;
END IF ;
8
SELECT INTO i d _ e x i s t s ( count ( ∗ ) > 0 ) : : BOOLEAN FROM
9
IF ( i d _ e x i s t s = TRUE) THEN
10
t e s t . t_rel_now WHERE i d = NEW. i d ;
RAISE EXCEPTION ’ i d ␣ c o r r e s p o n d s ␣ t o ␣ i d ␣ i n ␣ e x i s t i n g ␣
11
o r ␣ d e l e t e d ␣ t u p l e ␣ from ␣ t h i s ␣ r e l a t i o n ’ ;
END IF ;
12
SELECT INTO i d _ e x i s t s ( count ( ∗ ) > 0 ) : : BOOLEAN FROM
13
IF ( i d _ e x i s t s = TRUE) THEN
14
t e s t . tempo_old WHERE i d = NEW. i d ;
RAISE EXCEPTION ’ i d ␣ c o r r e s p o n d s ␣ t o ␣ i d ␣ i n ␣ e x i s t i n g ␣
15
o r ␣ d e l e t e d ␣ t u p l e ␣ from ␣ t h i s ␣ r e l a t i o n ’ ;
END IF ;
16
RETURN NEW;
17
ELSIF (TG_OP = ’UPDATE’ ) THEN
18
IF (OLD. i d <> NEW. i d ) THEN
19
RAISE EXCEPTION ’ cannot ␣ change ␣ column ␣ i d ’ ;
20
END IF ;
21
91
6.1. Beispiel 1: Archivierungstabelle
IF (OLD. name = NEW. name ) THEN
RETURN NULL;
22
23
END IF ;
24
INSERT INTO t e s t . tempo_old VALUES (OLD. tt_begin ,
25
current_timestamp − time_atom ( ) , OLD. id , OLD. name
);
NEW. t t _ b e g i n := current_timestamp ;
26
RETURN NEW;
27
ELSIF (TG_OP = ’DELETE ’ ) THEN
INSERT INTO t e s t . t _ r e l _ o l d VALUES (OLD. tt_begin ,
28
29
current_timestamp − time_atom ( ) , OLD. id , OLD. name
);
RETURN OLD;
END IF ;
END;
$BODY$ LANGUAGE ’ p l p g s q l ’ VOLATILE;
30
31
32
33
Tupel, die durch UPDATE bzw. DELETE ihre Gültigkeit verlieren, werden vor der Manipulation kopiert und in der Archivierungstabelle gespeichert
(Zeile 25 bzw. 29). Die Triggerfunktion stellt sicher, dass die Transaktionszeit
bei INSERT-Operation richtig gesetzt wird (Zeile 6) und dass das Schlüsselattribut id nicht dupliziert wird (Zeile 10, 14, 19).
Damit diese Funktion nun auch tatsächlich ausgeführt wird, müssen wir
nun einen Trigger für die Modifikationsoperationen auf der Tabelle t_rel_now
erzeugen. Der Trigger soll diese Funktion aufrufen, wenn er ausgelöst wird.
Codebeispiel 6.3: Triggererzeugung für das Archivierungsmuster
CREATE TRIGGER t t _ t r i g g e r
BEFORE INSERT OR UPDATE OR DELETE
2
ON t e s t . t_rel_now
3
FOR EACH ROW EXECUTE PROCEDURE t e s t . tfunc_t_rel_now ( ) ;
4
In Codebeispiel 6.3 wird der Trigger so erzeugt, dass er vor jeder INSERT-,
UPDATE- und DELETE-Operation feuert und die Funktion aus Codebeispiel 6.2 ausführt.
92
1
6.1. Beispiel 1: Archivierungstabelle
6.1.2
Makro
Wie das Beispiel im vorhergehenden Abschnitt gezeigt hat, muss sehr viel
zusätzlicher SQL-Code geschrieben werden, wenn man eine gewöhnliche Relation als TT-Relation mit Archivierungstabelle modellieren möchte. Wird
das Muster auf weitere Relationen im Schema angewendet, so muss für jede
Relation „ähnlicher“ Code verfasst werden, der sich nur in wenigen Teilen unterscheidet. In den meisten Fällen wird ein Datenbankentwickler den bisher
geschriebenen Code kopieren und in mühsamer und fehleranfälliger Arbeit
manuell an den anwendungsspezifischen Stellen anpassen. Im vorliegenden
Beispiel müssten bspw. Namen und Attribute der Tabelle für die Erzeugung
der Triggerfunktion angepasst werden. Bei einem grossen Schema mit unzähligen Tabellen kann diese Arbeit sehr viel Zeit in Anspruch nehmen. Kann
der SQLPP helfen, diesen Aufwand zu reduzieren?
In einem ersten Schritt muss man sich überlegen, welche Teile des Codes
sich nicht verändern und in der Makrodefinition wiederverwendet werden
können. Die Teile des Codes, die sich ändern, werden als Parameter übergeben. Im folgende Beispiel (Codebeispiel 6.4) wird eine mögliche Makrodefinition gezeigt:
Codebeispiel 6.4: Makrodefinition für das Archivierungsmuster
#DEFINE MACRO M_HISTORY_PATTERN (
1
schema ,
2
relation_name ,
3
time_columns_create ,
4
pkey ,
5
columns_create ,
6
old_equals_new ,
7
col_with_old ) {
8
9
CREATE TABLE #< schema #> . #< name #>_now ( #<
10
time_columns_create #> ,
#< columns_create #> , CONSTRAINT pkey_ #< name #>_now
11
PRIMARY KEY ( #< pkey #> ) ) WITHOUT OIDS ;
12
93
6.1. Beispiel 1: Archivierungstabelle
CREATE TABLE #< schema #> . #< name #> _old ( #<
13
time_columns_create #> ,
#< columns_create #> , CONSTRAINT pkey_ #< name #> _old
14
PRIMARY KEY ( #< pkey #> , t t _ b e g i n ) ) WITHOUT OIDS ;
15
CREATE OR REPLACE FUNCTION #< schema #> . tfunc_ #< name #>
16
_now ( ) RETURNS " t r i g g e r " AS
DECLARE
17
i d _ e x i s t s BOOLEAN;
BEGIN
18
19
IF (TG_OP = \\ ’INSERT\\ ’ ) THEN
IF (NEW. tt_end != \\ ’ i n f i n i t y \\ ’ : : timestamp OR NEW.
20
21
t t _ b e g i n != current_timestamp ) THEN
RAISE EXCEPTION \\ ’ cannot ␣ i n s e r t ␣ data ␣ t h a t ␣ i s ␣ not ␣
22
v a l i d ␣ from ␣ c u r r e n t ␣ time ␣ t o ␣now␣ ( i n f i n i t y ) \\ ’ ;
END IF ;
23
SELECT INTO i d _ e x i s t s ( count ( ∗ ) > 0 ) : : b o o l e a n FROM
24
#< schema #> . #< name #>_now WHERE #< pkey #> =
NEW. #< pkey #> ;
IF ( i d _ e x i s t s = TRUE) THEN
RAISE EXCEPTION \\ ’ i d ␣ c o r r e s p o n d s ␣ t o ␣ i d ␣ i n ␣ e x i s t i n g
25
26
␣ o r ␣ d e l e t e d ␣ t u p l e ␣ from ␣ t h i s ␣ r e l a t i o n \\ ’ ;
END IF ;
27
SELECT INTO i d _ e x i s t s ( count ( ∗ ) > 0 ) : : b o o l e a n FROM
28
#< schema #> . #< name #> _old WHERE #< pkey #> =
NEW. #< pkey #> ;
IF ( i d _ e x i s t s = TRUE) THEN
RAISE EXCEPTION \\ ’ i d ␣ c o r r e s p o n d s ␣ t o ␣ i d ␣ i n ␣ e x i s t i n g
29
30
␣ o r ␣ d e l e t e d ␣ t u p l e ␣ from ␣ t h i s ␣ r e l a t i o n \\ ’ ;
END IF ;
31
RETURN NEW;
32
ELSIF (TG_OP = \\ ’UPDATE\\ ’ ) THEN
IF (OLD. #< pkey #> <> NEW. #< pkey #> ) THEN
RAISE EXCEPTION \\ ’ cannot ␣ change ␣ column ␣ i d \\ ’ ;
END IF ;
94
33
34
35
36
6.1. Beispiel 1: Archivierungstabelle
IF ( #< old_equals_new #> ) THEN
37
RETURN NULL;
38
END IF ;
39
INSERT INTO #< schema #> . #< name #> _old VALUES (OLD.
40
tt_begin , current_timestamp − time_atom ( ) , #<
col_with_old #> ) ;
NEW. t t _ b e g i n := current_timestamp ;
41
RETURN NEW;
42
ELSIF (TG_OP = \\ ’DELETE\\ ’ ) THEN
43
INSERT INTO #< schema #> . #< name #> _old VALUES (OLD.
44
tt_begin , current_timestamp − time_atom ( ) , #<
col_with_old #> ) ;
RETURN OLD;
END IF ;
45
END;
46
LANGUAGE ’ p l p g s q l ’ VOLATILE;
47
48
CREATE TRIGGER time_ #< name #> _ t r i g g e r
49
BEFORE INSERT OR UPDATE OR DELETE
50
ON #< schema #> . #< name #>_now
51
FOR EACH ROW EXECUTE PROCEDURE
52
#< schema #> . tfunc_ #< name #>_now ( ) ;
53
};
54
In Zeile 2-8 von Codebeispiel 6.4 werden die Parameter des Makros definiert. Sie werden später, beim Aufruf in Codebeispiel 6.5, erläutert. Diese
Makrodefinition soll lediglich zeigen, wie die Triggerfunktion aus Codebeispiel 6.2 und der Trigger aus Codebeispiel 6.3 in Zeile 16-47 bzw. 49-53
parametrisiert wurden. Die Parameterbelegung wird auch verwendet, um die
beiden Tabellen zu erzeugen (Zeile 10-14).
Der Makroaufruf angewendet auf dieses Beispiel wäre:
Codebeispiel 6.5: Makroaufruf für die konkrete Anwendung des Archivierungsmusters
#EXEC M_HISTORY_PATTERN(
1
’ test ’ ,
2
’ t_rel ’ ,
3
95
6.1. Beispiel 1: Archivierungstabelle
’ t t _ b e g i n ␣TIMESTAMP␣WITHOUT␣TIME␣ZONE␣NOT␣NULL␣DEFAULT␣
4
current_timestamp , ␣ tt_end ␣TIMESTAMP␣WITHOUT␣TIME␣ZONE
␣NOT␣NULL␣DEFAULT␣ ’ ’ i n f i n i t y ’ ’ : : ␣TIMESTAMP␣WITHOUT␣
TIME␣ZONE ’ ,
’ id ’ ,
5
’ name␣TEXT ’ ,
6
’OLD. name␣=␣NEW. name ’ ,
7
’OLD. id , ␣OLD. name ’
8
);
In Zeile 2-3 von Codebeispiel 6.5 werden Schema- und Tabellenname spezifiziert. Der Makroprozessor erstellt zwei Tabellen, indem er den Suffix now
für die Tabelle mit den aktuell gültigen Tupeln und den Suffix old für die Archivierungstabelle an den Parameter relation_name (vgl. Codebeispiel 6.4)
anhängt.
In den folgenden Zeilen (4-5) folgt die Definition der Zeitstempel und des
Schlüsselattributs. Der Wert von columns_create aus Zeile 6 definiert alle
weiteren Spalten der Tabelle. Auf Zeile 8 folgt ein Parameterwert, der dazu
verwendet wird, zu prüfen, ob bei einem UPDATE überhaupt eine Änderung
an den veränderbaren Attributen durchgeführt wurde (siehe Zeile 22, 23 in
Codebeispiel 6.2). Im Prinzip kennt der Makroprozessor diese Spalten bereits
von columns_create, ist aber nicht in der Lage, ohne Kontrollstrukturen und
Felder solche Information umzustrukturieren und wiederzuverwenden. Diese Redundanzen in der Parameterliste sind nicht optimal und es muss eine
bessere Lösung angestrebt werden.
Mit Kontrollstrukturen, die in der Lage sind, Parametereingaben umzuformen, könnten solche Redundanzen vermieden werden. Kontrollstrukturen
könnten bspw. durch Java-Makros realisiert werden (vgl. 5.5.3 auf Seite 85).
Das Beispielmuster in Abschnitt 6.2 auf Seite 98 wird zeigen, dass es nicht
unbedingt notwendig ist, den SQLPP zu erweitern, um an Schemainformationen zu gelangen.
96
9
6.1. Beispiel 1: Archivierungstabelle
6.1.3
Fazit
Wird ein Makro für die Erzeugung eines Musters definiert, so unterscheidet
man zwischen Codeabschnitten, die sich bei jeder konkreten Anwendung des
Musters ändern und Codeabschnitten, die immer gleich bleiben. Der zu erzeugende Code wird im Rumpf der Makrodefinition eingesetzt, wobei die veränderbaren Codeabschnitte durch Parameterreferenzen ersetzt werden. Der
SQLPP generiert die DDL-Operationen, die bspw. für die Erzeugung des
Archivierungsmusters notwendig sind, indem er die konkrete Parameterbelegung des Makroaufrufs an den referenzierenden Stellen im Rumpf einsetzt.
Dieses Verfahren wird in dieser Arbeit als einfache Textersetzung bezeichnet. einfache TexterEinfache Textersetzung genügt, um dieses Muster umzusetzen. Das Bei- setzung
spiel zeigt, dass wesentlich weniger Code produziert werden muss, wenn die
Grösse des Schemas, gemessen an der Anzahl Tabellen, ausreichend ist. Für
den vorliegenden Fall lohnt sich die Erstellung einer Makrodefinition bereits,
wenn man die Daten von mehr als einer Tabelle in einem Schema versionieren möchte, da der Code für die Makrodefinition in etwa den Umfang des
SQL-Codes für die Archivierung von einer Tabelle hat (siehe Beispielcode
in Abschnitt 6.1.1). Die Makrodefinition ist eine parametrisierte Version der
Operationen für die Erzeugung der beiden Tabellen, der Triggerfunktion und
des Triggers.
Das Beispiel zeigt aber auch die Grenzen der einfachen Textersetzung:
Es ist nicht ideal, dass der Makroaufruf für jede einzelne Tabelle wiederholt
werden muss. Wenn man bspw. alle Tabellen eines bestehenden Schemas
zu TT-Relationen mit Archivierungstabellen ummodellieren möchte, wäre es
wünschenswert, man könnte dies in einem Schritt (mit einem Makroaufruf)
tun. Dafür sind jedoch Informationen über das bestehende Schema notwendig, über die der SQLPP nicht verfügt (vgl. 5.5.3 auf Seite 84). Auch die
Überschneidung von Wertebereichen verschiedener Parameter aufgrund von
fehlenden Kontrollstrukturen ist nicht optimal. Das nächste Beispiel zeigt,
wie man durch Verwendung einer externen Sprache Schemainformation und
Kontrollstrukturen nutzen kann, ohne dass dafür der Makroprozessor erweitert werden muss.
97
6.2. Beispiel 2: Star-Schema
6.2
6.2.1
Beispiel 2: Star-Schema
Implementierung in SQL
Das Star-Schema besteht aus mehreren Dimensionstabellen, die alle durch eine Fremdschlüsselbeziehung von der Faktentabelle referenziert werden (vgl. 3.4.3
auf Seite 44). Der folgende Beispielcode zeigt Auszüge aus der Schemadefinition für die Verkaufsfakten einer Warenhauskette. Sie umfasst die Dimensionen „Produkt“, „Filiale“ und „Zeit“, die auf die Faktentabelle salesfact und
die Dimensionstabellen productdimension, timedimension und storedimension abgebildet wurden.
Codebeispiel 6.6: Produktdimension eines Star Schemas
CREATE TABLE p r o d u c t d i m e n s i o n (
1
i d INTEGER NOT NULL,
2
productname CHARACTER VARYING,
3
c a t e g o r y CHARACTER VARYING,
4
s u b c a t e g o r y CHARACTER VARYING,
5
brand CHARACTER VARYING,
6
weightkg DOUBLE PRECISION ,
7
p r i c e d o l l a r s NUMERIC( 1 0 , 2 ) ,
8
CONSTRAINT pk_productdimension PRIMARY KEY ( i d ) ) ;
9
Codebeispiel 6.7: Verkaufsfakten eines Star Schemas
CREATE TABLE s a l e s f a c t (
1
s a l e s d o l l a r s INTEGER,
2
p ro d uc t di m en s io n _ id INTEGER NOT NULL,
3
s t o r e d i m e n s i o n _ i d INTEGER NOT NULL,
4
timedimension_id INTEGER NOT NULL,
5
CONSTRAINT " pk_SalesFact " PRIMARY KEY ( timedimension_id ,
6
s t o r e d i m e n s i o n _ i d , p ro d uc t di m e ns i on _ id ) ,
CONSTRAINT fk_productdimension_id FOREIGN KEY (
7
p ro d uc t di m en s io n _ id ) REFERENCES p r o d u c t d i m e n s i o n ( i d )
,
CONSTRAINT f k _ s t o r e d i m e n s i o n _ i d FOREIGN KEY (
s t o r e d i m e n s i o n _ i d ) REFERENCES s t o r e d i m e n s i o n ( i d ) ,
98
8
6.2. Beispiel 2: Star-Schema
CONSTRAINT fk_timedimension_id FOREIGN KEY (
9
timedimension_id ) REFERENCES t i m e d i m e n s i o n ( i d ) ) ;
Alle Dimensionen müssen analog zur Produktdimension aus Codebeispiel 6.6 als Tabellen mit einem Primärschlüssel erzeugt werden. Die Faktentabelle aus Codebeispiel 6.7 referenziert die Dimensionstabellen über Fremdschlüsselbeziehungen (Zeile 7 - 9).
6.2.2
Makro
Ein Makro soll nun so spezifiziert werden, dass es auf Basis der Faktentabellen die DDL-Operationen aus Codebeispiel 6.6 und die Primär- und
Fremdschlüsselbeziehungen aus Codebeispiel 6.7 für jede Dimensionstabelle
erzeugen kann. Das Makro muss dafür die Faktentabelle, die zu erstellende
Dimensionstabelle sowie ihre Attribute und Schlüsselattribute kennen. Diese
Informationen können als Parameter übergeben werden. Damit lässt sich eine
Dimensionstabelle und eine Fremdschlüsselbeziehung zwischen dieser Tabelle und der Faktentabelle erzeugen, doch weitere Operationen werden benötigt, um den Primärschlüssel auf der Faktentabelle anzupassen. Um diesen
aus den gegebenen Parametern anzupassen, müsste der SQLPP in der Lage
sein, bspw. die einzelnen Fremdschlüsselbeziehungen durch Iteration über der
Menge der Schlüsselbeziehungen auszulesen. Dies ist ein Problem, denn der
Makroprozessor verfügt über
1. keine Kontrollstrukturen, um Parametereingaben umzustrukturieren
(z. B. Schleifen),
2. keine Schemainformationen.
Datenbanksysteme verfügen in der Regel über eigene Informationsschemata,
welche Metadaten1 über sämtliche Objekte der Datenbank speichern. Diese
Metadaten können wir uns bei der Umsetzung von Makros zunutze machen
und gleichzeitig davon profitieren, dass die meisten Datenbanksysteme über
eine prozedurale Sprache verfügen (so auch PostgreSQL). Diese ermöglicht
1
Metadaten sind Daten über Daten [Gep02, Gep07]
99
6.2. Beispiel 2: Star-Schema
es uns, die Schemainformationen auszulesen und umzuformen. Das Makro
soll nun SQL-Code für die Metadatengewinnung und -aufbereitung sowie die
DDL-Operation erzeugen, die zur Erstellung oder Erweiterung eines StarSchemas notwendig sind.
Bei der Erzeugung eines Star-Schemas könnte man mit der Erzeugung
der Faktentabelle beginnen und danach Dimension um Dimension „anhängen“. Eine Makrodefinition, die ein Star-Schema um eine weitere Dimension
erweitert, könnte man z. B. so implementieren:
Codebeispiel 6.8: Makrodefinition für die Erzeugung von Dimensionstabellen
#DEFINE MACRO Build_Dim ( fact_table_name , dim_table_name ,
1
a t t r i b u t e _ d e f , fk_attribute_name , f k _ a t t r i b u t e _ t y p e ) {
...
Im Rumpf erstellen wir zunächst eine Dimensionstabelle mit Primärschlüssel:
CREATE TABLE #< dim_table_name #> ( #< fk_attribute_name #>
2
#< f k _ a t t r i b u t e _ t y p e #> , #< a t t r i b u t e _ d e f #> ,
CONSTRAINT pk_#< dim_table_name #> PRIMARY KEY ( #<
fk_attribute_name #> ) ) ;
Sobald die Dimensionstabelle existiert, kann die Faktentabelle angepasst
werden, indem das Fremdschlüsselattribut eingefügt und eine Referenz auf
die Dimensionstabelle gelegt wird:
ALTER TABLE #< fact_table_name #> ADD COLUMN #<
3
dim_table_name #>_#< fk_attribute_name #> #<
f k _ a t t r i b u t e _ t y p e #> ;
ALTER TABLE #< fact_table_name #> ADD CONSTRAINT fk_ #<
dim_table_name #>_#< fk_attribute_name #> FOREIGN KEY (
#< dim_table_name #>_#< fk_attribute_name #> )
REFERENCES #< dim_table_name #> ( #< fk_attribute_name
#> ) ;
100
4
6.2. Beispiel 2: Star-Schema
Nun muss der Primärschlüssel der Faktentabelle angepasst werden. Der
Primärschlüssel ist die Kombination aller Fremdschlüssel der Faktentabelle.
Die Fremdschlüsselbeziehungen sind als Metadaten im Informationsschema
gespeichert. In diesem Beispiel erstellt das Makro eine Funktion in pl/pgSQL. Diese Funktion extrahiert den Namen des Primärschlüssels (Zeile 13)
und die Namen der Primärschlüsselattribute, die dazu verwendet werden,
um einen SQL-Befehl zu konstruieren, der für die Erzeugung eines neuen
Primärschlüssels verwendet wird (Zeile 17-21).
CREATE OR REPLACE FUNCTION BUILD_NEW_PK( new_fk_column TEXT)
5
RETURNS INTEGER AS
DECLARE
6
v_column_name RECORD;
7
pk_columns t e x t ;
8
pk_name t e x t ;
9
s tr_ qu er y t e x t ;
10
BEGIN
11
pk_columns :=new_fk_column ;
12
EXECUTE ’SELECT␣ constraint_name ␣FROM␣ information_schema
13
. t a b l e _ c o n s t r a i n t s ␣WHERE␣ table_name ␣=␣ ’ | |
q u o t e _ l i t e r a l ( l o w e r ( ’ #< fact_table_name #> ’ ) ) | |
’␣
AND␣ c o n s t r a i n t _ t y p e ␣=␣ ’ | | q u o t e _ l i t e r a l ( ’PRIMARY␣
KEY ’ ) INTO pk_name ;
14
IF pk_name IS NOT NULL then
15
RAISE NOTICE ’%␣ a l r e a d y ␣ e x i s t s ␣and␣ w i l l ␣ be ␣ dropped ’ ,
16
pk_name ;
FOR v_column_name IN s e l e c t column_name from
17
information_schema . constraint_column_usage WHERE
constraint_name = pk_name LOOP
:= pk_columns | |
pk_columns
’ , ␣ ’ | | v_column_name . column_name
;
END LOOP;
18
/∗ drop l e g a c y pk ∗/
19
101
6.2. Beispiel 2: Star-Schema
s t r _ q u e r y = ’ALTER␣TABLE␣ #< fact_table_name #> ␣DROP␣
20
CONSTRAINT␣ ’ | | quote_ident ( pk_name ) ;
EXECUTE s t r _ q u e r y ;
21
ELSE
22
RAISE NOTICE ’ pk␣ d i d ␣ not ␣ y e t ␣ e x i s t :% ’ , pk_name ;
23
pk_name:= ’pk_#< fact_table_name #> ’ ;
24
END IF ;
25
/∗ c r e a t e new pk ∗/
26
s t r _ q u e r y = ’ALTER␣TABLE␣ #< fact_table_name #> ␣ADD␣
CONSTRAINT␣ ’ | | quote_ident ( pk_name ) | |
KEY␣ ( ’ | | pk_columns | |
27
’ ␣PRIMARY␣
’) ’;
EXECUTE s t r _ q u e r y ;
28
RETURN 1 ;
29
END;
30
LANGUAGE p l p g s q l ;
31
Wir beenden den Makro-Rumpf mit dem Funktionsaufruf:.
SELECT BUILD_NEW_PK( ’ #< dim_table_name #>_#<
1
fk_attribute_name #> ’ ) ;
};
2
Wenn das Makro nun aufgerufen wird, erzeugt das Makro für die spezifizerte Fakten-Dimension-Beziehung eine Funktion, welche sofort verwendet
wird.
6.2.3
Fazit
Um komplexere Muster zu definieren und für die Wiederverwendung zu speichern, benötigt man zusätzliche Kontrollstrukturen und Schemainformationen. Die meisten DBS bieten eine Schnittstelle zu Programmiersprachen an,
welche die Extraktion und Manipulation von Schemainformationen ermöglicht. Mit der Verwendung von datenbankspezifischen Sprachen wächst aber
leider auch die Abhängigkeiten der erstellten Makrodefinitionen vom verwen102
6.3. Beispiel 3: Vererbungstabellen
deten DBS. Die Umsetzung mit dieser Methode kann dennoch datenbankunabhängig durchgeführt werden, indem verschiedene, datenbankspezifische
Implementierungen für das Muster erstellt werden. Diese Implementierungen
können dann durch bedingte Übersetzung (vgl. Abschnitt 5.3.7 und 5.5.2) zur
Laufzeit ausgetauscht werden.
Wenn die Datenbank über keine Sprache verfügt, welche die erwähnten
Eigenschaften besitzt, können komplexe Entwurfsmuster nur dann umgesetzt
werden, wenn eine interne Datenstruktur für die Speicherung von Schemaobjekten und Kontrollstrukturen vorhanden ist. Für die Implementierung bieten
sich Schemaobjekte und Java-Makros gemäss Abschnitt 5.5.3 auf Seite 84 an.
6.3
Beispiel 3: Vererbungstabellen
Das Muster für Tabellenvererbung ist besonders wegen seiner Komplexität
interessant. Es kann als Härtetest für die Umsetzbarkeit von komplexen Mustern mit einer externen Programmiersprache wie pl/pgSQL verwendet werden.
Als Beispiel für die Umsetzung des Musters wurde die von PostgreSQL
verwendete Implementierung von Tabellenvererbung eingesetzt. In Abschnitt
3.3 haben wir die Probleme dieser Implementierung kennen gelernt. Die Massnahmen, die zur Bewältigung dieser Probleme getroffen werden müssen, sind:
1. Simulierte Primärschlüssel, die sich über die gesamte Tabellenhierarchie erstrecken, müssen erzeugt werden. Die gleichen Massnahmen müssen für Unique Constraints (simulierte Unique Constraints) angewendet werden.
2. Simulierte Fremdschlüsselbeziehungen referenzieren die simulierten Primärschlüssel aus (1.).
3. Fremdschlüssel und Benutzerberechtigungen müssen bei der Erzeugung
von Subtabellen manuell vererbt werden.
Auf eine ähnliche Weise, wie das Star-Schema umgesetzt wurde, kann man
ein Muster definieren, welches die Probleme der Vererbung in PostgreSQL be103
6.3. Beispiel 3: Vererbungstabellen
hebt. Im nächsten Abschnitt wird die Implementierung in pgSQL gezeigt. Es
wird jedoch nicht näher auf die Makrodefinitionen eingegangen, da diese analog zu den Makrodefinitionen des Star-Schemas (vgl. Codebeispiel 6.8) umgesetzt wurde. Die Makrodefinition erzeugt zunächst pl/pgSQL-Funktionen,
die sie danach für die Schemaerzeugung verwendet. Ein Einblick in die Makrodefinition würde also keine neuen Erkenntnisse liefern und wäre aufgrund
der Komplexität, welche im SQL-Code ersichtlich ist, zu umfangreich.
6.3.1
Implementierung in SQL
Zunächst müssen die Tabellen erzeugt werden. In unserem Beispiel wählen
wir eine einfache Hierarchie mit einer Supertabelle und zwei Subtabellen, die
von der Supertabelle erben. Geerbt wird in pgSQL mit dem Schlüsselwort
INHERITS. Um einen Index auf dem Primärschlüsselattribut id anzulegen,
wird für jede Tabelle ein Primärschlüssel definiert. Dieser Primärschlüssel
genügt jedoch nicht, um Duplikate innerhalb der gesamten Hierarchie zu
vermeiden:
Codebeispiel 6.9: Erzeugen der Wurzel einer Vererbungshierarchie
CREATE TABLE t e s t . t_super (
1
i d INTEGER,
2
"name" TEXT,
3
CONSTRAINT pk_t_super PRIMARY KEY ( i d ) ) ;
4
Codebeispiel 6.10: Erzeugung einer Subtabelle einer Vererbungshierarchie
CREATE TABLE t e s t . t_sub1 (
−− e r b t :
−− e r b t :
i d INTEGER,
2
"name" TEXT,
3
sub1_name TEXT ) INHERITS ( t e s t . t_super ) ;
Analog zu t_sub1 wird noch eine zweite Subtabelle t_sub2 angelegt. Nun
können wir den „simulierten Primärschlüssel“ erstellen. Dafür verwenden wir
einen Trigger:
Codebeispiel 6.11: Triggerfunktion um einen Primärschlüssel über einer Vererbungshierarchie zu simulieren
104
1
4
6.3. Beispiel 3: Vererbungstabellen
CREATE OR REPLACE FUNCTION tfunc_t_super ( ) RETURNS t r i g g e r
1
AS $BODY$
DECLARE
2
i d _ e x i s t s BOOLEAN;
3
BEGIN
4
IF (TG_OP = ’INSERT ’ ) THEN
5
SELECT INTO i d _ e x i s t s ( count ( ∗ ) > 0 ) : : b o o l e a n FROM
6
IF ( i d _ e x i s t s = TRUE) THEN
7
t_super WHERE i d = NEW. i d ;
RAISE EXCEPTION ’ i d ␣ c o r r e s p o n d s ␣ t o ␣ i d ␣ from ␣ s u p e r ␣ o r
8
␣ any ␣ o f ␣ i t s ␣ c h i l d ␣ r e l a t i o n s ␣ ’ ;
END IF ;
9
RETURN NEW;
10
ELSIF (TG_OP = ’UPDATE’ ) THEN
11
IF (OLD. i d <> NEW. i d ) THEN
12
RAISE EXCEPTION ’ cannot ␣ change ␣ column ␣ i d ’ ;
13
END IF ;
14
RETURN NEW;
15
END IF ;
16
END;
17
$BODY$ LANGUAGE ’ p l p g s q l ’ ;
18
Eine Anfrage gegen Attribute der Supertabelle, wie bspw. SELECT id
FROM t_super, liefert die Tupel der gesamten Hierarchie zurück. Deshalb
genügt die Anfrage auf Zeile 6, um bei INSERT-Operation zu prüfen, ob eine
id innerhalb der gesamten Hierarchie noch nicht existiert. Bei UPDATEOperationen wird im 2. Teil der Funktion (Zeile 11-15) verhindert, dass die
id geändert wird. Für DELETE-Operationen werden keine Triggeraktionen
benötigt.
Diese Triggerfunktion muss von allen Tabellen von der Wurzel abwärts
verwendet werden. Wir müssen für jede Tabelle einen Trigger für die Operationen UPDATE und INSERT definieren, der diese Triggerfunktion verwendet. Dies ist der Code für die Erstellung des Triggers auf der Supertabelle:
105
6.3. Beispiel 3: Vererbungstabellen
Codebeispiel 6.12: Trigger auf der Wurzel einer Vererbungshierarchie, der die
Simulierung des Primärschlüssels auslöst
CREATE TRIGGER t _ s u p e r _ t r i g g e r
1
BEFORE INSERT OR UPDATE
2
ON t_super
3
FOR EACH ROW EXECUTE PROCEDURE tfunc_t_super ( ) ;
4
Für jeden Unique Constraint, der zusätzlich zum Primärschlüssel in der
Wurzel definiert wird, muss eine weitere Triggerfunktion geschrieben und für
jede Tabelle in der Hierarchie müssen weitere Trigger erzeugt werden, welche
die Funktion aus Codebeispiel 6.11 und alle anderen Funktionen, die Primärschlüssel oder Unique Key Constraints simulieren, aufrufen. Fremdschlüssel
und Benutzerberechtigungen, die in der Wurzel definiert sind, müssen analog
dazu in allen Subtabellen definiert werden.
Ein weiteres Problem sind Fremdschlüssel, die Tabellen der Vererbungshierarchie referenzieren. Auch hierfür muss ein Constraint simuliert werden,
indem eine Triggerfunktion sicherstellt, dass das referenzierte Tupel existiert.
PostgreSQL bietet verschiedene Optionen für den Abgleich zwischen referenzierender und referenzierter Tabelle bei normalen Fremdschlüsseln an, die
aus einer Kombination von Attributen bestehen:
• MATCH SIMPLE: Eine Teilmenge der Fremdschlüsselattributwerte darf
Null sein. Es werden diejenigen Attributwerte verglichen, die nicht Null
sind.
• MATCH FULL: Kein Attributwert darf Null sein, alle Attributwerte
werden verglichen.
In beiden Varianten ist es zudem erlaubt, dass alle Attribute Null sind. Zur
Vereinfachung verwenden wir in unserem Beispiel keine Schlüssel, die aus
mehreren Attributen zusammengesetzt sind. Die Testskripte und Makros,
die für dieses Projekt erstellt wurden, sind jedoch in der Lage, solche Fremdschlüssel auch für eine Kombination von Attributen zu erzeugen.
Zunächst müssen wir eine neue Tabelle mit einem Fremdschlüsselattribut
erzeugen:
106
6.3. Beispiel 3: Vererbungstabellen
Codebeispiel 6.13: Tabelle die durch einen simulierten Fremdschlüssel die Vererbungshierarchie referenziert
CREATE TABLE t e s t . t _ f k r e l (
1
i d _ r e f INTEGER,
2
comment TEXT) ;
3
Das Attribut id_ref soll in diesem Beispiel als Fremdschlüssel das Attribut id der Vererbungshierarchie referenzieren. Das Codebeispiel 6.14 zeigt,
wie eine Triggerfunktion, welche auf der Tabelle t_fkrel die referentielle Integrität bei INSERT- und UPDATE-Operationen überprüft, implementiert
werden kann.
Codebeispiel 6.14: Triggerfunktion, um eingehende Fremdschlüssel einer Vererbungshierarchie zu simulieren
CREATE OR REPLACE FUNCTION f k e y _ r e l a t i o n _ f k _ s u p e r _ r l _ i d ( )
1
RETURNS t r i g g e r AS $BODY$
DECLARE
2
f k e y _ t h e r e BOOLEAN;
3
BEGIN
4
IF (TG_OP = ’INSERT ’ ) THEN
5
IF (NOT (NEW. i d _ r e f IS NULL) ) THEN
6
SELECT INTO f k e y _ t h e r e EXISTS (SELECT ∗ FROM
7
IF (NOT f k e y _ t h e r e ) THEN
8
t_super WHERE i d = NEW. i d _ r e f ) ;
RAISE EXCEPTION ’ t a b l e ␣ f k e y _ r e l a t i o n : ␣ i d ␣
9
f o r e i g n ␣ key ␣ c o n s t r a i n t ␣ v i o l a t e d ’ ;
END IF ;
10
END IF ;
11
RETURN NEW;
12
END IF ;
13
IF (TG_OP = ’UPDATE’ ) THEN
14
IF (NOT (NEW. i d _ r e f IS NULL) ) THEN
15
SELECT INTO f k e y _ t h e r e EXISTS (SELECT ∗ FROM
16
IF (NOT f k e y _ t h e r e ) THEN
17
t _ s up e r _ r e l WHERE i d = NEW. i d ) ;
107
6.3. Beispiel 3: Vererbungstabellen
RAISE EXCEPTION ’ t a b l e ␣ f k e y _ r e l a t i o n : ␣ i d ␣
18
f o r e i g n ␣ key ␣ c o n s t r a i n t ␣ v i o l a t e d ’ ;
END IF ;
END IF ;
20
RETURN NEW;
21
END IF ;
END;
$BODY$
22
23
LANGUAGE ’ p l p g s q l ’ ;
Eine Überprüfung muss jeweils nur dann stattfinden, wenn der neue Wert
von id_ref nicht Null ist (Zeile 6 und 15), denn Null -Werte werden nicht referenziert. Wie schon beim „künstlichen Primärschlüssel“ kann man davon
profitieren, dass Anfragen gegen die Wurzel einer Vererbungshierarchie immer die Tupel der gesamten Hierarchie zurückliefern (Zeile 7 und 16). Eingefügt oder verändert werden Tupel nur dann, wenn der Wert von id_ref in
der Vererbungshierarchie existiert (Zeile 8 und 17). Somit ist die referenzielle
Integrität bei INSERT-Operationen sichergestellt.
Werden in der Vererbungshierarchie referenzierte Attribute verändert oder
gelöscht, gibt es verschiedene Strategien, wie die referenzielle Integrität aufrechterhalten werden kann. PostgreSQL bietet folgende Varianten an:
• NO ACTION/RESTRICT: Unterbindet UPDATE- oder DELETE-Operationen, die gegen die referenzielle Integrität verstossen würden.
• CASCADE: Wird dies für DELETE-Operationen spezifiziert, so werden alle Tupel gelöscht, die ein Tupel referenzieren, welches gelöscht
werden soll. Für UPDATE-Operationen wird das Fremdschlüsselattribut verändert, wenn das referenzierte Attribut verändert wird.
• SET NULL: Setzt das Fremdschlüsselattribut auf Null, wenn das referenzierte Attribut verändert oder gelöscht wird.
• SET DEFAULT: Setzt das Fremdschlüsselattribut auf einen bestimmten, vordefinierten Wert, wenn das referenzierte Attribut verändert
oder gelöscht wird.
108
19
24
6.3. Beispiel 3: Vererbungstabellen
Für das Codebeispiel 6.15 wurde CASCADE sowohl für UPDATE als auch
für DELETE für den künstlichen Fremdschlüssel verwendet. Die anderen
Strategien könnten aber mit dieser Methode genauso gut umgesetzt werden.
Codebeispiel 6.15: Simulierte Update-Strategie eines eingehenden Fremdschlüssels einer Vererbungshierarchie
CREATE OR REPLACE FUNCTION t f u n c _ f k _ r e f _ c a s c a d e ( ) RETURNS
1
t r i g g e r AS $BODY$
BEGIN
2
IF (TG_OP = ’UPDATE’ ) THEN
3
IF (NOT ( (OLD. i d = NEW. i d ) OR (OLD. i d IS NULL) ) )
4
THEN
UPDATE t _ f k _ r e l SET i d _ r e f=NEW. i d WHERE i d _ r e f =
5
OLD. i d ;
END IF ;
6
RETURN NEW;
7
END IF ;
8
IF (TG_OP = ’DELETE ’ ) THEN
9
IF (NOT ( (OLD. i d IS NULL) ) ) THEN
DELETE FROM f k t b l WHERE r e f i d 1 =
refid2 =
10
OLD. i d 1 AND
11
OLD. i d 2 ;
END IF ;
12
RETURN OLD;
13
END IF ;
14
END;
15
$BODY$ LANGUAGE ’ p l p g s q l ’ VOLATILE
16
Der Trigger, der diese Funktion verwendet, wird für UPDATE- und die
DELETE-Operationen auf der referenzierenden Tabelle t_fk_rel angelegt.
6.3.2
Makros
Für die Implementierung einer simple Vererbungshierarchie mit einer Supertabelle, zwei Subtabellen und einer referenzierenden Tabelle muss bereits
sehr viel zusätzlicher Code erstellt werden. Dieses Beispiel umfasst fast 100
Zeilen Code, obwohl auf weitere Constraints wie Fremdschlüssel oder Unique
109
6.3. Beispiel 3: Vererbungstabellen
Constraints auf der Wurzeltabelle verzichtet wurde. Z. B. müsste für einen
zusätzlichen Unique Constraint auf der Wurzel nochmals zwischen 40 und
50 Zeilen Code geschrieben werden, der sich allerdings in den meisten Teilen
mit dem Code für den simulierten Primärschlüssel decken würde. Würde man
diesen neuen Unique Constraint mit einem Fremdschlüssel referenzieren wollen, kämen nochmals 40-50 Zeilen Code dazu. Dieser „ähnliche“ Code eignet
sich gut, um mit Makros parametrisiert und erzeugt zu werden.
Für die Umsetzung schreiben wir Makros, die die Funktionen in Abschnitt
6.3.1 erzeugen, und ein Makro, welches diese Makros aufruft. Es bietet sich
an, Makros zu verwenden, die andere Makros aufrufen, weil wir auf diese
Weise dieselben Parameter wiederverwenden können und nur ein Makro aufrufen müssen (vgl. Abschnitt 5.3.5 „Makros, die Makros aufrufen“). Dieses
Makro könnte für die Erzeugung von Subtabellen einer Hierarchie bspw. folgendermassen implementiert werden:
Codebeispiel 6.16: Makro um eine Subtabelle in eine Vererbungshierarchie einzufügen
/∗ i n c l u d e m _ i n h e r i t _ c o n s t r a i n t s ∗/
#INCLUDE / r e s o u r c e s / t e s t / i n h e r i t a n c e / i n h e r i t _ c o n s t r a i n t s .
1
2
txt ;
/∗ i n c l u d e m_copy_grants ∗/
3
#INCLUDE / r e s o u r c e s / t e s t / i n h e r i t a n c e / i n h e r i t _ g r a n t s . t x t ;
4
#DEFINE MACRO m_add_child ( schema , sub_rel , super_pk ,
5
super_rel , sub_attributes ) {
/∗ c r e a t e sub r e l a t i o n ∗/
CREATE TABLE i n h e r i t a n c e . #< s ub_ re l #> ( #< s u b _ a t t r i b u t e s
6
7
#> ) INHERITS ( #< s u p e r _ r e l #> ) ;
ALTER TABLE i n h e r i t a n c e . #< s u b _ r e l #> ADD CONSTRAINT pk_
8
#< s u b _ r e l #> PRIMARY KEY ( #< super_pk #> ) ;
/∗ s h a r e primary key and unique c o n s t r a i n t s by s i m u l a t i n g
i t with a t r i g g e r f u n c t i o n ∗/
#EXEC m _ i n h e r i t _ c o n s t r a i n t s (#schema , #s u p e r _ r e l , #
9
10
super_rel ) ;
#EXEC m _ i n h e r i t _ c o n s t r a i n t s (#schema , #sub_rel , #s u p e r _ r e l
);
110
11
6.3. Beispiel 3: Vererbungstabellen
/∗ i n h e r i t g r a n t s ∗/
12
#EXEC m_copy_grants (#s u p e r _ r e l , #sub_rel , #schema ) ; } ;
13
In Zeile 2 und 4 werden die Makrodefinitionen einkopiert, die pl/pgSQLFunktionen erzeugen. m_inherit_constraints erzeugt den Trigger und die
Triggerfunktion aus Codebeispiel 6.12 bzw. Codebeispiel 6.11, um einen Primärschlüssel oder Unique Key Constraint zu simulieren. Die zweite einkopierte Makrodefinition, m_copy_grants aus der Datei inherit_grants.txt, erzeugt
eine Funktion, welche die Benutzerberechtigungen von der Supertabelle super_rel in die Subtabelle sub_rel kopiert. Beide Makros werden im Rumpf
der Makrodefinition m_add_child aufgerufen. Das Makro verfügt neben den
Namen der Super- und Subtabelle über Parameter für den Schemanamen
schema, den Primärschlüssel super_pk und einen Parameter für die Attribute der Subtabelle. Dieser Parameter wird durch eine kommaseparierten Liste
von Attributen in der Form „Attributname Attributtyp“ belegt.
Nach dem gleichen Prinzip kann man für die Simulation von eingehenden
Fremdschlüsselbeziehungen in der Vererbungshierarchie verfahren:
1. Es wird ein Makro aus einer Datei einkopiert und aufgerufen, welches
Trigger und Triggerfunktion (siehe Codebeispiel 6.14) für den simulierten Fremdschlüssel auf der referenzierenden Tabelle erzeugt.
2. Ein Makro, welches die Funktion aus Codebeispiel 6.15 erzeugt, wird
einkopiert und aufgerufen.
3. Ein Makro wird einkopiert und aufgerufen, welches eine Funktion erzeugt, die wiederum für jede Tabelle in der Vererbungshierarchie einen
Trigger für den Aufruf der Funktion aus (2.) erzeugt.
Die dafür benötigten Parameter umfassen eine Liste der Fremdschlüsselattribute und der referenzierten Attribute, den Namen der Wurzeltabelle und
der referenzierenden Tabelle, sowie mögliche weitere Informationen wie Schemanamen und Namensgebung der zu erstellenden Trigger und Funktionen.
111
6.4. Zusammenfassung
6.4
Zusammenfassung
Die Beispielmuster in diesem Kapitel haben gezeigt, dass es möglich ist, simple Muster, wie bspw. das Archivierungsmuster, durch einfache Textersetzung
zu erzeugen. Durch den Mangel an mächtigeren Kontrollstrukturen ergeben
sich allerdings in diesem Beispiel Redundanzen in der Parameterbelegung
solcher Makros. Um diese zu beseitigen oder komplexere Muster mit dem
SQLPP umzusetzen, sind Kontrollstrukturen und der Zugang zu Schemainformationen notwendig.
Für die weiteren Beispiele (2 und 3) in diesem Kapitel wurde eine externe
prozedurale Datenbanksprache verwendet, die über die benötigten Kontrollstrukturen verfügt und den Zugriff auf Schemainformationen ermöglicht. Die
Beispiele haben gezeigt, dass sich mit dieser Methode auch komplexe Muster
wie die Vererbung umsetzen lassen. Da externe Datenbanksprachen meist
datenbankspezifisch sind, kann die Portierung von Mustern zu Problemen
führen. Durch die Verwendung von bedingter Übersetzung bietet der SQLPP
jedoch die notwendigen Rahmenbedingungen, um eine datenbankunabhängige Übersetzung von solchen Mustern zu ermöglichen.
Der SQLPP muss erweitert werden, falls Datenbanksysteme verwendet
werden, die keine Sprache mit den genannten Eigenschaften unterstützt. Diese Erweiterung würde gemäss den in Abschnitt 5.3.8 geforderten zusätzlichen Anforderungen eine interne Repräsentation des Datenbankschemas und
zusätzliche Kontrollstrukturen, bspw. durch Java-Makros (siehe Seite 85),
beinhalten. Die Einbindung von diesen zusätzlichen Anforderungen könnte
den Aufwand für die Implementierung reduzieren, da es nicht mehr notwendig wäre, datenbankspezifische Implementierungen, z. B. in Form von SQLFunktionen, zu schreiben. Demgegenüber würde der Aufwand für die Entwicklung und Einbindung von datenbankspezifischen Sprach-Plug-Ins (vgl.
Abschnitt 5.5.1) erhöht, weil Extraktion und Abbildung von Schemainformationen zusätzlich spezifiziert werden müsste.
112
Kapitel 7
Diskussion und Ausblick
7.1
Ergebnisse und Zusammenfassung
In dieser Diplomarbeit wurden Entwurfsmuster der Schemadefinition, hier als
Datenbankentwurfsmuster bezeichnet, beschrieben. Um diese Entwurfsmuster wiederverwenden zu können, wurde ein Makroprozessor (SQLPP: SQL
Preprocessor) entworfen und implementiert, welcher in der Lage ist, die Muster zu speichern und bei Bedarf zu konkretisieren.
Diese Arbeit hat gezeigt, dass Datenbankentwurfsmuster existieren und
dass sie mit Hilfe des SQLPP wiederverwendet werden können. Es wurde
eine Einteilung der Muster in drei Kategorien vorgeschlagen:
• Muster zur Abbildung anderer Datenmodelle bilden verschiedenartige
Datenmodelle aufeinander ab. Zu diesen Mustern zählen Muster für
die temporalrelationale Abbildung, Datawarehouse-Muster für multidimensionalrelationale Abbildungen und Muster für objektrelationale
Abbildungen.
• Muster für Anfrage und Speicherplatzoptimierung dienen dazu, Daten
zu reorganisieren, um Speicherplatzprobleme zu beseitigen oder die Performance von Anfragen zu verbessern. Zu dieser Kategorie von Mustern
zählen die Partitionierung und die Denormalisierung.
• Muster der Datenbankunabhängigkeit fördern die Datenbankunabhän113
7.1. Ergebnisse und Zusammenfassung
gigkeit, indem sie für datenbankspezifische Implementierungen eine generische, datenbankunabhängige Lösung anbieten. Ein Muster, welches
diese Absicht verfolgt, ist das Adapter-Muster, welches auch in der objektorientierten Programmierung angewendet wird.
Auf Basis der Anforderungsspezifikation aus Kapitel 4 wurden drei Lösungsvorschläge für die Umsetzung eines Werkzeugs für die Speicherung und Wiederverwendung von Datenbankentwurfsmustern identifiziert. Im Vergleich
mit Spracherweiterungen und benutzerdefinierten Funktionen besitzt der SQLPP als Makroprozessor die besten Eigenschaften, betrachtet über die Dimensionen Wiederverwendbarkeit, Aufwand und Mächtigkeit. Vor allem im
Bereich der Wiederverwendbarkeit besitzt ein Makroprozessor klare Vorteile
gegenüber den beiden anderen Vorschlägen.
Damit die Sprachspezifikation in eine Grammatik für die Makrosprache
und eine Grammatik für den SQL-Dialekt unterteilt werden kann, wurde
der SQLPP so entworfen, dass die Übersetzung in drei Teilphasen ausgeführt wird. Die Trennung der Grammatiken reduzierte die Komplexität der
Sprachspezifikation und förderte die Modularität des Systems in Bezug auf
die Einbindung andere Datenbanksysteme, welche bei Bedarf als Plug-Ins
„eingesteckt“ werden können.
Die Umsetzung von Beispielmustern in Kapitel 6 hat gezeigt, dass blosse
Textersetzung durch Makros nicht ausreicht, um komplexe Datenbankentwurfsmuster umzusetzen. Für diese Art von Mustern ist es notwendig, dass
das Werkzeug über Schemainformationen und mächtigere Kontrollstrukturen verfügt. Es wurden zwei alternative Möglichkeiten vorgestellt, wie der
SQLPP Kontrollstrukturen und Schemainformationen verwenden kann.
1. Makros erzeugen benutzerdefinierte Funktionen einer Datenbanksprache und verwenden das Informationsschema der Datenbank, um auf
Metadaten von Schemaobjekten zuzugreifen.
2. Der SQLPP bildet Schemainformationen auf eine interne Datenstruktur ab. Zusätzlich wird die Makrosprache erweitert, damit der Datenbankentwickler Makros mit zusätzlichen Kontrollstrukturen definieren
kann.
114
7.2. Weitere Forschung
Die Umsetzung der Beispielmuster hat gezeigt, dass mit der ersten Variante
Muster beliebiger Komplexität umgesetzt werden können, ohne dass der Makroprozessor dafür erweitert werden muss. Als Nachteil muss die Abhängigkeit von einer Datenbanksprache erwähnt werden. Sie führt zu zusätzlichem
Aufwand bei der Portierung von Makros, die solche Funktionen verwenden.
Im schlimmsten Fall ist keine Portierung möglich, weil das Datenbanksystem
keine Programmiersprache zur Verfügung stellt.
Bei der 2. Variante fällt die Abhängigkeit von einer externen Programmiersprache weg. Demgegenüber steht aber der Nachteil, dass der Aufwand
für die Entwicklung von Plug-Ins durch die Extraktion von Schemainformationen aufwändiger wird.
Für die Umsetzung von Kontrollstrukturen gibt es zwei Möglichkeiten:
1. Die Makrosprache wird mit eigenen Kontrollstrukturen ergänzt.
2. Es wird ermöglicht, das Makros in einer vollständigen Programmiersprache definiert werden können, z. B. als Java-Makros.
Da Java-Makros durch die Verwendung der Programmiersprache Java mächtige Kontrollstrukturen ermöglichen und im Vergleich zu zusätzlichen internen Kontrollstrukturen mit weniger Aufwand implementiert und integriert
werden können, wurde noch während dieser Diplomarbeit damit begonnen,
an einem Proof of Concept für solche Makros zu arbeiten.
7.2
7.2.1
Weitere Forschung
Katalog von Entwurfsmustern
Diese Arbeit hat gezeigt, dass Datenbankentwurfsmuster existieren und wie
sie wiederverwendet werden können. Sie macht einen Vorschlag einer Taxonomie von Mustern (vgl. Abschnitt 3.1). Die gefundenen Muster und Kategorien
sind lediglich eine Auswahl von gängigen Mustern, die für den Schemaentwurf verwendet werden und bilden keineswegs eine vollständige Aufzählung.
Weitere Forschung könnte sich damit beschäftigen, solche Muster zu sammeln
115
7.2. Weitere Forschung
und zu katalogisieren, ähnlich wie dies durch die „Gang of Four“ in [GHJV04]
für objektorientierte Entwurfsmuster gemacht wurde.
7.2.2
Makro-Bibliothek
Die gefundenen Entwurfsmuster aus 7.2.1 könnten unter Verwendung des
SQLPP als Makros definiert werden, um damit die Makro-Bibliothek zu erweitern. Dadurch vereinfacht sich die Benutzung des SQLPP, weil weniger
Code für die Definition von Makros geschrieben werden muss und meist Makrodefinitionen wiederverwendet werden können.
7.2.3
Schemainformationen und Java-Makros
Weiterführende Arbeiten mit dem SQLPP könnten die Einführung von JavaMakros vervollständigen und den SQLPP mit Schemainformationen ausstatten. Die Schemainformationen müssten in einer internen Datenstruktur
vollständig gespeichert werden. Das API für die Definition von Makros könnte so erweitert werden, dass die Schemainformations-Objekte innerhalb von
Makrodefinition verwendet werden können.
7.2.4
Zusätzliche semantische Überprüfungen
Die Schemainformationen könnten auch dafür verwendet werden, eine zusätzliche semantische Überprüfung bei der Verarbeitung von Makros durchzuführen. So könnte bspw. geprüft werden, ob eine Tabelle, die referenziert
wird, überhaupt existiert. Wenn der Makroprozessor in der Lage wäre, solche Überprüfungen durchzuführen, wäre es auch möglich, den SQLPP so zu
erweitern, dass er SQL-Skripts automatisch reorganisieren könnte, wenn eine
falsche Reihenfolge der aufgerufenen SQL-Befehle zu Fehlern führen würde.
Solche semantischen Bedingungen würde man in den datenbankspezifischen Plug-Ins vordefinieren. Es wäre aber auch denkbar, dass zusätzliche
Constraints oder Randbedingungen vom Benutzer definiert werden können.
Solche Constraints würden logische Ausdrücke über Schemaobjekten bilden,
die auf ihre Gültigkeit geprüft werden. Wird bei der Verwendung von Makros
116
7.2. Weitere Forschung
eine dieser Bedingung verletzt, dann informiert der SQLPP den Benutzer.
Z. B. könnte ein Constraint festlegen, dass eine Tabelle nur von einer Tabelle
erben darf, um Mehrfachvererbung zu verhindern.
7.2.5
Integrierte Entwicklungsumgebung (IDE) für Makros
Eine integrierte Entwicklungsumgebung (IDE: Integrated Development Environment) würde das Schreiben von Makrodefinition beschleunigen, indem es
die Fehlersuche erleichtert und damit den Aufwand für die Definition von
Makros reduziert. Weitere Arbeiten könnten sich damit beschäftigen, die
Sprachspezifikationen des SQLPP wiederzuverwenden, um bspw. ein EclipsePlug-In für die Vorübersetzung (Precompilation) zu entwerfen und zu implementieren. Dieses Plug-In könnte bei der Definition von Makros innerhalb
von Eclipse verwendet werden, um den Entwickler umgehend über syntaktische Fehler zu informiert. Das Plug-In könnte aber auch dazu verwendet
werden, um die Syntax von SQL-Code verschiedener Sprachdialekte und Makroaufrufe interaktiv zu überprüfen.
7.2.6
Graphische Entwicklungshilfen
Der SQLPP könnte in Zukunft mit einer graphischen Benutzerschnittstelle
ausgestattet werden, die den Benutzer sowohl bei der Definition von Makros,
als auch bei deren Verwendung unterstützt. Zum Beispiel könnten mit Hilfe der Schemainformations-Objekte aus Abschnitt 7.2.3 die Veränderungen
visualisiert werden, die die Ausführung von Makros in einem Schema verursacht. Oder der Benutzer bekommt die Möglichkeit, Makros mit Hilfe eines
Drag-and-Drop-Werkzeugs zu definieren, indem Schemaobjekte und Makrodefinitionen der Makro-Bibliothek (vgl. 7.2.2) graphisch dargestellt und in eine Schablone für Makrodefinitionen „gezogen“ werden können. Damit wäre es
möglich, auch mit geringen SQL- und MeSQL-Kentnissen DDL-Operationen
zu erzeugen.
117
7.2. Weitere Forschung
7.2.7
Datenbankunabhängigkeit
Auch die Entwicklung und Einbindung von weiteren Plug-Ins, die andere Datenbanksysteme und ihre Sprachdialekte unterstützen, könnten in Zukunft
umgesetzt werden. Verfügt man durch die zusätzlichen Plug-Ins über weitere SQL-Sprachspezifikationen, könnte der SQLPP dazu verwendet werden,
eine Erweiterung des SQLPP umzusetzen und zu testen, die die Sprachunabhängigkeit von SQL-Anfragen zum Ziel hat. Der SQLPP könnte z. B.
wiederverwendet werden, um eine Abstraktion oder ein Framework für datenbankunabhängigen Datenbankzugriff zu entwerfen und zu implementieren.
Entwickler, die z. B. in Java mit JDBC [Gep02, Ull06, JDK04] auf Datenbanken zugreifen, müssen datenbankspezifischen SQL-Code schreiben und
einen datenbankspezifischen JDBC-Treiber für den Datenbankzugriff verwenden. Besser wäre es, der Entwickler könnte standardisierten SQL-Code schreiben. Zukünftige Arbeiten könnten sich damit beschäftigen, einen SQLPPJDBC-Treiber zu implementieren, der in Verwendung mit JDBC standardisiertes SQL vom SQLPP in einen datenbankspezifischen Dialekt übersetzen
lässt. JDBC würde die übersetzten SQL-Anfragen an die Datenbank weiterleiten. Auf diese Weise könnte der Entwickler standardisierten, datenbankunabhängigen SQL-Code für seine Applikationen verfassen.
Als Alternative mit demselben Ziel könnten Datenbankentwurfsmuster
der Datenbankunabhängigkeit (vgl. Abschnitt 3.1.3) umgesetzt werden.
118
Teil II
Anhang
119
Glossar
API
Application Programming Interface
DBMS
Database Management System, Datenbankverwaltungssystem
DBS
Database System, Datenbanksystem
DDL
Data Definition Language, Datendefinitionssprache
DFA
Deterministic Finite Automaton, deterministischer Endlicher Automat
DML
Data Manipulating Language, Datenmanipulationssprache
ETL
Extract Transform Load, Extraktion Transformation Laden
IDE
Integrated Development Environment, integrierte Entwicklungsumgebung
MeSQL
Macro extended SQL
OID
Object Identifier, Objektidentifikator
OLAP
Online Analytic Processing
SCD
Slowly Changing Dimensions
SQLPP
SQL Preprocessor, SQL Präprozessor
SQL
Structured Query Language
121
TT
Transaction Time, Transaktionszeit
UDT
User-defined types, benutzerdefinierte Typen
VT
Valid Time, Gültigkeitszeit
122
Abbildungsverzeichnis
2.1
2.2
2.3
3.1
3.2
3.3
Vereinfachte Produktionsregel des Nichtterminals „create_stmt“
in der SQL-Sprache. . . . . . . . . . . . . . . . . . . . . . . . 16
Vereinfachter Syntaxbaum für die Ableitung eines SQL-Befehls 17
Syntaxbäume einer mehrdeutigen kontextfreien Grammatik
für den Ausdruck 3 + 4 − 2 [ASU99] . . . . . . . . . . . . . . . 19
Vererbung typisierter Tabellen in SQL:99 [Gep02] . . . . . . . 39
Snowflake-Schema mit den Dimensionen Zeit, Ort und Produkt für die Verkaufsfakten . . . . . . . . . . . . . . . . . . . 44
Star-Schema mit den Dimensionen Zeit, Ort und Produkt für
die Verkaufsfakten . . . . . . . . . . . . . . . . . . . . . . . . 45
4.1
UML Anwendungsfalldiagramm . . . . . . . . . . . . . . . . . 54
5.1
5.2
5.3
5.4
5.5
Die drei Lösungsvarianten im Vergleich . . . . . . . . . .
Steckersystem für die Anbindung von SQL-Dialekten . .
Die 3 Phasen des SQLPP . . . . . . . . . . . . . . . . . .
Gesamte Architektur des SQLPP im Überblick . . . . . .
Vereinfachtes Klassendiagramm der MeSQL-Befehlstypen
123
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
62
69
72
74
82
ABBILDUNGSVERZEICHNIS
124
Liste von Codebeispielen
5.1
Abgeschlossene Parameterreferenz . . . . . . . . . . . . . . . . 77
5.2
Makrodefinition . . . . . . . . . . . . . . . . . . . . . . . . . . 77
5.3
Makroaufruf . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
5.4
Einkopieren einer MeSQL-Datei . . . . . . . . . . . . . . . . . 79
5.5
Einkopieren einer SQL-Datei . . . . . . . . . . . . . . . . . . . 80
5.6
Bedingte Übersetzung . . . . . . . . . . . . . . . . . . . . . . 81
5.7
Pseudocode der Erzeugung eines Aufrufgrafen . . . . . . . . . 87
5.8
Pseudocode der Zyklensuche . . . . . . . . . . . . . . . . . . . 87
6.1
Erzeugung einer Basis- und einer Archivierungstabelle . . . . . 90
6.2
Triggerfunktion für die Archivierung . . . . . . . . . . . . . . 91
6.3
Triggererzeugung für das Archivierungsmuster . . . . . . . . . 92
6.4
Makrodefinition für das Archivierungsmuster . . . . . . . . . . 93
6.5
Makroaufruf für die konkrete Anwendung des Archivierungsmusters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
6.6
Produktdimension eines Star Schemas . . . . . . . . . . . . . . 98
6.7
Verkaufsfakten eines Star Schemas . . . . . . . . . . . . . . . . 98
6.8
Makrodefinition für die Erzeugung von Dimensionstabellen . . 100
6.9
Erzeugen der Wurzel einer Vererbungshierarchie . . . . . . . . 104
6.10 Erzeugung einer Subtabelle einer Vererbungshierarchie . . . . 104
6.11 Triggerfunktion um einen Primärschlüssel über einer Vererbungshierarchie zu simulieren . . . . . . . . . . . . . . . . . . 104
6.12 Trigger auf der Wurzel einer Vererbungshierarchie, der die Simulierung des Primärschlüssels auslöst . . . . . . . . . . . . . 105
6.13 Tabelle die durch einen simulierten Fremdschlüssel die Vererbungshierarchie referenziert . . . . . . . . . . . . . . . . . . . 107
125
LISTE VON CODEBEISPIELEN
6.14 Triggerfunktion, um eingehende Fremdschlüssel einer Vererbungshierarchie zu simulieren . . . . . . . . . . . . . . . . . . 107
6.15 Simulierte Update-Strategie eines eingehenden Fremdschlüssels einer Vererbungshierarchie . . . . . . . . . . . . . . . . . . 109
6.16 Makro um eine Subtabelle in eine Vererbungshierarchie einzufügen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
126
Literaturverzeichnis
[Amb06]
Ambler, Scott W. Object Relational Mapping. http://www.
agiledata.org/essays/mappingObjects.html. 2006
[Ame99a] American National Standard Institute: Information Technology Database Language SQL - Part 1: SQL/Framework. 1999
[Ame99b] American National Standard Institute: Information Technology Database Language SQL - Part 2: Foundation(SQL/Foundation).
1999
[AS06]
Ambler, Scott W. ; Sadalage, Pramod J.: Refactoring Databases - Evolutionary Database Design. Addison-Wesley, 2006
[ASU99]
Aho, Alfred V. ; Sethi, Ravi ; Ullman, Jeffrey D.: Compilerbau
Teil 1. 2. Addison-Wesley, 1999
[Ber05]
Bernstein, Abraham. So what is a (Diploma) Thesis? A few
thoughts for first-timers. 2005
[BG09]
Bauer, Andreas ; Günzel, Holger: Data Warehouse Systems Architektur, Entwicklung, Anwendung. 3. dpunkt.verlag, 2009
[Cod90]
Codd, Dr. Edgar F.: The Relational Model for Database Management - Version 2. Addison-Wesley, 1990
[DDN03]
Demeyer, Serge ; Ducasse, Stéphane ; Nierstrasz, Oscar:
Object-Oriented Reengineering Patterns. Morgan Kaufmann,
2003
127
LITERATURVERZEICHNIS
[EK08]
Esselborn-Krumbiegel, Helga: Von der Idee zum Text - Eine
Anleitung zum wissenschaftlichen Schreiben. 3. Schöningh, 2008
[FP03]
Feuerstein, Steven ; Pribyl, Bill: Oracle PL/SQL Programmierung. 2. O’Reilly, 2003
[FP07]
Frej, Dieter ; Parr, Terence.
ANTLR v3 documentation. http://www.antlr.org/wiki/display/ANTLR3/ANTLR+v3+
documentation. 2007
[GAB+ 02] Glinz, Martin ; Arnold, Martin ; Berner, Stefan ; Joos, Stefan ; Ryser, Johannes ; Meier, Silvio ; Merlo-Schett, Nancy
; Seybold, Christian: Entwicklungsrichtlinien für Java-Software.
3, 2002
[Gep02]
Geppert, Andreas: Objektrelationale und objektorientierte Datenbankkonzepte und -systeme. dpunkt.verlag, 2002
[Gep07]
Geppert, Andreas. Data Warehousing. Vorlesungsskript. 2007
[GHJV04] Gamma, Erich ; Helm, Richard ; Johnson, Ralph ; Vlissides,
John: Entwurfsmuster: Elemente wiederverwendbarer objektorientierter Software. Addison-Wesley, 2004
[Gli03]
Glinz, Martin. Software Engineering I. Vorlesungsskript. 2003
[Gül03]
Gülcü, Ceki: The Compete Log4J Manual. QoS.ch, 2003
[JCD+ 93] Jensen, Christian S. ; Clifford, James ; Dyreson, Curtis ;
Gadia, Shashi K. ; Grandi, Fabio ; jodia, Sushil J. ; Montanari, Nick Kline A. ; Nonen, Daniel ; Peressi, Elisa ; Pernici,
Barbara ; Roddick, John F. ; Sarda, Nandlal L. ; Scalas, Maria R. ; Segev, Arie ; Snodgrass, Richard T. ; Soo, Mike D. ;
Tansel, Abdullah ; Tiberio, Paolo. Proposed Temporal Database Concepts. Glossar. 1993
[JDK04]
128
JDK 5.0 Documentation. Sun Microsystems. http://java.sun.
com/j2se/1.5.0/docs/index.html. 2004
LITERATURVERZEICHNIS
[JEH00]
John E. Hopcroft, Jeffrey D. U.: Einführung in die Automatentheorie, Formale Sprachen und Komplexitätstheorie. 4. Oldenbourg, 2000
[JEH06]
John E. Hopcroft, Jeffrey D. U.: Introduction to Automata
Theory, Languages and Computation. 3. Pearson, 2006
[Jen00]
Jensen, Christian S.: Temporal Database Management, Aalborg
University, Diss., 2000
[Kli04]
Kline, Kevin E.: SQL in a Nutshell. 2. O’Reilly, 2004
[MKB05] Marlowe, Thomas J. ; Ku, Cyril S. ; Benham, James W.: Design Patterns for Database Pedagogy - A Proposal. In: ACM
SIGCSE Bulletin 37 (2005), Nr. 1
[NN04]
Niedermair, Elke ; Niedermair, Michael: Latex Praxisbuch Studienausgabe. Franzis, 2004
[Par07]
Parr, Terrence: The Definitive ANTLR Reference - Building
Domain-Specific Languages. 2007
[Pos08]
The PostgreSQL Global Development Group: PostgreSQL 8.3.1
Documentation. 2008
[Rec02]
Rechenberg, Peter: Technisches Schreiben (nicht nur) für Informatiker. Hanser, 2002
[Ref02]
Reflection. Sun Microsystems. http://java.sun.com/j2se/1.
5.0/docs/guide/reflection/index.html. 2002
[RQZ07]
Rupp, Chris ; Queins, Stefan ; Zengler, Barbara: UML 2
glasklar. 3. Carl Hanser, 2007
[Sch06]
Schäffer, Dr. B. Objektorientierte Softwareentwicklung. Vorlesungsskript. 2006
[Sno00]
Snodgrass, Richard T.: Developing Time-Oriented Database
Applications in SQL. Morgan Kaufmann, 2000
129
LITERATURVERZEICHNIS
[Ull06]
130
Ullenboom, Christian: Java ist auch eine Insel. 5. Galileo
Computing, 2006
Herunterladen