Migration von REST nach GraphQL in einer bestehenden Webapplikation

 
WEITER LESEN
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