GraphQL - zapytania, mutacje i schema

Kontynuujemy przygodę ze światem GraphQLa i zabieramy się za realne zagadnienia 🥳 Dziś poznasz zapytania, mutacje, scheme, typy i wiele więcej, a za tydzień, ze zdobytą dziś wiedzą podbijamy Reakta!

Poniżej, jak zawsze, mały spis treści 📖


Spis treści


Zanim zaczniemy, będziemy dziś korzystać z The Rick and Morty API o którym wspominałem w poprzednim wpisie oraz ze SWAPI, które udostępnia tam Star Warsowe API.

Zapytania

Jeśli jeszcze nie słyszałeś o zapytaniach (en. query) zajrzyj do poprzedniego wpisu. Spójrzmy na podstawowe zapytanie:

{
  characters {
    results {
      name
      id
      status
      origin {
        name
      }
    }
  }
}

Wyciągamy tutaj poszczególne pola i zostają nam one zwrócone w formie JSONa.

{
  "data": {
    "characters": {
      "results": [
        {
          "name": "Rick Sanchez",
          "id": "1",
          "status": "Alive",
          "origin": {
            "name": "Earth (C-137)"
          }
        }
        // ...
      ]
    }
  }
}

Dostajemy w odpowiedzi wszystkie postacie, które posiadają imię, id czy pochodzenie.

Argumenty

Tak jak wspomniałem wyciągamy wszystkie postacie, załóżmy, że chcielibyśmy pobrać dane tylko o konkretnej postaci, w tym przypadku o Ricku Sanchezie. Do tego idealnie sprawdzają się argumenty:

{
  character(id: 1) {
    name
  }
}

W odpowiedzi dostaniemy:

{
  "data": {
    "character": {
      "name": "Rick Sanchez"
    }
  }
}

Argumenty mogą być różnych typów, mogą być też różne, może być to np. name czy unit ale to wszystko zależy od Twojego API. W takim razie jak sprawdzić czy możemy podać jakiś argument do zapytania?

Wystarczy kliknąć w DOCS po prawej stronie, następnie wybieramy odpowiednie zapytanie i otrzymujemy informacje o argumentach i ich typach.

Argumenty w GraphQL

Aliasy

Skorzystajmy teraz z SWAPI, możemy tutaj zapytać daną planetę, podajemy do query argument name, dzięki temu możemy zapytać o tytuł filmu, w którym dana planeta się pojawiła.

Problem pojawia się gdy chcemy pobrać dwie różniące się planety, wtedy pojawiają się aliasy. Nadajemy im nazwy i po dwukropku wykonujemy zapytanie:

{
  filmsWithHoth: Planet(name: "Hoth") {
    films {
      title
    }
  }
  filmsWithTatooine: Planet(name: "Tatooine") {
    films {
      title
    }
  }
}

W odpowiedzi dostajemy następujące dane:

{
  "data": {
    "filmsWithHoth": {
      "films": [
        {
          "title": "A New Hope"
        }
        // ...
      ]
    },
    "filmsWithTatooine": {
      "films": [
        {
          "title": "The Phantom Menace"
        }
        // ...
      ]
    }
  }
}

Imienne query

Wcześniej tworzyliśmy anonimowe zapytania, ale w praktyce i w realnej aplikacji, używamy nazw dla zapytań i mutacji:

query CharacterName {
  character(id: 1) {
    name
  }
}

Są one niezbędne przy korzystaniu ze zmiennych 👇

Zmienne

Do tej pory wszystkie wartości podawaliśmy jako stringi / numbery, były to wartości statyczne. Najczęściej jednak będziemy chcieli dynamicznie zaciągnąć dane na podstawie jakieś zmiennej.

query StarshipClass($name: String) {
  Starship(name: $name) {
    class
  }
}

Podobnie jak argumenty, zmienne podajemy w nawiasach przy nazwie naszej operacji poprzedzając ją $. Następnie możemy wykorzystać tą zmienną gdzie tylko chcemy. Przy nazwie zmiennej możesz jeszcze zauważyć String, jest to typ zmiennej w schema definition language.

Ta zmienna jest opcjonalna, jeśli dodamy na końcu typu ! stanie się ona wymagana. Do zmiennych możemy również przypisywać defaultowe wartości:

query StarshipClass($name: String = "Death Star") {
  Starship(name: $name) {
    class
  }
}

Fragmenty

Stwórzmy nowe query, jednocześnie praktykując wykorzystanie aliasów:

query MainCharacters {
  rick: character(id: 1) {
    name
    id
    status
    origin {
      name
      dimension
    }
  }

  morty: character(id: 2) {
    name
    id
    status
    origin {
      name
      dimension
    }
  }
}

