Informatik für Lehrerinnen

Werbung
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
Herunterladen