GraphQL Sorgu Çalıştırma

Harun Akgün
4 min readOct 29, 2018

--

Önceki Sayfa > Doğrulama

Bir GraphQL sorgusu, doğrulaması yapıldıktan sonra çalıştırılması için GraphQL sunucusuna iletilir.

Bir tip sistemi olmadan GraphQL sorguları çalıştıramaz. Bir sorgunun nasıl çalıştırıldığını göstermek için örnek bir tip sistemi kullanalım. Bu tip sistemi daha önceki örneklerimizde kullandığımız tip sisteminin bir parçasıdır.

type Query {
human(id: ID!): Human
}
type Human {
name: String
appearsIn: [Episode]
starships: [Starship]
}
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
type Starship {
name: String
}

Bu sorgu çalıştırıldığında neler olduğunu anlatmak için uçtan uca bir örnek yapalım:

sorgu:

{
human(id: 1002) {
name
appearsIn
starships {
name
}
}
}

cevap:

{
"data": {
"human": {
"name": "Han Solo",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"starships": [
{
"name": "Millenium Falcon"
},
{
"name": "Imperial shuttle"
}
]
}
}
}

GraphQL sorgularındaki her alanı sonraki tipte veri dönen önceki tip methodu ya da fonksiyonu olarak düşünebilirsiniz. Aslında GraphQL tam olarak böyle çalışır. Her alandaki her tip GraphQL sunucusunun geliştiricisi tarafından sağlanan resolver adında bir fonksiyon tarafından desteklenir. Bir alan çalıştırıldığında bu alanla ilişkili resolver çağırılır ve bir sonraki değeri üretir.

Eğer bir alan string ya da integer gibi skalar bir değer üretirse çalıştırma tamamlanmış demektir. Ancak bir alan obje tipinde değer üretirse sorgu bu objenin alt alanları için de çalışmaya devam eder. Çalıştırma süreci skalar değerlere ulaşılana kadar devam eder. GraphQL sorguları her zaman skalar değerlerle tamamlanır.

Kök alanlar & Resolver’lar

Her GraphQL sunucusunun en üst seviyesinde GraphQL API’sine mümkün olan tüm giriş noktalarını temsil eden bir tip tanımlanır. Bu tip genellikle Root Tipi ya da Query tipi olarak adlandırılır.

Bu örnekte Query tipimiz human adında argüman olarak id bekleyen bir alan sağlar. Bu alanla ilişkili olan resolver fonksiyon muhtemelen veritabanına bağlanacak ve Human objesini oluşturacaktır.

Query: {
human(obj, args, context, info) {
return context.db.loadHumanByID(args.id).then(
userData => new Human(userData)
)
}
}

Bu örneği JavaScript ile yazdık, ancak GraphQL sunucusu bir çok başka dille yazılmış olabilir. Her resolver fonksiyonu dört adet argüman alır;

  • obj Önceki obje, genellikle kök sorgu üzerindeki alanlar için kullanılmaz.
  • args GraphQL sorgusundan alana özel olarak gönderilmiş olan argümanlar.
  • context Her resolver tarafından sağlanan ve mevcut giriş yapmış kullanıcı ya da veritabanına erişme bilgisi gibi önemli içerik(context) bilgilerini içeren bir değer.
  • info alana özel ve mevcut sorgu ile ilişkili bilgileri ve şema detaylarını içeren bir değer. Daha fazla bilgi için GraphQLResolveInfo tipine göz atın.

Asenkron Resolver’lar

Resolver fonksiyonu içinde neler olduğuna yakından bakalım:

human(obj, args, context, info) {
return context.db.loadHumanByID(args.id).then(
userData => new Human(userData)
)
}

GraphQL sorgusu tarafından iletilen argümanlar içerisindeki id parametresini kullanarak kullanıcı verisini veri tabanından çekmek için gereken veritabanı erişimi context kullanılarak alınmış. Veri tabanına sorgu atmak asenkron bir işlem olduğu için bu fonksiyon bir Promise döner. JavaScript’te promise’ler asenkron değerlerle çalışmak için kullanılır. Ancak benzeri konseptler diğer birçok dilde de vardır, bunlara genel olarak Futures, Tasks ya da Deferred adı verilir. Veritabanı işini bitirip değer döndürdükten sonra yeni bir Human objesi oluşturulup işleme devam edilir.

