Kapitel 4. Ein Schema entwerfen
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
GraphQL wird deinen Entwurfsprozess verändern. Anstatt deine APIs als eine Sammlung von REST-Endpunkten zu betrachten, wirst du deine APIs als Sammlungen von Typen betrachten. Bevor du mit deiner neuen API loslegst, musst du über die Datentypen, die deine API offenlegen wird, nachdenken, sie besprechen und formell definieren. Diese Sammlung von Typen wird als Schema bezeichnet.
Schema First ist eine Entwurfsmethodik, die alle Teams auf die gleiche Seite der Datentypen bringt, aus denen deine Anwendung besteht. Das Backend-Team hat eine klare Vorstellung von den Daten, die es speichern und bereitstellen muss. Das Frontend-Team hat die Definitionen, die es braucht, um mit der Entwicklung von Benutzeroberflächen zu beginnen. Alle haben ein klares Vokabular, mit dem sie über das System, das sie aufbauen, kommunizieren können. Kurz gesagt: Alle können sich an die Arbeit machen.
Um die Definition von Typen zu vereinfachen, verfügt GraphQL über eine Sprache, mit der wir unsere Schemata definieren können: die Schema Definition Language (SDL). Genau wie die GraphQL Query Language ist die GraphQL SDL unabhängig von der Sprache oder dem Framework, das du für deine Anwendungen verwendest, gleich. GraphQL-Schemadokumente sind Textdokumente, die die in einer Anwendung verfügbaren Typen definieren und später sowohl von Clients als auch von Servern zur Validierung von GraphQL-Anfragen verwendet werden.
In diesem Kapitel werfen wir einen Blick auf die GraphQL SDL und erstellen ein Schema für eine Foto-Sharing-Anwendung.
Typen definieren
Der beste Weg, um etwas über GraphQL-Typen und Schemata zu lernen, ist, eines zu bauen. In der Anwendung zum Teilen von Fotos können sich die Nutzer mit ihrem GitHub-Konto anmelden, um Fotos zu posten und Nutzer auf diesen Fotos zu markieren. Die Verwaltung von Nutzern und Beiträgen ist eine Funktion, die für fast jede Internetanwendung wichtig ist.
Die PhotoShare-Anwendung wird zwei Haupttypen haben: User
und Photo
. Beginnen wir mit dem Entwurf des Schemas für die gesamte Anwendung.
Typen
Die zentrale Einheit eines jeden GraphQL-Schemas ist der Typ. In GraphQL stellt ein Typein benutzerdefiniertes Objekt dar und diese Objekte beschreiben die Kernfunktionen deiner Anwendung. Eine Social-Media-Anwendung besteht zum Beispiel ausUsers
und Posts
. Ein Blog würde aus Categories
und Articles
bestehen. Die Typen repräsentieren die Daten deiner Anwendung.
Wenn du Twitter von Grund auf neu aufbauen würdest, würde ein Post
den Text enthalten, den der Nutzer senden möchte. (In diesem Fall wäre Tweet
vielleicht ein besserer Name für diesen Typ.) Wenn du Snapchat aufbaust, würde Post
ein Bild enthalten, das besserSnap
genannt werden sollte. Wenn du ein Schema definierst, legst du eine gemeinsame Sprache fest, die dein Team verwenden wird, wenn es über deine Domänenobjekte spricht.
Ein Typ hat Felder, die die mit jedem Objekt verbundenen Daten darstellen. Jedes Feld gibt eine bestimmte Art von Daten zurück. Das kann eine ganze Zahl oder ein String sein, aber auch ein benutzerdefinierter Objekttyp oder eine Liste von Typen.
Ein Schema ist eine Sammlung von Typdefinitionen. Du kannst deine Schemas in eine JavaScript-Datei als String oder in eine beliebige Textdatei schreiben. Diese Dateien haben normalerweise die Erweiterung .graphql
.
Definieren wir den ersten GraphQL-Objekttyp in unserer Schemadatei - Photo
:
type Photo { id: ID! name: String! url: String! description: String }
Zwischen den geschweiften Klammern haben wir die Felder von Photo
definiert. Die url
der Photo
ist ein Verweis auf den Speicherort der Bilddatei. Diese Beschreibung enthält auch einige Metadaten über das Photo
: ein name
und ein description
. Schließlich hat jedes Photo
ein ID
, einen eindeutigen Bezeichner, der als Schlüssel für den Zugriff auf das Foto verwendet werden kann.
Jedes Feld enthält Daten eines bestimmten Typs. Wir haben in unserem Schema nur einen benutzerdefinierten Typ definiert, den Photo
, aber GraphQL verfügt über einige eingebaute Typen, die wir für unsere Felder verwenden können. Diese eingebauten Typen werden skalare Typen genannt. Die Felder description
, name
und url
verwenden den Skalar-TypString
. Die Daten, die zurückgegeben werden, wenn wir diese Felder abfragen, sind JSON-Strings. Das Ausrufezeichen gibt an, dass das Feld nicht nullbar ist, was bedeutet, dass die Felder name
und url
bei jeder Abfrage Daten zurückgeben müssen. description
ist löschbar, was bedeutet, dass die Fotobeschreibungen optional sind. Bei einer Abfrage könnte dieses Feld null
zurückgeben.
Das Feld Photo
's ID
gibt einen eindeutigen Bezeichner für jedes Foto an. In GraphQL wird der Skalar-Typ ID
verwendet, wenn ein eindeutiger Bezeichner zurückgegeben werden soll. Der JSON-Wert für diesen Bezeichner ist ein String, der jedoch als eindeutiger Wert validiert wird.
Skalare Typen
Die eingebauten Skalartypen von GraphQL (Int
, Float
, String
, Boolean
,ID
) sind sehr nützlich, aber es kann vorkommen, dass du deine eigenen Skalartypen definieren möchtest. Ein Skalartyp ist kein Objekttyp. Er hat keine Felder. Bei der Implementierung eines GraphQL-Dienstes kannst du jedoch angeben, wie benutzerdefinierte Skalartypen validiert werden sollen, zum Beispiel:
scalar DateTime type Photo { id: ID! name: String! url: String! description: String created: DateTime! }
Hier haben wir einen benutzerdefinierten Skalar-Typ erstellt: DateTime
. Jetzt können wir herausfinden, wann jedes Foto created
war. Jedes Feld, das mit DateTime
gekennzeichnet ist, gibt einen JSON-String zurück, aber wir können den benutzerdefinierten Skalar verwenden, um sicherzustellen, dass dieser String serialisiert, validiert und als offizielles Datum und Uhrzeit formatiert werden kann.
Du kannst benutzerdefinierte Skalare für jeden Typ deklarieren, den du validieren musst.
Hinweis
Das graphql-custom-types npm-Paket enthält einige häufig verwendete benutzerdefinierte Skalartypen, die du schnell zu deinem Node.js GraphQL-Dienst hinzufügen kannst.
Enums
Aufzählungstypen oder Enums sind skalare Typen, die es einem Feld ermöglichen, einen begrenzten Satz von Stringwerten zurückzugeben. Wenn du sicherstellen willst, dass ein Feld einen Wert aus einer begrenzten Menge von Werten zurückgibt, kannst du einen enum
Typ verwenden.
Erstellen wir zum Beispiel einen enum
Typ mit dem Namen PhotoCategory
, der die Art des Fotos definiert, das gepostet wird, und zwar aus einer Reihe von fünf möglichen Optionen: SELFIE
, PORTRAIT
, ACTION
, LANDSCAPE
oder GRAPHIC
:
enum PhotoCategory { SELFIE PORTRAIT ACTION LANDSCAPE GRAPHIC }
Du kannst Aufzählungstypen verwenden, um Felder zu definieren. Fügen wir ein category
Feld zu unserem Photo
Objekttyp hinzu:
type Photo { id: ID! name: String! url: String! description: String created: DateTime! category: PhotoCategory! }
Jetzt, wo wir category
hinzugefügt haben, stellen wir sicher, dass er einen der fünf gültigen Werte zurückgibt, wenn wir den Dienst implementieren.
Verbindungen und Listen
Wenn du GraphQL-Schemas erstellst, kannst du Felder definieren, die Listen von beliebigen GraphQL-Typen zurückgeben. Listen werden erstellt, indem man einen GraphQL-Typ mit eckigen Klammern umgibt. [String]
definiert eine Liste von Strings und[PhotoCategory]
eine Liste von Fotokategorien. Wie in Kapitel 3 beschrieben, können Listen auch aus mehreren Typen bestehen, wenn wir die Typenunion
oder interface
einbeziehen. Am Ende dieses Kapitels werden wir diese Arten von Listen ausführlicher besprechen.
Manchmal kann das Ausrufezeichen bei der Definition von Listen ein wenig knifflig sein. Wenn das Ausrufezeichen nach der schließenden eckigen Klammer steht, bedeutet es, dass das Feld selbst nicht nullbar ist. Wenn das Ausrufezeichen vor der schließenden eckigen Klammer steht, bedeutet das, dass die in der Liste enthaltenen Werte nicht nullbar sind. Wo immer du ein Ausrufezeichen siehst, ist der Wert erforderlich und kann nicht null zurückgeben. Tabelle 4-1 definiert diese verschiedenen Situationen.
Liste Erklärung | Definition |
---|---|
|
Eine Liste von löschbaren Integer-Werten |
|
Eine Liste von nicht-nullbaren Integer-Werten |
|
Eine nicht löschbare Liste von löschbaren Integer-Werten |
|
Eine nicht-nullbare Liste von nicht-nullbaren Integer-Werten |
Die meisten Listendefinitionen sind nicht-nullbare Listen mit nicht-nullbaren Werten. Das liegt daran, dass wir in der Regel nicht wollen, dass die Werte in unserer Liste Null sind. Wir sollten alle Nullwerte im Voraus herausfiltern. Wenn unsere Liste keine Werte enthält, können wir einfach ein leeres JSON-Array zurückgeben, zum Beispiel[]
. Ein leeres Array ist technisch gesehen nicht null: Es ist einfach ein Array, das keine Werte enthält.
Die Möglichkeit, Daten zu verbinden und mehrere Arten von zusammenhängenden Daten abzufragen, ist eine sehr wichtige Funktion. Wenn wir Listen mit unseren benutzerdefinierten Objekttypen erstellen, nutzen wir diese leistungsstarke Funktion und verbinden Objekte miteinander.
In diesem Abschnitt erfährst du, wie du eine Liste verwenden kannst, um Objekttypen zu verbinden.
Eins-zu-Eins-Verbindungen
Wenn wir Felder erstellen, die auf benutzerdefinierten Objekttypen basieren, verbinden wir zwei Objekte. In der Graphentheorie wird eine Verbindung oder ein Link zwischen zwei Objekten als Kante bezeichnet. Die erste Art von Verbindung ist eine Eins-zu-Eins-Verbindung, bei der wir einen einzelnen Objekttyp mit einem anderen einzelnen Objekttyp verbinden.
Fotos werden von Nutzern gepostet, daher sollte jedes Foto in unserem System eine Kante enthalten, die das Foto mit dem Nutzer verbindet, der es gepostet hat. Abbildung 4-1 zeigt eine einseitige Verbindung zwischen zwei Typen: Photo
und User
. Die Kante, die die beiden Knoten miteinander verbindet, heißt postedBy
.
Schauen wir uns an, wie wir dies im Schema definieren würden:
type User { githubLogin: ID! name: String avatar: String } type Photo { id: ID! name: String! url: String! description: String created: DateTime! category: PhotoCategory! postedBy: User! }
Zunächst haben wir einen neuen Typ zu unserem Schema hinzugefügt, User
. Die Nutzer der PhotoShare-Anwendung werden sich über GitHub anmelden. Wenn der Nutzer sich anmeldet, erhalten wir seine githubLogin
und verwenden sie als eindeutigen Bezeichner für seinen Nutzerdatensatz. Wenn er/sie seinen/ihren Namen oder sein/ihr Foto auf GitHub hinzugefügt hat, speichern wir diese Informationen optional in den Feldernname
und avatar
.
Als Nächstes fügen wir die Verbindung hinzu, indem wir ein postedBy
Feld zum Fotoobjekt hinzufügen. Da jedes Foto von einem Nutzer gepostet werden muss, wird dieses Feld auf den Typ User!
gesetzt; das Ausrufezeichen wird hinzugefügt, damit das Feld nicht leer ist.
One-to-Many-Verbindungen
Es ist eine gute Idee, GraphQL-Dienste möglichst ungerichtet zu halten. Das gibt unseren Kunden die größtmögliche Flexibilität bei der Erstellung von Abfragen, da sie den Graphen von jedem beliebigen Knoten aus durchlaufen können. Alles, was wir tun müssen, um diese Praxis zu befolgen, ist, einen Pfad zurück von User
Typen zu Photo
Typen zu erstellen. Das bedeutet, dass wir bei einer Abfrage von User
alle Fotos sehen sollten, die ein bestimmter Nutzer gepostet hat:
type User { githubLogin: ID! name: String avatar: String postedPhotos: [Photo!]! }
Indem wir das Feld postedPhotos
zum Typ User
hinzugefügt haben, haben wir einen Pfad zurück zum Photo
des Nutzers erstellt. Das Feld postedPhotos
gibt eine Liste der Photo
Typen zurück, also der Fotos, die vom übergeordneten Benutzer gepostet wurden. Da ein Benutzer viele Fotos posten kann, haben wir eine One-to-many-Verbindung erstellt. One-to-many-Verbindungen, wie sie in Abbildung 4-2 dargestellt sind, werden häufig erstellt, wenn ein übergeordnetes Objekt ein Feld enthält, das andere Objekte auflistet.
Ein gängiger Ort, um One-to-Many-Verbindungen hinzuzufügen, sind unsere Root-Typen. Um unsere Fotos oder Benutzer in einer Abfrage verfügbar zu machen, müssen wir die Felder unseres Query
Stammtyps definieren. Schauen wir uns an, wie wir unsere neuen benutzerdefinierten Typen zum Root-Typ Query
hinzufügen können:
type Query { totalPhotos: Int! allPhotos: [Photo!]! totalUsers: Int! allUsers: [User!]! } schema { query: Query }
Das Hinzufügen des Typs Query
definiert die Abfragen, die in unserer API verfügbar sind. In diesem Beispiel haben wir zwei Abfragen für jeden Typ hinzugefügt: eine, um die Gesamtzahl der verfügbaren Datensätze für jeden Typ zu liefern, und eine weitere, um die vollständige Liste dieser Datensätze zu liefern. Außerdem haben wir den Typ Query
auf schema
als Datei hinzugefügt. Damit sind unsere Abfragen in unserer GraphQL-API verfügbar.
Jetzt können unsere Fotos und Benutzer mit dem folgenden Abfrage-String abgefragt werden:
query { totalPhotos allPhotos { name url } }
Many-to-Many-Verbindungen
Manchmal wollen wir Listen von Knotenpunkten mit anderen Listen von Knotenpunkten verbinden. Unsere PhotoShare-Anwendung ermöglicht es Nutzern, andere Nutzer auf jedem Foto, das sie veröffentlichen, zu identifizieren. Dieser Vorgang wird als Tagging bezeichnet. Ein Foto kann aus vielen Nutzern bestehen, und ein Nutzer kann in vielen Fotos markiert sein, wie Abbildung 4-3 zeigt.
Um diese Art von Verbindung zu erstellen, müssen wir Listenfelder sowohl zu den Typen User
als auch zu Photo
hinzufügen.
type User { ... inPhotos: [Photo!]! } type Photo { ... taggedUsers: [User!]! }
Wie du siehst, besteht eine Many-to-Many-Verbindung aus zweiOne-to-Many-Verbindungen. In diesem Fall kann eine Photo
viele getaggte Nutzer haben und eine User
kann in vielen Fotos getaggt sein.
Durch Typen
Manchmal möchtest du bei der Erstellung von Many-to-Many-Beziehungen einige Informationen über die Beziehung selbst speichern. Da es in unserer Foto-Sharing-App keinen wirklichen Bedarf für einen Durchgangstyp gibt, verwenden wir ein anderes Beispiel, um einen Durchgangstyp zu definieren: eine Freundschaft zwischen Benutzern.
Wir können viele Nutzer mit vielen Nutzern verbinden, indem wir ein Feld unterUser
definieren, das eine Liste anderer Nutzer enthält:
type User { friends: [User!]! }
Hier haben wir für jeden Nutzer eine Liste von Freunden definiert. Nehmen wir an, wir wollen Informationen über die Freundschaft selbst speichern, z. B. wie lange sich die Nutzer kennen oder wo sie sich kennengelernt haben.
In diesem Fall müssen wir die Kante als einen benutzerdefinierten Objekttyp definieren. Wir nennen dieses Objekt einen Durchgangstyp, weil es ein Knoten ist, der zwei Knoten verbinden soll. Definieren wir einen Durchgangstyp namensFriendship
, mit dem wir zwei Freunde verbinden können, der aber auch Daten darüber liefert, wie die Freunde verbunden sind:
type User { friends: [Friendship!]! } type Friendship { friend_a: User! friend_b: User! howLong: Int! whereWeMet: Location }
Anstatt das Feld friends
direkt auf einer Liste von anderenUser
Typen zu definieren, haben wir ein Friendship
erstellt, um die friends
zu verbinden. Der TypFriendship
definiert die beiden verbundenen Freunde: friend_a
undfriend_b
. Er definiert auch einige Detailfelder darüber, wie die Freunde verbunden sind: howLong
und whereWeMet
. Das Feld howLong
ist ein Int
, das die Länge der Freundschaft definiert, und das Feld whereWeMet
verweist auf einen benutzerdefinierten Typ namens Location
.
Wir können das Design des Friendship
Typs verbessern, indem wir eine Gruppe von Freunden als Teil der Freundschaft zulassen. Vielleicht hast du deine besten Freunde in der ersten Klasse zur gleichen Zeit kennengelernt. Wir können zwei oder mehr Freunde in die Freundschaft aufnehmen, indem wir ein einziges Feld namens friends
hinzufügen:
type Friendship { friends: [User!]! how_long: Int! where_we_met: Location }
Wir haben nur ein Feld für alle friends
in einerFriendship
aufgenommen. Jetzt kann dieser Typ zwei oder mehr Freunde widerspiegeln.
Listen mit verschiedenen Typen
In GraphQL müssen unsere Listen nicht immer den gleichen Typ zurückgeben. In Kapitel 3 haben wir die Typen union
und interfaces
vorgestellt und gelernt, wie man mit Fragmenten Abfragen für diese Typen schreibt. Schauen wir uns nun an, wie wir diese Typen in unser Schema einfügen können.
Wir werden hier einen Zeitplan als Beispiel verwenden. Du könntest einen Zeitplan haben, der aus verschiedenen Ereignissen besteht, die jeweils unterschiedliche Datenfelder erfordern. Zum Beispiel können die Details zu einem Treffen einer Lerngruppe oder einem Training völlig unterschiedlich sein, aber du solltest in der Lage sein, beide zu einem Zeitplan hinzuzufügen. Du kannst dir einen Tagesplan als eine Liste mit verschiedenen Arten von Aktivitäten vorstellen.
Es gibt zwei Möglichkeiten, wie wir ein Schema für einen Zeitplan in GraphQL definieren können: Unions und Schnittstellen.
Gewerkschaftstypen
In GraphQL ist ein Union-Typ ein Typ, mit dem wir einen von mehreren verschiedenen Typen zurückgeben können. In Kapitel 3 haben wir eine Abfrage namens schedule
geschrieben, die eine Agenda abfragte und andere Daten zurückgab, wenn es sich bei dem Tagesordnungspunkt um ein Training handelte, als wenn es sich um eine Lerngruppe handelte. Schauen wir uns das hier noch einmal an:
query schedule { agenda { ...on Workout { name reps } ...on StudyGroup { name subject students } } }
In der täglichen Agenda des Schülers/der Schülerin könnten wir dies durch die Erstellung eines union
Typs namens AgendaItem
handhaben:
union AgendaItem = StudyGroup | Workout type StudyGroup { name: String! subject: String students: [User!]! } type Workout { name: String! reps: Int! } type Query { agenda: [AgendaItem!]! }
AgendaItem
fasst Lerngruppen und Trainings unter einem einzigen Typ zusammen. Wenn wir das Feld agenda
zu unserem Query
hinzufügen, definieren wir es als eine Liste von Trainings oder Lerngruppen.
Es ist möglich, so viele Typen wie gewünscht unter einer einzigen Vereinigung zusammenzufassen. Trenne einfach jeden Typ mit einer Pipe:
union = StudyGroup | Workout | Class | Meal | Meeting | FreeTime
Schnittstellen
Eine andere Möglichkeit, Felder zu behandeln, die mehrere Typen enthalten können, ist die Verwendung einer Schnittstelle. Schnittstellen sind abstrakte Typen, die von Objekttypen implementiert werden können. Eine Schnittstelle definiert alle Felder, die in jedem Objekt enthalten sein müssen, das sie implementiert. Schnittstellen sind eine gute Möglichkeit, den Code in deinem Schema zu organisieren. So wird sichergestellt, dass bestimmte Typen immer bestimmte Felder enthalten, die abgefragt werden können, egal welcher Typ zurückgegeben wird.
In Kapitel 3 haben wir eine Abfrage für ein agenda
geschrieben, die eine Schnittstelle verwendet, um Felder für verschiedene Elemente in einem Zeitplan zurückzugeben. Das wollen wir hier wiederholen:
query schedule { agenda { name start end ...on Workout { reps } } }
So könnte die Abfrage einer agenda
aussehen, die Schnittstellen implementiert. Damit ein Typ eine Schnittstelle zu unserem Zeitplan bilden kann, muss er bestimmte Felder enthalten, die alle Tagesordnungspunkte implementieren. Zu diesen Feldern gehören die Zeiten name
, start
und end
. Es spielt keine Rolle, welche Art von Terminplanungselement du hast, sie alle brauchen diese Angaben, um in einem Terminplan aufgeführt zu werden.
Hier sehen wir, wie wir diese Lösung in unserem GraphQL-Schema umsetzen würden:
scalar DataTime interface AgendaItem { name: String! start: DateTime! end: DateTime! } type StudyGroup implements AgendaItem { name: String! start: DateTime! end: DateTime! participants: [User!]! topic: String! } type Workout implements AgendaItem { name: String! start: DateTime! end: DateTime! reps: Int! } type Query { agenda: [AgendaItem!]! }
In diesem Beispiel erstellen wir eine Schnittstelle namens AgendaItem
. Diese Schnittstelle ist ein abstrakter Typ, den andere Typen implementieren können. Wenn ein anderer Typ eine Schnittstelle implementiert, muss er die von der Schnittstelle definierten Felder enthalten. Sowohl StudyGroup
als auch Workout
implementieren die Schnittstelle AgendaItem
, also müssen beide die Felder name
, start
und end
verwenden. Die Abfrage agenda
gibt eine Liste der AgendaItem
Typen zurück. Jeder Typ, der die Schnittstelle AgendaItem
implementiert, kann in der Listeagenda
zurückgegeben werden.
Beachte auch, dass diese Typen auch andere Felder implementieren können. EinStudyGroup
hat ein topic
und eine Liste von participants
, und ein Workout
hat noch reps
. Du kannst diese zusätzlichen Felder in einer Abfrage auswählen, indem du Fragmente verwendest.
Sowohl Union-Typen als auch Schnittstellen sind Werkzeuge, mit denen du Felder erstellen kannst, die verschiedene Objekttypen enthalten. Es liegt an dir zu entscheiden, wann du das eine oder das andere verwendest. Im Allgemeinen ist es eine gute Idee, Union-Typen zu verwenden, wenn die Objekte völlig unterschiedliche Felder enthalten. Sie sind sehr effektiv. Wenn ein Objekttyp bestimmte Felder enthalten muss, um eine Schnittstelle zu einem anderen Objekttyp zu bilden, musst du eher eine Schnittstelle als einen Union-Typ verwenden.
Argumente
Argumente können zu jedem Feld in GraphQL hinzugefügt werden. Sie ermöglichen es uns, Daten zu senden, die das Ergebnis unserer GraphQL-Operationen beeinflussen können. In Kapitel 3 haben wir uns mit Benutzerargumenten in unseren Abfragen und Mutationen beschäftigt. Schauen wir uns nun an, wie wir Argumente in unserem Schema definieren.
Der Typ Query
enthält Felder, die allUsers
oderallPhotos
auflisten, aber was passiert, wenn du nur einen User
oder einen Photo
auswählen möchtest? Du müsstest einige Informationen über den Benutzer oder das Foto angeben, das du auswählen möchtest. Du kannst diese Informationen zusammen mit meiner Anfrage als Argument senden:
type Query { ... User(githubLogin: ID!): User! Photo(id: ID!): Photo! }
Genau wie ein Feld muss auch ein Argument einen Typ haben. Dieser Typ kann mit einem der skalaren Typen oder Objekttypen definiert werden, die in unserem Schema verfügbar sind. Um einen bestimmten Benutzer auszuwählen, müssen wir seine eindeutige githubLogin
als Argument senden. Die folgende Abfrage wählt nur den Namen und den Avatar von MoonTahoe
aus:
query { User(githubLogin: "MoonTahoe") { name avatar } }
Um Details über ein einzelnes Foto auszuwählen, müssen wir die ID des Fotos angeben:
query { Photo(id: "14TH5B6NS4KIG3H4S") { name description url } }
In beiden Fällen waren Argumente erforderlich, um Details über einen bestimmten Datensatz abzufragen. Da diese Argumente erforderlich sind, werden sie als nicht-nullbare Felder definiert. Wenn wir die id
odergithubLogin
bei diesen Abfragen nicht angeben, gibt der GraphQL-Parser einen Fehler zurück.
Daten filtern
Die Argumente müssen nicht nullbar sein. Wir können optionale Argumente mit nullbaren Feldern hinzufügen. Das bedeutet, dass wir Argumente als optionale Parameter angeben können, wenn wir Abfrageoperationen ausführen. Wir könnten zum Beispiel die Fotoliste, die von der Abfrage allPhotos
zurückgegeben wird, nach Fotokategorien filtern:
type Query { ... allPhotos(category: PhotoCategory): [Photo!]! }
Wir haben der Abfrage allPhotos
ein optionales Feld category
hinzugefügt. Die Kategorie muss mit den Werten des AufzählungstypsPhotoCategory
übereinstimmen. Wenn kein Wert mit der Abfrage gesendet wird, können wir davon ausgehen, dass dieses Feld jedes Foto zurückgibt. Wenn jedoch eine Kategorie angegeben wird, sollten wir eine gefilterte Liste von Fotos in derselben Kategorie erhalten:
query { allPhotos(category: "SELFIE") { name description url } }
Diese Abfrage würde die name
, description
und url
von jedem Foto zurückgeben, das als SELFIE
kategorisiert wurde.
Datenauslagerung
Wenn unsere PhotoShare-Anwendung erfolgreich ist, was sie sein wird, wird sie eine Menge Users
und Photos
haben. Es ist vielleicht nicht möglich, jede User
oder jedePhoto
in unserer Anwendung zurückzugeben. Wir können GraphQL-Argumente verwenden, um die Menge der Daten zu steuern, die von unseren Abfragen zurückgegeben werden. Dieser Prozess wird Data Paging genannt, weil eine bestimmte Anzahl von Datensätzen zurückgegeben wird, um eine Seite von Daten darzustellen.
Um das Paging von Daten zu implementieren, fügen wir zwei optionale Argumente hinzu:first
, um die Anzahl der Datensätze zu erfassen, die in einer einzigen Datenseite zurückgegeben werden sollen, und start
, um die Startposition oder den Index des ersten zurückzugebenden Datensatzes zu definieren. Wir können diese Argumente zu unseren beiden Listenabfragen hinzufügen:
type Query { ... allUsers(first: Int=50 start: Int=0): [User!]! allPhotos(first: Int=25 start: Int=0): [Photo!]! }
Im vorangegangenen Beispiel haben wir optionale Argumente für first
und start
hinzugefügt. Wenn der Kunde diese Argumente nicht mit der Abfrage angibt, verwenden wir die angegebenen Standardwerte. Die Abfrage allUsers
liefert standardmäßig nur die ersten 50 Nutzer und die Abfrage allPhotos
liefert nur die ersten 25 Fotos.
Der Kunde kann einen anderen Bereich von Benutzern oder Fotos abfragen, indem er Werte für diese Argumente angibt. Wenn wir zum Beispiel die Benutzer 90 bis 100 auswählen wollen, können wir dies mit der folgenden Abfrage tun:
query { allUsers(first: 10 start: 90) { name avatar } }
Diese Abfrage wählt nur 10 Jahre aus, beginnend mit dem 90sten Nutzer. Sie sollte die name
und avatar
für diesen speziellen Bereich von Nutzern zurückgeben. Wir können die Gesamtzahl der Seiten, die auf dem Client verfügbar sind, berechnen, indem wir die Gesamtzahl der Einträge durch die Größe einer Datenseite teilen:
pages = pageSize/total
Sortieren
Wenn wir eine Liste von Daten abfragen, möchten wir vielleicht auch festlegen, wie die zurückgegebene Liste sortiert werden soll. Auch dafür können wir Argumente verwenden.
Stellen wir uns ein Szenario vor, in dem wir die Möglichkeit einbauen wollen, beliebige Listen von Photo
Datensätzen zu sortieren. Eine Möglichkeit, diese Herausforderung zu meistern, besteht darin, enum
s zu erstellen, die angeben, welche Felder zum Sortieren von Photo
Objekten verwendet werden können, sowie Anweisungen, wie diese Felder zu sortieren sind:
enum SortDirection { ASCENDING DESCENDING } enum SortablePhotoField { name description category created } Query { allPhotos( sort: SortDirection = DESCENDING sortBy: SortablePhotoField = created ): [Photo!]! }
Hier haben wir die Argumente sort
und sortBy
zu der Abfrage allPhotos
hinzugefügt. Wir haben einen Aufzählungstyp namens SortDirection
erstellt, mit dem wir die Werte des Arguments sort
auf ASCENDING
oderDESCENDING
beschränken können. Wir haben auch einen weiteren Aufzählungstyp fürSortablePhotoField
erstellt. Da wir die Fotos nicht nach irgendeinem Feld sortieren wollen, haben wir die Werte von sortBy
auf vier der Fotofelder beschränkt: name
description
, category
oder created
(das Datum und die Uhrzeit, zu der das Foto hinzugefügt wurde). Sowohl sort
als auch sortBy
sind optionale Argumente, d.h. sie werden standardmäßig auf DESCENDING
und created
gesetzt, wenn keines der Argumente angegeben wird.
Kunden können jetzt selbst bestimmen, wie ihre Fotos sortiert werden, wenn sie eineallPhotos
Abfrage stellen:
query { allPhotos(sortBy: name) }
Diese Abfrage gibt alle Fotos sortiert nach absteigendem Namen zurück.
Bisher haben wir Argumente nur zu Feldern des Typs Query
hinzugefügt, aber es ist wichtig zu wissen, dass du Argumente zu jedem Feld hinzufügen kannst. Wir könnten die Argumente zum Filtern, Sortieren und Blättern zu den Fotos hinzufügen, die von einem einzelnen Nutzer gepostet wurden:
type User { postedPhotos( first: Int = 25 start: Int = 0 sort: SortDirection = DESCENDING sortBy: SortablePhotoField = created category: PhotoCategory ): [Photo!]
Das Hinzufügen von Paginierungsfiltern kann dazu beitragen, die Menge der Daten zu reduzieren, die eine Abfrage zurückgeben kann. Wir besprechen die Idee der Datenbegrenzung ausführlicher in Kapitel 7.
Mutationen
Mutationen müssen im Schema definiert werden. Genau wie Abfragen werden auch Mutationen in einem eigenen benutzerdefinierten Objekttyp definiert und dem Schema hinzugefügt. Technisch gesehen gibt es keinen Unterschied zwischen der Definition einer Mutation oder einer Abfrage in deinem Schema. Der Unterschied liegt in der Absicht. Wir sollten Mutationen nur dann erstellen, wenn eine Aktion oder ein Ereignis etwas am Zustand unserer Anwendung ändert.
Mutationen sollten die Verben in deiner Anwendung darstellen. Sie sollten aus den Dingen bestehen, die die Nutzer mit deinem Dienst tun können sollen. Wenn du deinen GraphQL-Dienst entwirfst, solltest du eine Liste mit allen Aktionen erstellen, die ein Nutzer mit deiner Anwendung durchführen kann. Das sind höchstwahrscheinlich deine Mutationen.
In der PhotoShare App können sich Nutzer/innen bei GitHub anmelden, Fotos posten und Fotos markieren. Alle diese Aktionen ändern etwas am Status der Anwendung. Nachdem sie sich bei GitHub angemeldet haben, ändern sich die aktuellen Nutzer, die auf den Client zugreifen. Wenn ein Nutzer ein Foto einstellt, gibt es ein zusätzliches Foto im System. Das Gleiche gilt für das Taggen von Fotos. Jedes Mal, wenn ein Foto markiert wird, werden neue Foto-Tag-Datensätze erstellt.
Wir können diese Mutationen zu unserem Stammmutationstyp in unserem Schema hinzufügen und sie für den Kunden verfügbar machen. Beginnen wir mit unserer ersten Mutation,postPhoto
:
type Mutation { postPhoto( name: String! description: String category: PhotoCategory=PORTRAIT ): Photo! } schema { query: Query mutation: Mutation }
Durch das Hinzufügen eines Feldes unter dem Typ Mutation
mit dem Namen postPhoto
können die Nutzer/innen Fotos einstellen. Zumindest können die Nutzer damit Metadaten zu den Fotos posten. Das Hochladen der eigentlichen Fotos wird in Kapitel 7 behandelt.
Wenn ein Nutzer ein Foto postet, ist mindestens die Angabe name
erforderlich. Die Angaben description
und category
sind optional. Wenn das Argument category
nicht angegeben wird, wird das gepostete Foto standardmäßig auf PORTRAIT
gesetzt. Ein Benutzer kann zum Beispiel ein Foto posten, indem er die folgende Mutation sendet:
mutation { postPhoto(name: "Sending the Palisades") { id url created postedBy { name } } }
Nachdem der/die Nutzer/in ein Foto gepostet hat, kann er/sie Informationen über das Foto auswählen, das er/sie gerade gepostet hat. Das ist gut, denn einige der Datensatzdetails über das neue Foto werden auf dem Server erstellt. Die ID
für unser neues Foto wird von der Datenbank erstellt. Die url
des Fotos wird automatisch erstellt. Außerdem wird das Foto mit einem Zeitstempel versehen, der das Datum und die Uhrzeit angibt, zu der das Foto created
wurde. Diese Abfrage wählt alle diese neuen Felder aus, nachdem ein Foto gepostet worden ist.
Außerdem enthält die Auswahlmenge Informationen über den Nutzer, der das Foto gepostet hat. Ein Benutzer muss angemeldet sein, um ein Foto zu posten. Wenn kein Benutzer angemeldet ist, sollte diese Mutation einen Fehler zurückgeben. Wenn ein Benutzer angemeldet ist, können wir über das Feld postedBy
herausfinden, wer das Foto gepostet hat. In Kapitel 5 wird beschrieben, wie man einen autorisierten Benutzer mithilfe eines Zugangstokens authentifiziert.
Eingabe-Typen
Wie du vielleicht bemerkt hast, sind die Argumente für einige unserer Abfragen und Mutationen ziemlich lang geworden. Es gibt eine bessere Möglichkeit, diese Argumente zu organisieren, indem du Eingabetypen verwendest. Ein Eingabetyp ist ähnlich wie ein GraphQL-Objekttyp, nur dass er nur für Eingabeargumente verwendet wird.
Lass uns die postPhoto
Mutation verbessern, indem wir einen Eingabetyp für unsere Argumente verwenden:
input PostPhotoInput { name: String! description: String category: PhotoCategory=PORTRAIT } type Mutation { postPhoto(input: PostPhotoInput!): Photo! }
Der Typ PostPhotoInput
ist wie ein Objekttyp, aber er wurde nur für Eingabeargumente geschaffen. Er erfordert ein name
und dasdescription
, aber die category
Felder sind weiterhin optional. Wenn du jetzt die postPhoto
Mutation sendest, müssen die Details über das neue Foto in einem Objekt enthalten sein:
mutation newPhoto($input: PostPhotoInput!) { postPhoto(input: $input) { id url created } }
Wenn wir diese Mutation erstellen, setzen wir den Typ der Abfragevariablen $input
so, dass er unserem Eingabetyp PostPhotoInput!
entspricht. Sie ist nicht nullbar, weil wir mindestens auf das Feld input.name
zugreifen müssen, um ein neues Foto hinzuzufügen. Wenn wir die Mutation senden, müssen wir die neuen Fotodaten in unsere Abfragevariablen eingeben, die unter dem Feld input
verschachtelt sind:
{
"input"
:
{
"name"
:
"Hanging at the Arc"
,
"description"
:
"Sunny on the deck of the Arc"
,
"category"
:
"LANDSCAPE"
}
}
Unser Input wird in einem JSON-Objekt zusammengefasst und zusammen mit der Mutation in den Abfragevariablen unter dem Schlüssel "input" gesendet. Da die Abfragevariablen als JSON formatiert sind, muss die Kategorie eine Zeichenkette sein, die zu einer der Kategorien aus dem Typ PhotoCategory
passt.
Eingabetypen sind der Schlüssel zum Organisieren und Schreiben eines klaren GraphQL-Schemas. Du kannst Eingabetypen als Argumente für jedes Feld verwenden. Mit ihnen kannst du sowohl das Paging als auch das Filtern von Daten in Anwendungen verbessern.
Schauen wir uns an, wie wir alle unsere Sortier- und Filterfelder mit Hilfe von Eingabetypen organisieren und wiederverwenden können:
input PhotoFilter { category: PhotoCategory createdBetween: DateRange taggedUsers: [ID!] searchText: String } input DateRange { start: DateTime! end: DateTime! } input DataPage { first: Int = 25 start: Int = 0 } input DataSort { sort: SortDirection = DESCENDING sortBy: SortablePhotoField = created } type User { ... postedPhotos(filter:PhotoFilter paging:DataPage sorting:DataSort): [Photo!]! inPhotos(filter:PhotoFilter paging:DataPage sorting:DataSort): [Photo!]! } type Photo { ... taggedUsers(sorting:DataSort): [User!]! } type Query { ... allUsers(paging:DataPage sorting:DataSort): [User!]! allPhotos(filter:PhotoFilter paging:DataPage sorting:DataSort): [Photo!]! }
Wir haben zahlreiche Felder unter Eingabetypen organisiert und diese Felder als Argumente in unserem Schema wiederverwendet.
Die Eingabetypen PhotoFilter
enthalten optionale Eingabefelder, mit denen der Kunde eine Liste von Fotos filtern kann. Der Typ PhotoFilter
enthält einen verschachtelten Eingabetyp, DateRange
, unter dem Feld createdBetween
.DateRange
muss Start- und Enddatum enthalten. MitPhotoFilter
können wir Fotos auch nach category
, search
string oder taggedUsers
filtern. Wir fügen all diese Filteroptionen zu jedem Feld hinzu, das eine Liste von Fotos liefert. So kann der Kunde selbst bestimmen, welche Fotos in der Liste angezeigt werden.
Es wurden auch Eingabetypen für das Blättern und Sortieren erstellt. Der EingabetypDataPage
enthält die Felder, die benötigt werden, um eine Seite mit Daten anzufordern, und der Eingabetyp DataSort
enthält unsere Sortierfelder. Diese Eingabetypen wurden zu jedem Feld in unserem Schema hinzugefügt, das eine Liste von Daten liefert.
Wir können eine Abfrage schreiben, die ziemlich komplexe Eingabedaten akzeptiert, indem wir die verfügbaren Eingabetypen verwenden:
query getPhotos($filter:PhotoFilter $page:DataPage $sort:DataSort) { allPhotos(filter:$filter paging:$page sorting:$sort) { id name url } }
Diese Abfrage akzeptiert optional Argumente für drei Eingabetypen:$filter
, $page
und $sort
. Mit Hilfe von Abfragevariablen können wir einige spezifische Details über die Fotos, die wir zurückgeben möchten, senden:
{
"filter"
:
{
"category"
:
"ACTION"
,
"taggedUsers"
:
[
"MoonTahoe"
,
"EvePorcello"
],
"createdBetween"
:
{
"start"
:
"2018-11-6"
,
"end"
:
"2018-5-31"
}
},
"page"
:
{
"first"
:
100
}
}
Diese Abfrage findet alle ACTION
Fotos, für die die GitHub-BenutzerMoonTahoe
und EvePorcello
zwischen dem 6. November und dem 31. Mai, der Skisaison, getaggt sind. Wir fragen auch nach den ersten 100 Fotos mit dieser Abfrage.
Eingabetypen helfen uns, unser Schema zu organisieren und Argumente wiederzuverwenden. Sie verbessern auch die Schema-Dokumentation, die GraphiQL oder GraphQL Playground automatisch erstellen. Dadurch wird deine API zugänglicher und leichter zu erlernen und zu verdauen. Schließlich kannst du Eingabetypen verwenden, um dem Client eine Menge Macht zu geben, um organisierte Abfragen auszuführen.
Rückgabe-Typen
Alle Felder in unserem Schema haben unsere HaupttypenUser
und Photo
zurückgegeben. Aber manchmal müssen wir zusätzlich zu den eigentlichen Nutzdaten auch Metainformationen über Abfragen und Mutationen zurückgeben. Wenn sich zum Beispiel ein Nutzer angemeldet hat und authentifiziert wurde, müssen wir zusätzlich zu den User
Nutzdaten ein Token zurückgeben.
Um sich mit GitHub OAuth anzumelden, müssen wir einen OAuth-Code von GitHub erhalten. Wir besprechen die Einrichtung deines eigenen GitHub OAuth-Kontos und die Beschaffung des GitHub-Codes in "GitHub-Autorisierung". Gehen wir zunächst davon aus, dass du einen gültigen GitHub-Code hast, den du an die githubAuth
Mutation senden kannst, um einen Nutzer anzumelden:
type AuthPayload { user: User! token: String! } type Mutation { ... githubAuth(code: String!): AuthPayload! }
Die Benutzer werden authentifiziert, indem sie einen gültigen GitHub-Code an diegithubAuth
Mutation senden. Bei Erfolg wird ein benutzerdefinierter Objekttyp zurückgegeben, der sowohl Informationen über den erfolgreich angemeldeten Benutzer als auch ein Token enthält, das zur Autorisierung weiterer Abfragen und Mutationen, einschließlich der postPhoto
Mutation, verwendet werden kann.
Du kannst benutzerdefinierte Rückgabetypen für jedes Feld verwenden, für das wir mehr als einfache Nutzdaten benötigen. Vielleicht wollen wir wissen, wie lange es dauert, bis eine Abfrage eine Antwort liefert, oder wie viele Ergebnisse in einer bestimmten Antwort gefunden wurden, zusätzlich zu den Nutzdaten der Abfrage. All dies kannst du mit einem benutzerdefinierten Rückgabetyp erledigen.
An dieser Stelle haben wir alle Typen vorgestellt, die dir bei der Erstellung von GraphQL-Schemas zur Verfügung stehen. Wir haben uns sogar ein wenig Zeit genommen, um Techniken zu besprechen, die dir helfen können, dein Schema besser zu gestalten. Aber es gibt noch einen letzten Stammobjekttyp, den wir vorstellen müssen - den TypSubscription
.
Abonnements
Subscription
Typen unterscheiden sich nicht von anderen Objekttypen in der GraphQL-Schemadefinitionssprache. Hier definieren wir die verfügbaren Abonnements als Felder in einem benutzerdefinierten Objekttyp. Wir müssen sicherstellen, dass die Abonnements das PubSub-Entwurfsmuster zusammen mit einer Art Echtzeittransport implementieren, wenn wir später in Kapitel 7 den GraphQL-Dienst aufbauen.
Wir können zum Beispiel Abonnements hinzufügen, die es unseren Kunden ermöglichen, auf die Erstellung neuer Photo
oder User
Typen zu warten:
type Subscription { newPhoto: Photo! newUser: User! } schema { query: Query mutation: Mutation subscription: Subscription }
Hier erstellen wir ein benutzerdefiniertes Subscription
Objekt, das zwei Felder enthält:newPhoto
und newUser
. Wenn ein neues Foto gepostet wird, wird dieses neue Foto an alle Kunden gesendet, die dasnewPhoto
Abonnement abonniert haben. Wenn ein neuer Benutzer erstellt wurde, werden seine Daten an alle Kunden weitergeleitet, die nach neuen Benutzern suchen.
Genau wie Abfragen oder Mutationen können auch Abonnements Argumente nutzen. Nehmen wir an, wir möchten dem newPhoto
Abonnement Filter hinzufügen, die bewirken, dass es nur nach neuen ACTION
Fotos sucht:
type Subscription { newPhoto(category: PhotoCategory): Photo! newUser: User! }
Wenn Nutzer das newPhoto
Abonnement abonnieren, haben sie jetzt die Möglichkeit, die Fotos zu filtern, die an dieses Abonnement gesendet werden. Um zum Beispiel nur neue Fotos von ACTION
zu filtern, können Kunden die folgende Operation an unsere GraphQL-API senden:
subscription { newPhoto(category: "ACTION") { id name url postedBy { name } } }
Dieses Abonnement sollte nur Details für ACTION
Fotos liefern.
Eine Subskription ist eine großartige Lösung, wenn es wichtig ist, Daten in Echtzeit zu verarbeiten. In Kapitel 7 erfahren wir mehr über die Implementierung von Subskriptionen für alle deine Anforderungen bei der Verarbeitung von Echtzeitdaten.
Schema Dokumentation
In Kapitel 3 wird erklärt, wie GraphQL über ein Introspektionssystem verfügt, mit dem du herausfinden kannst, welche Abfragen der Server unterstützt. Wenn du ein GraphQL-Schema schreibst, kannst du optionale Beschreibungen für jedes Feld hinzufügen, die zusätzliche Informationen über die Typen und Felder des Schemas liefern. Die Bereitstellung von Beschreibungen kann es deinem Team, dir selbst und anderen Nutzern der API erleichtern, dein Typsystem zu verstehen.
Fügen wir zum Beispiel Kommentare zum Typ User
in unserem Schema hinzu:
""" A user who has been authorized by GitHub at least once """ type User { """ The user's unique GitHub login """ githubLogin: ID! """ The user's first and last name """ name: String """ A url for the user's GitHub profile photo """ avatar: String """ All of the photos posted by this user """ postedPhotos: [Photo!]! """ All of the photos in which this user appears """ inPhotos: [Photo!]! }
Indem du drei Anführungszeichen über und unter deinem Kommentar zu jedem Typ oder Feld hinzufügst, gibst du den Benutzern ein Wörterbuch für deine API an die Hand. Zusätzlich zu den Typen und Feldern kannst du auch Argumente dokumentieren. Schauen wir uns die Mutation postPhoto
an:
Replace with: type Mutation { """ Authorizes a GitHub User """ githubAuth( "The unique code from GitHub that is sent to authorize the user" code: String! ): AuthPayload! }
Die Argument-Kommentare teilen den Namen des Arguments und ob das Feld optional ist. Wenn du Eingabetypen verwendest, kannst du diese wie jeden anderen Typ dokumentieren:
""" The inputs sent with the postPhoto Mutation """ input PostPhotoInput { "The name of the new photo" name: String! "(optional) A brief description of the photo" description: String "(optional) The category that defines the photo" category: PhotoCategory=PORTRAIT } postPhoto( "input: The name, description, and category for a new photo" input: PostPhotoInput! ): Photo!
Alle diese Dokumentationshinweise werden dann in der Schemadokumentation im GraphQL Playground oder GraphiQL aufgeführt, wie in Abbildung 4-4 gezeigt. Natürlich kannst du auch Introspektionsanfragen stellen, um die Beschreibungen dieser Typen zu finden.
Das Herzstück aller GraphQL-Projekte ist ein solides, gut definiertes Schema.Dieses dient als Fahrplan und Vertrag zwischen den Frontend- und Backend-Teams, um sicherzustellen, dass das erstellte Produkt immer dem Schema entspricht.
In diesem Kapitel haben wir ein Schema für unsere Foto-Sharing-Anwendung erstellt. In den nächsten drei Kapiteln zeigen wir dir, wie du eine vollwertige GraphQL-Anwendung erstellst, die den Vertrag des gerade erstellten Schemas erfüllt.
Get GraphQL lernen now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.