- 23 Eyl 2016
- 2,001
- 11
GraphQL
Neden GraphQL?
Son yıllarda, bir web API tasarlarken REST, standart olarak kullanılmaya başlanılmıştı. Her ne kadar REST'in stateless sunucular (session bilgisi tutmayan sunucular) gibi güzel özellikleri olsa da REST, hiç esnek değil.
GraphQL'de burada araya girerek REST'in bu esnek olmama sorununu çözüyor ve bize çok daha esneklik ve efektiflik sunuyor. REST ile uğraşırken karşılaşılan birçok eksikliği çözerek bize çok daha güçlü bir API sağlıyor.
API'dan veri çekerken REST ve GraphQL'in en temel farklılıklarını göstermek için örnek bir senaryo düşünelim. Bir blog uygulaması, uygulama belirli bir kullanıcının verilerini göstermesi gerekiyor. Aynı ekran ayrıca bu kullanıcının son 3 takipçisini gösterecek. Bu soruna REST ve GraphQL ile nasıl çözüm buluruz?
REST API ile, bu işlem için büyük ihtimalle birden çok endpoint'e erişmemiz gerekecektir. Bu örnekle, bu endpoint'ler /users/<id>, /users/<id>/posts ve /users/<id>/followers olabilir.
GraphQL'de ise bir istek ile birden fazla veri alabiliyoruz. Yani REST ile birden çok endpointe istek göndermemiz gerekirken GraphQL'de tek bir endpoint'e gönderdiğimiz isteği geliştirerek tüm verileri bir sorgu ile alabiliyoruz.
Bu örnekte de görebileceğiniz gibi tek bir istek ile kolayca birkaç veriye ulaşabiliyoruz.
Ayrıca sorguya ve dönen veriye dikkatli bakarsanız görebilirsiniz ki REST'teki gibi dönen sabit alanlar yok. Sadece istediğimiz alanlar dönüyor. Ne REST'teki gibi fazla veri alıyoruz ne de REST'teki gibi bir endpoint istediğimiz verileri göndermemesi sorunuyla karşılaşmıyoruz.
Nerelerde GraphQL uygun çözüm olmayabilir?
Genel olarak performans gerektiren yerlerle GraphQL biraz geride kalıyor. Öncelikle, REST API'larda sıkça gördüğümüz cache işlemi, GraphQL'de aynı sorguya rastlama ihtimalimizin oldukça az olduğundan GraphQL'de cache verimli olmuyor. Fakat yine de, Prisma ve Dataloader gibi kütüphaneler ile cache işlemi yapılabiliyor. Zira hala tamamen verimli olmuyor.
Bir diğer performans sıkıntısı ile kullanıcıya veri olarak ne alacağını seçme özgürlüğünü vermemizden kaynaklanıyor. Aşağıdaki örneğe bakarsanız ne diyeceğimi anlayacaksınız. Hem bir sanatçıyı, hem parçalarını, hem yorumlarını hem de yorum atan kullanıcı bilgilerini çekiyoruz. Bu da tabii sunucuyu zorluyor.
Temel Konseptler
Schema Definition Language (SDL)
GraphQL, API şemalarını tanımlamak için kullanılan kendine özgü bir sisteme sahip. Şema yazmada kullanılan bu syntax, Schema Definition Language olarak adlandırılmaktadır.
Person adında SDL kullanılarak tanımlanan basit bir tür örneği:
Bu tür, 2 alana sahip, ve bunlar name ve age olarak adlandırılmış. Birinin türü String ve diğeri de Int. Ayrıca ! işareti de bu alanın gerekli olduğunu belirtiyor.
Ayrıca burada oluşturduğumuz türleri başka yerlerde de kullanabiliriz. Örneğin Person türü, Post türünde kullanılabilir.
Ayrıca bu ilişkinin diğer ucu da Person'da tanımlanmalı.
(Buradaki köşeli parantez, posts alanının Post türünden oluşan bir liste olduğunu belirtmekte.)
Sorgular
Yukarıdaki örneklerde de gördüğümüz gibi GraphQL'in yaklaşımı genelde REST'e göre farklı. Birden çok endpoint yerine genelde tek bir endpoint üzerinden farklı sorgular ile farklı veriler çekiyor. Bu sorguları da özelleştirebildiğimiz için oldukça esneklik sunuyor bize.
O zaman GraphQL ile örnek bir sorguyu inceleyelim:
Bu sorgudaki allPersons alanı, sorgunun temel alanıdır. Bu temel alanın altına yazılan diğer her şey ise sorgunun payload'udur. Burada bu sorgunun payloadında belirlilen tek alan, name'dir.
Bu sorgu, veritabanındaki bütün kişileri döndürecektir. Örnek bir yanıta bakalım:
Fark edebilirsiniz ki yanıtta kişilerin sadece name verisi döndürülmüş. Bunun sebebi, sadece bu alanı sorgumuzda belirtmemizdir.
Fakat bu sorguyu birazcık geliştirip, kişilerin yaşlarını da almak istersek. Sorguya age alanını eklememiz yeterli olacaktır.
GraphQL'in bir diğer büyük özelliklerinden biri de iç içe bilgi alımını desteklemesidir. Örneğin bir kişinin postlarını almak isterseniz aşağıdaki gibi iç içe (nested) bir sorgu yazarak hem kişi bilgisi alırken hem de post bilgisi alabilirsiniz.
Argümanlar
GraphQL'de bir alan, eğer şemada belirtildiyse argümanlara sahip olabilir. Örneğin allPersons alanı sadece belirli sayıda kişi döndürmesi için bir last parametresine sahip olabilir. Argümana sahip bir sorgu şu şekilde gözükecektir:
Mutasyonlar ile Veri Yazmak
Sunucudan veri okumak gibi bir çok uygulamada ayrıca sunucuda saklanan veride değişiklik yapmamızı da gerektiriyor. GraphQL ile bu değişiklikler, mutasyonlar ile yapılıyor.
Genel olarak üç tür mutasyon var:
Mutasyonlar da normal sorgular ile aynı yapıya sahipler, fakat bunlar her zaman mutation kelimesi ile başlamalı. Örneğin yeni bir kişi yaratabileceğimiz aşağıdaki örneği inceleyelim:
Sorguya baktığınızda aslında bu sorgunun da önceki sorgularımızla çok benzer olduğunu göreceksiniz. Bunda da createPerson adında bir temel alan var ve daha önce öğrendiğimiz argümanlar da burada kullanılmış.
Ayrıca bir sorgu gibi yine bu mutasyonda da payload belirtebiliriz ve bu sayede yeni oluşan verinin propertylerine erişebiliriz. Bu örneğimizde bu istediğimiz veriler name ve age olduğu ve bunları zaten biz verdiğimiz için çok da işlevsel görünmeyebilir ama id gibi bizim bilmediğimiz verileri de alabilirdik.
Örnek bir yanıt ise bu şekilde:
GraphQL'e özgü olarak karşılaşabileceğiniz bir özellik de GraphQL'in özel ID türlerinin olması. Bu türler sayesinde yeni bir nesne oluşturulduğunda, ID de otomatik olarak sunucu tarafından oluşturuluyor. Örneğin Person türümüzü şu şekilde geliştirebiliriz:
Artık yeni bir kişi oluşturulduğunda bu nesneye otomatik olarak bir ID de atanıyor ve sorgumuzda bu ID'yi isteyebiliyoruz:
Abonelik ile Anlık Güncellemeler
Günümüzde birçok uygulama, sunucuda bir değişiklik olduğu an hemen bilgi sahibi olma ihtiyacını duyabiliyor. GraphQL ise bu tür durumlar için abonelikler denen çok güzel bir konsept sunuyor.
Bir evente abone olunduğunda, sunucu sabit bir bağlantı oluşturacaktır. Bu event her meydana geldiğinde ise sunucu eşleşen veriyi gönderecektir. "istek-yanıt döngüsünü" takip eden sorgular ve mutasyonlardan farklı olarak abonelikler verilerin bir stream'i şeklinde gönderilmektedir.
Person türündeki eventlere abone olduğumuz bir örnek:
İstemci bu abonelik istediğini sunucuya gönderdikten sonra bir bağlantı sunucu ve istemci arasında açılacaktır. Bir yeni bir Person oluşturan mutasyon oluştuğunda ise sunucu yeni kişinin bilgisini gönderecektir:
Şema Oluşturmak
Artık sorgular, mutasyonlar ve abonelikler hakkında bilginiz olduğuna göre biraz da bu örnekleri çalıştırmamızı sağlayan şemaları nasıl yazabileceğimize bakalım. Şema, API'ın yapabileceklerini ve istemcinin veriyi nasıl isteyebileceğini açıkladığı için en önemli şeylerden biridir.
Genel olarak, bir şema GraphQL türlerinin bir koleksiyonudur. Fakat bir şema yazarken bazı özel temel türler de var.
Bu Query, Mutation ve Subscription türleri, istemci tarafından gönderilen istekler için entry point'lerdir. Örneğin önceden gördüğümüz allPerson'a izin vermek için, Query türü şu şekilde yazılabilir:
API'ın temel alanı, allPersons'tur. Daha önceden gördüğümüz last argümanını eklemek için ise şu şekilde değiştirebiliriz türü:
Mutasyonlar için de benzer şekilde Mutations türüne createPerson'u ekleyebiliriz:
Fark edebilirsiniz ki temel alan, yeni kişi için gerekli olan name ve age argümanlarını alıyor.
Son olarak ise abonelikler için, newPerson temel alanını ekleyebiliriz:
Bunların hepsini bir araya getirirsek, bu konuda işlediğimiz tüm her şeyin tam bir şeması şu şekildedir:
Neden GraphQL?
Son yıllarda, bir web API tasarlarken REST, standart olarak kullanılmaya başlanılmıştı. Her ne kadar REST'in stateless sunucular (session bilgisi tutmayan sunucular) gibi güzel özellikleri olsa da REST, hiç esnek değil.
GraphQL'de burada araya girerek REST'in bu esnek olmama sorununu çözüyor ve bize çok daha esneklik ve efektiflik sunuyor. REST ile uğraşırken karşılaşılan birçok eksikliği çözerek bize çok daha güçlü bir API sağlıyor.
API'dan veri çekerken REST ve GraphQL'in en temel farklılıklarını göstermek için örnek bir senaryo düşünelim. Bir blog uygulaması, uygulama belirli bir kullanıcının verilerini göstermesi gerekiyor. Aynı ekran ayrıca bu kullanıcının son 3 takipçisini gösterecek. Bu soruna REST ve GraphQL ile nasıl çözüm buluruz?
REST API ile, bu işlem için büyük ihtimalle birden çok endpoint'e erişmemiz gerekecektir. Bu örnekle, bu endpoint'ler /users/<id>, /users/<id>/posts ve /users/<id>/followers olabilir.
GraphQL'de ise bir istek ile birden fazla veri alabiliyoruz. Yani REST ile birden çok endpointe istek göndermemiz gerekirken GraphQL'de tek bir endpoint'e gönderdiğimiz isteği geliştirerek tüm verileri bir sorgu ile alabiliyoruz.
Bu örnekte de görebileceğiniz gibi tek bir istek ile kolayca birkaç veriye ulaşabiliyoruz.
Ayrıca sorguya ve dönen veriye dikkatli bakarsanız görebilirsiniz ki REST'teki gibi dönen sabit alanlar yok. Sadece istediğimiz alanlar dönüyor. Ne REST'teki gibi fazla veri alıyoruz ne de REST'teki gibi bir endpoint istediğimiz verileri göndermemesi sorunuyla karşılaşmıyoruz.
Nerelerde GraphQL uygun çözüm olmayabilir?
Genel olarak performans gerektiren yerlerle GraphQL biraz geride kalıyor. Öncelikle, REST API'larda sıkça gördüğümüz cache işlemi, GraphQL'de aynı sorguya rastlama ihtimalimizin oldukça az olduğundan GraphQL'de cache verimli olmuyor. Fakat yine de, Prisma ve Dataloader gibi kütüphaneler ile cache işlemi yapılabiliyor. Zira hala tamamen verimli olmuyor.
Bir diğer performans sıkıntısı ile kullanıcıya veri olarak ne alacağını seçme özgürlüğünü vermemizden kaynaklanıyor. Aşağıdaki örneğe bakarsanız ne diyeceğimi anlayacaksınız. Hem bir sanatçıyı, hem parçalarını, hem yorumlarını hem de yorum atan kullanıcı bilgilerini çekiyoruz. Bu da tabii sunucuyu zorluyor.
Kod:
artist(id: '1') {
id
name
tracks {
id
title
comments {
text
date
user {
id
name
}
}
}
}
Temel Konseptler
Schema Definition Language (SDL)
GraphQL, API şemalarını tanımlamak için kullanılan kendine özgü bir sisteme sahip. Şema yazmada kullanılan bu syntax, Schema Definition Language olarak adlandırılmaktadır.
Person adında SDL kullanılarak tanımlanan basit bir tür örneği:
Kod:
type Person {
name: String!
age: Int!
}
Bu tür, 2 alana sahip, ve bunlar name ve age olarak adlandırılmış. Birinin türü String ve diğeri de Int. Ayrıca ! işareti de bu alanın gerekli olduğunu belirtiyor.
Ayrıca burada oluşturduğumuz türleri başka yerlerde de kullanabiliriz. Örneğin Person türü, Post türünde kullanılabilir.
Kod:
type Post {
title: String!
author: Person!
}
Ayrıca bu ilişkinin diğer ucu da Person'da tanımlanmalı.
Kod:
type Person {
name: String!
age: Int!
posts: [Post!]!
}
(Buradaki köşeli parantez, posts alanının Post türünden oluşan bir liste olduğunu belirtmekte.)
Sorgular
Yukarıdaki örneklerde de gördüğümüz gibi GraphQL'in yaklaşımı genelde REST'e göre farklı. Birden çok endpoint yerine genelde tek bir endpoint üzerinden farklı sorgular ile farklı veriler çekiyor. Bu sorguları da özelleştirebildiğimiz için oldukça esneklik sunuyor bize.
O zaman GraphQL ile örnek bir sorguyu inceleyelim:
Kod:
{
allPersons {
name
}
}
Bu sorgudaki allPersons alanı, sorgunun temel alanıdır. Bu temel alanın altına yazılan diğer her şey ise sorgunun payload'udur. Burada bu sorgunun payloadında belirlilen tek alan, name'dir.
Bu sorgu, veritabanındaki bütün kişileri döndürecektir. Örnek bir yanıta bakalım:
Kod:
{
"allPersons": [
{ "name": "Johnny" },
{ "name": "Sarah" },
{ "name": "Alice" }
]
}
Fark edebilirsiniz ki yanıtta kişilerin sadece name verisi döndürülmüş. Bunun sebebi, sadece bu alanı sorgumuzda belirtmemizdir.
Fakat bu sorguyu birazcık geliştirip, kişilerin yaşlarını da almak istersek. Sorguya age alanını eklememiz yeterli olacaktır.
Kod:
{
allPersons {
name
age
}
}
GraphQL'in bir diğer büyük özelliklerinden biri de iç içe bilgi alımını desteklemesidir. Örneğin bir kişinin postlarını almak isterseniz aşağıdaki gibi iç içe (nested) bir sorgu yazarak hem kişi bilgisi alırken hem de post bilgisi alabilirsiniz.
Kod:
{
allPersons {
name
age
posts {
title
}
}
}
Argümanlar
GraphQL'de bir alan, eğer şemada belirtildiyse argümanlara sahip olabilir. Örneğin allPersons alanı sadece belirli sayıda kişi döndürmesi için bir last parametresine sahip olabilir. Argümana sahip bir sorgu şu şekilde gözükecektir:
Kod:
{
allPersons(last: 2) {
name
}
}
Mutasyonlar ile Veri Yazmak
Sunucudan veri okumak gibi bir çok uygulamada ayrıca sunucuda saklanan veride değişiklik yapmamızı da gerektiriyor. GraphQL ile bu değişiklikler, mutasyonlar ile yapılıyor.
Genel olarak üç tür mutasyon var:
- yeni veri oluşturma
- olan veriyi güncelleme
- olan veriyi silme
Mutasyonlar da normal sorgular ile aynı yapıya sahipler, fakat bunlar her zaman mutation kelimesi ile başlamalı. Örneğin yeni bir kişi yaratabileceğimiz aşağıdaki örneği inceleyelim:
Kod:
mutation {
createPerson(name: "Bob", age: 36) {
name
age
}
}
Sorguya baktığınızda aslında bu sorgunun da önceki sorgularımızla çok benzer olduğunu göreceksiniz. Bunda da createPerson adında bir temel alan var ve daha önce öğrendiğimiz argümanlar da burada kullanılmış.
Ayrıca bir sorgu gibi yine bu mutasyonda da payload belirtebiliriz ve bu sayede yeni oluşan verinin propertylerine erişebiliriz. Bu örneğimizde bu istediğimiz veriler name ve age olduğu ve bunları zaten biz verdiğimiz için çok da işlevsel görünmeyebilir ama id gibi bizim bilmediğimiz verileri de alabilirdik.
Örnek bir yanıt ise bu şekilde:
Kod:
"createPerson": {
"name": "Bob",
"age": 36,
}
GraphQL'e özgü olarak karşılaşabileceğiniz bir özellik de GraphQL'in özel ID türlerinin olması. Bu türler sayesinde yeni bir nesne oluşturulduğunda, ID de otomatik olarak sunucu tarafından oluşturuluyor. Örneğin Person türümüzü şu şekilde geliştirebiliriz:
Kod:
type Person {
id: ID!
name: String!
age: Int!
}
Artık yeni bir kişi oluşturulduğunda bu nesneye otomatik olarak bir ID de atanıyor ve sorgumuzda bu ID'yi isteyebiliyoruz:
Kod:
mutation {
createPerson(name: "Alice", age: 36) {
id
}
}
Abonelik ile Anlık Güncellemeler
Günümüzde birçok uygulama, sunucuda bir değişiklik olduğu an hemen bilgi sahibi olma ihtiyacını duyabiliyor. GraphQL ise bu tür durumlar için abonelikler denen çok güzel bir konsept sunuyor.
Bir evente abone olunduğunda, sunucu sabit bir bağlantı oluşturacaktır. Bu event her meydana geldiğinde ise sunucu eşleşen veriyi gönderecektir. "istek-yanıt döngüsünü" takip eden sorgular ve mutasyonlardan farklı olarak abonelikler verilerin bir stream'i şeklinde gönderilmektedir.
Person türündeki eventlere abone olduğumuz bir örnek:
Kod:
subscription {
newPerson {
name
age
}
}
İstemci bu abonelik istediğini sunucuya gönderdikten sonra bir bağlantı sunucu ve istemci arasında açılacaktır. Bir yeni bir Person oluşturan mutasyon oluştuğunda ise sunucu yeni kişinin bilgisini gönderecektir:
Kod:
{
"newPerson": {
"name": "Jane",
"age": 23
}
}
Şema Oluşturmak
Artık sorgular, mutasyonlar ve abonelikler hakkında bilginiz olduğuna göre biraz da bu örnekleri çalıştırmamızı sağlayan şemaları nasıl yazabileceğimize bakalım. Şema, API'ın yapabileceklerini ve istemcinin veriyi nasıl isteyebileceğini açıkladığı için en önemli şeylerden biridir.
Genel olarak, bir şema GraphQL türlerinin bir koleksiyonudur. Fakat bir şema yazarken bazı özel temel türler de var.
Kod:
type Query { ... }
type Mutation { ... }
type Subscription { ... }
Bu Query, Mutation ve Subscription türleri, istemci tarafından gönderilen istekler için entry point'lerdir. Örneğin önceden gördüğümüz allPerson'a izin vermek için, Query türü şu şekilde yazılabilir:
Kod:
type Query {
allPersons: [Person!]!
}
API'ın temel alanı, allPersons'tur. Daha önceden gördüğümüz last argümanını eklemek için ise şu şekilde değiştirebiliriz türü:
Kod:
type Query {
allPersons(last: Int): [Person!]!
}
Mutasyonlar için de benzer şekilde Mutations türüne createPerson'u ekleyebiliriz:
Kod:
type Mutation {
createPerson(name: String!, age: Int!): Person!
}
Fark edebilirsiniz ki temel alan, yeni kişi için gerekli olan name ve age argümanlarını alıyor.
Son olarak ise abonelikler için, newPerson temel alanını ekleyebiliriz:
Kod:
type Subscription {
newPerson: Person!
}
Bunların hepsini bir araya getirirsek, bu konuda işlediğimiz tüm her şeyin tam bir şeması şu şekildedir:
Kod:
type Query {
allPersons(last: Int): [Person!]!
}
type Mutation {
createPerson(name: String!, age: Int!): Person!
}
type Subscription {
newPerson: Person!
}
type Person {
name: String!
age: Int!
posts: [Post!]!
}
type Post {
title: String!
author: Person!
}
Son düzenleme: