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