Persistenz
Ansatz der Speicheranbindung von Yesod
In der heutigen Zeit kommen selbst kleinere Webanwendungen kaum noch ohne die Anbindung an ein System aus der Speicherebene aus. Schon für einfache Lösungen des Content Managements ist eine Speicheranbindung wie beispielsweise die Verwendung von Datenbanken eine Pflichtaufgabe bei der Entwicklung der Applikation. So ist es wenig überraschend, dass auch das Yesod-Framework mit einer Standardlösung für das Problem der persistenten Speicherung entsprechender Daten aufwartet. Persistent nennt sich diese und bietet durch Abstraktion vom schließlich eingesetzten Datenbankbackend eine unabhängige und flexible Implementierung der Speicheranbindung.
Ähnlich zu dem Bestreben, Datentypen aus der objektorientierten Programmierung in eine Repräsentation des relationalen Schemas, welches bei den Mainstream-Datenbankverwaltungssystemen eingesetzt wird, umzuwandeln, hat es auch in der Haskell-Welt bereits zahlreiche Versuche gegeben, Haskell-Werte dergestalt in Datenbanken zu sichern, dass diese auch nach dem Ladevorgang on-the-fly weiterverwendet werden können. Dies mag für einige Anwendungsfälle eine elegante Lösung bedeuten, ist für die überwiegende Mehrheit der in der Praxis relevanten Aufgabenstellungen jedoch ohne Wert, da z. B. oftmals bereits ein Datenbankmodell vorhanden ist, welches nicht zu verändern ist und an das sich die Webanwendung anzupassen hat. Die Aufgabe von Persistent besteht darin, gleichermaßen typsicher und effizient Speicherfunktionalitäten bereitzustellen, ohne dabei an eine bestimmte technische Umsetzung der eigentlichen Datenbank gebunden zu sein.[1]
Im Yesod Web wird ein schönes Einführungsbeispiel veranschaulicht, welches den entscheidenden Vorteil von Persistent gegenüber vielen anderen Standardlösungen auf den Punkt bringt. Angenommen, es sollen Personendaten in einer Datenbank abgespeichert werden. Im Kontext einer Datenbankabfragesprache würde der Befehl zum Anlegen dieser Datenbanktabelle beispielsweise wie folgt aussehen.
1CREATE TABLE Person (
2 id PRIMARY KEY,
3 name VARCHAR NOT NULL,
4 age INTEGER
5)
Eine daran angelehnte Haskell-Datentypdefinition würde dann in etwa der folgenden entsprechen.
1data Person = Person
2 { personName :: String
3 , personAge :: Int
4 }
Auf den ersten Blick macht es den Eindruck, als wenn an dieser Stelle nichts schief gehen könnte. Doch was passiert, wenn das Datenbankverwaltungssystem ein ungetyptes Resultat liefert? Und was ist, wenn die Datenbank, an welche eine auszuwertende Anfrage gesendet wurde, erst zur Laufzeit einen entsprechenden Syntaxfehler aufzeigt? In der Praxis wird man bei dynamischen Programmiersprachen wie PHP oder Ruby nicht um Unit-Tests herumkommen, welche für jedwede Anfrage ihre Korrektheit verifizieren. Bei Yesod ist dies meist nicht notwendig, da die starke Typisierung von Haskell in fast jedem Fall sicherstellt, dass nur wohldefinierte und die vom Nutzer gewünschten Anfragen typsicher ausgeführt werden. Es wird sich zudem zeigen, dass mögliche Fehler bereits bei der Kompilierung aufgedeckt werden.[2]
Technischer Hintergrund
Für die technische Umsetzung der Anbindung der Webanwendung an die Speicherschicht führt Yesod einen Datentypen zur Kennzeichnung eines Wertes ein, welcher für die Interaktion von Daten mit der Datenbank zum Einsatz kommt.
1data PersistValue = PersistText Text
2 | PersistByteString ByteString
3 | PersistInt64 Int64
4 | PersistDouble Double
5 | PersistBool Bool
6 | PersistDay Day
7 | PersistTimeOfDay TimeOfDay
8 | PersistUTCTime UTCTime
9 | PersistNull
10 | PersistList [PersistValue]
11 | PersistMap [(T.Text, PersistValue)]
12 | PersistForeignKey ByteString
Mit diesen Datentypen kommt der Entwickler in der Regel nicht in Berührung. Sie dienen der Realisierung verschiedener Datenbankspezialisierungen. In diesem Zusammenhang müssen die Datentypen dergestalt umgeformt werden, dass sie einen Datenaustausch mit der entsprechenden Datenbankvariante ermöglichen.[3]
Einfache praktische Anwendungen
Wie auch schon bei der Definition von Routen oder der Anwendung von Templates wie Hamlet, Cassius und Julius, ermöglichen Quasi-Quotations im Kontext der Definition von persistent zu speichernden Entitäten die automatische Code-Generierung bei gleichzeitig bequemer und lesbarer Konfiguration durch den Entwickler. So lässt sich beispielsweise durch den folgenden Codeausschnitt das zuvor gezeigte Einführungsbeispiel in der Datenbank etablieren.
1mkPersist [persist|
2Person
3 name String
4 age Int
5|]
Die folgende Befehlskette ermöglicht eine beispielhafte Interaktion mit dem soeben definierten Datenbankmodell.
1main = withSqliteConn ":memory:" $ runSqlConn $ do
2 personId <- insert $ Person "Max Mustermann" 32
3 person <- get personId
4 liftIO $ print person
Dieser Code ist so anschaulich, dass er sich nahezu von selbst erklärt. Der
do
-Block zwischen den Zeilen 2 und 4 enthält die eigentliche Interaktion
mit der Datenbank. Wie in Zeile 1 beschrieben, erfolgt in diesem Fall der Austausch mit
einer SQLite-Datenbank. Dabei handelt es sich um eine Single-Connection, welche die
Verbindung mit der Datenbank nur für die hier aufgeführten Operationen aufrecht erhält.
In der Praxis wird man eine Umsetzung präferieren, welche in der Lage ist,
unterschiedliche Verbindungen zu verwalten und diese je nach Anwendungsfall auch länger
aufrecht zu erhalten als dies hier geschieht. In Zeile 2 erfolgt zunächst das Anlegen einer
32jährigen Person mit dem Namen «Max Mustermann». Die Funktion insert
erhält
dazu einen Wert vom Datentyp Person
und liefert die ID des angelegten
Tabelleneintrags. Diese ID ist typsicher und auf die Entität Person
bezogen.
Dies stellt beispielsweise sicher, dass die ID nicht für eine andere Datenbanktabelle
missbraucht werden kann. Die Funktion get
empfängt diese ID und liefert
den angeforderten Datensatz in Form des Haskell-Datentyps
Person
.[4]
Dieses Beispiel allein wird nicht funktionieren. Dies liegt daran, dass die
Datenbanktabelle Person
noch nicht existiert. Persistent trifft diese
Vorkehrungen erst auf Anweisung. Fügt man dem obigen Beispiel zwischen den Zeilen 1 und 2
die Zeile runMigration $ migrate (undefined :: Person)
bei, so wird die
fehlende Tabelle entsprechend der Entitätskonfigurationen angelegt. Dies funktioniert
ebenso automatisch für Änderungen am Datenbankschema. Über den
Migration
-Mechanismus versucht Persistent, die gewünschte Änderung am
gedanklichen Datenbankschema durch ALTER TABLE
-Anweisungen und notwendige
Datenkopien auch auf technischer Ebene im relationalen Modell umzusetzen. Umbenennungen
von Entitätsattributen sind von diesem Prozess jedoch leider ausgeschlossen. Ebenso ist
das Löschen von Attributen aufgrund von Datenverlust sicherheitshalber standardmäßig deaktiviert,
kann über die Funktion runMigrationUnsafe
jedoch erzwungen werden. Eine
hilfreiche Funktion in diesem Zusammenhang ist printMigration
, welche den
SQL-Code zurückgibt, welchen Persistent
für die Schemaänderung durchführen würde. Dieser kann dann den eigenen Vorstellungen
entsprechend angepasst und ausgeführt werden, um auch Änderungen durchzuführen, die allein
mit Persistent nicht ohne weiteres möglich
wären.[5]
Attribute
Persistent ermöglicht Entwicklern die Angabe zusätzlicher beschreibender Attribute
für jedes Feld einer Entität. Beispielsweise kann jedem Entitätsfeld ein
Maybe
-Attribut beigefügt werden, um das Feld als optional zu kennzeichnen.
Dies wird auf Datenbankebene über den Wert NULL
realisiert. Über das
Attribut default
kann analog zur bekannten Konvention aus den
Datenbanksprachen zudem ein Standardwert angegeben
werden.[6]
Auch assoziierte Typen können im Rahmen der Beschreibung von persistenten Daten definiert werden. Dabei stehen die folgenden Attribute zur Verfügung.
- Eq
=
- Ne
<>
- Lt
<
- Le
<=
- Gt
>
- Ge
>=
- In
IN
In diesem Zusammenhang werden die obigen Vergleichsoperationen als für den Interaktionsprozess wohldefiniert beschrieben. In der Folge generiert Persistent automatisch Datenkonstruktoren für einen Filter der Entität. Eine gegebene Beschreibung
1mkPersist [persist|
2Person
3 name String Eq
4 age Int Lt
5|]
würde die Datentypdefinition
data Filter Person = PersonNameEq String | PersonAgeLt Int
einführen, so dass Personen durch den beispielhaften Aufruf
1main = withSqliteConn ":memory:" $ runSqlConn $ do
2 runMigration migrateAll
3 persons <- selectList
4 [ PersonNameEq "Tom"
5 , PersonAgeLt 18
6 ]
7 [] 0 0
8 liftIO $ print persons
ausgewählt werden würden, welche jünger als 18 sind und Tom heißen. Dies wird durch eine Liste von Filtern gewährleistet, deren Elemente nacheinander angewandt werden. Auf diese Weise kann auch bei Vergleichen im Rahmen einer Datenbankabfrage Typsicherheit erlangt werden, welche von vielen Datenbanken auf nativem Weg nicht garantiert würde.[7]
Anzumerken ist jedoch, dass bei Persistent nicht jede Art der Filterung
von Tabelleneinträgen typsicher unterstützt wird. Möchten Entwickler eine Menge
von Einträgen beispielsweise mit dem in SQL
verfügbaren LIKE
-Mechanismus einschränken, so ist dies nur über einen
alternativen Ansatz zum Senden von Anfragen möglich, bei dem zu sendener roher
SQL-Code anzugeben ist. In diesem
Fall muss auf die Typsicherheit verzichtet
werden.[8]
Relationen
Ein unverzichtbarer Bestandteil eines Webframeworks ist die Unterstützung von Beziehungen zwischen Entitäten. Selbstverständlich können auch bei Yesod anhand der Persistent-Erweiterung diese Verknüpfungen von Daten beschrieben und verwaltet werden. Das folgende Beispiel richtet eine Many-To-Many-Relation zwischen Personen und Hobbys ein. Eine Person kann also mehrere Hobbys haben, welche wiederum auch von anderen Personen gepflegt werden können.
1mkPersist [persist|
2Person
3 name String
4Hobby
5 name String
6PersonHobby
7 person PersonId
8 hobby HobbyId
9 UniquePersonHobby person hobby
10|]
Dabei fallen zunächst die Typen PersonId
und HobbyId
auf, welche nichts anderes als Alias-Typen für die entsprechenden Fremdschlüssel
auf die referenzierte Entität darstellen. Im Falle des Verweises auf eine Person
hat der Schlüssel z. B. den Typen
type PersonId = Key Person
, was den angesprochenen Mechanismus
hervorhebt, dass Schlüssel unmittelbar mit der zugehörigen Datenbanktabelle
verbunden sind. Der Ausdruck UniquePersonHobby person hobby
führt schließlich einen UNIQUE
-Key ein, welcher sicherstellt, dass
das eine Hobby-Person-Beziehung nicht mehrfach gespeichert werden
kann.[9]
- [1] Michael Snoyman, Yesod Web Framework Book: Persistent, Introduction http://www.yesodweb.com/show/topic/188
- [2] Michael Snoyman, Yesod Web Framework Book: Persistent, Solving the boundary issue http://www.yesodweb.com/show/topic/119
- [3] Michael Snoyman, Yesod Web Framework Book: Persistent, Types http://www.yesodweb.com/show/topic/123
- [4] Michael Snoyman, Yesod Web Framework Book: Persistent, Code Generation http://www.yesodweb.com/show/topic/122
- [5] Michael Snoyman, Yesod Web Framework Book: Persistent, Migrations http://www.yesodweb.com/show/topic/125
- [6] Michael Snoyman, Yesod Web Framework Book: Persistent, Attributes http://www.yesodweb.com/show/topic/126
- [7] Michael Snoyman, Yesod Web Framework Book: Persistent, Associated Types http://www.yesodweb.com/show/topic/127
- [8] Michael Snoyman, Yesod Web Framework Book: Persistent, Raw SQL http://www.yesodweb.com/show/topic/352
- [9] Michael Snoyman, Yesod Web Framework Book: Persistent, Relations http://www.yesodweb.com/show/topic/120