Resolver fonksiyonlar Promise’ler göz önünde bulundurarak tasarlanmalıdır, ancak GraphQL sorguları Promise’ler ile ilgilenmez. Sadece human alanı daha sonra name alanı içeren bir obje olarak dönsün ister. Çalıştırma sırasında GraphQL Promise’ler, Future’ler ve Task’ların tamamlanmasını bekler ve bu işi optimal eşzamanlılıkta yapar.

Basit Resolverlar

Artık Human objesi elimizde olduğuna göre GraphQL alt alanlar ile ilgili yürütmeye devam edebilir.

Human: {
name(obj, args, context, info) {
return obj.name
}
}

Bir GraphQL sunucusu bir sonraki adımı belirleyen bir tip sistemi ile desteklenmektedir. human alanı herhangi bir değer döndürmeden önce bile GraphQL sonraki adımı bilir.

Bu durumda Human içerisindeki name alanını çözümlemek çok kolaydır. name resolver fonksiyonu çağırılır, argümanlar arasındaki obj bir önceki alanda yer alan new Human objesidir. Bu durumda Human objesinin name adında bir alt alanı olduğunu bildiğimizden doğrudan bu bilgiyi okuyup dönebiliriz.

Aslında bir çok GraphQL kütüphanesi bu tarz basit resolver’ları yazmanıza gerek bırakmaz. Alan için bir resolver olmadığında aynı isimdeki property’nin okunup dönülmesi gerektiğini varsayar.

Skalar’a Zorlama

name alanının değeri çözümlenirken, eş zamanlı olarak appearsIn ve starships alanları da çözümlenebilir. appearsIn alanı için de çok basit bir resolver yazılabilir, yakından bakalım:

Human: {
appearsIn(obj) {
return obj.appearsIn // [ 4, 5, 6 ] döner
}
}

Farkındaysanız tip sistemimiz appearsIn alanının Enum bir değer döneceğini belirtiyor, ancak bu fonksiyon bir rakam döner. Aslında sonuç objesine bakarsak bu alanın gerçekte Enum değerleri döndürdüğünü fark ederiz. Burada ne oluyor?

Bu durum skalar’a zorlama’nın (scalar coercion) bir örneğidir. Tip sistemi bu resolver’ın üreteceği sonuçtan ne bekleyeceğini bilir ve dönen değeri başka bir resolver fonksiyon sayesinde API dökümantasyonunda belirtilen değere çevirir. Bu durumda, içinde 4,5 ve 6 gibi rakamları kullanan bir Enum beklenmesi gerektiği belirtilmiş olabilir, ancak GraphQL tip sistemi bu değerleri Enum değerleri olarak görüntüleyebilir.

Liste Resolver’ları

Bir önceki örnekte appearsIn gibi bir alan bir liste döndüğünde ne olduğunu biraz gözlemleme şansı bulduk. Enum değerlerden oluşan bir liste döndü, ve tip sistemimiz de bunu beklediği için dönen değerleri alakalı Enum değerlerle değiştirdi. Peki starships alanı çözümlendiğinde ne olur?

Human: {
starships(obj, args, context, info) {
return obj.starshipIDs.map(
id => context.db.loadStarshipByID(id).then(
shipData => new Starship(shipData)
)
)
}
}

Bu alanın çözümleyicisi sadece bir Promise değil, bir Promise’ler listesi döner. Human objesi içinde bu kişinin kullandığı her birStarships objesinin id’sini döndü. Bu durumda her bir starship objesini bu id’leri kullanarak çözümlememiz gerekiyor.

GraphQL eş zamanlı olarak tüm bu Promise’lerin çözümlenmesini bekler, ve bir liste ile karşılaştığında listedeki her bir elemanın name alanını eşzamanlı olarak yüklemeye devam eder.

Sonuç Üretmek

Her bir alan çözümlenirken sonuç olarak üretilen değer alan adı ya da alias’ı key, çözümlenen değer ise value olmak üzere bir key-value objesine yerleştirilir. En dipten başlayarak Query tipine kadar bu işlem devam eder. Tüm bu çözümlemeler tamamlanınca yapılan sorgunun yapısını birebir yansıtan bir sonuç üretilir ve genellikle JSON olarak isteği yapan istemciye dönülür.

Sorgu ve cevaba son kez bakıp tüm çözümleyici fonksiyonlar çalıştıktan sonra ortaya çıkan sonucu görelim:

sorgu:

{
human(id: 1002) {
name
appearsIn
starships {
name
}
}
}

cevap:

{
"data": {
"human": {
"name": "Han Solo",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"starships": [
{
"name": "Millenium Falcon"
},
{
"name": "Imperial shuttle"
}
]
}
}
}

--

--