Informatik für Lehrer_innen Sebastian Fischer Inhaltsverzeichnis 1 Objektorientierte Programmierung Programmierung ohne Objekte . . . Datenkapselung . . . . . . . . . . . Datenabstraktion . . . . . . . . . . Vererbung . . . . . . . . . . . . . . Überschreiben von Methoden . . . Dynamische Bindung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Datenbanksysteme Relationales Datenmodell . . . . . . . . . . . . . . . . . . . Datensätze referenzieren . . . . . . . . . . . . . . . . . Konsistenzbedingungen . . . . . . . . . . . . . . . . . . Structured Query Language (SQL) . . . . . . . . . . . . . . . Datenabfrage . . . . . . . . . . . . . . . . . . . . . . . Datenmanipulation . . . . . . . . . . . . . . . . . . . . Transaktionen . . . . . . . . . . . . . . . . . . . . . . . . . . Einschub: Ruby-spezifische Sprachkonstrukte . . . . . . . . . Symbole, Hash-Tabellen und benannte Parameter . . . . Blöcke . . . . . . . . . . . . . . . . . . . . . . . . . . . Datenbank-Programierung mit Ruby . . . . . . . . . . . . . . Datensätze abfragen . . . . . . . . . . . . . . . . . . . Datensätze erzeugen, verändern, speichern und löschen Definition eines Datenbank-Schemas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 . 4 . 5 . 6 . 7 . 8 . 10 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 11 12 15 16 16 20 20 21 21 22 23 24 26 28 3 Softwaretechnik Entwicklungsprozesse . . . . . . . . . . . . . Entwurf und Modellierung . . . . . . . . . . Klassen- und Sequenzdiagramme . . . Entwurfsmuster: Model-View-Controller Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 30 31 31 31 35 4 Web-Anwendungen mit Ruby on Rails Datenbanken in Rails . . . . . . . . . . Routen, Controller und Views . . . . . Dozenten anzeigen . . . . . . . . Neue Dozenten anlegen . . . . . Dozenten löschen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 38 39 40 41 43 . . . . . . . . . . . . . . . 3 1 Objektorientierte Programmierung Ein objektorientiertes Programm ist zusammengebaut aus Objekten, die zu verwaltende Daten und zugehörige Operationen zusammenfassen. Verschiedene Arten von Objekten werden durch (Objekt-)Klassen beschrieben, wodurch sich ein modularer Aufbau von Programmen ergibt. Programmierung ohne Objekte Bevor Objektorientierte Programmierung erfunden wurde, wurden häufig globale Daten durch Prozeduren manipuliert. Zum Beispiel kann man eine Tabelle mit Hilfe einer globalen Variablen $table (in Ruby beginnen globale Variablen mit dem $-Zeichen) darstellen und Funktionen und Prozeduren definieren, die auf diese Variable zugreifen und sie verändern: $table = [ ] d e f add_row ! ( row ) $ t a b l e = $ t a b l e + [ row ] end d e f g e t _ e n t r y ( row_index , column_index ) r e t u r n $ t a b l e [ row_index ] [ column_index ] end d e f s e t _ e n t r y ! ( row_index , column_index , e n t r y ) $ t a b l e [ row_index ] [ column_index ] = e n t r y end Die Prozedur add_row! fügt der globalen Tabelle eine Zeile hinzu, die als Array von Einträgen übergeben wird. Die Funktion get_entry liefert den durch die gegebenen Zeilen- und SpaltenIndizes beschriebenen Eintrag zurück und die Prozedur set_entry ! verändert diesen Eintrag. Wir können diese Implementierung einer Tabelle wie folgt verwenden: add_row ! ( [ 1 , 2 , 3 ] ) add_row ! ( [ 4 , 5 , 6 ] ) set_entry !(1 ,1 , get_entry (1 ,0)) set_entry !(1 ,0 ,2) Nach diesen Aufrufen hat die globale Variable $table den Wert [[1,2,3],[2,4,6]] . 4 Datenkapselung Datenkapselung Die Manipulation globaler Daten ist in mehrfacher Hinsicht problematisch: 1. Der Sichtbarkeitsbereich globaler Variablen ist nicht begrenzt. Dadurch ist schwerer ersichtlich, welche Teile des Programms welche Daten verändern, da potentiell jede Prozedur jede globale Variable verändern kann. Programmierfehler haben weitreichendere Konsequenzen, wenn augenscheinlich unabhängige Teile eines Programms die selben globalen Daten manipulieren. 2. Die Verwendung der globalen Variablen $table im obigen Beispiel erlaubt es nicht, mit mehreren Tabellen gleichzeitig zu programmieren. Um eine zweite Tabelle darzustellen, müsste man den kompletten Quelltext kopieren und anschließend die Namen der globalen Variablen sowie aller Funktionen und Prozeduren ändern um Konflikte zu vermeiden. Um die Zugriffsoperationen nicht für jede zu verwendende Tabelle kopieren zu müssen, könnte man ihnen die zu verwendende Tabelle als zusätzlichen Parameter übergeben. Die Objektorientierte Programmierung geht einen anderen Weg und ordnet stattdessen die Zugriffsoperationen den zu verwendenden Daten zu. Wie wir später sehen werden, erlaubt diese Vorgehensweise, abhängig von den zugrunde liegenden Daten Operationen unterschiedlich zu definieren. Objekte fassen Daten und zugehörige Operationen zusammen. Mehrere Objekte können durch mehrfache Instanziierung erzeugt und unabhängig voneinander verwendet werden. Objekte einer Klasse heißen deshalb auch Instanzen dieser Klasse. Die Operationen eines Objektes heißen Methoden. Die Daten eines Objekts sind nicht global zugreifbar sondern hinter Zugriffsmethoden versteckt. Der Sichtbarkeitsbereich der Daten ist auf die Klassendefinition beschränkt. Programmierfehler haben dadurch weniger weitreichende Konsequenzen als beim geteilten Zugriff auf globale Daten. Wir können das obige Beispiel mit Hilfe von Objektorientierter Programmierung wie folgt anpassen: c l a s s Table def i n i t i a l i z e ( ) @table = [ ] end d e f add_row ! ( row ) @table = @table + [ row ] end d e f g e t _ e n t r y ( row_index , column_index ) r e t u r n @table [ row_index ] [ column_index ] end d e f s e t _ e n t r y ! ( row_index , column_index , e n t r y ) 5 1 Objektorientierte Programmierung @table [ row_index ] [ column_index ] = e n t r y end end Statt einer globalen Variable $table verwendet diese Implementierung eine Instanzvariable @table, die in der Konstruktormethode initialize initializiert wird. Die Implementierung der drei Methoden gleicht der vorigen Implementierung verwendet aber die Instanzvariable @table statt der globalen Variable $table. Wir können diese Implementierung wie folgt verwenden: t a b l e 1 = T a b l e . new t a b l e 2 = T a b l e . new t a b l e 1 . add_row ! ( [ 1 , 2 , 3 ] ) t a b l e 1 . add_row ! ( [ 4 , 5 , 6 ] ) table1 . set_entry !(1 ,1 , table1 . get_entry (1 ,0)) table1 . set_entry !(1 ,0 ,2) Die Instanzvariable @table des Objektes table1 hat nach diesen Aufrufen den Wert [[1,2,3],[2,4,6]] . Die Instanzvariable @table des Objektes table2 wird nach ihrer Initializierung nicht mehr verändert und behält den Wert [] . Datenabstraktion Methoden beschränken, wie auf Daten zugegriffen werden kann. Zum Beispiel erlaubt die obige Implementierung (anders als das zugrunde liegende Array @table) nicht, die Anzahl der Zeilen und Spalten einer Tabelle abzufragen. Darüber hinaus können Methoden Sicherheitsabfragen beinhalten und sicherstellen, dass Daten nur auf gültige Weise manipuliert werden. Zum Beispiel könnte die Klasse Table so erweitert werden, dass sie nur das Hinzufügen von Zeilen mit einer bestimmten Spaltenanzahl erlaubt, um Tabellen mit Zeilen, die unterschiedlich viele Einträge enthalten zu verhindern (Übung). Oft bieten Methoden eine abstrakte, standardisierte Sicht auf die Daten und trennen dadurch die Implementierung eines Objektes von seiner Schnittstelle. Da die Daten eines Objektes außerhalb der Klassendefinition nicht sichtbar sind, besteht die Schnittstelle eines Objektes aus dessen Methoden. Abstraktion von der konkreten Implementierung führt zur Reduktion der Komplexität von Programmen, da Schnittstellen ohne Kenntnis ihrer Implementierung verwendet werden können. Darüber hinaus kann eine von ihrer Schnittstelle getrennte Implementierung ausgetauscht werden, ohne dass Programmteile, die die Schnittstelle verwenden, geändert werden müssen, was die modulare Entwicklung von Programmen unterstützt. Wir können den nicht destruktiven Teil der Schnittstelle von Tabellen auch ohne zugrunde liegendes Array implementieren. Zum Beispiel können Einträge einer Multiplikationstabelle allein aus den Zeilen- und Spalten-Indizes berechnet werden: 6 Vererbung class MultiplicationTable d e f g e t _ e n t r y ( row_index , colum_index ) r e t u r n ( row_index +1) ∗ ( column_index +1) end end Objekte dieser Klasse können in Programmen, die auf Tabellen nur lesend zugreifen, anstelle von Objekten der Klasse Table verwendet werden. Schreibender Zugruff auf Multiplikationstabellen ist nicht sinnvoll und deshalb hier nicht implementiert. Da dieser Implememtierung von Multiplikationstabellen keine Daten zugrunde liegen, brauchen wir keine initialize -Methode zu implementieren. Vererbung Klassen können in Hierarchien organisiert werden, in denen sogenannte Unterklassen von Oberklassen erben. Unterklassen können in Oberklassen implementierte Funktionalität nutzen oder in veränderter Form bereitstellen und erweitern. Gemeinsame Funktionalität unterschiedlicher Klassen kann deshalb in einer gemeinsamen Oberklasse definiert werden, um sie nicht zu duplizieren. Oft verwendet man Vererbung, um hierarchische Untertyp-Beziehungen abzubilden. Als Spezialisierung der Table-Klasse implementiern wir eine Klasse zum Zugriff auf CSV Dateien. Wir haben bereits früher eine Funktion readCSV definiert, die eine in einer CSVDatei abgespeicherte Tabelle als Array von Zeilen einliest. Wir können diese Funktion wie folgt verwenden, um Objekte einer Klasse CSV_Table zu initialisieren: r e q u i r e ’ . / readCSV . r b ’ r e q u i r e ’ . / t a b l e . rb ’ c l a s s CSV_Table < T a b l e def i n i t i a l i z e ( c s v _ f i l e ) @table = readCSV ( c s v _ f i l e ) end end Die beiden ersten Zeilen laden die Funktion readCSV und die Implementierung der TableKlasse aus entsprechenden Ruby-Dateien. Die Klasse CSV_Table definieren wir als Unterklasse von Table, indem wir die Oberklasse Table mit dem <-Zeichen hinter den Klassennamen CSV_Table schreiben. Eine Klasse kann höchstens eine Oberklasse haben. Die Konstruktormethode initialize bekommt den Namen einer CSV-Datei übergeben und initialisiert die Instanzvariable @table mit dem Inhalt dieser Datei. Da CSV_Table eine Unterklasse von Table ist, können wir alle Methoden von Table-Objekten verwenden, um auf den Inhalt einer CSV-Datei zuzugreifen. Nachdem die aus einer CSV-Datei eingelesenen Daten geändert wurden, wäre es wünschenswert, die veränderte Tabelle wieder als CSV-Datei abspeichern zu können. Wir können dazu der Table-Klasse eine Methode writeCSV hinzufügen, die dann automatisch auch in CSV_Table-Objekten zur Verfügung steht (Übung). 7 1 Objektorientierte Programmierung Überschreiben von Methoden In der Regel spezialisiert eine Unterklasse das Verhalten ihrer Oberklasse. Standardmäßig stehen dabei alle Methoden der Oberklasse auch in der Unterklasse zur Verfügung. Allerdings kann die Unterklasse auch neue Methoden hinzufügen und existierende Methoden mit einer neuen Implementierung überschreiben. Ein Objekt der Unterklasse bleibt dabei überall verwendbar, wo ein Objekt der Oberklasse verwendbar ist, kann sich aber anders verhalten. Als Beispiel für eine Klassenhierarchie, in der Methoden überschrieben werden, implementieren wir geometrische Figuren im Cartesischen Koordinatensystem. Wir setzen voraus, dass eine Klasse Point zur Darstellung Cartesischer Koordinaten existiert (Übung) und aus der Datei point .rb importiert werden kann. Als Basisklasse aller geometrischer Figuren definieren wir die Klasse Shape: r e q u i r e ’ . / point . rb ’ c l a s s Shape def i n i t i a l i z e ( point ) @location = point end def area ( ) p u t s ( " a r e a n o t implemented " ) end end Die Konstruktormethode initialize bekommt einen Punkt point übergeben, der in der Instanzvariablen @location abgelegt wird. Die Shape-Klasse definiert eine Methode area, die von Unterklassen überschrieben werden und den Flächeninhalt der etsprechenden Figur zurückgeben soll. Statt wie hier selbst eine Fehlermeldung auszugeben, könnten wir die Definition der area-Methode auch weglassen. Ruby gibt automatisch eine Fehlermeldung aus, wenn auf einem Objekt eine Methode aufgerufen wird, die nicht definiert ist. Als Spezialfall der Shape-Klasse definieren wir nun eine Klasse zur Darstellung von Kreisen: c l a s s C i r c l e < Shape def i n i t i a l i z e ( center , r a d i u s ) super ( c e n t e r ) @radius = r a d i u s end def area ( ) r e t u r n Math : : P I ∗ @radius ∗∗2 end end Die Klasse Circle erbt von der Shape-Klasse verfügt also wie diese über die Instanzvariable @location. Wir verwenden die @location-Variable um den Mittelpunkt des Kreises abzuspeichern und initialisieren sie mit der Konstruktormethode initialize der Oberklasse. Da 8 Überschreiben von Methoden die Circle-Klasse selbst eine initialize -Methode definiert, greifen wir auf die initialize Methode der Oberklasse mit Hilfe des Schlüsselwortes super zu. Ein Aufruf von super wird in einen entsprechenden Aufruf der aufrufenden Methode in der Oberklasse übersetzt, in diesem Fall also in einen Aufruf der initialize -Methode der Shape-Klasse. Zusätzlich zur initialisierung des Kreismittelpunktes speichert der Konstruktor für Kreise auch den übergebenen Radius ab. Den abgespeicherten Radius verwenden wir in der Implementierung der area-Methode zur Berechnung der Kreisfläche. Diese Implementierung der area-Methode überschreibt die Implementierung aus der Oberklasse und liefert ein Ergebnis statt eine Fehlermeldung auszugeben. Zum Beispiel liefert das folgende Programm die Ausgabe 28.274333882308138, nämlich die Fläche eines Kreises mit Radius 3: c i r c l e = C i r c l e . new ( P o i n t . new ( 1 , 2 ) , 3 ) puts ( c i r c l e . area ) Als weitere geometrische Figur definieren wir Rechtecke als Unterklasse von Shape: c l a s s R e c t a n g l e < Shape d e f i n i t i a l i z e ( t o p _ l e f t , width , h e i g h t ) super ( t o p _ l e f t ) @width = width @height = h e i g h t end def area ( ) r e t u r n @width ∗ @height end end Wir verwenden wieder super, um die Instanzvariable @location mit Hilfe der initialize Methode aus der Oberklasse zu initialisieren. Diesmal interpretieren wir die übergebenen Koordinaten als linke obere Ecke eines Rechtecks. Darüberhinaus speichern wir die übergebene Breite und Höhe in Instanzvariablen, die wir zur Berechnung des Flächeninhalts eines Rechtecks verwenden. Die angegebene Implementierung der area-Methode überschreibt wieder die Implementierung aus der Oberklasse und liefert ein Ergebnis statt eine Fehlermeldung auszugeben. Als Spezialfall von Rechtecken können wir Quadrate wie folgt definieren: c l a s s Square < R e c t a n g l e d e f i n i t i a l i z e ( t o p _ l e f t , width ) s u p e r ( t o p _ l e f t , width , width ) end end Die initialize -Methode der Klasse Square ruft die entsprechende Methode der RectangleKlasse auf und übergibt dabei die Breite auch als Höhe. Die Square-Klasse definiert keine 9 1 Objektorientierte Programmierung area-Methode und erbt deshalb die von der Rectangle-Klasse überschriebene area-Methode, die auch für Quadrate das richtige Ergebnis liefert. Das folgende Programm erzeugt ein Quadrat und gibt seinen Flächeninhalt (36) aus: s q u a r e = Square . new ( P o i n t . new ( 4 , 5 ) , 6 ) puts ( square . area ) Dynamische Bindung Wenn ein Objekt einer Unterklasse eine Methode ihrer Oberklasse überschreibt, wird beim Aufruf dieser Methode auf einem Objekt der Unterklasse die Implementierung der Unterklasse verwendet. Dieses Verhalten heißt dynamische Bindung, weil die Implementierung erst zur Laufzeit gewählt wird, je nachdem zu welcher Klasse das verwendete Objekt gehört. Das folgende Programm gibt zunächst den Flächeninhalt eines Kreises mit Radius 3 aus und dann den eines Quadrates mit Seitenlänge 6: shapes = [ c i r c l e , square ] f o r i i n 0 . . s h a p e s . s i z e −1 shape = s h a p e s [ i ] p u t s ( shape . a r e a ) end Im ersten Schleifendurchlauf wird also die area-Implementierung aus der Circle-Klasse verwendet und im zweiten die aus der Rectangle-Klasse. Mit der zu Beginn erwähnten Idee, den Operationen die Daten als zusätzliches Argument zu übergeben, ließe sich dieses Verhalten nicht ohne Weiteres umsetzen, da dabei die im Schleifenrumpf verwendete area-Operation vor Ablauf des Programms (statisch) festgelegt, statt wie hier zur Laufzeit (dynamisch) ausgewählt, würde. 10 2 Datenbanksysteme Datenbanksysteme erlauben Abfrage, Manipulation und Verwaltung gespeicherter Daten. Sie haben um Ziel, Daten dauerhaft, effizient und konsistent zu speichern. Auch die Tabellen-Implementierung aus dem vorigen Kapitel erlaubt die dauerhafte Speicherung von Daten. Zum Beispiel können die in den Tabellen-Objekten gespeicherten Daten in CSV-Dateien geschrieben und dadurch dauerhaft gespeichert werden. Um auf in CSV-Dateien gespeicherte Daten zuzugreifen, muss unsere Implementierung allerdings die gesamte Datei einlesen. Bei großen Datenmengen hat diese Vorgehensweise einen entsprechend hohen Speicherbedarf zur Folge. Sie ist dementsprechend ineffizient oder sogar unpraktikabel. Darüber hinaus bietet unsere Implementierung keine Konsistenzprüfung der Daten. Anwendungsprogramme müssten zum Beispiel eigene Prüfungen implementieren, wenn eine gewisse Tabellen-Spalte immer Zahlen und eine andere nur Zeichenketten enthalten soll. Datenbanksysteme erlauben einen effizienteren Zugriff auf abgelegte Daten als unsere Tabellen-Implementierung. Auch stellen sie verschiedene Konsistenzprüfungen bereit, auf die wir später genauer eingehen. Relationales Datenmodell Datenmodelle bestimmen, in welcher Form Daten in der Datenbank angelegt werden. Am häufigsten wird das sogenannte relationale Datenmodell verwendet. Dabei werden Daten ähnlich wie bei der Implementierung aus dem vorherigen Kapitel in Tabellen abgelegt. Jede Zeile einer Tabelle entspricht einem Datensatz. Die Spalten einer Tabelle werden auch Attribut genannt und Datensätze bestehen dementsprechend aus Attributwerten. Als Beispiel betrachten wir die folgende Tabelle mit den Attributen DozentNachname, DozentVorname, und VorlesungsTitel. DozentNachname DozentVorname VorlesungsTitel Huch Frank Informatik für Nebenfächler Fischer Sebastian Weiterbildung Informatik Huch Frank Weiterbildung Informatik Tabelle 2.1: Vorlesungsverzeichnis 11 2 Datenbanksysteme Die Tabelle enthält (den Zeilen entsprechend) drei Datensätze mit (den Spalten entsprechend) jeweils drei Attributwerten. Die Attributwerte des ersten Datensatzes sind zum Beispiel die Zeichenketten “Huch”, “Frank”, und “Informatik für Nebenfächler”. Während die Reihenfolge der Attribute (also Spalten) relevant ist, spielt die Reihenfolge der Datensätze (also Zeilen) im relationalen Datenmodell allerdings keine Rolle. (Deshalb ist es fragwürdig, wie eben vom “ersten Datensatz” zu sprechen.) Auch Mehrfachvorkommen von Zeilen werden ignoriert. Eine Tabelle wird also nicht als Liste sondern als Menge von Datensätzen interpretiert. Da Datensätze mathematisch als Tupel dargestellt werden können, entspricht eine Tabelle einer Menge von Tupeln, also einer Relation. So erklärt sich der Name relationales Datenmodell. Datensätze referenzieren Relationale Datenbanken können mehrere Tabellen verwalten, die aufeinander verweisen. Um auf Datensätze einer Tabelle verweisen zu können, müssen diese eindeutig referenzierbar sein. Dies geschieht mit Hilfe sogenannter Schlüssel. Als Beispiel betrachten wir die beiden folgenden Tabellen, die die selben Daten darstellen wie das obige Vorlesungsverzeichnis. DozentNachname VorlesungsTitel Huch Informatik für Nebenfächler Fischer Weiterbildung Informatik Huch Weiterbildung Informatik Tabelle 2.2: Vorlesungen DozentNachname DozentVorname Huch Frank Fischer Sebastian Tabelle 2.3: Dozenten Bei dieser Darstellung werden die Vorlesungstitel in der Tabelle Vorlesungen nur noch zusammen mit den Nachnamen der Dozenten gespeichert. Die zu den Nachnamen gehörenden Vornamen sind in Tabelle Dozenten den Nachnamen zugeordnet. Diese Darstellung setzt vorraus, dass es keine zwei Dozenten mit dem selben Nachnamen gibt. Ansonsten wäre die Zuordnung der Dozenten zu einer Vorlesung nicht eindeutig. Wir gehen zunächst vereinfachend von solcher Eindeutigkeit aus. Das Attribut DozentNachname legt dann die Datensätze in der Tabelle Dozenten eindeutig fest und wird deshalb Schlüssel der Tabelle genannt. Im Allgemeinen bezeichnet man als Schlüssel eine Menge von Attributen, deren Attributwerte die Datensätze einer Tabelle eindeutig festlegen. (Der eben diskutierte Schlüssel ist also 12 Relationales Datenmodell eigentlich die einelementige Menge, die nur aus dem Attribut DozentNachname besteht.) Eine Tabelle kann mehrere Schlüssel haben. Die Menge aller Attribute einer Tabelle ist immer ein Schlüssel, da Datensätze gemäß des relationalen Datenmodells nicht doppelt vorkommen. Zum Beispiel ist die Menge {DozentVorname} ebenfalls ein Schlüssel für die Tabelle Dozenten, wenn wir vorraussetzen, dass es keine zwei Dozenten mit dem selben Vornamen gibt. Die Tabelle Vorlesungen hat keine einelementigen Schlüssel, da es zu jedem Dozent mehrere Vorlesungen geben kann und umgekehrt. Der einzige Schlüssel der Tabelle Vorlesungen ist also die Menge aller Attribute {DozentNachname,Vorlesungstitel}. Schlüssel aus mehreren Attributen werden Verbundschlüssel genannt. Benutzer einer relationalen Datenbank müssen zu jeder Tabelle einen Schlüssel angeben, der zur Referenzierung ihrer Datensätze verwendet wird. Dieser so ausgezeichnete Schlüssel einer Tabelle wird Primärschlüssel genannt. Weitere Schlüssel können als sogenannte Sekundärschlüssel deklariert werden, was hilfreich sein kann, um Suchanfragen zu beschleunigen. Die Tabelle Vorlesungen hat nur einen Schlüssel, der also auch der Primärschlüssel ist. Für die Tabelle Dozenten haben wir als Primärschlüssel {DozentNachname} gewählt und verwenden entsprechende Attributwerte zur Referenzierung von Dozenten aus der Tabelle Vorlesungen. Die Attributmenge {DozentNachname} wird deshalb Fremdschlüssel in der Tabelle Vorlesungen genannt. (Sie ist kein Schlüssel dieser Tabelle!) Im Allgemeinen müssen Primär- und Fremdschlüssel nicht identisch sein. Es genügt, wenn die zugehörigen Typen der Attribute kompatibel sind. Die Darstellung mit zwei Tabellen hat gegenüber der ursprünglichen Darstellung als eine einzige Tabelle Vorlesungsverzeichnis den Vorteil, dass Vornamen von Dozenten nicht mehr redundant gespeichert werden. Um einen Vornamen eines Dozenten zu ändern, braucht nur noch ein einziger Datensatz in der Tabelle Dozenten geändert werden statt aller zugehörigen Datensätze in der Tabelle Vorlesungsverzeichnis. Dadurch wird auch vermieden, dass der selbe Dozent versehentlich mit verschiedenen Vornamen gespeichert wird. Unsere Annahme, dass Dozenten eindeutig über ihren Nachnamen (oder Vornamen) identifiziert werden können ist unrealistisch. Statt wie in der Tabelle Vorlesungen die Menge aller Attributwerte als Primäschlüssel zu verwenden, können wir der Tabelle künstlich ein Attribut hinzufügen, welches die Datensätze eindeutig festlegt. Relationale Datenbanksysteme unterstützen die Definition solcher Attribute mit einer fortlaufenden Nummer als Attributwert. Durch die fortlaufende Nummerierung sind solche Attribute automatisch Schlüssel und werden Surrogatschlüssel genannt. Um Dozenten mit gleichen Vor- oder Nachnamen nicht von vornherein auszuschließen, ändern wir die Tabelle Dozenten wie folgt. DozentID DozentNachname DozentVorname 0 Huch Frank 1 Fischer Sebastian Tabelle 2.4: Dozenten 13 2 Datenbanksysteme Auch ohne die Eindeutigkeit von Vor- oder Nachnamen vorausszusetzen ist nun {DozentID} ein Schlüssel der Tabelle Dozenten. Wenn wir ihn als Primärschlüssel festlegen, können wir statt {DozentNachname} nun {DozentID} als Fremdschlüssel in der Tabelle Vorlesungen verwenden. DozentID VorlesungsTitel 0 Informatik für Nebenfächler 1 Weiterbildung Informatik 0 Weiterbildung Informatik Tabelle 2.5: Vorlesungen Diese Darstellung eines Vorlesungsverzeichnis enthält noch immer Redundanz, da die Vorlesungstitel mehrfach gespeichert werden. Um auch diese Redundanz zu eliminieren, zerlegen wir die Tabelle Vorlesungen wie folgt in zwei Tabellen. DozentID VorlesungsID 0 0 1 1 0 1 Tabelle 2.6: IstDozent VorlesungsID VorlesungsTitel 0 Informatik für Nebenfächler 1 Weiterbildung Informatik Tabelle 2.7: Vorlesungen Zur Verknüpfung der Dozenten mit Vorlesungen verwenden wir nun die Tabelle IstDozent. Diese referenziert die Tabellen Dozenten und Vorlesungen jeweils über Fremdschlüssel. Die Tabelle Vorlesungen identifiziert die Vorlesungstitel mit Hilfe des Surrogatschlüssels {VorlesungsID}, der auch ihr Primärschlüssel ist. Die Tabelle IstDozent ist die einzige ohne einen einelementigen Schlüssel. Sie representiert eine sogenannte N-zu-M-Beziehung zwischen Dozenten und Vorlesungen. Neben N-zuM-Beziehungen, die mit einer extra Tabelle dargestellt werden, gibt es auch 1-zu-1- und 1-zu-N-Beziehungen. Diese können einfacher (und ohne unnötige Redundanz) ohne extra Tabelle dargestellt werden. Hätte zum Beispiel jede Vorlesung nur einen Dozenten, so würde es genügen, ein zusätzliches Attribut DozentID als Fremdschlüssel in der Tabelle Vorlesungen zu verwenden. In diesem Fall wäre also die Version 2.5 der Tabelle Vorlesungen bereits redundanzfrei. 14 Relationales Datenmodell Ein Sonderfall ergibt sich bei 1-zu-1- und 1-zu-N-Beziehungen, wenn statt der 1 auch eine 0 zugelassen sein soll. Gemäß Version 2.5 der Tabelle Vorlesungen muss es zu jeder Vorlesung einen Dozenten geben. Ein Vorlesungstitel ohne Dozent lässt sich in die Tabelle nicht eintragen, ohne den Wert des Attributs DozentID leer zu lassen. Um dies zu erlauben, gibt es den sogenannten NULL-Wert, der als undefinierter Attributwert fungiert. Die drei Tabellen Dozenten, IstDozent und Vorlesungen representieren die selbe Information wie die ursprüngliche Tabelle Vorlesungsverzeichnis, wobei Redundanz in der ursprünglichen Darstellung eliminiert wurde. Die ursprüngliche Tabelle kann in gängigen relationalen Datenbanksystemen als sogenannte Sicht extrahiert werden. Wir werden dies am Beispiel des Datenbankmoduls von OpenOffice nachvollziehen. Konsistenzbedingungen Relationale Datenbanksysteme implementieren verschiedene Konsitenzprüfungen, die sicherstellen, dass die gespeicherten Daten sinnvoll interpretiert werden können. Die einfachste Konsistenzbedingung ist die sogenannte Bereichsintegrität. Diese fordert, dass Attributwerte zu einem dem Attribut zugeordneten Wertebereich (bzw. Typ) gehören. Zum Beispiel müssen im obigen Beispiel die Werte der Attribute DozentID und VorlesungsID Zahlen sein. Das Einfügen von Datensätzen mit ungültigen Attributwerten wird vom Datenbanksystem verhindert. Die Forderung, dass der Primärschlüssel einer Tabelle die enthaltenen Datensätze eindeutig festlegt, wird als Entitätsintegrität bezeichnet. Das Einfügen von Datensätzen, deren zum Primärschlüssel gehörige Werte bereits in einem anderen Datensatz vorkommen, wird vom Datenbanksystem verhindert. Aus der Referenzierung von Tabellen untereinander ergibt sich der Begriff der referentiellen Integrität. Diese fordert, dass zu den Werten eines Fremdschlüssels ein entsprechender Datensatz in der referenzierten Tabelle existiert. Gegebenenfalls ist auch NULL als Fremdschlüsselwert erlaubt (siehe oben). Verboten sind jedoch in jedem Fall Werte ungleich NULL zu denen kein Datensatz existiert. Ein Datenbanksystem verhindert das Einfügen eines Datensatzes, deren Fremdschlüssel keinen existierenden Datensatz referenziert. Referentielle Integrität kann nicht nur durch Einfügen in der referenzierenden Tabelle sondern auch durch Löschen (oder Ändern) von Datensätzen in der referenzierten Tabelle verletzt werden. Um dies zu verhindern, gibt es verschiedene Strategien. Die einfachste Strategie ist, das Löschen von referenzierten Datensätzen zu verbieten. Falls NULL-Werte als Fremdschlüssel erlaubt sind, können Referenzen auf einen gelöschten Datensatz durch NULL überschrieben und dadurch gelöscht werden. Eine dritte Strategie ist sogenannte Löschweitergabe, bei der referenzierende Datensätze zusammen mit dem referenzierten Datensatz gelöscht werden. Diese Vorgehensweise bietet sich insbesondere bei Tabellen an, die nur aus Fremdschlüsseln bestehen (wie IstDozent im obigen Beispiel), da dabei nur Referenzen gelöscht werden. Neben den diskutierten Konsistenzbedingungen wird auch sogenannte logische Integrität betrachtet, die aber in der Regel nicht von Datenbanksystem unterstützt wird. Zum Beispiel 15 2 Datenbanksysteme könnte man fordern, dass eine Vorlesung nur eine bestimmte Anzahl von Dozenten haben darf. Solche Bedingungen müssen von Anwendern eines Datenbanksystems selbst erfüllt werden. Structured Query Language (SQL) SQL ist eine Sprache zur Abfrage, Manipulation und Verwaltung der in einer relationalen Datenbank gespeicherten Information. Wir beschränken uns hier auf die Abfrage sowie die Manipulation (also das Einfügen, Verändern und Löschen) von Daten. Datenabfrage SQL erlaubt die Abfrage von Daten mit Hilfe der SELECT-Anweisung. Zum Beispiel liefert die folgende Anweisung alle in der Tabelle Dozenten gespeicherten Nachnamen. SELECT DozentNachname FROM Dozenten Es ist üblich (aber nicht notwendig) Schlüsselworte von SQL (wie SELECT und FROM) in Großbuchstaben zu schreiben, um sie von Attribut und Tabellennamen abzusetzen. Das Ergebnis einer SELECT-Anweisung ist eine Tabelle. Die obige Anweisung liefert zum Beispiel das folgende Ergebnis. DozentNachname Huch Fischer Es ist möglich, mehrere Attribute gleichzeitig abzufragen, wie das folgende Beizpiel zeigt. SELECT DozentNachname , DozentVorname FROM Dozenten Das Ergebnis dieser SELECT-Anweisung ist die folgende Tabelle. DozentNachname DozentVorname Huch Frank Fischer Sebastian Zur Abfrage aller beteiligten Attribute kann ein Stern statt der Attributliste geschrieben werden. Die Anweisung SELECT ∗ FROM Dozenten 16 Structured Query Language (SQL) liefert als Ergebnis die komplette Tabelle Dozenten inklusive DozentID: DozentID DozentNachname DozentVorname 0 Huch Frank 1 Fischer Sebastian Die SELECT-Anweisung kann auch verwendet werden, um Daten aus verschiedenen Tabellen zu kombinieren. Die folgende Anweisung kombiniert alle Nachnamen von Dozenten mit allen Vorlesungstiteln. SELECT DozentNachname , V o r l e s u n g s T i t e l FROM Dozenten , V o r l e s u n g e n Das Ergebnis dieser Anweisung ist die folgende Tabelle. DozentNachname VorlesungsTitel Huch Informatik für Nebenfächler Huch Weiterbildung Informatik Fischer Informatik für Nebenfächler Fischer Weiterbildung Informatik Alle Nachnamen mit allen Titeln zu kombinieren entspricht aber nicht unserem Datenmodell, in dem wir die Zuordnung zwischen Dozenten und Vorlesungen in einer N-zu-M-Beziehung IstDozent spezifiziert haben. Um diese Zuordnung in die Abfrage einfließen zu lassen, können wir das Ergebnis der obigen Anweisung mit einer Bedingung einschränken. Bedingungen können in einer SELECT-Anweisung nach dem Schlüsselwort WHERE notiert werden. Die folgende Anweisung schrankt die obige so ein, dass nur zusammengehörige Dozenten und Vorlesungen im Ergebnis vorkommen. SELECT FROM WHERE AND DozentNachname , V o r l e s u n g s T i t e l Dozenten , Vorlesungen , I s t D o z e n t Dozenten . DozentID = I s t D o z e n t . DozentID Vorlesungen . VorlesungsID = IstDozent . VorlesungsID Der Übersichtlichkeit halber ist diese Anfrage in mehreren Zeilen notiert. Die mit FROM beginnende Zeile enthält nun zusätzlich die Tabelle IstDozent, damit wir in der Bedingung auf ihre Attribute zugreifen können. Da die Attribute DozentID und VorlesungsID in verschiedenen Tabellen vorkommen, greifen wir mit qualifizierten Namen, die die Tabelle bestimmen, auf sie zu. Die Bedingung ist eine Konjunktion aus zwei Schlüssel-Vergleichen, die mit den Schlüsselwort AND gebildet wird. Das Ergebnis dieser Anfrage ist die folgende Tabelle. 17 2 Datenbanksysteme DozentNachname VorlesungsTitel Huch Informatik für Nebenfächler Huch Weiterbildung Informatik Fischer Weiterbildung Informatik Dieses Ergebnis enthält nun nur noch die in der Tabelle IstDozent aufgelisteten Kombinationen von Dozenten und Vorlesungen. Eine ähnliche Anfrage haben wir bereits in OpenOffice konstruiert, um das Vorlesungsverzeichnis zu bestimmen (dort haben wir auch die Vornamen der Dozenten ausgegeben). Im obigen Ergebnis kommen Dozentennamen und Vorlesungstitel mehrfach vor. Wenn wir statt der Nachnamen und der Titel nur die Nachnamen (oder nur die Titel) abfragen, bleiben die Mehrfachvorkommen im Ergebnis erhalten. Anders als im relationalen Datenmodell, werden in SQL Mehrfachvorkommen nicht ignoriert. Auch die Reihenfolge der Datensätze ist in SQL, anders als im relationalen Datenmodell, von Bedeutung. SELECT FROM WHERE AND DozentNachname Dozenten , Vorlesungen , I s t D o z e n t Dozenten . DozentID = I s t D o z e n t . DozentID Vorlesungen . VorlesungsID = IstDozent . VorlesungsID Die Variante der obigen Anweisung liefert das folgende Ergebnis. DozentNachname Huch Huch Fischer Die Reihenfolge der Datensätze ist hierbei nicht spezifiziert, kann aber durch einen ORDER BY-Zusatz beeinflusst werden. SELECT FROM WHERE AND ORDER BY DozentNachname Dozenten , Vorlesungen , I s t D o z e n t Dozenten . DozentID = I s t D o z e n t . DozentID Vorlesungen . VorlesungsID = IstDozent . VorlesungsID DozentNachname Diese Anfrage liefert die Nachnamen der Dozenten in alphabetischer Reihenfolge. DozentNachname Fischer Huch Huch 18 Structured Query Language (SQL) Die Berechnung von mehrfach vorkommenden Datensätzen erscheint zunächst wenig sinnvoll. Wir können Datensatzgruppierung mit Hilfe eines GROUP BY-Zusatzes verwenden, um Mehrfachvorkommen zu einem einzigen Datensatz zusammenzufassen. Dies ist besonders im Zusammenhang mit Aggregationsfunktionen sinnvoll. Zum Beispiel berechnet die folgende Anweisung die Anzahl der Dozenten zu jeder Vorlesung. SELECT V o r l e s u n g s T i t e l , COUNT( Dozenten . DozentID ) AS DozentAnzahl FROM Dozenten , Vorlesungen , I s t D o z e n t WHERE Dozenten . DozentID = I s t D o z e n t . DozentID AND V o r l e s u n g e n . V o r l e s u n g s I D = I s t D o z e n t . V o r l e s u n g s I D GROUP BY V o r l e s u n g s T i t e l Die letzte Zeile bewirkt, dass alle Datensätze mit dem gleichen Vorlesungstitel im Ergebnis zu einem Datensatz zusammengefasst werden. Der Ausdruck COUNT(Dozenten.DozentID) berechnet die Anzahl der zum Attribut DozentID gehörenden Werte in einem gruppierten Datensatz und gibt diese Anzahl als Wert des mit dem Schlüsselwort AS neu definierten Attributs DozentAnzahl aus. Das Ergebnis der obigen Anweisung ist die folgende Tabelle. VorlesungsTitel DozentAnzahl Informatik für Nebenfächler 1 Weiterbildung Informatik 2 Dieses Beispiel demonstriert gleichzeitig die Gruppierung von Datensätzen, die Verwendung der Aggregationsfunktion COUNT und die Definition neuer Attribute im Ergebnis einer SELECT-Anweisung. Die zuletzt berechnete Tabelle können wir alternativ auch mit Hilfe einer geschachtelten SELECT-Anweisung berechnen. Da das Ergebnis einer SELECT-Anweisung eine Tabelle ist, können wir es überall dort einsetzen, wo Tabellen stehen dürfen, also zum Beispiel nach dem Schlüsselwort FROM. Auch einige Aggregationsfunktionen nehmen als Argument eine geschachtelte SELECT-Anweisung. Ist das Ergebnis einer SELECT-Anweisung eine Tabelle mit nur einem Eintrag, können wir diesen auch als Attributwert nach dem SELECT-Schlüsselwort verwenden, wie das folgende Beispiel zeigt. SELECT V o r l e s u n g s t i t e l , ( SELECT COUNT( DozentID ) FROM I s t D o z e n t WHERE V o r l e s u n g e n . V o r l e s u n g s I D = I s t D o z e n t . V o r l e s u n g s I D ) AS DozentAnzahl FROM V o r l e s u n g e n Hier wird ein Vorlesungstitel mit dem Ergebnis einer geschachtelten Abfrage kombiniert, die die Anzahl der Datensätze mit der zugehörigen VorlesungsID in der Tabelle IstDozent berechnet. Das Ergebnis ist das selbe wie das der vorigen Abfrage. 19 2 Datenbanksysteme Datenmanipulation Mit SQL können gespeicherte Daten nicht nur abgefragt sondern auch verändert werden. Die INSERT-Anweisung fügt einer Tabelle Datensätze hinzu. Zum Beispiel füllt die folgende Anweisung die Tabelle Dozenten mit Werten. INSERT INTO Dozenten ( DozentNachname , DozentVorname ) VALUES ( " Huch " , F r a n k " ) , ( " F i s c h " , " S e b a s t i a n " ) Wir gehen hierbei davon aus, dass das Attribut DozentID vom Datenbanksystem automatisch (zum Beispiel fortlaufend) vergeben wird, geben also nur die Vor- und Nachnamen von Dozenten an. Die UPDATE-Anweisung ändert Datensätze einer Tabelle. Die folgende Anweisung ändert den Nachnamen eines Dozenten. UPDATE Dozenten SET DozentNachname = " F i s c h e r " WHERE DozentVorname = " S e b a s t i a n " Falls hierbei mehrere Dozenten mit dem Vornamen "Sebastian" in der Datenbank abgespeichert wären, würde der Nachname von allen durch " Fischer " ersetzt werden. Um unbeabsichtigte Änderungen zu vermeiden, sollte der zu verändernde Datensatz deshalb besser über seinen Primärschlüssel eindeutig referenziert werden. Die DELETE-Anweisung löscht Datensätze aus einer Tabelle. Zum Beispiel löscht die folgende Anweisung alle Dozenten mit dem Nachnamen "Huch". DELETE FROM Dozenten WHERE DozentNachname = " Huch " Neben der Manipulation von Daten bietet SQL auch Konstrukte zur Verwaltung von Daten, also zum Beispiel zum Anlegen von Tabellen oder zur Spezifikation von Zugriffsrechten, auf die wir hier jedoch nicht eingehen. Transaktionen Transaktionen fassen mehrere Anweisungen zur Abfrage und Manipulation von Datenbanken zu einer Einheit zusammen. Um möglichst viele Transaktionen in möglichst kurzer Zeit auszuführen, werden Transaktionen von Datenbanksystemen in der Regel parallel abgearbeitet. Dabei werden bestimmte Bedingungen eingehalten, die durch den Begriff ACID (für die Englischen Begriffe: Atomicity, Consistency, Isolation und Durability) zusammengefasst werden. Atomicity fordert, dass eine Transaktion nach außen hin als Einheit erscheint, also entweder ganz oder garnicht ausgeführt wird. Consistency fordert, dass die Datenbank nach Ausführung einer Transaktion in einem konsistenten Zustand ist, wenn sie es vorher war. 20 Einschub: Ruby-spezifische Sprachkonstrukte Isolation fordert, dass unterschiedliche Transaktionen sich nicht gegenseitig beeinflussen, selbst wenn das Datenbanksystem sie parallel zueinander ausführt. Durability fordert, dass die Änderungen einer erfolgreich abgearbeiteten Transaktion dauerhaft gespeichert werden. Zum Beispiel auch bei einem späteren Stromausfall. Transaktionen können entweder erfolgreich abgearbeitet oder abgebrochen werden. Bei einem Transaktionsabbruch werden mit einem sogenannten rollback die bis dahin vorgenommen Änderungen rückgängig gemacht. Bei erfolgreicher Abarbeitung werden die Änderungen mit einem sogenannten commit gespeichert. In SQL definiert man eine Transaktion durch die Schlüsselworte BEGIN TRANSACTION sowie COMMIT und ROLLBACK. Einschub: Ruby-spezifische Sprachkonstrukte Bevor wir uns der Datenbankprogrammierung in Ruby widmen, lernen wir ausgewählte Sprachkonstrukte kennen, die dabei zur Anwendung kommen. Symbole, Hash-Tabellen und benannte Parameter In Ruby können statt Zeichenketten sogenannte Symbole verwendet werden. Diese haben eine eingeschränktere Schnittstelle (sie bieten zum Beispiel keine Methoden zur Konkatenation oder zur Abfrage ihrer Länge) werden aber intern effizienter dargestellt. Symbole werden mit einem vorangestellten Doppelpunkt geschrieben (:foo, :bar, :baz). Da Symbole effizienter verglichen werden können als Zeichenketten, eignen sie sich besonders gut als Schlüssel in sogenannten Hash-Tabellen. Hash-Tabellen ähneln Arrays, werden aber nicht über Zahlen sondern über sogenannte Schlüssel indiziert. Sie ordnen also den Schlüsseln gewisse Werte zu. Das folgende Ruby-Programm illustriert die Verwendung von Hash-Tabellen mit Symbolen als Schlüssel. t a b l e = { : f o o => 41 , : b a r => 42 , : baz => 43 } puts t a b l e [ : bar ] Dieses Programms gibt den, dem Schlüssel :bar zugeordneten, Wert 42 auf dem Bildschirm aus. In welcher Reihenfolge die Einträge der Hash-Tabelle notiert werden, spielt hierbei keine Rolle. Eine interessante Besonderheit ergibt sich bei Funktionen und Prozeduren, die eine HashTabelle als Parameter haben. Beim Aufruf solcher Funktionen und Prozeduren können die geschweiften Klammern weggelassen werden, so dass es aussieht als hätten sie benannte Parameter: def print_name ( a r g s ) puts args [ : f i r s t ] + " " + args [ : l a s t ] end p r i n t _ n a m e : l a s t => " Huch " , : f i r s t => " F r a n k " 21 2 Datenbanksysteme Dieses Programm gibt die Zeichenkette "Frank Huch" auf dem Bildschirm aus. Blöcke Blöcke fassen Anweisungs-Sequenzen zu einer Einheit zusammen, die als Argument an Funktionen und Prozeduren übergeben werden kann ohne sie vorher auszuführen. Funktionen und Prozeduren, die einen Block als Argument erhalten, können diesen im Rumpf (auch mehrfach) ausführen. Arrays bieten Methoden mit Blöcken als Parameter, die typische Schleifenkonstrukte abstrahieren. Die Methode each, zum Beispiel, führt den übergebenen Block einmal für jedes Element eines Arrays aus und übergibt dabei jeweils das entsprechende Element als Argument an den Block. Blöcke können also parametrisiert werden. Das folgende Ruby-Programm zeigt zwei Arten Blöcke zu schreiben und eine entsprechende for -Schleife mit dem selben Verhalten wie die beiden Aufrufe von each. a = [41 ,42 ,43] f o r i i n 0 . . a . s i z e −1 puts a [ i ] end a . each do |n| puts n end a . each { |n| p u t s n } Blöcke können mit dem Schlüsselwort do oder in geschweiften Klammern notiert werden. Die Parameter eines Blocks stehen in jedem Fall zwischen senkrechten Strichen. Die Schreibweise mit geschweiften Klammern eignet sich besonders für Blöcke mit nur einer Anweisung. Die each-Methode liefert als Ergebnis das Array zurück, auf dem sie aufgerufen wurde. Die collect -Methode hingegen liefert ein neues Array, deren Elemente sich durch die Ausführung des Blockes ergeben. Zum Beispiel liefert der Ausdruck [ 4 1 , 4 2 , 4 3 ] . c o l l e c t { |n| n − 40 } das Array [1,2,3] zurück. Es ist auch möglich Arrays mit Hilfe von Blöcken zu filtern. Die Array-Methode find_all liefert ein Array als Ergebnis, in das die Elemente übernommen werden, für die der übergebene Block true zurückliefert. Zum Beispiel liefert [ 4 1 , 4 2 , 4 3 ] . f i n d _ a l l { |n| n % 2 == 1 } das Array [41,43] zurück. Schließlich betrachten wir noch die Methode inject , die alle Elemente eines Arrays zu einem Ergebnis akkumuliert, das nicht unbedingt ein Array sein muss. Zum Beispiel können wir mit inject wie folgt die Elemente eines Arrays addieren: 22 Datenbank-Programierung mit Ruby [ 4 1 , 4 2 , 4 3 ] . i n j e c t ( 0 ) { |sum , n| sum + n } Der Wert dieses Ausdrucks ist 126. An diesem Beispiel sehen wir, dass die Methode inject neben dem Block noch einen Startwert (hier 0) als ersten Parameter nimmt und dass sie zwei Argumente an den Block übergibt: ein Zwischenergebnis (hier sum genannt) und ein Element des Arrays (hier n). Blöcke sind hilfreich zur Abstraktion von Schleifen. Dies ist jedoch nicht ihre einzige Anwendung, wie wir bald sehen werden. Datenbank-Programierung mit Ruby In Ruby ist es möglich auf relationale Datenbanken ohne (sichtbare) Verwendung von SQL zuzugreifen. Die in der Datenbank gespeicherten Daten werden dazu in Ruby-Objekte transformiert, die programmatisch erzeugt, aus der Datenbank gelesen, verändert und gespeichert werden können. Die Transformation von Datensätzen der Datenbank folgt standardmäßig gewissen Namenskonventionen um die nötige Konfiguration auf ein Minimum zu beschränken. Zwar ist es möglich, durch optionale Konfiguration auf beliebige Datenbank-Schemata zuzugreifen. Um die Konventionen kennenzulernen, passen wir aber das Datenbank-Schema aus den vorherigen Kapiteln an die in Ruby verwendeten Konventionen an. Da diese auch englische Pluralbildung umfassen, übersetzen wir die verwendeten Namen ins Englische. Die folgenden Tabellen representieren das zuvor besprochene Vorlesungsverzeichnis mit neuen Namen für Tabellen und Attribute. id lastname firstname 1 Huch Frank 2 Fischer Sebastian Tabelle 2.8: lecturers id title 1 Informatik für Nebenfächler 2 Weiterbildung Informatik Tabelle 2.9: lectures 23 2 Datenbanksysteme lecturer_id lecture_id 1 1 1 2 2 2 Tabelle 2.10: lecturers_lectures Ruby unterstützt den Zugriff auf verschiedene relationale Datenbank-Systeme. Leider ist der Zugriff auf mit OpenOffice erstellte Datenbanken nicht ohne Weiteres möglich. Am einfachsten ist die Verwendung des Datenbanksystems SQLite. Dies speichert wie OpenOffice eine Datenbank in einer einzigen Datei, die mit dem Firefox Plugin SQLite Manager verwaltet werden kann. Wir gehen im Folgenden davon aus, dass die oben gezeigte Datenbank in der Datei lectures . sqlite gespeichert ist. Wir können dann in Ruby auf diese Datenbank zugreifen, indem wir die active_record-Bibliothek importieren und eine Verbindung zur SQLite Datenbank aufbauen: require ’ active_record ’ A c t i v e R e c o r d : : Base . e s t a b l i s h _ c o n n e c t i o n ( : a d a p t e r => " s q l i t e 3 " , : d a t a b a s e => " l e c t u r e s . s q l i t e " ) Die Argumente der Prozedur establish_connection werden hier in Form einer Hash-Tabelle übergeben. Datensätze abfragen Wir können nun Ruby-Klassen definieren um auf die in der Datenbank gespeicherten Daten zuzugreifen. Zum Zugriff auf Dozenten definieren eine Klasse Lecturer als Unterklasse von ActiveRecord::Base. c l a s s L e c t u r e r < A c t i v e R e c o r d : : Base end Der Rumpf der Klassendefinition bleibt hier zunächst leer. Ruby erzeugt die Klasse und ihre Methoden allein anhand von Namenskonventionen. Die zugehörige Tabelle ergibt sich als (klein geschriebener) Plural des Klassennamens (hier also lecturers ). Die Methoden der Klasse Lecturer ergeben sich wiederum aus den Attributen dieser Tabelle. Objekte der Klasse Lecturer entsprechen Datensätzen (also Zeilen) der Tabelle lecturers . Wir können auf die Datensätze mit Hilfe von Klassenmethoden zugreifen. Der folgende Ruby-Code demonstriert die Abfrage des ersten und des letzten eingetragen Dozenten. p Lecturer . f i r s t p Lecturer . l a s t 24 Datenbank-Programierung mit Ruby Dieser Code erzeugt für unsere Datenbank die folgende Bildschirm-Ausgabe: #<L e c t u r e r i d : 1 , l a s t n a m e : " Huch " , f i r s t n a m e : " F r a n k"> #<L e c t u r e r i d : 2 , l a s t n a m e : " F i s c h e r " , f i r s t n a m e : " S e b a s t i a n"> Die Ausgabe zeigt die Attribute der abgefragten Datensätze mit entsprechenden Attributwerten. Statt wie hier einzeln auf die Datensätze zuzugreifen, können wir auch alle in einer Schleife durchlaufen. Wir verwenden dazu die Klassenmethode find_each, die analog zu each für Arrays einen Block als Argument nimmt. Innerhalb des übergebenen Blocks können wir mit automatisch generierten Zugriffsmethoden auf die Attributwerte der übergebenen Datensätze zugreifen. L e c t u r e r . f i n d _ e a c h do | l e c t u r e r | puts l e c t u r e r . f i r s t n a m e + " " + l e c t u r e r . lastname end Dieser Aufruf gibt nacheinander "Frank Huch" und "Sebastian Fischer " aus. Um auf die von einer Dozentin gehalten Vorlesungen zuzugreifen, können wir die Definition der Klasse Lecturer um eine entsprechende Assoziation erweitern: c l a s s L e c t u r e r < A c t i v e R e c o r d : : Base has_and_belongs_to_many : l e c t u r e s end c l a s s L e c t u r e < A c t i v e R e c o r d : : Base has_and_belongs_to_many : l e c t u r e r s end Gleichzeitig definieren wir die Klasse Lecture zum Zugriff auf Vorlesungen. Die Methode has_and_belongs_to_many beschreibt die N-zu-M-Beziehung zwischen Dozentinnen und Vorlesungen. Die Zuordnung wird per Konvention aus der Tabelle lecturers_lectures gelesen. Nach Definition dieser Zuordnung können wir das obige Programm zur Ausgabe von Dozenten wie folgt erweitern. L e c t u r e r . f i n d _ e a c h do | l e c t u r e r | puts l e c t u r e r . f i r s t n a m e + " " + l e c t u r e r . lastname l e c t u r e r . l e c t u r e s . each do | l e c t u r e | puts " − " + lecture . t i t l e end end Die Klasse Lecturer verfügt nun über eine Methode lectures , die ein Array aller Vorlesungen einer Dozentin lieftert. Die Bildschirm-Ausgabe dieses Programms listet daher nun zu jedem Dozenten auch dessen Vorlesungen auf. F r a n k Huch − I n f o r m a t i k f u e r Nebenfaechler − Weiterbildung Informatik 25 2 Datenbanksysteme Sebastian Fischer − Weiterbildung Informatik Wir können auch umgekehrt die Vorlesungen mit jeweils allen zugeordneten Dozenten auflisten: L e c t u r e . f i n d _ e a c h do | l e c t u r e | puts lecture . t i t l e l e c t u r e . l e c t u r e r s . each do | l e c t u r e r | puts " − " + l e c t u r e r . f i r s t n a m e + " " + l e c t u r e r . lastname end end Die Ausgabe dieses Programm ist I n f o r m a t i k f u e r Nebenfaechler − F r a n k Huch Weiterbildung Informatik − F r a n k Huch − Sebastian Fischer Neben N-zu-M-Beziehungen können auch 1-zu-1-, und 1-zu-N-Beziehungen beschrieben werden. Dazu ruft man has_one bzw. has_many mit einem Tabellen-Namen (Singular bzw. Plural) auf. In der Tabelle, die den Fremdschlüssel enthält, definiert man die Beziehung mit der Methode belongs_to. In der Übung werden wir einige dieser Beziehungen definieren. Die active_record Bibliothek erlaubt auch, Abfragen mit Bedingungen einzuschränken, Ergebnisse zu gruppieren und dergleichen mehr (analog zu SQL). Wir gehen hier jedoch nicht weiter darauf ein. Welche Funktionalität unterstützt wird, hängt von der verwendeten Bibliotheksversion ab und ist in deren Dokumentation nachzulesen. Datensätze erzeugen, verändern, speichern und löschen Wir können mit Ruby nicht nur lesend sondern auch schreibend auf die Datenbank zugreifen. Zum Beispiel können wir die Attributwerte von abgefragten Datensätzen ändern oder ganz neue Daten in die Datenbank eintragen. Die folgenden Zeilen erzeugen eine neue Vorlesung "Funktionale Programmierung" des Dozenten "Frank Huch". f h u = L e c t u r e r . f i n d _ b y _ l a s t n a m e " Huch " f u n k _ p r o g = L e c t u r e . new f u n k _ p r o g . t i t l e = " F u n k t i o n a l e Programmierung " funk_prog . l e c t u r e r s = [ fhu ] funk_prog . save Wie wir sehen, gibt es für jedes Attribut destruktive Methoden zum Setzen der entsprechenden Attribut-Werte. Auch über Assoziationen erreichbare Werte können durch solche Methoden verändert werden. Wir weisen hier der neu erstellten Vorlesung den Dozent fhu zu, den wir 26 Datenbank-Programierung mit Ruby mit der automatisch generierten Methode find_by_lastname herausgesucht haben. Durch den Aufruf der Methode save wird der neue Datensatz (inklusive Vorlesungs-Zuordnung) in der Datenbank gespeichert. Statt einen Dozenten einer Vorlesung zuzuordnen, können wir auch umgekehrt eine Vorlesung einem Dozenten zuordnen: weiter = Lecture . find ( 2 ) wollw = L e c t u r e r . new wollw . l a s t n a m e = " Wollweber " wollw . f i r s t n a m e = " K a i " wollw . l e c t u r e s = [ w e i t e r ] wollw . s a v e Hier greifen wir über den Primärschlüssel der lectures-Tabelle auf die Vorlesung “Weiterbildung Informatik” zu und speichern den entsprechenden Datensatz in der Variablen weiter. Später verwenden wir diese Variable dann zur Definition der Vorlesungen des neu angelegten Dozenten. Obwohl wir die Verbindung zwischen Dozenten und Vorlesungen nur in jeweils einer Richtung definiert haben, können wir sie in beiden Richtungen abfragen. Die Ausgabe der oben gezeigten Schleifen ist nun wie folgt: F r a n k Huch − I n f o r m a t i k f u e r Nebenfaechler − Weiterbildung Informatik − F u n k t i o n a l e Programmierung Sebastian Fischer − Weiterbildung Informatik K a i Wollweber − Weiterbildung Informatik I n f o r m a t i k f u e r Nebenfaechler − F r a n k Huch Weiterbildung Informatik − F r a n k Huch − Sebastian Fischer − K a i Wollweber F u n k t i o n a l e Programmierung − F r a n k Huch Zum Löschen von Datensätzen gibt es unterschiedliche Methoden. Die Methode delete löscht nur die zu dem Datensatz gehörige Zeile aus der Datenbank und lässt jene daher möglicherweise in einem inkonsistenten Zustand. Die Methode destroy löscht hingegen auch alle referenzierten Datensätze. In unserem Beispiel sollten wir also funk_prog . destroy wollw . d e s t r o y 27 2 Datenbanksysteme aufrufen, um die eben erzeugten Datensätze inklusive ihrer Verknüpfungen wieder zu löschen. Mit active_record-Objekten kann nicht nur auf Tabellen sondern auch auf Sichten (Views) einer Datenbank zugegriffen werden. Sofern die Sichten nicht veränderbar sind, kann auf diese jedoch nur lesend zugegriffen werden. (Manche Datenbanksysteme bieten sogenannte updateable views auf die auch schreibend zugegriffen werden kann.) Definition eines Datenbank-Schemas Bisher sind wir davon ausgegangen, dass die Datenbank, auf die wir zugreifen, bereits existiert, also zum Beispiel mit dem Firefox Plugin SQLite Manager erzeugt wurde. Wir können die Datenbank allerdings auch mit Ruby anlegen. Die active_record-Bibliothek stellt zu diesem Zweck sogenannte Migrations bereit. Diese spezifizieren, wie ein Datenbank-Schema von einem Zustand in einen anderen überführt werden kann und wieder zurück. Wir begnügen uns hier damit, unsere Datenbank aus dem Zustand “nicht vorhanden” in den Zustand “alle Tabellen vorhanden” zu überführen, also ohne dabei Zwischenschritte zu modellieren. Wir definieren eine Migration LectureDatabase als Unterklasse von ActiveRecord::Migration. c l a s s LectureDatabase < ActiveRecord : : Migration d e f s e l f . up ... end d e f s e l f . down ... end end Die (hier noch nicht gezeigten) Klassen-Methoden up und down spezifizieren, welche Anweisungen ausgeführt werden müssen um unsere Datenbank zu erstellen bzw. wieder zu löschen. Um die Tabellen unserer Datenbank zu erzeugen, schreiben wir also entsprechende Anweisungen in den Rumpf der Methode up. Die folgende Anweisung erzeugt die Dozenten-Tabelle. c r e a t e _ t a b l e : l e c t u r e r s do | t | t . s t r i n g : lastname t . string : firstname end Die Methode create_table nimmt als erstes Argument ein Symbol mit dem Namen der zu erzeugenden Tabelle. Das zweite Argument ist ein Block, der die Attribute der Tabelle spezifiziert. Das dem Block übergebene Argument bietet dazu für jeden unterstützten Spaltentyp eine Methode, die als Argument den Namen der Spalte als Symbol übergeben bekommt. Zusätzlich zu den spezifizierten Attributen wird automatisch eine Spalte id angelegt, die als Primärschlüssel fungiert. Wahlweise können auch andere Spalten als Primärschlüssel deklariert werden. Verbundschlüssel sind jedoch nicht ohne Weiteres möglich. 28 Datenbank-Programierung mit Ruby Die Vorlesungs-Tabelle erzeugen wir auf ähnliche Weise: c r e a t e _ t a b l e : l e c t u r e s do | t | t . string : title end Auch hier wird automatisch der Primärschlüssel id erzeugt. Die Verknüpfungs-Tabelle lecturers_lectures soll keinen automatisch generierten Primärschlüssel erhalten. Wir erreichen dies mit der Option : id => false. c r e a t e _ t a b l e : l e c t u r e r s _ l e c t u r e s , : i d => f a l s e do | t | t . integer : lecturer_id t . integer : lecture_id end Die so definierte Tabelle hat keinen Primärschlüssel. Da wir sie nur als Verknüpfungstabelle verwenden (die Datensätze also nur über Fremdschlüssel referenzieren), ist dies unproblematisch. Schließlich definieren wird die down-Methode der Migration, die alle erzeugten Tabellen aus der Datenbank löscht. d e f s e l f . down drop_table : l e c t u r e r s drop_table : lectures drop_table : l e c t u r e r s _ l e c t u r e s end Wir können unsere Datenbank jetzt durch den Aufruf LectureDatabase.up erzeugen und durch LectureDatabase.down wieder löschen. Nach dem Erzeugen ist die Datenbank noch leer. Wir können wie im vorherigen Abschnitt gezeigt, Datensätze erzeugen und abspeichern. 29 3 Softwaretechnik Softwaretechnik umfasst systematische Methoden, die die Entwicklung von Software begleiten. Dazu gehören die Anforderungsanalyse, der Entwurf, die Erstellung, Wartung, Konfiguration sowie die Qualitätssicherung von Software. Bezüglich des Entwurfs und der Entwicklung von Software beschäftigen wir uns mit Modellierungstechniken und Entwurfsmustern für objektorientierte Programme. Bezüglich der Qualitätssicherung befassen wir uns mit dem Test ihrer Bestandteile. Entwicklungsprozesse Die Softwaretechnik definiert festgelegte Vorgehensweisen, an die man sich zur Entwicklung umfangreicher Software halten kann. Diese Vorgehensweisen sollen sicherstellen, dass die Software arbeitsteilig kooordiniert entwickelt werden kann. Sie werden Entwicklungsprozesse oder -modelle genannt. Im sogenannten Wasserfallmodell der Software-Entwicklung werden die Anforderungsanalyse, der Entwurf, die Entwicklung, das Testen und die Wartung in genau dieser Reihenfolge nacheinander durchgeführt. Eine neue Phase beginnt also erst, wenn die vorherige Phase abgeschlossen ist. Eine Variante des Wasserfallmodells ist das V-Modell, das die Phasen noch genauer festlegt und dabei Test-, Wartungs- und Auslieferungsphasen den Analyse-, Entwurfs- und Entwicklungsphasen gegenüberstellt. Ein Problem des Wasserfallmodells (auch des V-Modells) ist, dass Designfehler erst spät, nämlich wärend der Test- oder Wartungsphase entdeckt werden. Inkrementelle Modelle versuchen diesen Nachteil zu beheben, indem das System in kleinere, separat mit dem Wasserfallmodell entwickelte, Teile zerlegt wird. Da sich dadurch die Zeitspanne zwischen Design- und Testphase verkürzt, können Fehler früher behoben werden. Das sogenannte Spiralmodell erlaubt es, Anforderungen und Design zu revidieren, indem die verschiedenen Phasen der Software-Entwicklung immer wieder durchlaufen werden. Dabei wird zunächst ein Prototyp des Systems entwickelt und benutzt um die Anforderungen und gegebenenfalls das Design zu überarbeiten. Auf Basis des überarbeiteten Designs wird eine neue Version des Systems entwickelt. Dieser Vorgang kann so lange wiederholt werden, bis sich die Anforderungen und das Design nicht mehr ändern. Im Extremfall werden verbesserte Prototypen in sehr schneller Folge entwickelt und die Anforderungen an diese schrittweise erweitert. Man startet also nicht mit einer kompletten Anforderungsanalyse sondern entwickelt neue Anforderungen, sobald vorherige zufriedenstellend umgesetzt sind. Bei diesem Modell wird das Programm häufig umgeschrieben, sowohl um es an neue Anforderungen anzupassen, als auch um die Implementierung existierender 30 Entwurf und Modellierung Anforderungen zu vereinfachen oder zu vereinheitlichen. Das Umschreiben eines Programms, ohne sein Verhalten zu verändern, nennt man Refactoring. Entwurf und Modellierung Wir wenden uns nun Aspekten des Entwurfs und der Modellierung von Software zu. Klassen- und Sequenzdiagramme Wir haben bereits Klassendiagramme kennengelernt, die die Schnittstelle von Objekten sowie die Beziehungen (Vererbung und Aggregation) zwischen deren Klassen grafisch darstellen. In Anlehnung an das Datenbankkapitel betrachten wir Klassen für Dozenten und Vorlesungen mit folgender Schnittstelle. Lecturer id firstname lastname destroy Course id title destroy Die Methode id liefert eine von der Datenbank generierte Zahl, die verwendet werden kann um ein Objekt der entsprechenden Klasse eindeutig zu referenzieren. Die Methode destroy löscht ein Objekt aus der Datenbank. Die restlichen Methoden liefern die Attributwerte der den Objekten entsprechenden Datensätze. Neben Klassendiagrammen, die die Daten eines objektorientierten Programms modellieren, werden wir Sequenzdiagramme kennenlernen, die Aspekte des Verhaltens eines Softwaresystems beschreiben. Sequenzdiagramme stellen Interaktionen unterschiedlicher Komponenten eines Systems in zeitlicher Abfolge dar. Entwurfsmuster: Model-View-Controller Die Entwicklung komplexer Software folgt in der Regel bestimmten Entwurfsmustern, die sich im Laufe der Zeit für wiederkehrende Problemstellungen herausgebildet haben. Die 31 3 Softwaretechnik Verwendung solcher Muster erleichtert es anderen Programmierern, die die Software nicht kennen, diese zu verstehen, wenn ihnen zugrundeliegende Muster bekannt sind. Ein gängiges Entwurfsmuster (Englisch: design pattern) für Programme, deren Benutzer interaktiv auf gespeicherte Daten zugreifen, ist das sogenannte Model-View-Controller pattern. Hierbei werden drei wichtige Aspekte solcher Programme getrennt voneinander entwickelt. Dies erlaubt zum Beispiel die arbeitsteilige Entwicklung der verschiedenen Aspekte und durch Verwendung einheitlicher Schnittstellen lassen sich unterschiedliche Implementierungen der Aspekte miteinander kombinieren. Das Model-View-Controller (MVC) pattern teilt interaktive Programme wie folgt auf: • Das Model implementiert den Zugriff auf die zugrundeliegenden Daten, auf die Benutzer zugreifen. • Der View definiert, wie ausgewählte Daten angezeigt werden (zum Beispiel textuell in einem Terminal, als HTML-Seite in einem Browser oder maschinenlesbar zum Beispiel im XML-Format). • Der Controller reagiert auf Benutzer-Eingaben, steuert den Zugriff auf die vom Modell bereitgestellte funktionalität und liefert die Daten, die vom View angezeit werden. Wir werden im Folgenden ein Ruby-Programm lectures −manager mit dem MVC pattern entwickeln, das Benutzern Zugriff auf unser Vorlesungsverzeichnis bereitstellt. Als Modell fungieren dabei mit Hilfe der activerecord-Bibliothek erstellte Klassen, von denen wir vorraussetzen, dass sie die oben gezeigte Schnittstelle implementieren. Darüber hinaus setzen wir für die gezeigten Klassen die Klassenmethoden find und all vorraus, die die Abfrage eines Datensatzes über dessen id bzw aller Datensätze als Array erlauben. Schließlich implementiert das Modell auch Methoden add_lecturer ( firstname ,lastname) und add_course( title ) zum Anlegen neuer Datensätze. Die Implementierung des Modells wird als Datei lectures −model.rb bereitgestellt. Den View werden wir in einer Datei lectures −view.rb implementieren. Da unsere Anwendung Text-basiert im Terminal laufen wird, implementieren wir Funktionen zur Darstellung der gespeicherten Daten als Zeichenketten. Den Controller implementieren wir in der Datei lectures −controller.rb. Er ist dafür zuständig, Dozenten und Vorlesungen mit Hilfe der Modellklassen aus der Datenbank auszulesen und gegebenenfalls an den View zur Anzeige zu übergeben. Unser Hauptprogramm lectures −manager.rb soll es Benutzern ermöglichen, neue Dozenten und Vorlesungen anzulegen, sowie existierende aufzulisten und zu löschen. Wir werden gemeinsam die entsprechende Funktionalitat für Dozenten entwickeln. Die entsprechenden Funktionen für Vorlesungen sind als Übung selbst zu schreiben. Wir implementieren zunächst eine main-Prozedur, die Benutzer die verfügbare Funktionalität auswählen lässt. Zunächst definieren wir dazu ein Array der Zugriffsfunktionen und zeigen sie als nummerierte Liste an. d e f main a c t i o n s = [ " P r i n t l e c t u r e r s " , " Create l e c t u r e r " , " Delete L e c t u r e r " ] for i in 1 . . actions . size 32 Entwurf und Modellierung p u t s i . t o _ s + " : " + a c t i o n s [ i − 1] end end Die Ausgabe dieses Programms ist wir folgt. 1: P r i n t l e c t u r e r s 2: Create l e c t u r e r 3: Delete l e c t u r e r Natürlich wollen wir Benutzern die verfügbare Funktionalität nicht nur anzeigen. Damit Benutzer die Funktionen auswählen können, bauen wir in die main-Prozedur eine Abfrage ein, welche Funktion ausgeführt werden soll. p u t s " Choose a c t i o n ( 1 . . " + a c t i o n s . s i z e . t o _ s + " ) " choice = g e t s . t o _ i Dies fügt der Ausgabe des obigen Programms die folgende Zeile hinzu. Choose a c t i o n ( 1 . . 3 ) Danach wartet das Programm auf eine Benutzereingabe, die in eine Zahl umgewandelt und in der Variablen choice gespeichert wird. Um die Benutzereingabe zu verarbeiten, verzweigen wir über dem Wert von choice mit einer sogenannten case-Anweisung. Diese ist nützlich um tief verschachtelte if −then−else-Anweisungen zu vermeiden, da sie von sich aus mehr als zwei Alternativen erlaubt. Wir fügen der main-Prozedur also die folgende Answeisung hinzu. case choice when 1 print_lecturers when 2 create_lecturer when 3 delete_lecturer end Die drei verwendeten Prozeduren entsprechen den auswählbaren Aktionen. Wir werden sie im Controller implementieren, den wir entsprechend im Hauptprogramm importieren. Bevor wir die einzelnen Funktionen in Ruby implementieren, modellieren wir sie grafisch mit den bereits erwähnten Sequenzdiagrammen (Tafelbild). Die Methode print_lecturers greift zunächst auf das Modell zu, um mit der Klassen-Methode Lecturer . all ein Array aller gespeicherten Dozenten abzufragen. Danach werden mit Hilfe der (im View noch zu definierenden) Methode lecturer_view die abgefragten Dozenten in eine Zeichenkette umgewandelt und nacheinander angezeigt. Die Ruby-Implementierung dieser Methode ist wie folgt. def p r i n t _ l e c t u r e r s lecturers = Lecturer . a ll 33 3 Softwaretechnik for i in 1 . . l e c t u r e r s . size p u t s l e c t u r e r _ v i e w ( l e c t u r e r s [ i − 1]) end end Da der Controller hier sowohl auf das Model als auch auf den View zugreift, müssen wir beide importieren. Zur Implementierung der beiden anderen Methoden, brauchen wir nicht auf den View zuzugreifen, da dabei Daten nur manipuliert und nicht angezeigt werden. Die Methode create_lecturer fragt zunächst den Vor- und den Nachnamen des neu zu erstellenden Dozenten ab und ruft dann die Methode add_lecturer aus dem Modell auf. def c r e a t e _ l e c t u r e r p r i n t " f i r s t name : " f i r s t n a m e = g e t s . chop p r i n t " l a s t name : " l a s t n a m e = g e t s . chop add_lecturer ( firstname , lastname ) end Zum Löschen eines Dozenten fragen wir dessen id vom Benutzer ab. Diese können wir an die Methode Lecturer . find übergeben, um ein Objekt des zu löschenden Dozenten zu erhalten. Auf diesem rufen wir dann die Methode destroy auf, um es aus der Datenbank zu entfernen. def d e l e t e _ l e c t u r e r p r i n t " l e c t u r e r id : " lecturer_id = gets . to_i Lecturer . find ( lecturer_id ) . destroy end Der Einfachheit halber verzichten wir auf die Behandlung jeglicher Eingabefehler. Eigentlich müssten wir hier testen, ob die eingegebene id zu einem existierenden Dozenten gehört. Schließlich definieren wir noch die Datei lecture −view.rb mit der Funktionen lecturer_view zur Umwandlung eines Dozenten in eine Zeichenkette. def l e c t u r e r _ v i e w ( l e c t u r e r ) return ( l e c t u r e r . id . to_s + " : " + lecturer . firstname + " " + l e c t u r e r . lastname ) end Da Benutzer zum Löschen von Dozenten deren id angeben müssen, bietet es sich an, diese bei der Anzeige mit auszugeben. Ein Beispiellauf unseres Programms könnte nun wie folgt aussehen. # ruby l e c t u r e −manager . r b 1: P r i n t l e c t u r e r s 2: Create l e c t u r e r 3: Delete L e c t u r e r Choose a c t i o n ( 1 . . 3 ) 34 Tests 1 1 : F r a n k Huch 2: Sebastian Fischer Tests Man unterscheidet verschiedene Arten zu testen je nach Abstraktionsebene. Systemtests testen mögliche Anwendungsfälle des kompletten Programms. Integrationstests testen, wie sich unterschiedliche Komponenten eines Systems zueinander verhalten, und orientieren sich dabei an der Schnittstelle der Komponenten. Sogenannte Unittests testen das Verhalten isolierter Komponenten. In der Regel werden zu jeder Klasse zugehörige Unittests definiert, die überprüfen, ob die bereitgestellten Methoden die Daten korrekt manipulieren. Die Tests sind dabei selbst Programme, die automatisiert ausgeführt werden können. Wenn alle Anforderungen durch Tests ausgedrückt sind, lässt sich nach einem Refactoring überprüfen, ob das umgearbeitete Programm die Anforderungen noch erfüllt. Implementiert man die Tests vor der Implementierung der getesteten Funktionalität spricht man von Test-getriebener Software-Entwicklung. In Ruby können wir Unittests mit der Standardbibliothek test/unit durchführen. Dazu definieren wir eine Unterklasse der Klasse Test :: Unit :: TestCase. Alle Methoden einer solchen Unterklasse, deren Name mit test_ beginnt, werden bei der Ausführung des Programms automatisch ausgeführt und eine Zusammenfassung der abgelaufenen Tests wird angezeigt. Innerhalb der Test-Methoden können wir die geerbte Methode assert verwenden. Diese erwartet einen Bool’schen Wert und nur wenn dieser true ist, gilt der Test als bestanden. Hier ist ein Beispiel für einen Unittest. require ’ t e s t / unit ’ r e q u i r e ’ l e c t u r e s −model ’ c l a s s L e c t u r e s T e s t < T e s t : : Unit : : TestCase def t e s t _ l e c t u r e r _ c r e a t i o n a d d _ l e c t u r e r ( " K a i " , " Wollweber " ) exists = false assert exists end end Dieser Test schlägt fehl, da assert mit dem Wert false aufgerufen wird. Entsprechend liefert dieses Programm die folgende Ausgabe. # ruby l e c t u r e s − t e s t . r b Loaded s u i t e l e c t u r e s − t e s t Started F F i n i s h e d i n 0.077256 seconds . 35 3 Softwaretechnik 1) Failure : t e s t _ l e c t u r e r _ c r e a t i o n ( L e c t u r e s T e s t ) [ l e c t u r e s −t e s t . rb : 8 ] : <f a l s e > i s n o t t r u e . 1 tests , 1 assertions , 1 failures , 0 errors Um zu testen, ob ein eingefügter Dozent nach dem Einfügen existiert, fügen wir die folgenden Anweisungen zwischen exists = false und assert exists ein. lecturers = Lecturer . a ll for i in 1 . . l e c t u r e r s . size l e c t u r e r = l e c t u r e r s [ i − 1] found = l e c t u r e r . f i r s t n a m e == " K a i " && l e c t u r e r . l a s t n a m e == " Wollweber " e x i s t s = e x i s t s || found i f found then lecturer . destroy end end Wir fragen ein Array aller Dozenten ab und überprüfen in einer Schleife, ob ein Dozent mit dem angegebeben Namen existiert. Um den eingefügten Dozenten wieder zu löschen, rufen wir die destroy-Methode auf, wenn wir einen Dozenten mit dem selben Namen gefunden haben. Dieser Test ist nun erfolgreich und die Ausgabe des Ruby-Programms wie folgt. # ruby l e c t u r e s − t e s t . r b Loaded s u i t e l e c t u r e s − t e s t Started . F i n i s h e d i n 0.142921 seconds . 1 tests , 1 assertions , 0 failures , 0 errors 36 4 Web-Anwendungen mit Ruby on Rails In Ruby können dynamische Web-Anwendungen mit dem Rails Framework programmiert werden. Wir werden in diesem Kapitel die Vorlesungsverwaltung aus dem vorherigen Kapitel als Web-Anwendung programmieren. Wie bei unserem textuellen Programm folgen wir dabei dem Model-View-Controller pattern. Unsere Anwendung greift auf die Datenbank zu, die wir im Kapitel zur Datenbankprogrammierung mit Ruby erstellt haben. Sie fügt dieser eine grafische Oberfläche zur Anzeige, zum Erstellen und zum Löschen von Dozenten hinzu. Entsprechende Funktionalität für Kurse soll als Übung programmiert werden. Das Rails Framework stellt umfangreiche Werkzeuge zur automatisierten Erstellung von WebAnwendungen bereit. Wir werden eine minimalistische Anwendung erstellen und dabei weitestgehend auf Rails-Automatismen verzichten. Basierend auf einem initial erstellten Anwendungsgerüst, werden wir alle weiteren Programmfragmente von Hand schreiben, auch dann wenn Rails diese automatisch generieren könnte. Nach dem Aufruf von # r a i l s LecturesManager in der Kommandozeile, erzeugt das Programm rails einen Ordner LecturesManager mit dem folgenden Inhalt. # ls app c o n f i g db doc l i b l o g p u b l i c R a k e f i l e README s c r i p t t e s t tmp vendor Für uns sind im Folgenden vor Allem die Ordner app, config und db interessant. Der Ordner script enthält Hilfsprogramme zur Entwicklung. Zum Beispiel können wir mit # script / server einen lokalen Webserver auf Port 3000 starten, in dem unsere Anwendung läuft. Das Verzeichnis app wird später den Ruby-Quelltext unserer Anwendung enthalten. Es hat die folgende Struktur, an der wir erkennen, dass Rails-Anwendungen dem Model-View-Controller pattern folgen. # l s app controllers helpers models views 37 4 Web-Anwendungen mit Ruby on Rails Datenbanken in Rails Da die activerecord-Bibliothek, die wir zur Datenbankprogrammierung in Ruby verwendet haben, Teil des Rails Frameworks ist, können wir unsere Datenbank fast ohne Änderungen übernehmen. Die Migration zur Erstellung der Vorlesungsdatenbank speichern wir in der Datei db/migrate/001 _create_all_tables .rb mit folgendem Inhalt. c l a s s CreateAllTables < ActiveRecord : : Migration d e f s e l f . up c r e a t e _ t a b l e : l e c t u r e r s do | t | t . s t r i n g : lastname t . string : firstname end create_table ( : courses ) { | t | t . s t r i n g : t i t l e } c r e a t e _ t a b l e : a s s o c s do | t | t . integer : lecturer_id t . integer : course_id end end d e f s e l f . down drop_table : l e c t u r e r s drop_table : courses drop_table : assocs end end Wir können nun die Datenbank mit dem Kommando rake db:migrate erzeugen. Statt die Modellklassen alle in einer Datei zu definieren, legen wir jedes Modell in einer eigenen Datei im Ordner app/models ab. Wir definieren Modelle für Dozenten, Vorlesungen sowie für deren Verknüpfung. # app / models / l e c t u r e r . r b c l a s s L e c t u r e r < A c t i v e R e c o r d : : Base has_many : a s s o c s has_many : c o u r s e s , : t h r o u g h => : a s s o c s end def i n s e r t _ l e c t u r e r ( f i rs t n am e , lastname ) l e c t u r e r = L e c t u r e r . new : f i r s t n a m e => f i r s t n a m e , : l a s t n a m e => l a s t n a m e l e c t u r e r . save return l e c t u r e r 38 Routen, Controller und Views end Wir sehen hier, dass wir die Attribute einer Modellinstanz auch als Hash-Tabelle im Konstruktor übergeben können, statt sie einzeln über Zugriffsmethoden zuzuweisen. # app / models / c o u r s e . r b c l a s s Course < A c t i v e R e c o r d : : Base has_many : a s s o c s has_many : l e c t u r e r s , : t h r o u g h => : a s s o c s end def i n s e r t _ c o u r s e ( t i t l e ) c o u r s e = Course . new : t i t l e => t i t l e course . save return course end Dozenten stehen mit Vorlesungen in einer N-zu-M-Beziehung, die durch ein weiteres Modell definiert wird. # app / models / a s s o c . r b c l a s s Assoc < A c t i v e R e c o r d : : Base belongs_to : l e c t u r e r belongs_to : course end def a s s i g n _ c o u r s e ( l e c t u r e r , course ) l e c t u r e r . courses = l e c t u r e r . courses + [ course ] end Die Definition der Hilfsfunktionen insert_lecturer , insert_course und assign_course ist eigentlich nicht nötig. Sie erleichtert uns aber die Eingabe von Beispieldatensätzen in der Rails Konsole. Durch den Aufruf von script /console können wir eine Konsole starten, in der wir auf unsere Anwendung zugreifen können. Diese Konsole können wir verwenden, um Beispieldaten in unsere Datenbank einzutragen. Routen, Controller und Views Um mit unserer Anwendung eine Liste von Dozenten anzeigen zu können, müssen wir dafür eine URL festlegen und diese einem Controller zuweisen. Dies geschieht über sogenannte Routen, die URLs auf Methoden (sogenannte Aktionen) von Controllern abbilden. Die Datei config / routes .rb definiert die Routen einer Rails-Anwendung. 39 4 Web-Anwendungen mit Ruby on Rails Dozenten anzeigen Die folgende Route assoziiert die URL / lecturers mit der index-Aktion eines LecturersController s. # c o n f i g / r o u t e s . rb A c t i o n C o n t r o l l e r : : R o u t i n g : : R o u t e s . draw do |map| map . l e c t u r e r s " / l e c t u r e r s " , : c o n d i t i o n s => { : method => : g e t } , : c o n t r o l l e r => : l e c t u r e r s , : a c t i o n => : i n d e x end Die Klassenmethode draw bekommt hier einen Block übergeben, dessen Parameter map dazu verwendet wird, die Routen zu definieren. Eine HTTP-GET Anfrage an die URL / lecturers wird hierdurch an die Methode index des LecturersController s weitergeleitet. Diesen müssen wir in einer entsprechenden Datei definieren. # app / c o n t r o l l e r s / l e c t u r e r s _ c o n t r o l l e r . r b class LecturersController < ApplicationController def index @lecturers = Lecturer . a l l end end Der LecturersController erbt von der vom Rails Framework bereitgestellten Klasse ApplicationController . Die index-Methode greift auf das Modell zu um ein Array aller Dozenten aus der Datenbank zu lesen. Es ist hierzu nicht notwendig, die Modellklassen zu importieren. Das wird von Rails automatisch erledigt. Bei unserer textbasierten Anwendung haben wir im Controller Funktionen des Views zur Anzeige von Dozenten aufgerufen. Dies passiert in Rails implizit, wenn die Umwandlung in entsprechenden Dateien im Verzeichnis app/views definiert ist. Die definierten Views haben dazu Zugriff auf alle im Controller definierten Instanzvariablen, hier also auf die Variable @lecturers. Views werden in Rails üblicherweise als HTML-Dateien mit eingebettem Ruby-Code definiert. Sie setzen sich zusammen aus einem Gerüst (Layout genannt) und dem eigentlichen Inhalt der anzuzeigenden Webseite. HTML-Dateien mit eingebettetem Ruby-Code werden in Dateien mit der Endung .html.erb gespeichert. Zur Anzeige der Dozenten legen wir die folgenden Dateien an. # app / views / l a y o u t s / l e c t u r e r s . html . e r b <html> <head> <t i t l e >L e c t u r e r s </ t i t l e > </head> <body> <%= y i e l d %> </body> </html> 40 Routen, Controller und Views Diese Datei definiert ein minimales Gerüst für eine HTML-Datei zur Anzeige von Dozenten. Sie besteht im Wesentlichen aus HTML-Code, bis auf den Inhalt des <body>-Tags. Hier wird der Ruby-Code yield aufgerufen, der den Inhalt der Seite in das Gerüst einfügt. Diesen Inhalt definieren wir in einem View, dessen Name der Aktion des Controllers entspricht, zu der er gehört. # app / views / l e c t u r e r s / i n d e x . html . e r b <h1>L e c t u r e r s </h1> <ul> <% @ l e c t u r e r s . each do | l e c t u r e r | %> <l i><%= h l e c t u r e r . f i r s t n a m e %><%= h l e c t u r e r . l a s t n a m e %></l i > <% end %> </ul> Dieser View erzeugt eine ungeordnete Liste (ul) von Dozenten, aus der vom Controller definierten Variablen @lecturers. Die Hilfsfunktion h wird in Rails verwendet, um anzuzeigende Nutzerdaten zu säubern, für den Fall, dass diese schadhaften Code enthalten. Wenn wir nun im Browser die Adresse http ::// localhost :3000/ lecturers eingeben, bekommen wir eine HTML-Seite mit den zuvor eingegebeben Dozenten angezeigt. Neue Dozenten anlegen Um neue Dozenten anzulegen, definieren wir zunächst wieder eine entsprechende Route. # c o n f i g / r o u t e s . rb # ... map . n e w _ l e c t u r e r " / l e c t u r e r s / new" , : c o n d i t i o n s => { : method => : g e t } , : c o n t r o l l e r => : l e c t u r e r s , : a c t i o n => : new # ... Wir können der Dozenten-Anzeigeseite einen Link zu dem Pfad dieser Route hinzufügen. # app / views / l e c t u r e r s / i n d e x . html . e r b # ... <%= l i n k _ t o "New l e c t u r e r " , n e w _ l e c t u r e r _ p a t h %> Die Methode link_to wird von Rails bereitgestellt und new_lecturer_path wird durch die Definition der obigen Route erzeugt. Diese legt fest, dass GET-Anfragen an die URL / lecturers /new von der Methode new des LecturersController behandelt werden sollen, die wir wie folgt definieren. 41 4 Web-Anwendungen mit Ruby on Rails # app / c o n t r o l l e r s / l e c t u r e r s _ c o n t r o l l e r . r b # ... d e f new @ l e c t u r e r = L e c t u r e r . new end # ... Die Methode new weist der Variablen @lecturer einen neu erzeugten Datensatz für Dozenten zu, der von einem entsprechenden View verwendet werden kann. # app / views / l e c t u r e r s / new . html . e r b <h1>New l e c t u r e r </h1> <% f o r m _ f o r @ l e c t u r e r do | f | %> <%= f . l a b e l : f i r s t n a m e %>: <%= f . t e x t _ f i e l d : f i r s t n a m e %><b r/> <%= f . l a b e l : l a s t n a m e %>: <%= f . t e x t _ f i e l d : l a s t n a m e %><b r/> <%= f . s u b m i t " C r e a t e " %> <% end %> Dieser View definiert mit der Rails-Funktion form_for ein HTML-Formular zur Eingabe der Attribute eines Dozenten. Beim Abschicken des Formulars wird automatisch eine POSTAnfrage an die URL / lecturers geschickt. Bei POST-Anfragen werden Daten an den Server übermittelt - in diesem Fall die Werte, die in das erzeugte Formular eingetragen wurden. Da wir bisher nur GET-Anfragen beantworten, brauchen wir eine neue Route. # c o n f i g / r o u t e s . rb # ... map . c r e a t e _ l e c t u r e r " / l e c t u r e r s " , : c o n d i t i o n s => { : method => : p o s t } , : c o n t r o l l e r => : l e c t u r e r s , : a c t i o n => : c r e a t e # ... Die URL ist die selbe wie zur Anzeige aller Dozenten, allerdings werden jetzt POST-, nicht GET-, Anfragen beantwortet. Dazu definieren wir im Controller die Methode create wie folgt. # app / c o n t r o l l e r s / l e c t u r e r s _ c o n t r o l l e r . r b # ... def c r e a t e @ l e c t u r e r = L e c t u r e r . new params [ : l e c t u r e r ] @lecturer . save redirect_to lecturers_path end 42 Routen, Controller und Views # ... Sie erzeugt zunächst einen neuen Dozenten-Datensatz aus den Formulareingaben. Diese stellt Rails in der Hash-Tabelle params zur Verfügung. Der Wert des Eintrags unter dem Schlüssel : lecturer ist dabei selbst eine Hash-Tabelle, die wir an den Konstruktor für Dozenten weitergeben. Die create-Aktion nutzt keinen automatisch zugeordneten View create .html.erb sondern implementiert explizit eine Weiterleitung zur Anzeige aller Dozenten. Der Pfad dorthin wird durch die Definition der entsprechenden Route in der Variablen lecturers_path gespeichert. Nach dem Abschicken des Formulars zur Eingabe eines neuen Dozenten wird also wieder die Seite zur Anzeige aller aufgerufen. Dozenten löschen Schließlich definieren wir noch eine Funktion zum Löschen existierender Dozenten. Dazu fügen wir der Anzeige von Dozenten entsprechende Links hinzu. # app / view / l e c t u r e r s / i n d e x . html . e r b # ... <ul> <% @ l e c t u r e r s . each do | l e c t u r e r | %> <l i><%= h l e c t u r e r . f i r s t n a m e %><%= h l e c t u r e r . l a s t n a m e %> [<%= l i n k _ t o " d e l e t e " , d e l e t e _ l e c t u r e r _ p a t h ( l e c t u r e r ) , : method => : d e l e t e %>]</ l i > <% end %> </ul> # ... Die Funktion delete_lecturer_path wird durch die im Folgenden definierte Route erzeugt. Der Parameter :method => :delete für die Funktion link_to sorgt dafür, dass durch Klick auf den Link eine HTTP-DELETE-Anfrage statt einer GET-Anfrage gestellt wird, für die wir nun eine Route definieren. # c o n f i g / r o u t e s . rb # ... map . d e l e t e _ l e c t u r e r " / l e c t u r e r s / : i d " , : c o n d i t i o n s => { : method => : d e l e t e } , : c o n t r o l l e r => : l e c t u r e r s , : a c t i o n => : d e s t r o y # ... Die URL dieser Route endet mit dem Symbol : id welches für die ID des zu löschenden Datensatzes steht. Diese ID kann im Controller aus der params-Tabelle abgefragt werden. 43 4 Web-Anwendungen mit Ruby on Rails # app / c o n t r o l l e r s / l e c t u r e s _ c o n t r o l l e r . r b # ... def d e s t r o y @ l e c t u r e r = L e c t u r e r . f i n d ( params [ : i d ] ) @lecturer . destroy redirect_to lecturers_path end # ... Diese Aktion sucht den zu löschenden Dozenten aus der Datenbank, löscht ihn und leitet dann zur Anzeige aller Dozenten um. Wir können nun über unsere Anwendung Dozenten anzeigen, erzeugen und löschen. Wir haben dazu (Rails-untypisch) Routen, Views und Controller von Hand definiert statt bereitgestellte Automatismen zu nutzen. Zum Beispiel könnten wir unsere ganze routes .rb-Datei durch die folgende erssetzen: A c t i o n C o n t r o l l e r : : R o u t i n g : : R o u t e s . draw do |map| map . r e f e r e n c e s : l e c t u r e r s , : e x c e p t => [ : show , : e d i t , : update ] end Diese Definition erzeugt unsere Routen. Ohne den :except-Parameter würden zusätzlich Routen zum Anzeigen, Editieren und Verändern einzelner Dozenten erzeugt. Mit dem Aufruf s c r i p t / g e n e r a t e c o n t r o l l e r L e c t u r e r s i n d e x new hätten wir uns Gerüste für die erstellten Controller und Views erzeugen lassen können. Es gibt weitere Generatoren für Modelle (script/generate model) und sogar zum Erstellen einer Standardimplementierung zum Zugriff auf eine neu erstellte Ressource (script/generate scaffold). Dieses Kapitel verwendet die uns zur Verfügung stehende Rails Version 2.3. Bei neueren Versionen müssen Kommandos und Implementierungen geringfügig angepasst werden. Die Tutorials auf guides.rubyonrails.org geben Aufschluss darüber. 44