Widzicie pewną zależność? Powtarzające się pola, takich powtórzeń może być przecież znacznie więcej... Takim sposobem tworzymy brzydki, powtarzalny kod, a tego nie chcemy.

Fragmenty na ratunek! 🧩

fragment characterFields on Character {
  name
  id
  status
  origin {
    name
    dimension
  }
}

Zacznijmy od początku fragment jest reużywalnym kawałkiem kodu, definiujemy go nazwą i poprzedzamy słowem fragment. Po on podajemy typ, w tym przypadku jest to Character. A co w środku? Pola, które chcemy ponownie wykorzystać!

query MainCharacters {
  rick: character(id: 1) {
    ...characterFields
  }

  morty: character(id: 2) {
    ...characterFields
  }
}

Wygląda to o wiele schludniej. Najlepsze jest to, że we fragmentach możemy również korzystać ze zmiennych! Daje nam to na prawdę dużą elastyczność.

Inlinowe fragmenty

Fragmenty mają jeszcze jedno świetne zastosowanie. Abstrahując już od naszego API, załóżmy, że posiadamy pole character, które może być typu Rick lub Morty. Dla każdego typu mamy inne pola specjalne. I w zależności od zmiennej chcemy te pola pobrać.

query MainCharacter($character: Character) {
  character(character: $character) {
    name
    id
    ... on Rick {
      iq
    }
    ... on Morty {
      tshirtColor
    }
  }
}

Niezależnie od typu postaci pobieramy imię i id, jeśli naszą postacią będzie Rick pobieramy dodatkowo iq, natomiast, jeśli będzie to Morty pobieramy tshirtColor.

Meta fields

Rozbudujmy nasz poprzedni przykład i dodajmy nowe postaci Summer i Jerrego. Tym razem nie będziemy wybierać konkretnych pól w zależności od typu, ale pobierzemy id danej postaci gdy to będzie posiadało w sobie er.

query searchCharacters {
  search(include: "er") {
    ... on Summer {
      id
    }
    ... on Rick {
      id
    }
    ... on Jerry {
      id
    }
    ... on Morty {
      id
    }
  }
}

W odpowiedzi dostaniemy następujące dane:

{
  "data": {
    "search": [
      {
        "id": "2"
      },
      {
        "id": "3"
      }
    ]
  }
}

Tutaj pojawia się problem, skąd mamy wiedzieć jakie id przynależy do danej postaci? Z pomocą przychodzą meta fields i __typename.

query searchCharacters {
  search(include: "er") {
    __typename
    ... on Summer {
      id
    }
    ... on Rick {
      id
    }
    ... on Jerry {
      id
    }
    ... on Morty {
      id
    }
  }
}

Gdy dodamy pole __typename na początku naszego zapytania, w odpowiedzi dostaniemy nazwę z danego typu.

{
  "data": {
    "search": [
      {
        "__typename": "Summer",
        "id": "2"
      },
      {
        "__typename": "Jerry",
        "id": "3"
      }
    ]
  }
}

Dyrektywy

Dodawaliśmy zmienne, żeby mieć większą kontrolę nad naszym zapytaniem. Krokiem dalej jest zaimplementowanie dyrektyw, które pozwalają nam dynamicznie zmieniać zapytanie.

Dyrektywę dodajemy ze znakiem @, w podstawowym GraphQLu mamy dwie dyrektywy:

  • @include(if: Boolean)
  • @skip(if: Boolean)

Ta pierwsza akceptuje pola gdy wartość if jest true, @skip omija dane pola gdy wartość jest true.

query RickFields($desktop: Boolean!) {
  rick: character(id: 1) {
    name
    id
    status
    origin @include(if: $desktop) {
      name
      dimension
    }
  }
}

Mamy tutaj zapytanie RickFields i zmienną $desktop, na podstawie tej zmiennej będziemy zaciągać pochodzenie Ricka. Jeżeli $desktop będzie false pochodzenie postaci nie zostanie pobrane.

Mutacje

Dotychczas rozmawialiśmy tylko o pobieraniu danych, ale przecież chcemy je też modyfikować!

Mutację tworzymy bardzo podobnie jak zapytania, także nie ma się czego bać, zamiast słówka query podajemy mutation.

mutation CreateCharacterForEpisode($ep: Episode!, $character: Character!) {
  createCharacter(episode: $ep, character: $character) {
    name
    id
  }
}

Podajemy tutaj zmienne i wykorzystujemy je przy argumentach, character nie jest tzw. typem skalarnym, a czymś w rodzaju obiektu, ten obiekt nosi nazwę input object type, ale o tym za chwilkę.

Tak wyglądają nasze zmienne:

{
  "ep": "Auto Erotic Assimilation",
  "character": {
    "name": "Pickle Rick",
    "id": 55
  }
}

A te dane zostaną zmienione na serwerze:

