Mając za sobą teoretyczne wprowadzenie do języka zapytań GraphQL można przejść do omówienia najważniejszej części tego standardu, a mianowicie systemu typów, który pozwala na definiowanie schematu API – GraphQL Schema Definition Language (SDL). Jest to specjalna składnia, która została bardzo dobrze udokumentowana i stworzona w taki sposób aby można było z nią pracować niezależnie od języka bądź frameworka.
Type System
GraphQL jest silnie typowany co oznacza, że każda definicja musi posiadać jakiś określony typ. Z pomocą przychodzi Type System, który udostępnia kilka możliwości na definiowanie elementów naszego API. Będąc przy tematyce aplikacji związanej z książkami rozpoczętej przy okazji pierwszego posta zadeklarujmy pierwsze typy naszego API.
type Book { id: ID! title: String! shortDescription: String! description: String pages: Int! isbn: String! releaseDate: String! isReleased: Boolean! author: Author! } type Author { id: ID! name: String! bio: String birthday: String! sex: String! books: [Book!]! }
Obiekt i pola
Najważniejszym i najczęściej wykorzystywanym elementem całej układanki jest Object Type, który w najprostszych słowach jest kolekcją pól. Biorąc pod lupę powyższe przykłady zadeklarowano tam dwa obiekty przy pomocy definicji type Book {}
i type Author {}
, z kolei wewnątrz tych deklaracji można zauważyć pola, które są określonych typów, np. name: String!
, czy pages: Int!
.
Typy skalarne
W GraphQL istnieje kilka wbudowanych typów skalarnych dla deklaracji pól:
String
– zbiór znaków w formacie UTF-8Int
– 32-bitowa liczba całkowitaFloat
– liczba zmiennoprzecinkowaBoolean
– wartośćtrue
lubfalse
ID
– typ reprezentujący unikalny identyfikator dla obiektu, najczęściej używany do ponownego pobrania (wykorzystywany przez cache). Serializowany jest w taki sam sposób jak typString
.
Interface
Interfejs w GraphQL w założeniu pełni rolę podobną do tej jak to ma miejsce w językach obiektowych, czyli wymusza pewien kontrakt. Obiekt, który implementuje dany interfejs musi zawierać pola, które zostały zadeklarowane w interfejsie.
Przykładowo do naszej aplikacji oprócz książek dodajemy obsługę magazynów. Możemy wymusić aby oba typy publikacji posiadały tytuł i datę wydania.
interface Publication { title: String! releasedDate: String! } type Magazine implements Publication { title: String! releasedDate: String! version: Int! } type Book implements Publication { title: String! releasedDate: String! pages: Int! }
Bardzo prawdopodobne jest, że autor mógł wydać zarówno książki jak i magazyny, dzięki interfejsowi nie trzeba uzależniać się od konkretnego typu publikacji, w tym wypadku możemy użyć większej abstrakcji jaką jest Publication
.
type Author { name: String!, publications: [Publication] }
Union
Ciekawym mechanizmem jest Union type, który pozwala na reprezentowaniu grupy obiektów nie posiadających tych samych pól. Bardzo dobrym przykładem jest zapytanie do wyszukiwarki, która może przeszukiwać zarówno po tytule książki jak i imieniu autora.
union SearchResult = Book | Author type Query { search(text: String!): SearchResult }
Dzięki takiej deklaracji można wywołać mniej więcej takie zapytanie:
query { search(text: "test") { ... on Book { title } ... on Author { name } } }
A w wyniku (jeśli zostaną znalezione odpowiednie rekordy) zobaczymy odpowiedź:
{ "data": { "search": [ { "name": "test book", }, { "name": "test author", } ] } }
Deklaracja schematu
Definiując schemat API dostępne są dwa elementy najwyższego poziomu – query
oraz mutation
, które są zwykłymi obiektami tworzone w taki sam sposób wszystkie inne. Wewnątrz nich deklarujemy możliwości naszego API. Definicja samego schematu jest banalna:
schema { query: Query mutation: Mutation } type Query { } type Mutation { }
Query
Query
jest obowiązkowym elementem w schemacie i odpowiada za odczyt API. Wszystkie definiowane pola wewnątrz tego obiektu można porównać do różnych endpointów API. Przyjętą zasadą jest, że elementy wystawiane za pośrednictwem query
są rzeczownikami wprost określającymi encję, która ma być pobrana – w powyższym przykładzie są to book
i author
.
Aby lepiej zobrazować całość można przerzucić poprzednie definicje obiektów do query
.
schema { query: Query } type Query { book(id: ID!): Book author(id: ID!): Author } type Book { id: ID! title: String! shortDescription: String! description: String pages: Int! isbn: String! releaseDate: String! isReleased: Boolean! author: Author! } type Author { id: ID! name: String! bio: String birthday: String! sex: String! books: [Book!]! }
Argumenty
W liniach 6 i 7 można zauważyć deklarację pól nieco innych niż w poprzednich przypadkach (np. book(id: String!)
), gdzie dodatkowo oprócz nazwy pola można zauważyć nawias z kolejną deklaracją – to nic innego jak wprowadzenie argumentu do zapytania – na jego podstawie można przekazać jakieś parametry według których chcemy pobrać dane. W powyższym przykładzie oczekiwany jest id
użytkownika i wykonane zapytanie wyglądałoby mniej więcej tak:
query { book(id: "1234") { title isbn } }
Mutation
Mutation
jest opcjonalną częścią, która pozwala na dodawanie, edytowanie lub usuwanie elementów w naszej aplikacji za pośrednictwem API. Jego definicja jest identyczna do typu query
. Jedyną różnicą jest zasada definiowania pól – w przeciwieństwie do query
w mutation
najczęściej pola nazywane są jako czasowniki, które jasno określają akcję jaka jest do wykonania. Uzupełniając powyższy przykład warto dodać możliwość tworzenia nowych książek.
Input Type
Zanim przejdziemy do przykładowego zadeklarowania mutation
warto przedstawić jeszcze jeden typ pominięty podczas omawiania wszystkich podstawowych typów w podrozdziale, który dotyczył Type System.
Dla modyfikowania lub tworzenia nowych elementów w aplikacji za pośrednictwem GraphQL stworzono specjalny typ input
, który zachowuje się bardzo podobnie jak zwykły obiekt, z tą różnicą, że podczas deklaracji zamiast słowa kluczowego type
używane jest input
.
schema { query: Query mutation: Mutation } type Mutation { createBook(input: BookInput): Book updateBook(id: ID!, input: BookInput): Book } input BookInput { title: String! pages: Int! isbn: String! }
W powyższym przykładzie można zaobserwować, że akcje createBook
i updateBook
oczekują jako argument obiektu BookInput
zaś zwracają obiekt Book
. Dla tak zadeklarowanego schematu tworzenie nowej książki wymaga podobnej akcji:
mutation { createBook(input: { title: "Test book", pages: 512, isbn: "12211" }) { id title } }
Podsumowanie
W dzisiejszym poście zostały omówione wszystkie najważniejsze i najbardziej podstawowe elementy języka zapytań GraphQL. Większość z tych elementów jest na tyle proste, że nie trzeba nawet się zastanawiać nad zasadą ich działania, takie też było główne założenie inżynierów Facebooka, którzy opracowywali ten standard – składnia ma być łatwa i czytelna.