Moderne Webanwendungen mit Haskell
Derzeit sind viele Webanwendungen in PHP geschrieben. Die Gründe dafür liegen auf der Hand: Die Entwicklung geht meist sehr schnell, PHP ist einfach zu erlernen und fast alle Webhoster haben mittlerweile Webserver mit PHP-Unterstützung installiert. Allerdings bringt die Verwendung von PHP auch einige Probleme mit sich. Damit eine PHP-Anwendung gut skaliert, sind viele aufwendige Optimierungen notwendig (siehe z.B. HipHop von Facebook). Außerdem ist PHP eine dynamische Sprache, und damit treten viele Fehler erst zur Laufzeit auf. Schließlich ist auch die Validierung von Eingaben und das Escapen von Ausgaben zumeist dem Programmierer selbst überlassen: SQL-Injections, XSS (Einschleusen von Code in fremde Webseiten durch Dritte) und andere Sicherheitslücken werden nicht auf Ebene der Programmiersprache verhindert (siehe zum Beispiel hier).
Deshalb möchte ich an einem kleinen Beispiel erläutern, wie man mit Haskell relativ einfach eine performante, sichere und moderne Webanwendung schreibt. Hierzu werde ich ein einfaches Blog implementieren. Das Blog wird das Erstellen, Anzeigen sowie das Kommentieren von Beiträgen unterstützen.
Um dem Artikel gut folgen zu können sind Grundlagen zu JavaScript, HTTP und Haskell hilfreich.
Bei einer modernen Webanwendung ist möglichst viel Logik direkt im Client, also im Browser in JavaScript, implementiert. Dank breiter AJAX-Unterstützung in den gängigen Browsern ist eine Kommunikation die ausschließlich Daten zwischen Server und Client überträgt auch problemlos möglich. Auch in unserem Blog werden wir deshalb alle Views und Controller in JavaScript implementieren. In Haskell müssen wir dann nur nur das Modell, eine Komponente die, die Daten akzeptiert und ausgibt (über eine REST-API: HTTP-GET um Objekte zu laden, HTTP-POST um neue Objekte anzulegen.), entwickeln. Für die Views verwenden wir die funktionale (Google) Soy-Templates-Sprache von Google, diese wird dann nach JavaScript kompiliert so dass wir unsere Views mit unserer JavaScript-Controller Logik ansteuern können.
Beginnen wir nun mit der Haskell-Komponente, dem Modell. Als Web-Framework verwenden wir scotty, als Datenbankabstraktionsschicht persistent(-mysql). Die Blogeinträge und Kommentare werden nach JSON (aeson) serialisiert. Die entsprechenden Haskell-Pakete sollten in den entsprechenden Versionen installiert sein (siehe cabal-Datei unten). Für persistent gibt es noch weitere Datenbankbackends neben MySQL, hier könnte man also ebenfalls SQLite oder PostgreSQL verwenden.
Definieren wir zunächst unsere Typen und deren Serialisierung:
Die LANGUAGE-Pragmas sind notwendig, damit fortgeschrittene Sprachfeatures angeschaltet sind, damit persistent und scotty richtig funktionieren. Nun
kommen die Imports der benötigten Module: Database.Persist
und Database.Persist.TH
aus persistent für den Datenbankzugriff, Data.Aeson
aus aeson
um Instanzen für die JSON-Serialisierung zu definieren, Data.Text
aus text um Texte effizient zu representieren und Control.Monad
um mzero
benutzen zu können.
Die anderen Beiden Module werden weiter unten erklärt.
Mit Hilfe von TemplateHaskell erzeugen wir nun neben den Typ-Definitionen automatisch auch die entsprechenden Instanzen für die Verwendung mit persistent:
Das Datenmodell ist denkbar einfach: Ein Blogbeitrag hat einen Titel, einen Inhalt, Tags und einen Autor. Ein Kommentar hat einen Autor, einen Inhalt und referenziert einen Blogbeitrag. Das mkMigrate
generiert zusätzlich noch eine Migrationsfunktion, dazu später mehr. persistUpperCase
benennt die Felder, indem der Name der Tabelle mit dem Namen des Feldes konkateniert wird, und der erste Buchstabe des Feldnamen wird groß geschrieben. (zB
NewsItemTitle) Mehr Informationen über das, was hier genau von TemplateHaskell generiert wird, findet man hier (Abschnitt „Code Generation“)
Damit wir später unsere Haskell-Typen einfach nach JSON serialisieren und von JSON deserialisieren können, müssen wir die Instanzen FromJSON
und ToJSON
aus aeson
implementieren. Die ToJSON
Instanzen beziehen sich allerdings nicht direkt auf den eigentlichen Typ, sondern auf die entsprechende Datenbank-Entity mit ID. Der Grund hierfür liegt
auf der Hand: Das persistent-Framework liefert als Antwort auf zum Beispiel selectList
eine Liste von solchen Entities. Da aeson bereits mit einer Serialisierung für [a]
kommt, können wir also unsere Liste von Entities dann ganz einfach serialisieren. Da wir beim Erzeugen von Kommentaren/Beiträgen dessen ID noch nicht kennen, und wir zum Einfügen (insert
) in persistent den „rohen“ Typ benötigen, schreiben wir hierfür eine FromJSON
Instanz. Die obj .: "key"
Funktion holt aus einem JSON Object
das Element mit dem Schlüssel "key"
(siehe hier).
Wir können nun also zum Beispiel folgendes parsen:
Nun können wir den eigentlichen Server implementieren. Hierzu habe ich als Framework scotty gewählt, weil es sehr klein, einfach und, meiner Meinung nach, perfekt geeignet ist um einen einfachen Server mit REST-API zu implementieren.
Die meisten Module sind bereits aus Types.hs bekannt. Web.Scotty
ist das Hauptmodul des scotty-Webframeworks.
Web.PathPieces
benötigen wir später um Parameter in eine Datenbank-Id zu parsen. Der RequestLogger aus
Network.Wai.Middleware.RequestLogger
ist zu Debug-Zwecken: Es ist eine Middleware, die
alle HTTP-Requests, die an unseren Server gehen, in die Konsole loggt. Da die SQL-Verbindung aus persistent in der ResourceT
Monade
laufen muss, benötigen wir die Funktion runResourceT
aus Control.Monad.Trans.Resource.
Diese Instanz ist notwendig, damit scotty Parameter in text parsen kann.
Hier definieren wir eine Hilfsfunktion, um einfach den HTML-Content-Type für statische Dateien angeben zu können.
Die Konfiguration für die Datenbank - MySQL-Benutzername, Passwort, Host und Datenbank.
Das runDB
sorgt dafür, dass unsere persistent-Aktionen in der richtigen Monade laufen - letztendlich wird pro Request eine neue Datenbankverbindung geöffnet
und dann wieder beendet. Man könnte hier übrigens noch eine Optimierung durchführen und einige Verbindungen bereits beim Start des Servers öffnen und offen
halten (ConnectionPool, ist mit persistent relativ einfach möglich), sodass dann bei einem Request zur Antwortzeit nicht noch die Verbindungszeit zur Datenbank hinzukommt.
Hier führen wir die persistent-Datenbank-Migrationen aus. Persistent legt als automatisch nicht existierende Tabellen und Felder an. Gibt es eine Migration, die persistent nicht selbst durchführen kann, so beendet sich der Server mit einer Fehlermeldung.
Jetzt werden die Routen definiert. scotty orientiert sich dabei sehr an sinatra (Ruby): eine Routen-Definition sieht zum Beispiel wie folgt aus:
Zunächst wählen wir die passende Funktion zu unseren HTTP-Request-Type, dann geben wir den Pfad an. Für Parameter schreiben wir :parameterName
. Diese können wir dann auf zwei Arten auslesen:
oder:
Die param
Funktion sucht übrigens bei POST-Requests auch in FormData nach einem entsprechend benannten Parameter. Um etwas an den Browser zurück zu geben können wir eine der folgenden Funktionen wählen:
text
einfach Texthtml
Text, den der Browser als HTML interpretieren sollfile
Eine Datei auf dem Server laden und an den Browser schickenjson
Ein beliebiges Haskell-Record senden, welches eine ToJSON-Instanz hat
Mehr dazu findet man in der scotty Dokumentation
In der scotty-Monade definieren wir zu sogenannten Routes eine Action. Zuerst fügen wir ein paar Routes
hinzu um die statischen HTML/JavaScript-Dateien zu laden. Dann kommt die REST-API: Zunächst definieren wir zwei GET-Routes /news
und /comments/:id
um
aus der Datenbank News-Einträge und deren Kommentare abzufragen. Mit selectList
aus persistent können wir sehr einfach entsprechende Anfrage durchführen.
Die Funktion nimmt als ersten Parameter Filter
und als zweiten weitere Optionen wie zum Beispiel sortieren oder Limits. Bei den Kommentaren beispielsweise
suchen wir nach allen Kommentaren, die zu der News mit der ID newsId
gehören. Mit fromPathPiece
wandeln wir die Eingabe in eine Datenbank-ID um - das
fromJust
ist an dieser Stelle auch nicht gefährlich, da jedes Request in seinem eigenen Thread lebt, und falls dieser per Exception beendet wird bekommt
unser JavaScript später einen HTTP-Fehlercode. Der Server läuft einfach weiter.
json
serialisiert dann das Ergebnis unserer Datenbank-Abfrage (was Dank unseren oben definierten Instanzen ohne Probleme möglich ist) und erzeugt
dann eine Antwort.
Nun implementieren wir noch das Hinzufügen von News und Kommentaren. Hierzu sind zwei neue POST-Routes notwendig: /news
und /comments
. Die parseComment
/parseNews
Funktion nimmt den POST-Body und parst diesen als JSON in unsere Datentypen. Mit insert
aus persistent speichern wir dann den Kommentar bzw. den Newsbeitrag.
Ein Foreign-Key-Constraint sorgt dafür, dass wir nur Kommentare zu existierenden News speichern können. Wenn das JSON-Parsen oder das Speichern fehlschlägt,
dann wird der Thread wieder beendet und unser JavaScript erhält einen Fehlercode. Für unsere REST-Schnittstelle gilt also: Wenn der Server ein Request
beantwortet, gab es keine Fehler. Ansonsten ist etwas mit der Eingabe falsch. Das ist zugegebenermaßen nicht optimal, da man zum Beispiel keine näheren
Informationen zum Fehler bekommt, aber genauere Fehlerbehandlung würde an dieser Stelle den Rahmen sprengen.
Das war‘s eigentlich schon - unser Server ist „fertig“! Natürlich fehlen hier noch Sachen wie zum Beispiel Authentifizierung (damit nicht jeder News verfassen kann), eine Suchfunktion, etc., aber auch das geht über den Umfang dieses Beitrags hinaus. Nun benötigen wir noch eine Main.hs:
Eine Cabal-Datei:
und eine Setup.hs-Datei:
erstellen, und unseren Server bauen:
Dann starten wir den Server:
Rufen wir im Browser nun http://localhost:8085
auf, bekommen wir File not found.
Zum Schluss noch ein kleiner Test unseres Servers mit curl
:
Keine Blogbeiträge vorhanden, da noch keiner in die Datenbank eingetragen wurde. Wir können mit CURL einen Beitrag hinzufügen:
Nun können wir diesen in unserer Liste von Beiträgen sehen:
Das war‘s für heute, den JavaScript/HTML-Client implementieren wir in Teil 2!