{
  "data": {
    "createCharacter": {
     "name": "Pickle Rick",
     "id": 55
    }
  }
}

Mutacji możemy na raz wysyłać wiele, działa to podobnie jak z zapytaniami, z jednym wyjątkiem, mutacje muszą poczekać na siebie, żeby zapobiec tzw. race conditions.

Schema i typy

Jeśli mieliście już do czynienia z jakimś silnie typowanym językiem np. TypeScriptem, to będzie czuli się jak w domu, no prawie. Jeśli nie, nie martw się przejdziemy przez wszystkie zagadnienia.

Scheme w GraphQLu kojarzymy bardziej z backendem niż z frontendem, jednak nauczenie się jej może Ci się przydać, zaufaj mi. W następnym wpisie będziemy definiować scheme po stronie klienta.

Więc czym jest ta magiczna schema i jak korzystać z typów w GraphQLu?

Z typów już korzystaliśmy, pisząc zapytanie korzystające ze zmiennej:

query StarshipClass($name: String) {
  Starship(name: $name) {
    class
  }
}

Typy skalarne

Zacznijmy od podstawowych typów, czyli typów skalarnych. W pakiecie od GraphQLa dostajemy:

  • String - ciąg znaków np. 'Rick'
  • Int - liczba całkowita, np. 6
  • Float - liczba zmiennoprzecinkowa, np. Math.random()
  • Boolean - true/false
  • ID - jest to specjalny, unikalny typ, bardzo ważny przy cachowaniu danych

Object types

Object types to typy złożone z typów skalarnych.

type Character {
  name: String!
  status: String
  episode: [Episode]
  origin: Location
  height(unit: Unit): Float
}

Typ definiujemy przy użyciu type, jednocześnie podając jego nazwę, w tym przypadku jest to Character. Jeśli chodzi o ! na końcu danego typu, dajemy znać GraphQLowi, że ten tym nie może być nullem. Konstrukcja [Episode] opisuje tablicę obiektów o typie Episode.

Pamiętacie argumenty? Trzeba je jakoś otypować, tak samo jak w zapytaniach, w nawiasach podajemy wartość i przypisujemy do niej typ, po dwukropku definiujemy jakiego typu ma być zwracana wartość.

Typy zapytań i mutacji

Oprócz standardowych object types i typów skalaranych mamy również do wykorzystania dwa bardzo ważne typy Query i Mutation. Definiują one tzw. entry point naszych zapytań. Wyglądają one dokładnie tak jak object types:

type Query {
  character(id: ID): Character
}

type Mutation {
  createCharacter(name: String!): Character
}

Enumy

Enumy w GraphQLu są specjalnymi typami skalarnymi, gdzie nasz typ jest ograniczony do konkretnych wartość.

enum Planet {
  Hoth
  Dagobah
  Tatooine
}

type Episode {
    planets: Planet
}

Jeżeli podamy, w naszej schemie typ Planet, GraphQL będzie spodziewał się Hoth, Dagobah lub Tatooine.

Interfejsy

Pamiętacie Inlinowe fragmenty? Implementowaliśmy tam Ricka i Mortiego, każdy z nich miał specjalne pola, jednak oboje mieli kilka wspólnych. Możemy stworzyć, dla tych wspólnych pól, interfejs, który potem zaimplementujemy w danym typie.

Zaimplementowanie interfejsu w typie, mówi nam, że każdy typ, który implementuje dany interfejs musi opisywać dane pola.

interface Character {
  name: String!
  id: ID!
}

type Rick implements Character {
  name: String!
  id: ID!
  iq: Int
}

type Morty implements Character {
  name: String!
  id: ID!
  tshirtColor: String
}

Union types

Unie oznaczają typ jeden z. Możemy np. utworzyć typ, który będzie typu Location lub Planet.

type Location {
  name: String
}

type Planet {
  name: String
}

union WhereAreYou = Location | Planet

Input types

Przy tworzeniu mutacji podawaliśmy typ $character:

mutation CreateCharacterForEpisode($ep: Episode!, $character: Character!) {
  createCharacter(episode: $ep, character: $character) {
    name
    id
  }
}

Wspominałem, że do tego wrócimy, więc dotrzymuję słowa 🤞

Ten typ ma taką samą konstrukcję jak object types, z dwiema różnicami. Zamiast type podajemy input i nie musimy podawać typów do poszczególnych pól, zamiast tego wrzucamy input type object, a GraphQL zajmie się resztą.

input Character {
  name: String
  id: ID!
}

Podsumowanie

To wszystko na dziś, dzięki za obecność!

Zachęcam Cię do pobawienia się GraphQLem w SWAPI i The Rick and Morty API.

Do usłyszenia!

Źródła

© Olaf Sulich 2020