Migration von REST nach GraphQL in einer bestehenden Webapplikation
←
→
Transkription von Seiteninhalten
Wenn Ihr Browser die Seite nicht korrekt rendert, bitte, lesen Sie den Inhalt der Seite unten
Migration von REST nach GraphQL in einer bestehenden Webapplikation Bachelor-Thesis im Fachbereich Informatik Autor Yannick Schröder Informatik 102751 inf102751@fh-wedel.de Erstgutachter Dr. Michael Predeschly mpr@fh-wedel.de Zweitgutachter M. Sc. Marcus Riemer mri@fh-wedel.de Eingereicht am 09. September 2020
Yannick Schröder Migration von REST nach GraphQL in einer bestehenden Webapplikation Bachelor-Thesis im Fachbereich Informatik, 09. September 2020 Gutachter: Dr. Michael Predeschly und M. Sc. Marcus Riemer Fachhochschule Wedel Feldstraße 143 22880 Wedel
Inhaltsverzeichnis 1 Einleitung 1 2 Grundlagen 3 2.1 REST API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 2.1.1 Client-Server-Modell . . . . . . . . . . . . . . . . . . . . . . . . . . 4 2.1.2 Zustandslosigkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2.1.3 Cache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2.1.4 Einheitliche Schnittstelle . . . . . . . . . . . . . . . . . . . . . . . . 6 2.1.5 Weitere Einschränkungen . . . . . . . . . . . . . . . . . . . . . . . 7 2.2 GraphQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 2.3 Ausgewählte Details des Typescript Typsystems . . . . . . . . . . . . . . 11 2.4 JSON Schema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 2.5 Postgres jsonb und hstore Typen . . . . . . . . . . . . . . . . . . . . . . . 16 3 Anforderungsanalyse 18 3.1 Aktuelles System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 3.2 Praxisbeispiel - Erweiterung des Datenmodels . . . . . . . . . . . . . . . . 19 3.2.1 Anlegen des Typescript Interfaces . . . . . . . . . . . . . . . . . . 19 3.2.2 Generierung der JSON Schema Definitionen . . . . . . . . . . . . . 20 3.2.3 Anlegen des Models in Rails . . . . . . . . . . . . . . . . . . . . . . 21 3.2.4 Anlegen eines Controllers in Rails . . . . . . . . . . . . . . . . . . 21 3.2.5 Dataservices auf dem Client . . . . . . . . . . . . . . . . . . . . . . 23 3.2.6 Komponenten auf dem Client . . . . . . . . . . . . . . . . . . . . . 24 3.2.7 Anlegen einer neuen Sicht . . . . . . . . . . . . . . . . . . . . . . . 25 3.3 Vorteile des bisherigen Ansatzes . . . . . . . . . . . . . . . . . . . . . . . . 26 3.3.1 Typescript Typsystem . . . . . . . . . . . . . . . . . . . . . . . . . 26 3.3.2 Typescript zu JSON Schema Generatoren . . . . . . . . . . . . . . 27 3.3.3 Modularität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 3.3.4 Typsicherheit zur Kompilierungszeit . . . . . . . . . . . . . . . . . 27 3.3.5 Typsicherheit zur Laufzeit . . . . . . . . . . . . . . . . . . . . . . . 28 iii
3.3.6 Stabilität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 3.4 Nachteile des bisherigen Ansatzes . . . . . . . . . . . . . . . . . . . . . . . 29 3.4.1 Manuelle SQL Queries . . . . . . . . . . . . . . . . . . . . . . . . . 29 3.4.2 Auswahl von Attributen . . . . . . . . . . . . . . . . . . . . . . . . 29 3.4.3 camelCase und snake_case Notationen . . . . . . . . . . . . . . . . 30 3.4.4 Hoher Aufwand . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 3.5 Anforderungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 3.5.1 Darstellungsvielfalt . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 3.5.2 Typdefinitionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 3.5.3 Typsicherheit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 3.5.4 Performance und Skalierbarkeit . . . . . . . . . . . . . . . . . . . . 37 3.5.5 Validierung von jsonb und hstore . . . . . . . . . . . . . . . . . . . 39 3.5.6 Benennungskonvention . . . . . . . . . . . . . . . . . . . . . . . . . 39 4 Implementierung 40 4.1 GraphQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 4.1.1 Integration von graphql-ruby . . . . . . . . . . . . . . . . . . . . . 42 4.2 Praxisbeispiel - Erweiterung des Datenmodels . . . . . . . . . . . . . . . . 44 4.2.1 Anlegen des Models in Rails . . . . . . . . . . . . . . . . . . . . . . 44 4.2.2 Anlegen eines GraphQL Objekttypen . . . . . . . . . . . . . . . . . 44 4.2.3 Anlegen eines Input Typen . . . . . . . . . . . . . . . . . . . . . . 46 4.2.4 Anlegen eines Query Endpunktes . . . . . . . . . . . . . . . . . . . 47 4.2.5 Anlegen der Resolver Klasse . . . . . . . . . . . . . . . . . . . . . . 48 4.2.6 Erstellen einer GraphQL Query . . . . . . . . . . . . . . . . . . . . 49 4.2.7 Codegenerierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 4.2.8 Anlegen einer Angular Komponenten . . . . . . . . . . . . . . . . . 52 4.2.9 Anlegen einer neuen Sicht . . . . . . . . . . . . . . . . . . . . . . . 53 4.3 Scalar Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 4.4 Enum Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 4.5 Mutationen als Objekttypen . . . . . . . . . . . . . . . . . . . . . . . . . . 55 4.6 Felder Auswahl, Sprachauswahl, Filtern und Sortieren . . . . . . . . . . . 57 4.7 Paginierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 4.7.1 Connection Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 4.7.2 Einheitliche Tabelle . . . . . . . . . . . . . . . . . . . . . . . . . . 61 4.8 Validieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 4.8.1 Multilinguale Strings . . . . . . . . . . . . . . . . . . . . . . . . . . 62 4.8.2 JSON Schema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 iv
4.9 Unerwartete Hindernisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 4.9.1 Fehlerhafte Codegenerierung der Angular Services . . . . . . . . . 64 4.9.2 Unmöglichkeit der Modellierung mancher Typen . . . . . . . . . . 64 5 Fazit 65 5.1 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 6 Literaturverzeichnis 69 7 Eidesstattliche Erklärung 74 v
Einleitung 1 Ziel dieser Arbeit ist die Migration von REST nach GraphQL in der von Marcus Rie- mer entwickelten Lehr-Entwicklungsumgebung BlattWerkzeug, mit Evaluierung ob die Migration den Aufwand Wert ist. GraphQL ist eine Abfragesprache und serverseitige Runtime zur Implementierung webba- sierter APIs. Die Sprache stellt eine Alternative zu populären REST-basierten APIs dar, wobei der Kernunterschied die Verlagerung der Entscheidung über die genauen Daten, die von API-Aufrufen zurückgegeben werden, von den Servern auf die Clients ist [GB]. GraphQL wurde 2015 von Facebook als eine laufende Arbeit veröffentlicht [Inca] und im November 2018 von Facebook in die neu gegründete GraphQL Foundation unter dem Dach der gemeinnützigen Linux Foundation ausgegliedert. Es wurde schnell populär, als neue Unternehmen und Hobbyisten begannen, es auszubauen. Schließlich wurde die Technologie von größeren Unternehmen übernommen, beginnend mit GitHub im Jahr 2016 und später von Twitter, Yelp, The New York Times, Airbnb und anderen [Foud]. Das System hinter BlattWerkzeug hat seit Veröffentlichung im Jahre 2016 [Rie16b] einige Veränderungen durchlebt. Zu Beginn wurde auf dem Client Typescript mit Angular 2 und auf dem Server Ruby mit Sinatra eingesetzt. Im Mai 2016 wurden JSON Schema Dateien in einem Schema Ordner auf Projekt Ebene bereitgestellt [Rie16a]. Im Juni 2017 wurde dann der Grundstein für einen Rails Server gelegt [Rie17a] und 2 Monate später auf eine PostgreSQL Datenbank umgestellt [Rie17b]. Das System funktioniert einwandfrei. Problematisch ist jedoch, dass im Client die Anforderungen mit der Zeit deutlich diverser wurden und zukünftig noch werden. Das aktuelle System stößt dabei an die Grenzen des noch zu vertretenden Entwicklungsaufwands und läuft damit Gefahr den Entwicklungsaufwand zu lähmen. Im Kern dieser Arbeit wird eine vorhandene REST-artige Schnittstelle durch eine neue- re Technologie (GraphQL) weitestgehend ersetzt. Zudem werden alle Berührungspunkte der REST-artigen Schnittstelle ebenfalls auf die Nutzung von GraphQL angepasst. Dazu gehören neben der naheliegenden Kommunikationsschnittstelle auf dem Server, auch cli- 1
entseitige Implementierungen, die durch die Migration von GraphQL angepasst werden müssen. Nachfolgend werden Grundlagen beschrieben, deren Kenntnis im späteren Ver- lauf vorausgesetzt wird. Anschließend werden das aktuelle System bewertet und Anforde- rungen an ein neues System formuliert. Zuletzt wird die Implementierung von GraphQL erläutert und ein Fazit gezogen, ob es eine lohnenswerte Migration für BlattWerkzeug ist. 2
Grundlagen 2 Das Hauptmotiv bei der Nutzung des Internets ist die Informationsaufnahme [Eim]. In- formationen werden auf unzähligen Webapplikationen bereitgestellt, die jeder mit einem Internetzugang einsehen kann, solange der Zugriff auf die Informationen nicht sonderlich geschützt wird. Um persistente und sensible Daten gesichert und nicht für Jedermann zugreifbar lagern zu können, werden sie serverseitig gehalten. Möchte man diese Daten zusätzlich filtern, sortieren oder mehrere Datensätze miteinander verknüpfen, wird eine Datenbank benö- tigt. Eine Datei, in der die Daten abgelegt werden, wäre auch eine Option, allerdings müsste man alle Methoden zum Filtern, Sortieren und Verknüpfen selber implementie- ren. Die in einer Datenbank gespeicherten Informationen sind also aus Nutzersicht nur über eine Anfrage an den Server abrufbar. Somit ist der Server das Bindeglied zwischen ei- nem Client und der Datenbank und kümmert sich um Aufgaben wie Authentifizierung des Nutzers und Überprüfung der Autorisierung bezüglich der angefragten Daten, aber auch um die Zusammensetzung und Ausführung von Datenbankabfragen. Daraus geht hervor, dass ein Client nur begrenzten Zugriff bekommt, da die Ausführung von vor- definierten Funktionen, die Anfragen an die Datenbank beinhalten, lediglich angefragt werden kann. Werden die vordefinierten Funktionen den Bedarf an Informationen nicht gerecht, müssen neue Funktionen entwickelt oder mithilfe von mehreren Anfragen die Daten zusammengesammelt werden. Dieser Entwicklungsaufwand könnte verringert werden, indem der Client mehr Flexibili- tät, Verantwortung und Effizienz besitzen würde, z.B. durch eine direkte Anbindung an die Datenbank. Er könnte exakt die benötigten Daten mit nur einer Anfrage direkt und effizient aus der Datenbank auslesen. Jedoch würde dieser Ansatz viele Gefahren mit sich bringen. Ein Client der direkten Datenbankzugriff erlangt, könnte unerwünschte Trans- aktionen in der Datenbank ausführen, wodurch der erwartete Datenbestand geändert, Einträge gar gelöscht oder sensible Daten anderer Nutzer abgefragt werden könnten. Al- 3
so sollten Zugriffsbeschränkungen erteilt werden, die auf der Datenbankschicht realisiert werden, da clientseitiger Code nach Belieben vom Nutzer eingesehen und verändert wer- den kann. Hinzu kommen weitere Herausforderungen, wenn die Verbindung zur Daten- bank veröffentlicht wird, wie zum Beispiel das Schützen vor zu exzessiver Nutzung oder das Ausnutzen von bekannten Sicherheitslücken bei nicht aktuellsten Versionen [Groc]. Alles in allem ist das ein Verfahren, von dem dringend abgeraten wird, da es in den wenigsten Fällen nutzbringend und sicher gehandhabt werden kann [svi]. Im Folgenden werden diese Probleme anhand von grundlegenden Inhalten, die aktuell Gegenstand des von Marcus Riemers entwickelten Systems [Rie16b] sind wieder aufge- griffen und bei Erläuterungen bzw. Code-Beispielen als bekannt vorausgesetzt. Dazu gehören die Unterkapitel 2.1 „REST API“, 2.3 „Ausgewählte Details des Typescript Typsystems“, 2.4 „JSON Schema“ und 2.5 „Postgres jsonb und hstore Typen“. Diese müssen für die Schaffung und Umsetzung von Verbesserungen grundlegend verstanden werden. Bei dem Kapitel 2.2 „GraphQL“ handelt es sich um eine Abfragesprache und Laufzeitumgebung, für die im Laufe der Arbeit evaluiert wird, ob sie gewinnbringend in das bestehende System migriert werden und Teile ersetzen kann. 2.1 REST API Der Begriff Representational State Transfer (abgekürzt REST, seltener auch ReST) wur- de erstmalig in der Dissertation „Architectural Styles and the Design of Network-based Software Architectures“ von Roy Thomas Fielding im Jahr 2000 geprägt [Fie00]. Er be- schreibt REST als einen Architekturstil für verteilte Systeme, welcher in eine einheitliche Schnittstelle für Kommunikation mündet. Dieser Architekturstil oder auch Programmier- paradigma wird durch verschiedene Software-Engineering-Prinzipien und Beschränkun- gen definiert. Im Folgenden werden die Prinzipien von REST näher erläutert. 2.1.1 Client-Server-Modell Der Ausgangspunkt des Client-Server-Modells ist eine strikte Trennung der Benutzer- oberfläche von der Datenhaltung/-verwertung. Das bedeutet wiederum, dass kein HTML, CSS und Javascript vom Server an den Client geschickt wird, sondern ausschließlich Da- tensätze meist in Form von XML oder JSON, die clientseitig in die Benutzeroberfläche eingebaut werden. Dadurch verbessert sich die Portabilität der Benutzeroberfläche in 4
Bezug auf die Anbindung an verschiedene Datenhaltungs/-verwertungs-Systeme, also die Wiederverwendbarkeit und die Skalierbarkeit aufgrund der Vereinfachung der Ser- verkomponenten. 2.1.2 Zustandslosigkeit Zustandslosigkeit ist eine Beschränkung in Bezug auf die Kommunikation zwischen Ser- ver und Client. Anfragen vom Client müssen alle Informationen beinhalten um diese interpretieren zu können. Insbesondere werden Anfragen ohne Bezug zu früheren Anfra- gen behandelt und keine Sitzungsinformationen - wie Authentifizierungs- und Authorisie- rungsinformationen - ausgetauscht bzw. verwaltet. Diese befinden sich ausschließlich auf dem Client und müssen bei Anforderung von geschützten Daten der Anfrage beigefügt werden. Vorteile aus dieser Beschränkung sind, dass Anfragen unabhängig voneinander betrach- tet werden können und somit z.B. von mehreren Maschinen parallel ausgeführt werden können, da jede Anfrage für sich eine vollständige Anforderung an den Server beschreibt. Zudem kann einfacher auf den Misserfolg einer Anfrage reagiert werden als auf eine er- folglose Kette von zusammenhängenden Anfragen und es ist nicht vonnöten Zwischenzu- stände bzw. Status zu speichern, welche die Ressourcenauslastung erhöhen würde. Dies kann jedoch zu einer verringerten Netzwerkleistung führen aufgrund von Zusatzinfor- mationen, die sich bei mehreren verschiedenen Anfragen wiederholen und erneut mit gesendet werden müssen. 2.1.3 Cache In Hinblick auf das Verbessern der Netzwerkleistung wurde ein Cache als Einschränkung hinzugefügt. Diese Einschränkung setzt voraus, dass Daten aus einer Antwort vom Server implizit oder explizit als cachefähig oder nicht cachefähig gekennzeichnet werden. Werden Daten in einer Antwort auf eine Anfrage als cachefähig gekennzeichnet, kann der Client diese Information speichern und erhält das Recht diese bei einer späteren gleichwertigen Anfrage wiederzuverwenden. Somit können Anfragen effizienter behandelt bzw. ganz durch eine direkt aus dem Cache geladene Antwort ersetzt werden. 5
2.1.4 Einheitliche Schnittstelle Ein zentrales Merkmal von REST ist die einheitliche und vom Dienst entkoppelte Schnitt- stelle. Auf jede Ressource muss über einen einheitlichen Satz an URLs, hinter denen sich Transaktionen zum Erstellen, Lesen, Aktualisieren und zum Löschen (CRUD) verbergen, zugegriffen werden können. Durch eine einheitliche Komponentenschnittstelle wird die Sichtbarkeit der einzelnen Interaktionen erhöht. Dies bedeutet, dass es für jede Ressource eine Menge fest definierter Interaktionen gibt, die sich in ihrer Struktur nur durch den Namen der Ressource und ihre Fremdbeziehungen unterscheiden. CRUD SQL HTTP URL Bedeutung Operation Create INSERT POST /projects Erstellen eines Projekts Read SELECT GET /projects Abrufen aller Projekte Read SELECT GET /projects/:id Abrufen eines Projekts Read SELECT GET /projects/:id/ Abrufen aller Nutzer eines users Projekts Read SELECT GET /users/:id/ Abrufen aller Freunde eines friends Nutzers Update UPDATE PATCH/PUT /projects/:id Aktualisieren eines Projekts Delete DELETE DELETE /projects/:id Löschen eines Projekts Tab. 2.1: Einheitliche REST Schnittstellen Das hat zur Folge, dass anwendungsspezifische Daten in einer standardisierten Form übertragen werden müssen, wodurch die Effizienz der Datenübertragung Mängel aufwer- fen kann. Insbesondere treten im Kontext der Arbeit zwei für REST bekannte Mängel auf, die hier näher erläutert werden. Overfetching Aufgrund der einheitliche Komponentenschnittstelle gibt es für eine Liste aller Projekte genau eine Route GET /projects, die zu jedem Projekt alle Attribute ungekürzt liefert. Wird nur ein Teil der Attribute benötigt, sind weitere Angaben nutzlos und damit ineffizient. Dennoch werden sie von der API mitgeliefert. Underfetching Ein weiteres Problem ist, dass mehrere Anfragen für Daten, die in Beziehung zu- einander stehen, benötigt werden. In Abbildung 2.1 muss für jeden Nutzer eines 6
Projektes erfragt werden, welche Freunde dieser hat. Vorausgesetzt wird, dass eine entsprechende Route für die Abfrage nach Freunden eines Nutzers existiert. Sind einem Projekt N Nutzer zugeordnet, muss für diese Information die dritte Abfrage N mal abgeschickt werden. Da zusätzlich noch die Liste von mit dem Projekt in Beziehung stehenden Nutzern erfragt wird, spricht man von N + 1 Abfragen. /projects/ 1 /projects//users { "projects": { /users//friends "id": 1, "name": "Esqulino", "public": false } } /projects/ 2 /projects//users { "user": [{ /users//friends "id": 1, "name": "Yannick", }, ... ] } /projects/ 3 /projects//users { "friends": [{ /users//friends "id": 2, "name": "Philipp", }, ... ] } Abb. 2.1: Abfragen von Projektdaten inklusive aller Nutzer die das Projekt einsehen können 2.1.5 Weitere Einschränkungen Zusätzlich gibt es noch die Einschränkung Layered System, welches das Prinzip eines hier- archisch in Schichten aufgebauten Systems beschreibt und eine optionale Einschränkung Code-On-Demand, welche die Client-Funktionalität durch Herunterladen und Ausführen von Code in Form von Applets oder Skripten erweitert. Diese werden im Rahmen der Arbeit nicht genutzt und deshalb nur am Rande erwähnt. 2.2 GraphQL Bei GraphQL handelt es sich um eine Abfragesprache für APIs und eine Laufzeitumge- bung zum Ausführen dieser Abfragen und Wiedergeben von Daten unter Verwendung 7
eines von für die Daten definierten Typensystems. Es ist an keinerlei Datenbanksysteme gebunden und lässt sich gut mit vorhandenen Code und Daten verbinden. Ein GraphQL Service entsteht durch das Definieren eines Typschemas, vergleichbar mit Datenbanktabellen. Zu jedem Attribut (Feld) eines Typs lassen sich - genauso wie bei Datenbanken - Datentypen und Restriktionen wie NOT NULL definieren. 1 type Project { 2 id: ID! 3 public: Boolean 4 } Listing 2.1: Project Typdefinition Das aufgeführte Codefragment 2.1 „Project Typdefinition“ definiert einen Typ Project mit zwei Feldern, welcher ein Programmierprojekt darstellen soll. • id: hat den von GraphQL vorgegebenen Typen ID, der als eindeutiger String ge- wertet wird und nicht dazu gedacht ist vom Menschen „lesbar“ zu sein. Zusätzlich wurde mit „!“ festgelegt, dass dieses Feld nicht Null sein darf. • public: besitzt den Typen Boolean, der den Wert Null annehmen kann. Es gibt an, ob das Projekt bereits veröffentlicht und für jeden zugreifbar gemacht wurde. Im Gegensatz zu einem Datenbank-Schema ermöglicht das Typsystem von GraphQL zu jedem Typen Argumente zu definieren, die wiederum einer Funktion (Resolver) überge- ben werden können, die aufgerufen wird, wenn das Feld im Kontext einer Query aufgelöst werden soll [TGb]. Für ein Beispiel wird der Typ Project um ein Feld erweitert: 1 enum LanguageEnum { 2 DE 3 EN 4 FR 5 } 6 7 type Project { 8 id: ID! 9 public: Boolean 10 name(language: LanguageEnum = DE): String! 11 } Listing 2.2: Erweiterung der Typdefinition von Project und Einführung eines Enums mit Ländercodes 8
• name: besitzt den Typ String, der ebenfalls nicht den Wert Null annehmen kann. Zusätzlich wurde dem Feld ein Argument language zugeteilt, welches den selbst definierten Datentypen LanguageEnum besitzt und den Default Wert DE setzt. Zusätzlich wird eine Funktion benötigt, die das Argument language verarbeiten kann und dementsprechend den Rückgabewert formuliert. Solche Funktionen nennen sich in der Welt von GraphQL Resolver. Aufgabe dieses Resolvers ist es - durch Aufruf der in Zeile 2 Listing 2.3 als Pseudocode aufgeführten Funktion translate - den Namen eines Projektes in die übergebene Sprache zu übersetzen. 1 name(obj , args , context , info) { 2 return translate(obj ,args['language ']) 3 } Listing 2.3: Resolver des Feldes name Damit nun ein Datentyp abgefragt werden kann, müssen Queries zu den Datentypen definiert werden. 1 type Query { 2 projects: [Project !]! 3 } Listing 2.4: GraphQL Query Typdefinition Der Query Typ gehört zum Typsystem von GraphQL. Er beinhaltet alle für das Schema definierten Queries (siehe Listing 2.4). In diesem Fall ist es lediglich eine Query mit dem Bezeichner projects, die ein Array vom Typ Project als Rückgabewert erwartet. Zusätzlich wurde angegeben, dass Projekte innerhalb des Arrays und das Array selber nicht Null sein dürfen. Es wird also mindestens eine leeres Array erwartet, aber keinesfalls Null oder ein Array, das mit Null-Werten gefüllt ist [Fouc]. Um Queries im GraphQL Schema bereitzustellen, wird auf oberster Ebene ein Einstiegs- punkt definiert (siehe Zeile 2 Listing 2.5). Über diesen können alle Anfragen gefunden werden, die der GraphQL Service behandeln soll. 1 schema { 2 query: Query 3 mutation: Mutation 4 } Listing 2.5: GraphQL Schema Definition Jetzt kann dem Client die Freiheit gewährt werden, eigene Abfragen für genau den Datensatz, der benötigt wird, zu formulieren. Zudem lässt sich anhand der gestellten 9
Abfrage die Struktur der erhaltenen Antwort festlegen. Dies könnte wie in Listing 2.6 aussehen. 1 query Projects{ 2 projects { 3 id 4 name(language: EN) 5 } 6 } Listing 2.6: GraphQL Query mit dem Bezeichner Projects Das aufgeführte Listing 2.6 „GraphQL Query mit dem Bezeichner Projects“ verkörpert eine GraphQL Query mit dem Bezeichner Projects, die für alle vorhandenen Projekte die Felder id und name zurück gibt. Aus dieser Query lässt sich nicht der Rückgabewert von projects erschließen. Diese Information erhält man als Entwickler lediglich aus den Query Definitionen. Bei Abschicken der Query wird neben der Query auch der Bezeichner als operationName mit gegeben. Nachdem eine GraphQL Query gegen das Typsystem validiert wurde, wird sie von dem GraphQL Modul ausgeführt und ein Ergebnis - typi- scherweise in Form von JSON - zurückgegeben, das die Form und Struktur der Anfrage spiegelt (siehe Listing 2.7). 1 { 2 "projects": [ 3 { 4 "id": "368 b6ee9 -2b1f -4661 -a82f -ff7b62dc9251" 5 "name": "Esqulino" 6 }, 7 { 8 "id": "b25c342e -f2b1 -4a74 -8124 - a7a688911380" 9 "name": "Trucklino" 10 } 11 ] 12 } Listing 2.7: JSON Antwort auf die Projects Query Dies könnte zum Beispiel der bereits definierte Query-Typ projects sein. Damit der GraphQL Server eine Anfrage an eine an den Server gebundene Datenbank schicken kann, wird die zum Query-Typ definierte Resolver-Funktion ausgeführt [Foub]. Innerhalb dieser Resolver-Funktion ist der Zugriff auf das Dateisystem festgelegt, sodass neben Datenbanken sogar Dateien als Speichermedium genutzt werden könnten. GraphQL bietet neben Queries zum Abfragen von Datensätzen auch Anfragen zum Spei- chern von Daten im gewählten Speichermedium. Solche Anfragen nennen sich Mutatio- nen (siehe Listing 2.8). Genau wie bei Resolvern eines Feldes wird bei einer Mutation Code hinzugefügt, der für das Erstellen oder Ändern von Datensätzen zuständig ist. Tech- nisch gesehen könnte jede Query auch so implementiert werden, dass diese das Speichern 10
von Daten bewirkt. Es ist jedoch nützlich, eine Konvention festzulegen, dass alle Ope- rationen, die Schreibvorgänge verursachen, explizit über eine Mutation gesendet werden sollten [TGa]. 1 type creationResponse { 2 id: String 3 errors: [String] 4 } 5 6 type Mutation { 7 createProject(name: String!, public: Boolean ):creationResponse 8 } 9 10 mutation CreateProject($name: String!, $public: Boolean ) { 11 createProject(name: $name , public: $public) { 12 id 13 errors 14 } 15 } Listing 2.8: GraphQL Mutation zum Erstellen eines Projektes • Zeile 1-4: Festlegung eines Typs mit den Feldern id und errors. Dieser Typ spiegelt den Rückgabewert einer Mutation zum Erstellen neuer Datensätze wider. Wenn die Mutation erfolgreich war, wird die id des neuen Datensatzes zurück gegeben. Wenn Fehler aufgetreten sind, wird das errors Feld mit diesen gefüllt. • Zeile 6-8: Definition eines Mutations-Typs, welcher alle Mutationen beinhaltet, ähn- lich wie beim Queries-Typ in Listing 2.4. Dieser erhält die Mutation createProject, welche zwei Argumente name und public übergeben bekommt. Letzteres ist dabei optional. • Zeile 10-15: Festlegen einer Mutation mit dem Operations Namen CreateProject, die das Argument name als String zwingend erwartet und das optionale Argument public bekommt. Diese Argumente werden dann der createProject Mutation übergeben. Als Antwort auf die Mutation werden die Felder id und errors erwar- tet. 2.3 Ausgewählte Details des Typescript Typsystems Typescript ist ein typisiertes Superset von Javascript, das zu reinem Javascript kom- piliert [Cord]. Das heißt, es beinhaltet alle Funktionalitäten von Javascript und wurde darüber hinaus erweitert und verbessert [Far]. Dazu gehört das in Typescript eingeführte Typsystem. Selbstverständlich besitzt Javascript ebenfalls Typen, doch kann eine Varia- 11
ble, auf die ursprünglich eine number zugewiesen wurde, auch als string enden. Das kann schnell zu unbedachten Seiteneffekten führen. Ein Typsystem ist eine Menge von Regeln, die jeder Variable, jedem Ausdruck, jeder Klasse, jeder Funktion, jedem Objekt oder Modul im System einen Typ zuweist. Diese Regeln werden zur Kompilierungszeit (statische Typprüfung) oder zur Laufzeit (dynami- sche Typprüfung) geprüft, um Fehler in einem Programm aufzudecken [Mwi]. Der Typescript-Compiler prüft zur Kompilierungszeit alle Variablen und Ausdrücke auf ihren Typen und entfernt anschließend alle Typinformationen bei der Konvertierung zu Javascript Code [Corb]. Die im folgenden Beispiel in Listing 2.9 deklarierte Funktion gibt die zweite Hälfte eines übergebenen Strings zurück. Der erste Aufruf der Funktion führt zu einem Fehler beim Kompilieren. Es wird also direkt darauf hingewiesen, dass es sich um ein fehlerhaften Code handelt. 1 function printSecondHalf(s: string) { 2 return s.substr(s.length /2); 3 } 4 printSecondHalf (123); // Error bereits zur Kompilierungszeit 5 printSecondHalf("hello"); // Ok - "llo" Listing 2.9: Typescript Funktion mit typisiertem Parameter Nach der Kompilierung sind alle Typinformationen entfernt worden, wodurch erst durch einen fehlerhaften Aufruf ein TypeError auftritt (siehe Listing 2.10). 1 function printSecondHalf(s) { 2 return s.substr(s.length /2); 3 } 4 printSecondHalf (123); // Error zur Laufzeit - TypeError: s.substr is not a function 5 printSecondHalf("hello"); // Ok - "llo" Listing 2.10: Zu Javascript kompilierte Funktion Nehmen wir an, wir möchten den in Kapitel GraphQL 2.2 „GraphQL“ erstellten Typen Project nutzen, um eine Funktion zu schreiben, die einen neuen Project Datensatz an den Server schickt. Um diesen Typen clientseitig nutzen zu können, können wir ein äqui- valentes Typescript Interface erstellen oder eines generieren lassen (siehe Listing 2.11). 1 interface Project { 2 id: string , 3 name: string 4 } Listing 2.11: Typescript Project Interface 12
Wollen wir jetzt einen neuen Datensatz an den Server schicken, können wir das Interface nutzen. Jedoch ist nur der Name des neuen Datensatzes bekannt, die id ist eine uuid und wird serverseitig generiert. Also wollen wir die id beim clientseitigen Erstellen außen vorlassen. Dafür bietet Typescript unter einer Vielzahl von Werkzeugen, die allgemeine Typtransformationen ermöglichen [Corc], Omit, das alle Attribute von T nimmt und anschließend K aus den Attributen entfernt (siehe Listing 2.12). 1 type PostProject = Omit ; // Äquivalent zu Pick Listing 2.12: Transformierter Project Typ Der Typ PostProject beinhaltet also alle Felder von Project, allerdings ohne id. Das Gegenstück zu Omit wäre Pick, welches aus dem Typ T nur die Attribute K nimmt. Mithilfe dieser Typen lässt sich eine typsichere Methode entwickeln, um einen neuen Datensatz an den Server schicken zu können (siehe Listing 2.13). 1 interface ProjectResponse { 2 project: Project , 3 error: string | null 4 } 5 const createProjectRecord = async (p: PostProject):Promise => { 6 return xmlhttp.postProject(p); 7 } 8 9 const newRecord: PostProject = { 10 name: "esqulino" 11 }; 12 13 const response: ProjectResponse = await createProjectRecord (newRecord); Listing 2.13: Typen und Methode zum Abschicken eines Project-Datensatzes Die Methode createProjectRecord erwartet also ein Project ohne id als Parameter und gibt ein ProjectResponse wieder. Der Code im Methodenrumpf ist hierbei nur Pseudocode. Der Typ ProjectResponse beinhaltet neben dem Project auch ein error Feld, welches in dem Kontext angibt, ob ein neuer Datensatz auf dem Server erstellt werden konnte oder nicht. Des Weiteren gibt es noch Exclude wodurch sich von T diejenigen Typen ausschließen lassen, die U zugeordnet werden können. Gäbe es meh- rere „Response“-Typen, ließe sich das Feld extrahieren, über welches auf die Datensätze zugegriffen werden kann (siehe Listing 2.14). 1 type DataKey = Exclude ; 2 const key: DataKey = "project"; 3 const project: Project = response[key]; Listing 2.14: Exclude zum Exkludieren von Schlüsseln 13
2.4 JSON Schema JSON-Schema ist ein Vokabular, mit dem JSON-Dokumente annotiert und validiert werden können [Orga]. Es wird zur Überprüfung genutzt, ob JSON Objekte die im JSON- Schema beschriebene Struktur einhalten. Der Vorgänger von JSON-Schema war das XML-Schema. Es erlaubt das Format eines XML-Dokuments zu definieren, d.h. welche Elemente erlaubt sind, die Anzahl und Rei- henfolge ihres Auftretens, welchen Datentyp sie haben sollen usw. Seit 2006 gibt es einen neuen Akteur auf dem Gebiet der Datenformate, Javascript Object Notation (JSON). Die JSON Daten sind viel kleiner als ihr XML-Gegenstück und ihre Instanzen sind gültige JavaScript-Objekte, was es interessant für Webentwickler macht, da sie beim Laden von Informationen in asynchronen Webanwendungen über AJAX (Asynchronous Javascript and XML) keinen separaten Konvertierungsschritt mehr benötigen [Nog]. Nehmen wir an, wir möchten den in Kapitel GraphQL erstellten Typen Project aus Listing 2.3 mit verschiedenen Attributen erweitern. Eine JSON Instanz soll mindestens folgende Attribute beinhalten, wobei die Angabe, ob es sich bei dem Entwickler um einen proudFather handelt, optional ist (siehe Listing 2.15). 1 { 2 "id":"de0d91a7 -61ae -49af -90d9 -5 a37dd883a01", 3 "name":"Esqulino", 4 "public": false , 5 "createdAt":1452985200000 , 6 "developer": { 7 "firstname":"Marcus", 8 "proudFather":true 9 } 10 } Listing 2.15: Ein Projekt als JSON Objekt Das passendes Schema dazu sieht folgendermaßen aus (siehe Listing 2.16). Neben den verwendeten Schlüsselwörtern gibt es noch eine Vielzahl weiterer, die es un- ter anderem erlauben Einschränkungen, Abhängigkeiten, Muster in Form von Regulären Ausdrücken oder die maximale oder minimale Anzahl an zu einem Objekt gehörende At- tribute festzulegen. Die hier verwendeten Schlüsselwörter haben folgende Bedeutung: • Zeile 2: $schema besagt, dass dieses Schema nach einem bestimmten Entwurf des Standards geschrieben ist, in erster Linie zur Versionskontrolle. 14
1 { 2 "$schema": "http ://json -schema.org/draft -07/ schema#", 3 "title": "project", 4 "description": "A project from Esqulino", 5 "type": "object", 6 "properties": { 7 "id": { 8 "type": "integer" 9 }, 10 "name": { 11 "type": "string" 12 }, 13 "public": { 14 "type": "boolean" 15 }, 16 "createdAt": { 17 "description": "Date of creation in milliseconds", 18 "type": "number" 19 }, 20 "developer": { 21 "description": "The developer of a project", 22 "type": "object" 23 "properties": { 24 "firstname": { 25 "description": "The forename of the developer", 26 "type": "string" 27 }, 28 "proudFather". { 29 "description": "Indicator if he is a father or not", 30 "type": "boolean" 31 } 32 }, 33 "required": [ "firstname" ] 34 }, 35 }, 36 "required": [ "id", "name", "developer", "createdAt" ] 37 } Listing 2.16: JSON Schema zu Projekt Objekt • Zeile 3: title/description haben nur beschreibenden Charakter. • Zeile 5: Das Schlüsselwort type für die Typüberprüfung definiert die erste Beschrän- kung für die JSON-Daten und in diesem Fall muss es sich um ein JSON-Objekt handeln. • Zeile 6: properties beschreibt, welche Attribute das Objekt haben darf. • Zeile 7-35: Definierung der Attribute eines Projektes, wobei in Zeile 20-34 ein weiteres Objekt als Attribut definiert wird. Dieses besitzt die beiden Attribute firstname als String und proudFather als Boolean. Die Angabe von firstname wird bei einem developer Objekt zwingend erwartet, proudFather ist optional. • Zeile 36: Da das Schlüsselwort required ein Array von Strings beinhaltet, können bei Bedarf mehrere Attribute angeben werden, die erwartet werden. 15
Nehmen wir an, ein Entwickler hat ein fehlerhaftes Projekt wie in Listing 2.17 „Ein fehlerhaftes Projekt“ erstellt. 1 { 2 "id":"de0d91a7 -61ae -49af -90d9 -5 a37dd883a01", 3 "public": false , 4 "developer":{ 5 "firstname":"Michael", 6 "professor": true 7 }, 8 "createdAt":"1452985200000" 9 } Listing 2.17: Ein fehlerhaftes Projekt Es kommt bei der Validierung dieses Objektes zu folgenden Verstößen: • name: ist im required Array angegeben und muss somit vorhanden sein. • createdAt: Es wurde ein falscher Datentyp angegeben, string statt number. • professor: Dieses Attribut ist nicht im properties Objekt angegeben und da- durch fehl am Platz. Die händische Erstellung solcher JSON-Schema kann bei einer Vielzahl von Typen schnell lästig werden. Um dem Problem entgegen zu wirken, lassen sich aus Typescript Interfaces passende JSON-Schema Dateien generieren. Der Vorteil daran ist, dass sich clientseitig definierte Datentypen durch die Generierung serverseitig validieren lassen; denn für die meisten gängigen Programmiersprachen sind JSON-Schema Validatoren entwickelt wor- den. Somit ist es unabhängig, welche Programmiersprache der Server nutzt [Orgb]. 2.5 Postgres jsonb und hstore Typen Das PostgreSQL Datenbanksystem kennt über den SQL-Standard hinaus die Datentypen hstore und jsonb zur Speicherung von JSON Strukturen oder assoziativen Arrays, die üblicherweise in NoSQL-Systemen gespeichert werden. Diese beiden Typen werden im Kontext der Arbeit für Objekte genutzt, die sich nur mit sehr großem Aufwand und zukünftigen Migrationen in ein Datenbankschema gießen lassen. Einer dieser Typen ist ein Objekt, das in Listing 2.18 ein multilingualen String darstellen soll. Zukünftig sollen weitere Sprachen ermöglicht werden. Würde man dieses Objekt als Ta- belle definieren, müsste bei Hinzufügen oder Entfernen einer Sprache eine Rails Migra- 16
1 { 2 "DE": "Die Drei", 3 "EN": "The three" 4 } Listing 2.18: Multilinguales Objekt tion durchgeführt werden, um das Datenbankschema anzupassen. Damit diese Objekte flexibel sein können, wird bei der Speicherung ein hstore Typ verwendet. Hstore differenziert sich von jsonb, indem es nur eine Ebene von Schlüssel-Werte-Paaren ohne weitere Verschachtelungen zulässt und diese als String abspeichert. Erst durch die Einführung von jsonb wurde aus Postgres auch eine dokumentenorientierte Datenbank. Denn im Gegensatz zu hstore können jsonb Datensätze beliebig tief verschachtelt werden und darüber hinaus werden sie in einem dekomprimierten Binärformat gespeichert, wo- durch die Eingabe aufgrund des zusätzlichen Konvertierungs-Overheads etwas langsamer, die Verarbeitung jedoch erheblich schneller ist, da kein Reparsen erforderlich ist [Grob]. Ansonsten haben beide Typen in vielen Dingen die gleichen Verhaltensweisen. Wie bei der Eingabe doppelter Schlüssel wird nur der letzte Wert beibehalten. Zudem wurden für beide Datentypen eine beachtliche Menge an Operationen und Funktionen bereitgestellt, die es möglich machen, auf SQL Ebene einen hstore oder jsonb Datensatz fast wie ein Hash in Ruby oder ein JSON-Objekt in Javascript zu behandeln [Groa]. 17
Anforderungsanalyse 3 Ziel dieser Arbeit ist die Evaluierung und Migration von REST nach GraphQL in die von Marcus Riemer entwickelte Lehr-Entwicklungsumgebung BlattWerkzeug zur Verbes- serung des aktuell genutzten Systems. Nachfolgend wird in diesem Kapitel die Funkti- onsweise des aktuellen Systems erläutert. Anschließend werden Anforderungen, die ein neues System erfüllen muss, formuliert und evaluiert. 3.1 Aktuelles System Marcus Riemer hat im Rahmen seiner Master-Thesis an der Fachhochschule Wedel die Lehr-Entwicklungsumgebung BlattWerkzeug als Webapplikation entwickelt, die sich an Kinder und Jugendliche richtet. Mit BlattWerkzeug lassen sich, gestützt durch Drag & Drop-Editoren, für beliebige SQLite-Datenbanken Abfragen formulieren und Oberflächen entwickeln [Rie16b, S. 2]. Seit dem Abschluss der Master-Thesis wird BlattWerkzeug im Rahmen eines Promotionsvorhabens weiterentwickelt. Der Server dieser Web-App ist auf Basis von Ruby on Rails gebaut. Er dient haupt- sächlich der Speicherung und Auslieferung von Daten. Kommuniziert wird über eine REST-artige JSON-Schnittstelle [Rie16b, S. 94]. Der Client wurde als eine Single-Page Application mit rein clientseitiger Visualisierung aufgebaut, die lediglich für den Zugriff auf serverseitige Ressourcen (Datenbank, gespei- cherte Ressourcen, gerenderte Seiten) Anfragen zum Server schickt. Programmiert wurde sie 2016 [Rie16b, S. 1] auf Basis von Angular 2 in Typescript. Zum aktuellen Zeitpunkt wird allerdings auf die Angular Version 10.0.4 gesetzt. Für die Wahl des einzusetzenden Datenbanksystems wurde sich beim Entwicklungsstart, auf Grund der Kriterien „Kostenlose Verfügbarkeit“, „Einfacher Betrieb“, „Einfache Backups“, „Tools zur Modellierung“ und „Externe Tools zur Entwicklung von SQL- Abfragen“ für eine SQLite Datenbank entschieden [Rie16b, S. 99–100]. Im November 18
2017 ist dann der Grundstein gelegt worden, um den Server mit einer PostgreSQL Da- tenbank zu verbinden [Rie17b], da diese es unter anderem ermöglicht JSON Objekte direkt zu speichern, ohne diese in Text Datentypen konvertieren zu müssen. Anhand eines Praxisbeispiels wird im Weiteren die Funktionsweise des Systems in Hin- blick auf das Hinzufügen neuer Daten unter Gewährleistung der Typsicherheit (siehe Unterabschnitt 3.5.3 „Typsicherheit“) verdeutlicht. 3.2 Praxisbeispiel - Erweiterung des Datenmodels Damit neue Datensätzen zwischen Server und Client typsicher mithilfe der bislang ge- nutzten REST API ausgetauscht werden können, sind mehrere Schritte erforderlich. Die Reihenfolge der nachfolgend aufgeführten Schritte ergibt sich aus dem bisherigen Ent- wicklungsprozess. 3.2.1 Anlegen des Typescript Interfaces Als erstes wird ein Typescript Interface für den Datensatz erstellt, der abgebildet werden soll. Wir erweitern den Datentyp Project aus Listing 2.2 erneut (siehe Listing 3.1): 1 export interface Project { 2 id: string; 3 name: MultiLangString; 4 public ?: boolean ; 5 slug ?: string; 6 userId ?: string; 7 createdAt ?: string; 8 updatedAt ?: string; 9 } 10 11 export interface User { 12 id: string; 13 name: string; 14 } Listing 3.1: Typescript Interface für die Darstellung eines Projektes • Zeile 2: id/public siehe Listing 2.2. • Zeile 3: name hat sich zu einem multilingualen Feld geändert. MultiLangString repräsentiert eine Map von String auf String ([key: string]: string;). 19
• Zeile 5: slug ist ein aus einem oder wenigen Wörtern bestehender benutzer- und suchmaschinenfreundlicher Text (sprechender Name) als Bestandteil einer URL [Wik20]. Diese Angabe ist optional. • Zeile 6: userId ist die ID des Nutzers, dem dieses Project zugeordnet ist (Fremd- schlüsselbeziehung). • Zeile 7: createdAt ist die optionale zeitliche Angabe, wann dieses Project erstellt wurde. • Zeile 8: updatedAt ist die optionale zeitliche Angabe, wann dieses Project zuletzt verändert wurde. • Zeile 11-14: User ist der mit dem Projekt in Beziehung stehende Nutzer. Wird ein Datensatz mit einem Projekt beim Server angefragt, lässt sich die Antwort des Servers auf eine Variable mit dem Typ Project zuweisen. Dadurch wird zur Kom- pilierungszeit ermöglicht, typsicher auf die einzelnen Felder des Interfaces zugreifen zu können. Im nächsten Schritt wird das Interface dem Server zur Verfügung gestellt. 3.2.2 Generierung der JSON Schema Definitionen Das Interface aus Listing 3.1 wurde clientseitig erstellt und kann auch nur dort verwen- det werden. Um es serverseitig nutzen zu können, wird daraus eine JSON Schema Datei generiert. Für die Generierung sind Einträge in einem Makefile nötig (siehe Listing 3.2), welches die Erstellung aller JSON Schema Dateien realisiert. Nach der Generierung be- finden sich zu jedem aufgeführten Typescript Interface ein passendes JSON Schema in einer eigenen Datei. Diese werden dann in einem Schema Ordner auf Projekt Ebene gehalten. Dass jedes Schema in einer eigenen Datei gespeichert ist, kommt dem Server bei der Validierung zu gute. Dieser lädt die Datei - deren Namen äquivalent zum ur- sprünglichen Typescript Interface ist - aus dem Ordner, liest das Schema aus und kann dieses zu Validierungszwecken nutzen. Mithilfe der JSON Schema Dateien können dann Datensätze validiert werden. 1 Project.json : $(SRC_PATH)/shared/project.ts 2 $(CONVERT_COMMAND ) Listing 3.2: TypeScript Interface für die Project Darstellung in einer Liste 20
3.2.3 Anlegen des Models in Rails Sollte das Interface aus Listing 3.1 einer neuen Datenbanktabelle entsprechen, muss eine Active Record Migration erstellt werden, die das Datenbankschema erweitert [Hanb]. 1 create_table "projects", id: :uuid , do |t| 2 t.string "slug" 3 t.hstore "name", default: {}, null: false 4 t.uuid "user_id" 5 t.datetime "created_at", null: false 6 t.datetime "updated_at", null: false 7 end Listing 3.3: Rails Migration zum hinzufügen einer projects Datenbanktabelle Durch Ausführung der Migration aus Listing 3.3 wird eine neue Tabelle mit der Be- zeichnung projects erstellt. Außerdem wird ein Active Record Model benötigt (siehe Listing 3.4), dem die projects-Tabelle zugeordnet wird. Zur Realisierung wird eine Ru- by Klasse, die von der Klasse ApplicationRecord erbt, mit dem selben Namen, den die Tabelle hat, erstellt. Die Rails Konvention sieht vor, dass Datenbanktabellen im Plural und das dazugehörige Model im Singular benannt werden [Hana]. 1 class Project < ApplicationRecord 2 # der Nutzer eines Projektes 3 belongs_to :user 4 end Listing 3.4: Model Auf diese Weise entsteht die Möglichkeit, die Spalten jeder Zeile in dieser Tabelle mit den Attributen der Instanzen des Models abzubilden. Jede Zeile dieser Tabelle stellt also ein „Projekt“ Datensatz mit den in Listing 3.2.1 aufgeführten Feldern dar. 3.2.4 Anlegen eines Controllers in Rails Um nun auf Anfragen reagieren und Daten aus Model Instanzen an den Client liefern zu können, bedarf es einem Controller. Controller haben die Aufgabe Anfragen zu verar- beiten, die vom Router (siehe Listing 3.5) an sie weitergeleitet wurden. Die Funktionen innerhalb eines Controllers sind dafür verantwortlich die angefragte Funktionalität auszu- führen und die entsprechende Antwort zu erzeugen. Bei einer Anfrage, die Projekt-Daten ausgeliefert bekommen soll, übernimmt die Controller Funktion die Aufgabe alle Daten aus dem Project-Model zu holen und gibt diese dann wie bei REST APIs üblich in JSON Form zurück. 21
1 scope 'project ' do 2 get '/', controller: 'projects ', action: :index 3 end Listing 3.5: Route entspricht URL ’/project/’ und leitet Anfrage an die ProjectsController Funktion index weiter 1 class ProjectsController < ApplicationController 2 def index 3 render json: Project.all.map (&: to_full_api_response ) 4 end 5 end Listing 3.6: Controller mit Funktion zum zurückgeben aller Project Instanzen In Zeile 3 des Controllers in Listing 3.6 werden alle Projekte aus der Datenbank geladen inkl. aller Beziehungen und in JSON Form zurück gegeben. Im aktuellen System hingegen werden die Projekte portioniert an den Client geliefert, damit nicht aus Versehen riesige Datensätze an den Client übertragen werden. Um gewährleisten zu können, dass die Antwort vom Server auch die erwarteten Daten liefert, wird ein Test geschrieben (siehe Listing 3.7), der prüft, ob die Antwort dem clientseitig erstellten Interface aus Listing 3.1 entspricht. Für die Validierung wird ein für Ruby entwickelter JSON Schema Validator genutzt. 1 it 'lists a single project ' do 2 FactoryBot.create (: project , :public) 3 get "/project/" 4 5 expect(response).to have_http_status (200) 6 7 parsed = JSON.parse(response.body) 8 expect(parsed['data ']. length).to eq 1 9 10 # Validierung gegen das "Project" interface 11 expect(parsed['data '][0]).to validate_against "Project" 12 end Listing 3.7: Test überprüft, ob bei Anfrage der Route ’/project/’ eine Antwort vom Typ Project folgt • Zeile 2: erstellt eine Project Instanz und speichert diese in der Datenbank. • Zeile 3: schickt ein GET Request an die Route „/project/“. • Zeile 5: erwartet den HTTP Status 200 . • Zeile 7: parst den response body in JSON. • Zeile 8: erwartet, dass die Länge der empfangenen Datensätze 1 ist. • Zeile 11: validiert die Antwort gegen das JSON Schema Project. 22
Der Server hat nun die Fähigkeit auf eine Anfrage nach allen Projekten zu antworten. Somit muss der Client noch die Möglichkeit erhalten, eine Anfrage zu erstellen und die Antwort grafisch abbilden zu können. 3.2.5 Dataservices auf dem Client In der Welt von Angular gibt es eine strikte Trennung zwischen Darstellung und Verar- beitung von Daten [Gooc]. Für die Verarbeitung von Daten - wie das Abrufen - werden Angular Services genutzt. Diese sind typischerweise Typescript Klassen, deren Verwen- dungszwecke genau definiert sind. Der in Listing 3.10 aufgeführte Service hat die Aufgabe Projekt-Daten zu verarbeiten. 1 @Injectable () 2 export class ProjectsDataService { 3 constructor ( private http: HttpClient) { } 4 // Die Antwort soll dem Typparameter "Project" entsprechen 5 readonly projects = this.http.get ('/project/'); 6 } Listing 3.8: Funktion zum Abruf aller Projekte vom Server • Zeile 1: @Injectable stellt sicher, dass der Compiler die notwendigen Metadaten erzeugt, um die Abhängigkeiten der Klasse zu erstellen, wenn die Klasse zur Lauf- zeit injiziert wird. • Zeile 2: Deklarierung der Klasse/des Services ProjectsDataService • Zeile 3: Injektion des HttpClient [Gooe] in den Service • Zeile 5: Nutzung des HttpClient zur Erstellung und zum Abschicken eines typi- sierten HTTP-Requests an die Route aus Listing 3.5. Dieser wird auf die readonly Variable projects geschrieben. Hinzuzufügen ist, dass dieses Beispiel nur bedingt dem aktuellen System entspricht, da eigentlich eine einheitliche Service Klasse mit einem Cache verwendet wird, von der der ProjectsDataService erbt. Diese Komplexität wurde aus Gründen der Übersichtlichkeit ausgelassen. Die Angabe des Antworttyps Project in Zeile 5 fungiert dabei zur Kom- pilierungszeit als Type Assertion [Cora] und erleichtert den Zugriff auf die Attribute der Antwort. Der vom Typescript Compiler erzeugte Javascript Code führt während der Laufzeit jedoch keine Überprüfung durch. Um Typfehler während der Laufzeit zu verhindern, muss der Entwickler spezielle Prüfungen durchführen, wie z.B. der Test in Listing 3.7. 23
Die Darstellung der erhaltenen Daten übernehmen dann Angular Komponenten, in die Services „injiziert“ werden können. Dadurch können Komponenten die Funktionen eines injizierten Services nach Belieben nutzen. 3.2.6 Komponenten auf dem Client Eine Angular Komponente entspricht einem Teilbaum des DOM-Baums, auch View ge- nannt. Somit wird eine Komponente für einen bestimmten Zweck erstellt, der in unserem Fall die grafische Auflistung der Projekt-Daten ist (siehe Listing 3.9). 1 @Component ({ 2 selector: "project -list", 3 templateUrl: "templates/project -list.html", 4 }) 5 export class ProjectListComponent { 6 // Injizierung des ProjectsDataService 7 constructor ( private _projectsData: ProjectsDataService) {} 8 9 readonly projects = this._projectsData.projects; 10 } Listing 3.9: Funktion zum Abruf aller Projekte vom Server • Zeile 1: Annotation einer Typescript-Klasse als Komponente. • Zeile 2: Der Wert von selector kann als HTML-Tag in Templates genutzt werden (), um diese Komponente instanziieren und das zugehörige Template innerhalb des Bezeichners rendern zu können. • Zeile 3: Festlegung des Pfades, wo sich das zu rendernde Template, also der darzu- stellende HTML Code, befindet. • Zeile 5: Deklarierung der Klasse/des Komponente ProjectListComponent. • Zeile 7: Das Injizieren des ProjectsDataService [Gooe] in die Komponente. • Zeile 9: Speichern der Funktion aus dem ProjectsDataService zum Abrufen der Projekt-Daten auf eine Instanzvariable. Eine Komponente stellt HTML Code dar, der in einer Datei gespeichert wird, die als Template bezeichnet wird. Innerhalb des Templates ist der Zugriff auf die nicht priva- ten Variablen der Komponenten gegeben. Das zugehörige Template project-list.html sieht wie in Listing 3.10 aus. 24
1 Listing 3.10: Funktion zum Abruf aller Projekte vom Server • Zeile 1: Aufruf der Komponente mit dem selector „project-list-item“. Diese über- nimmt hier die Darstellung eines einzelnen Projektes innerhalb einer Liste und verdeutlicht damit die Modularität von Angular Komponenten. • Zeile 2: *ngFor ist die „Repeater“-Direktive [Goob] in Angular. Sie ermöglicht ein gegebenes HTML Template einmal für jeden Wert in einem Array zu wiederholen, wobei jedes Mal der Array-Wert als Kontext übergeben wird. Das Array projects kommt aus der Komponente in Listing 3.9 Zeile 9. • Zeile 3: Übergibt den Wert aus dem Array an eine mit @Input() annotierte Variable project in der Komponente mit dem Selektor project-list-item . Diese Schritte sind in ihrer Gänze nur bei der Einführung neuer Entitäten notwendig. Bei der Nutzung von Subtypen kann ein Teil der umgesetzten Schritte wiederverwendet werden, bzw. ist nur einmal erforderlich, wie das Ausführen einer Datenbankmigration. 3.2.7 Anlegen einer neuen Sicht Schritt Beschreibung Aktuelles System √ 3.2.1 Anlegen eines Interfaces √ 3.2.2 Eintrag in Makefile Anlegen einer Datenbank Migration X 3.2.3 Anlegen des Models X √ Route definieren Anlegen des Controllers X 3.2.4 √ Controller Funktion schreiben √ Tests schreiben Anlegen eines Angular Services X 3.2.5 √ Funktion zum Abschicken einer Query √ Anlegen einer Angular Komponenten 3.2.6 √ Anlegen eines Templates Tab. 3.1: Funktionsweise des aktuellen Systems in Bezug auf die Erstellung neuer Sichten auf bereits vorhandene Datensätze Das Hinzufügen eines neuen Datensatzes erfolgt in 12 Schritten (siehe Tabelle 3.1). Der Entwicklungsaufwand für diesen einmaligen Prozesses ist noch vertretbar. Problematisch 25
wird allerdings das wiederholte Anlegen einer neuen Sicht. Wird nur eine Teilmenge der Attribute des erstellten Datensatzes benötigt, müssen die meisten Schritte (8 von 12) wiederholt werden. Je diverser die Sichten auf dem Client werden, desto öfter muss der Prozess, in dem Server und Client gleichermaßen involviert sind, erneut durchlaufen werden. Um Daten zu liefern, die zu der in einem Interface definierten Teilmenge passen, wurden serverseitig mit der Funktion .slice nur die geforderten Attribute extrahiert (ansons- ten Overfetching). Somit entsteht für jede Sicht ein neuer Scope (SQL Abfrage) im Model. Bisher existierten zwei verschiedene Scopes pro Model: to_list_api_response ist für die Darstellung von Projekten für jeden Nutzer im Frontpage Bereich gedacht. to_full_api_response liefert alle im Model enthaltenen und mit dem Model verbun- denen Daten für den Admin Bereich. Wird eine Sicht benötigt, die zu jedem Projekt noch den Namen des zugehörigen Nutzers anzeigt, muss mit der Funktion .includes die Beziehung zur Ergebnismenge hinzugefügt werden, da ansonsten übermäßig viele SQL Queries ausgeführt werden (Underfetching). Möchte der Client also eine neue Sicht erstellen, müssen auf dem Server eine Route, eine Controller Funktion und dazu Tests entwickelt sowie ein Scope für die Antwort geschrieben werden. Daraus ergibt sich ein beachtlicher Aufwand, den es im Kontext der Arbeit zu minimieren gilt. 3.3 Vorteile des bisherigen Ansatzes Die Verwendung des derzeitigen Systems hat viele Vorteile, deren Gewichtung es in Hinsicht auf die Migration von REST nach GraphQL zu evaluieren gilt. Nachfolgend werden die wichtigen Vorteile erläutert. 3.3.1 Typescript Typsystem Ein Vorteil ist die Verwendung des umfangreichen Typescript Typsystems. Dieses ermög- licht neben Typüberprüfungen zur Kompilierungszeit auch Vererbungen zwischen Inter- faces, die Abbildung verschiedenster Typvarianten wie Union Types, zur Ermöglichung verschiedener Typen innerhalb einer Variablen, Intersection Types zum zusammenfügen von Typen, Generische Typen sowie Utility Types um bestehende Typen zu manipulie- 26
Sie können auch lesen