Kategoria:GraphQL

GraphQL - zapytania, mutacje i schema

  • Czas potrzebny na przeczytanie:9 minut
  • Opublikowane:

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:

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:

W odpowiedzi dostaniemy:

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.

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:

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.

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:

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! 馃З

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:

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:

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:

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艣膰.

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膮.

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

O autorze

Olaf Sulich

Olaf jest Frontend Developerem, blogerem i nosi rybacki kapelusz 馃帺 Pisze o wszystkim co zwi膮zane z frontendem, ale nie boi si臋 backendu i design贸w 馃 Ma g艂ow臋 pe艂n膮 pomys艂贸w i nadziej臋, 偶e znajdziesz tutaj co艣 dla siebie!

Do艂膮cz do spo艂eczno艣ci!

Bo w programowaniu licz膮 si臋聽ludzie

Wchodz臋