In der Datenbank des E-Learning-Systems autotool
steht, welcher Student welche Aufgabe wann gelöst hat, sowie weitere Verwaltungsinformationen, z.B. für jede Aufgabe der Bearbeitungszeitraum und zu welcher Vorlesung sie gehört.
Für die typsichere Verarbeitung benutzen wir
Das ist alles hier sehr schön erklärt. Ich zeige im Folgenden, wie das im autotool angewendet wird.
Das DB-Schema wurde ursprünglich (ca. 2000) direkt in SQL beschrieben. Ein Beispiel für eine Tabellen-Deklaration ist:
CREATE TABLE `aufgabe` (
`ANr` int(11) NOT NULL AUTO_INCREMENT,
`Name` text CHARACTER SET utf8 NOT NULL,
`VNr` bigint(20) NOT NULL,
`Highscore` text CHARACTER SET utf8 NOT NULL,
`Von` datetime NOT NULL,
`Bis` datetime NOT NULL,
`Config` text CHARACTER SET utf8 NOT NULL,
`Remark` text,
`Typ` text CHARACTER SET utf8 NOT NULL,
`Status` text CHARACTER SET utf8 NOT NULL,
`server` text CHARACTER SET utf8 NOT NULL,
`signature` text CHARACTER SET utf8 NOT NULL,
PRIMARY KEY (`ANr`),
KEY `aufgabe_VNr_fkey` (`VNr`)
) ENGINE=MyISAM AUTO_INCREMENT=2592 DEFAULT CHARSET=latin1 COMMENT='aufgabe';
Die persistent-Verbindung wurde später hinzugefügt. Das DB-Schema wurde dabei nicht geändert.
Die Datei config/models enthält u.a. diese (von Hand geschriebenen) Deklarationen:
Aufgabe sql=aufgabe
Id sql=ANr
name Text sql=Name
vorlesungId VorlesungId sql=VNr
highscore HiLo sql=Highscore
von UTCTime sql=Von
bis UTCTime sql=Bis
konfiguration AufgabeKonfiguration sql=Config
hinweis Text Maybe sql=Remark
typ AufgabeTyp sql=Typ
status Status sql=Status
server ServerUrl sql=server
signatur Text sql=signature
Daraus werden Typdeklarationen abgeleitet,
data Aufgabe
= Aufgabe {aufgabeName :: !Text,
aufgabeVorlesungId :: !Key Vorlesung,
aufgabeHighscore :: !Control.Types.HiLo,
aufgabeVon :: !UTCTime,
... }
data instance EntityField Aufgabe typ where
...
AufgabeVorlesungId
:: typ ~ Key Vorlesung => EntityField Aufgabe typ
AufgabeHighscore
:: typ ~ Control.Types.HiLo => EntityField Aufgabe typ
AufgabeVon
:: typ ~ UTCTime => EntityField Aufgabe typ
die die Beziehungen zwischen Tabellen und ihren Spalten beschreiben. Anhand dieser Beschreibung werden Queries statisch typisiert und damit fehlerhafte Queries bereits zur Übersetzungszeit erkannt.
Diese Deklarationen werden während der Kompilation durch den Aufruf von persistFileWith
im Modul Models erzeugt. Den dabei entstehenden Quelltext sieht man also nicht, aber die Wirkung kann man in ghci so betrachten:
cd yesod
stack repl autotool-yesod:lib
:i Aufgabe
Für neue hinzugefügte Tabellen genügt es, eine Deklaration in config/models
anzugeben. Beim nächsten Systemstart wird die Tabelle in der DB erzeugt.
Das bedeutet: wenn man ein Projekt neu beginnt, deklariert man alle Tabellen so und braucht überhaupt kein SQL zu schreiben.
Alle Routen, die der Service beantwortet, sind hier verzeichnet: config/routes, z.B.
...
/vorlesung/#VorlesungId/aufgaben AufgabenR GET !tutor !einschreibung
Der URL https://autotool.imn.htwk-leipzig.de/new/vorlesung/236/aufgaben
wird demnach abgebildet in einen Aufruf der Funktion
getAufgabenR :: VorlesungId -> Handler Html
aus dem Modul Handler.Aufgaben
Die Liste aller Aufgaben für die gewählte Vorlesung wird so bestimmt:
aufgabenListe :: Set TimeStatus -> VorlesungId -> Handler Html
aufgabenListe disp vorlesung = do
...
aufgaben <- runDB $ selectList [AufgabeVorlesungId ==. vorlesung] []
...
Das ist statisch typsicher: aus dem Typ von AufgabeVorlesungId
wird bestimmt, daß es sich um die Tabelle Aufgabe
handelt. Es ist garantiert, daß die Spalte vorlesungId
in dieser Tabelle existiert und die Einträge tatsächlich Schlüssel für die Tabelle Vorlesung
sind. Es ist statisch garantiert, daß in dieser Anfrage die Werte mit Schlüsseln von Vorlesungen verglichen werden, denn der Vergleichsoperator hat diesen Typ:
(==.) :: PersistField typ => EntityField v typ -> typ -> Filter v
dabei stimmt die Typvariable typ
im ersten und zweiten Argument überein.
Die Query mit selectList
kann ergänzt werden durch Spezifikationen zum Ordnen und Abschneiden der Resultate, zum Beispiel:
getLogR :: Handler Html
getLogR = do
entries <- runDB $ selectList [ ] [ Desc EventId , LimitTo 100 ]
...
Insbesondere für Anfragen, die JOIN benutzen, wird esqueleto als eine zusätzliche Abstraktionsschicht oberhalb von persistent
empfohlen. Wird derzeit im autotool nicht benutzt, wäre aber an einigen Stellen sinnvoll, vgl. Issue 316.
Die Bibliothek persistent
beschreibt eine tiefe Einbettung der Datenbankabfragesprache (SQL) in die Gastsprache (Haskell).
Jede Tabelle, jede Zeile, jeder Spalte, jede Anfrage, aber auch jeder Teil-Ausdruck einer Anfrage wird durch einen Ausdruck der Gastsprache beschrieben.
Dadurch kann man bei der Programmierung von DB-Zugriffen
der Gastsprache direkt anwenden.
Das ist Lichtjahre besser als die Benutzung von SQL-Query-Strings in Programmtexten. Das sieht z.B. so aus
public List findWithName(String name) {
return em.createQuery(
"SELECT c FROM Customer c WHERE c.name LIKE :custName")
.setParameter("custName", name)
.setMaxResults(10).getResultList(); }
und bietet eine Unzahl von Fehlermöglichkeiten, die alle erst zur Laufzeit festgestellt werden können, u.a.
"SELCT c FROM ..."
"SELECT c FROM Cstomer ...
"SELECT c FROM Customer where c.nme ...
".setParameter("cstName", ..)
und manche Fehler werden auch zur Laufzeit nicht erkannt:
Vgl. dazu auch Folien aus VL Fortgeschrittene Programmierung