# GraphQL 18.04.2024 ## Beschreibung GraphQL ist ein fortschrittlicher API-Standard, der eine effiziente, leistungsstarke und flexible Alternative zu REST bietet. Ursprünglich von Facebook entwickelt und open-source, wird es nun von einer globalen Gemeinschaft gepflegt. Im Gegensatz zu traditionellen REST-APIs mit mehreren festen Endpunkten bietet GraphQL einen einzigen Endpunkt und liefert genau die Daten, die der Client anfragt. Im Kern ermöglicht GraphQL eine deklarative Datenabfrage, bei der Clients genau angeben können, welche Daten sie benötigen. Diese Abfragesprache arbeitet mit einem serverseitigen Laufzeitumgebung zusammen, die Anfragen basierend auf einem anpassbaren Typsystem für die Daten ausführt. GraphQL ist nicht an eine bestimmte Datenbank oder Speicherengine gebunden. ## Entwicklungs-Setup ### Vorbedingungen #### Backend ##### Docker [Installation per `apt` Package Manager](https://docs.docker.com/engine/install/ubuntu/) (Advanced Packaging-Tool) 1. alte Pakete deinstallieren `for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done` 2. Docker apt Repository registrieren ``` # Add Docker's official GPG key: sudo apt-get update sudo apt-get install ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc # Add the repository to Apt sources: echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update ``` 3. Docker installieren `sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin` 4. (optional) Installation prüfen durch Ausführen des Hello-World images `sudo docker run hello-world` #### Frontend ##### yarn 1. [Node.js installieren](https://github.com/nodesource/distributions) (per `apt` Package Manager) (getestet unter Ubuntu, siehe [hier](https://github.com/nodesource/distributions?tab=readme-ov-file#table-of-contents) für andere Linux Distributionen) ``` curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - &&\ sudo apt-get install -y nodejs ``` 2. Yarn Feature Flag aktivieren: ``` corepack enable ``` 3. Prüfen, ob die Installation erfolgreich war: 1. `yarn --version` ausführen => Version sollte `1.x.x` sein 2. zu `/todo-app/packages/todo-app/` navigieren 3. `yarn --version` ausführen => Version sollte `4.x.x` sein ### Projekt initialisieren und Abhängigkeiten installieren 1. zu `/todo-app/` navigieren 2. ``` yarn ``` ausführen ### Starten der Anwendung **Der Stand der Svelte Anwendung mit GraphQL befindet sich auf dem Git branch `svelte-graphql-dotnet`!** 1. zu `/todo-app/` navigieren 2. ``` yarn be:docker:create-image ``` ausführen 3. ``` yarn be:docker:run ``` ausführen 4. ``` yarn fe:build ``` ausführen 5. ``` yarn fe:run ``` ausführen 6. Anwendung öffnen [http://localhost:4173/](http://localhost:5173/) Das .NET Backend kann mit `yarn be:docker:stop` wieder gestoppt werden. ### Erläuterungen - `yarn` (alias für `yarn install`) installiert alle npm Pakete, die als Abhängigkeiten der Svelte Anwendung gelistet sind. Außerdem wird das Projekt initialisiert, sodass yarn scripte ausgeführt werden können. - `yarn be:docker:create-image` baut die .NET API in einem Docker Container und erstellt ein lokales Docker Image `dva/todo-api` - `yarn be:docker:run` verwendet Docker compose, um - einen `todo-api` Container zu starten - einen `postgres` Container zu starten - `yarn fe:build` baut die Svelte Anwendung - `yarn fe:run` startet die Svelte Anwendung ## Vorteile - stark typisiertes Schema: klar definierte Operationen, einschließlich Eingabeargumenten und mögliche Antworten - kein Underfetching und Overfetching: Client diktiert die Form der Abfragen und entscheidet welche expliziten Felder abgefragt werden sollen - ermöglicht Frontend-getriebene Entwicklung: die Views können unabhängig von etwaigen Endpunkten entwickelt werden und umgekehrt - API Kompositionen möglich: eine GraphQL API kann aus mehreren anderen GraphQL APIs zusammengesetzt werden - zahlreiche Open-Source-Ressourcen und große Community ## REST vs. GraphQL Ausgangssituation: In einer Blogging-Anwendung muss eine App die Titel der Beiträge eines bestimmten Benutzers anzeigen. Auf demselben Bildschirm werden auch die Namen der letzten 3 Follower dieses Benutzers angezeigt. Mit REST müssen mehrere Queries an verschiedene Endpunkte getätigt werden, um die benötigten Daten abzurufen. ![Blogging Anwendung mit REST](https://i.imgur.com/VRyV7Jh.png) Mit GraphQL kann der Client in einer Query explizit die benötigten Daten angeben. ![Blogging Anwendung mit GraphQL](https://i.imgur.com/z9VKnHs.png) ## Schema Definition Language (SDL) Das Schema beschreibt, wie der Client Daten abfragen kann und definiert somit die Möglichkeiten bei der Verwendung der API. Das Schema wird grundlegend aus einer Zusammenstellung von Typen implementiert. ``` type Post { title: String! author: Person! } ``` Der Typ im oberen Beispiel definiert einen Post in einer Blogging-Anwendung. Ein Post wird durch einen Titel `title` und einem Autor `author` definiert. Die beiden Felder werden durch das `!` als "required" markiert. ``` type Person { name: String! age: Int! posts: [Post!]! } ``` Der Typ `Person` wird mit den Feldern `name`, `age` und `posts` definiert, wobei letzteres als Array vom Typ `Post` festgelegt und somit eine 1-n-Beziehung implementiert wurde. Neben den einfachen Typen gibt es auch sogenannte root-Typen Query, Mutation und Subscription, welche als konzeptionelle Entrypoints für Client-Requests dienen. ``` type Query { allPersons: [Person!]! } ``` Die Query `allPersons` dient demnach zur Abfrage aller Personen. ``` type Mutation { createPerson(name: String!, age: Int!): Person! } ``` Das `createPerson`-Objekt des root-Typs `Mutation` dient zur Erstellung eines neuen `Person`-Eintrags. Diese Mutation bietet zwei Parameter für den Namen und das Alter der Person an. Mithilfe von Mutationen können `create`-, `update`- und `delete`-Operationen implementiert werden. ## Queries (Client) ``` query GetPersonData { person(name: "Johnny") { name age posts { title } } } ``` Diese Query nimmt ein Namen als Argument und gibt den Namen, das Alter und die Titel der Blog-Einträge der Person aus. Das keyword `query` und den Namen der Operation `GetPersonData` sind zwar optional, können aber das Debuggen erleichtern und Mehrdeutigkeit verhindern. Zusätzlich dazu können über Parameter des `operation name` Variablen wie folgt definiert werden: ``` query GetPersonData($personName: String! = "Johnny") { person(name: $personName) { name age posts { title } } } ``` Dadurch können redundante Query-Definitionen verhindert werden. Im Allgemeinen funktionieren Variablen identisch, wie Parameter von Funktionen in gängigen Programmiersprachen. Sie werden mit `$` gekennzeichnet und weisen immer einen Typ (in diesem Fall `String!` auf). Darüber hinaus kann man den Variablen Default-Werte zuweisen (hier "Johnny"). Verwendet werden diese Abfragen wie folgt: ``` { "personName": "Johnny" } ``` Die Verwendung der vordefinierten "Endpunkte" `allPersons` und `createPerson` funktioniert wie folgt: ``` { allPersons { name age posts { title } } } ``` Unter Verwendung der `allPersons`-Query werden hier von allen Personen in der Datenbank der Name, das Alter und die Überschriften aller Blog-Einträge der jewiligen Person abgefragt. Wenn lediglich die Blog-Beiträge, jedoch nicht die Namen oder das Alter der Personen benötigt werden, können die entsprechenden Felder in der Abfrage einfach ausgelassen werden, ohne weitere Entrypoints in der API definieren zu müssen. ``` mutation CreatePerson($name: String!, $age: Int!) { createPerson(name: $name, age: $age) { name age } } ``` Mithilfe von Variablen kann auch die `createPerson`-Operation implementiert werden. ## Verwendung von GraphQL in der TodoApp ### Umsetzung im Backend mit GraphQL.NET [GraphQL.NET](https://graphql-dotnet.github.io/) ist ein open source NuGet Paket für die .NET Umgebung. Es erlaubt das Definieren von GraphQL Queries und Mutations, ohne dabei auf starke Typisierung verzichten zu müssen. Dabei können beliebige Datenquellen verwendet werden. Beliebige Queries können in einer Klasse definiert werden, die von `ObjectGraphType` erbt: ``` public sealed class ToDoQuery : ObjectGraphType { public ToDoQuery(IToDoService toDoService) { Name = "Query"; Field("toDos") .ResolveAsync(async _ => new ToDos { Lists = await toDoService.GetLists() }); } } ``` Dabei muss ein Typ für die Query und ein Typ für den Return Type angegeben werden. Die Typen der Query müssen dabei das interface `IGrpahType` implementieren. Anschließend können Queries und Mutations dem Schema hinzugefügt werden: ``` public class ToDoSchema : Schema { public ToDoSchema(ToDoQuery query, ToDoMutation mutation, IServiceProvider sp) : base(sp) { Query = query; Mutation = mutation; } } ``` Das Schema, die Queries und die Mutations können anschließend in der Startup registriert werden. Zusätzlich muss ein Json Serializer spezifiziert werden: ``` builder.Services.AddGraphQL(graphQlBuilder => graphQlBuilder .AddSystemTextJson() .AddSchema(ServiceLifetime.Scoped) .AddGraphTypes()); ``` Mit der folgenden Zeile wird dann automatisch der GraphQL endpunkt erzeugt: ``` app.UseGraphQL(); ``` ### ApolloClient ApolloClient dient als Bibliothek für die Verwaltung von Datenbankzugriffen über GraphQL. Die Bibliothek übernimmt den gesamten Anfragezyklus von Anfang bis Ende und bietet dabei ggf. Lade- und Fehlerzustände an. Zusätzlich dazu bietet ApolloCLient weitere Features, die den Umgang mit GraphQL vereinfachen können. Zum Beispiel ist es möglich in einer Query auch lokale Daten einzubeziehen. ``` const GET_DOG = gql` query GetDogByBreed($breed: String!) { dog(breed: $breed) { images { url id isLiked @client } } } `; ``` In dem oberen Beispiel wird durch das Keyword `@client` angegeben, dass es sich beim Feld `isLiked` um einen Boolean im lokalen Storage handelt. Ein weiteres Key-Feature des ApolloClients ist die Implementierung eines lokalen Caches. ``` const client = new ApolloClient({ cache: new InMemoryCache(), }); ``` Sobald man einen Cache an ApolloClient übergeben hast, versucht Apollo Client automatisch, die unterschiedlichen Objekte aus den Antwortdaten einer Abfrage zu identifizieren und in separate Einträge innerhalb seines Caches zu speichern. In Verbindung mit dem Package `svelte-apollo` lassen sich auch weitere Szenarien umsetzen: ```js const books = query(SEARCH_BY_AUTHOR, { variables: { author, search }, }); // `books` is refetched when author or search change $: books.refetch({ author, search }); ``` In diesem Codebeispiel wird eine weitere Query abgesetzt, sobald sich entweder der Wert der Variable `author` oder `search` ändert. Damit ist recht wenig Code erforderlich, um dynamisch auf Änderungen zu reagieren und neue Daten abzufragen. ### Erzeugung des Apollo-Clients Im Folgenden wird die Funktion setApolloClient definiert, die den ApolloClient mit der gegebenen Backend-URL und einem In-Memory-Cache initialisiert. ```js export function setApolloClient() { const client = new ApolloClient({ uri: 'https://localhost:5000/graphql', cache: new InMemoryCache(), }); setClient(client); return client; } ``` ### Aufrufen von Funktionen des Clients Der Aufruf der Funktion `query` auf den `client` verlangt eine query, die optional Parameter enthalten kann. Die Queries haben wir in der Datei `graphql-api.ts` hinterlegt und in den jeweils benötigten Dateien importiert. Die `getAllLists` Query schaut wie folgt aus: ```js export const getAllLists = gql` query { toDos { lists { id name # description <- desch isn kommentar und wird nicht mitgefetscht items { id sequenceNumber name # description <- desch isn auchn kommentar und wird ebenso nicht mitgefetscht } } } }`; ``` In der App.svelte wird der Apollo-Client erstmalig initialisiert und in den Kontext gesetzt und anschließend die `client.query` Funktion aufgerufen. Den restlichen schon bestehenden Code unserer Todo-App, der auf einer Rest HTTP Schnittstelle aufgebaut war, konnten wir größtenteils behalten, da die `query`-Funktion nach wie vor ein Promise zurückliefert. ```js const client = setApolloClient(); const todoListsPromise = client.query({ query: getAllLists, }).then((result) => { const { lists }= result.data.toDos; selectedListId = lists[0]?.id; todoLists = lists; return lists; }); ``` Nach dem einmaligen Setzen des Clients in der App.svelte mit `setApolloClient()` kann dieser mithilfe der `getClient()` in jeder anderen Svelte-Komponente, die in der App-Komponente genested ist, aufgerufen werden. Im Hintergrund wird hier der Svelte Kontext verwendet, welcher es ermöglicht, Daten zu setzen, die innerhalb des DOM Trees für alle darunterstehenden Komponenten verfügbar sind. ## Nachteile von GraphQL - Komplexität: Bei einfachen REST-APIs mit konsistenten Daten ist REST vorzuziehen, während GraphQL für Projekte mit sich schnell ändernden Daten und Ressourcen bei der Anpassung der API viele Probleme lösen kann - erschwertes Caching: GraphQL verwendet im Gegensatz zu REST keine GET-Nachrichten (sondern POSTs), wodurch das klar definierte und durch Browser und Webserver unterstütze Caching-Verhalten nicht übertragen werden kann - sicherheitskritisch: GraphQL kann bei unvorsichtiger Implementierung sicherheitslücken öffnen und z.B. Table-Scans ermöglichen ### Fazit GraphQL bietet einige Vorteile, darunter Flexibilität und Effizienz. Da auch nur ein einziger Endpoint für die Abfrage nötig ist, ist GraphQL eine gute Alternative zu REST, wenn es sich um komplexere Projekte, mit schnell ändernden Daten mit großen Datensätzen handelt. Im Bezug auf eine Todo-Listen Anwendung reicht aufgrund der Einfachheit eine REST-API vollkommen aus. Die Struktur der Anwendung ist relativ einfach strukturiert und erfordert nicht die Komplexität die GraphQL bietet. Daher ist eine REST-API ausreichend um die CRUD-Operationen auf dieses Datenmodell zu unterstützen. Zudem sollte man abschließend noch erwähnen, dass das Errorhandling und die höhere Komplexität im Vergleich zu REST es erschwert, schnell und effizient zu Entwickeln. ## Quellen - [GraphQL Dokumentation](https://graphql.org/learn/) - [Svelte Apollo: GitHub](https://github.com/timhall/svelte-apollo) - [Svelte Apollo: Tutorial](https://hasura.io/learn/graphql/svelte-apollo) - [How to GraphQL](https://www.howtographql.com/) - [YouTube: What is GraphQL?](https://www.youtube.com/watch?v=yWzKJPw_VzM) - [Advantages and Disadvantages of GraphQL](https://stablekernel.com/article/advantages-and-disadvantages-of-graphql/)