GraphQL ve Spring Boot ile API sorgulama

Tahir KARDAK
12 min readDec 27, 2020

Uzun bir aradan sonra uzun bir yazı ile merhaba. Bu yazıdan sonra tahmini 1–2 aylık bir ara daha vereceğim. Bu aradan sonra ayda en az bir yazı paylaşamayı düşünüyorum.

Bu yazımda Spring Boot ile Graphql kullanarak Api’lar üzerinde ihtiyacımız olan verilerin nasıl getirilebileceğini anlatmaya çalışacağım. Kaynak kodlar makalenin sonunda mevcut

Öncelikle GraphQL nedir ve neden kullanmalıyız konusuna değinmek istiyorum. Eğer Graphql’i biliyorsanız direkt örnek kod bölümüne geçebilirsiniz. Bu bölüm biraz uzun olacak.

İçerik:

GraphQL?

GraphQL, Facebook tarafından geliştirilmiş, REST için daha esnek ve verimli bir alternatif sunan bir API standartıdır. Kısaca istemcinin, sunucudan (endpointten) ihtiyacı kadar veriyi (alanları) sorgulamasını sağlayan bir yaklaşımdır.

API sorgulama dilidir, veritabanı sorgulama değildir!

Şu şekilde örneklendirebiliriz. Kullanıcı bilgilerini getiren bir rest servis yazdığımızı düşünelim(Java kullanılıyoruz.). Kullanıcı Entity’sinde adı, soyadı, email bilgilerinin yanı sıra sahip olduğu rolleri listesi, uygulama üzerindeki izinlerinin listesi ve ekip arkadaşlarının listesinin yer aldığını düşünelim. Bu listelerin Fetch tipini de Lazy olarak ayarlandığını; Entity DTO dönüşümünü MapStruct, Dozer yada kendi mapper sınıfımız ile yaptığımızı düşünelim ve kullanıcının adını ve soyad bilgisini getirmek için bir rest servisi yazdığımızı düşünelim.

Yapmış olduğumuz mapper strajesine göre Lazy olan liste tipindeki alanlarda veri tabanından sorgulanabilir. Duruma göre entity nin ilişkili olduğu entity ler içinde veri tabanından veri getirilecektir. İlişkilerin fazla olduğu bir yapıda duruma göre 20(örnek bir rakam) üzeri gereksiz bir sürü sorgu çalışabilir. Tek bir api çağrısında 100 üzeri tane sorgu çalıştığına şahit oldum :)

İstemci; email adresi ile kullanıcı bilgilerini restten çektiğinde restten bir sürü bilgi gelecektir. Gelen bilgi içerisnden sadece kullanıcının adı ve soyadı bilgisi ekranda gösterilmek istenmesine rağmen ihtiyacı olamayan roller, izinler gibi alanlarda gelmiştir. Bu durum sunucu istemci arasında gereksiz bir veri trafiğine neden olmuştur. İstemci sadece ad ve soyad bilgisine ihtiyaç duyuyorsa bunun için projection veya mapper işlemi yaparak ayrı bir api, kullanıcının adı soyadı ve rol bilgisi için ayrı bir mapper ve api çağrısı yapması gerekmektedir.

Kullanıcı Entitysi üzerinde farklı veri talepleri için ayrı bir rest servisi ve mapper işlemi yapması gerekmektedir. Backend kodu bu durumda git gide yönetilmez bir hale gelecektir. Ayrıca bir kullanıcıya başka bilgilere ihtiyaç olursa onun içinde ayrı bir rest çağrısı yapmak gerekmektedir.

Graphql burada devreye giriyor ve tek bir api üzerinden ihtiyacı olduğu kadar verileri getirilmesine, isteğe göre tek bir api çağrısı ile birden çok kaynaktan veri getirilmesine yardımcı olur. Rest ile GraphQL karpılaştırması için GraphQL vs REST — A Consumers View videosunu izlemenizi tavsiye ederim

GraphQL temel olarak şu bileşenlerden oluşur: Schema, Type, Resolver. Şimdi bu kavramlara yakından bakalım.

Schema: Veri yapısının tanımlanmasına imkan sağlayan bileşendir . Java tarafında buna entity tanımı diyebiliriz. Schema Definition Language(SDL) ile bu tanımlamaları kolay ve okunaklı bir şekilde yapabilir. Schema; Input, Type, Query, Mutation, Subscription, vb. Type bileşenlerini içerir.

Type:

1. Scalar Types: Çoğu programlama dilinden de bildiğiniz veri tipleridir. GraphQL aşağıdaki tipleri destekler.

id: Int          # 32 bit sayı değeri.
amount: Float # Ondalıklı sayı değeri.
name: String # UTF‐8 alfa numerik değeri.
active: Boolean # true ya da false değeri.
id: ID # Alfa numerik benzersiz tanımlayıcı değerini tutar
input userInput{
firstname: String
lastname: String
}# objelerin parametre olarak kullanılmasına olanak sağlar

ID: Genellikle UUID formatında değerleri tutmak için kullanılır.

input: Query, Mutation, Subscription’lara parametre olarak objeleri geçmesine yardımcı olan özel bir Object Type’lardır. Input type’lar başka bir input type içinde kullanılabilir.

Date ve DateTime gibi scalar tipleri kullanmak için graphql-java-extended-scalars projeye ekleyeceğiz.

2. Object Types: Kendi tiplerimizi tanılamaya olanak sağlar. Object typler başka bir object type içinde kullanılabilir.

type User {
id: Int
firstname: String
lastname: String
role: [Role]
email: String
}
type Role{
id: Int
name: String
user: User
}

Dikkat: type içersinde kesinlikle input kullanılamaz. Tam tersi de geçerlirdir.

Backend tarafında mutlaka input ve object type’ler için aynı isimde ve aynı alanlara sahip classlar olmalı

3. Query Type: GraphQL’de sorgu hazarlamak için kullanılır.

type Query {
userByEmail: [User]
}

4. Mutation Type: GraphQL’ de yazma, güncelleme, silme işlemleri yapabilmek için kullanılır.

type Mutation {
createUser(input: userInput): User
}

Resolver: Schema’da tanımladığımız Query, Mutation ve Subscription’lar için çalışacak fonksiyon tanımlamarını içerir. Her bir Query, Mutation ve Subscription için bir resolver’ımız bulunması gerekir. Bu sayede GraphQL kendisine gelen isteklere ait verilerin hangi kaynaktan nasıl alınacağı veya kaydedileceğini çözebilsin.

Spring boot örnek Resolver
public class UserMutation implements GraphQLMutationResolver {
....
public User createUser(input:userInput){
...
}
}
public class UserQueryResolver implements GraphQLQueryResolver {
...
public User userByEmail(String email) {
....
}
}

Dikkat Edilecek Konular

  • alan(field) isimleri camelCase olmalıdır.
  • type isimleri PascalCase olmalıdır.
  • input isimleri PascalCase olmalıdır.
  • Enum değerleri ENUM_NAME olmalıdır.
  • input ve object type için backend tarafında aynı isimde sınıfılar oluşturulmalı.
  • Query ve Mutation’larda yazılan metotlar, backend tarafında aynı isimle oluşturulmalı. Query’lerde için başına get konularak da oluşturulabilir.
  • Graphql’da type, input, query, mutation vb tanımlarken isimler tekil olmalı. Tüm bu tanımlar tek dosyada veya ayrı ayrı dosyada yapılsa bile tekil olmalıdır. Aksi takdirde proje build olamayacaktır.
# Hatalı tanım
type kullanici: String
input kullanici: String
#Doğru
type kullanici: String
input kullaniciInput: String

GraphQL ile ilgili açıklamayı burada bitiriyorum. Graphql Learn sayfasından Interface, Enum, Directive vb konulara yakından bakabilirsiniz. Yazı içierisinde graphql ile önemli gördüğüm konuları açıklamaya devam edeceğim.

Spring Boot İle Graphql

Spring Boot GraphQL için güncel Github Repository’si için aşağıdaki bağlantıyı kullanabilirsiniz.

Spring Starter ile projemizi oluşturduktan sonra GraphQL için gerekli olan bağımlılıklar pom.xml dosyasına ekliyoruz. Makalede testlerle ilgili herhangi anlatım olmayacağı için pom’a test bağımlılıklarını eklemiyorum.

Proje Yapısı:

Proje’nin github linkini yazının sonunda bulabilirsiniz. application.yml dosyasında aşağıdaki ayarları yapıyoruz

graphql:
servlet:
maxQueryDepth: 5 // iç içe type larda en gazla sorgulama sayısı
exception-handlers-enabled: true
tracingEnabled: false

Projemizinin yapısı şu şekildedir. Aşağıdaki resimde gösterilen numaralar ve harfleri makalenin ilgili bölümlerinde şu şekilde kullanacağım ve bu bölüme referans edeceğim. Örnek: (1-da) veya (3-graphql) .

Yukarıda sayı ile gösterilen kısımların açıklaması şu ve içerikleri şekildedir

  1. da: Veriye erişim katmanı. Bu bölümde entity ve reposity sınıfları mevuttur.

2. exceptions: Graphql’de hata yönetimi için oluşturulmuş sınıfın bulunduğu bölümdür.

3.graphql: Graphql için yaratılmış tüm sınıfların bulunduğu bölümdür. İçeriği şu şekildedir.

___a. config: Date, Datetime gibi custom scalar tiplerin tanımlandığı konfigürasyon sınıfının bulunduğu bölümdür.

___b. context: Graphql Dataloader kullanarak karşılaştığımız N+1 sorunu için kullanacağımız Context bilgisinin tanımlandığı bölümdür. Konuyla iligili detaylı açılama makalenin ilerleyen bölümlerinde olacaktır.

___c. inputs: Input tiplerinin tanımlandığı bölümdür. aynı tanımlamalar aşağıdaki 7. bölümde gösterilen graphql shcema tanımlamasında da bulunmaktadır.

___d. instruentation: Grapql execution adımlarının takip edilip log’lanması için kullanılan bölümdür. Client tarafından yapılan sorgular ve parametreler loglanmak istenilirse bu bölümdeki instruentation sınıfı kullanılabilir

___e. listener: Sunucuya yapılan request taleplerinin loglandığı bölümdür.

___f. resolver: 7. bölümde tanımlanan Query ve Mutation lara ait metotların tanımlandığı bölümdür.

4. service: Veriye erişim için kullanılan sınıfların bulunduğu bölümdür.

6. grahiqleditor: GraphiQL Editör’ü her açıldığında, varsayılan olarak, kullanılmak için hazırlanan query, mutation ve variable’ların bulunduğu bölümdür. Query ve Mutation’ların bulunduğu dosya uzantısı graphql olamalı. Bu iki dosya için application.yml dosyasında aşağıdaki gösterilen ayarlar yapımalıdır.

# graphiql için yaml dosya ayarları
graphiql:
mapping: /graphiql
endpoint:
graphql: /graphql
subscriptions:
timeout: 30
reconnect: false
static:
basePath: /
enabled: true
pageTitle: GraphiQL
cdn:
enabled: false
version: 0.13.0
props:
resources:
query: graphiqleditor/querymutation.graphql
variables: graphiqleditor/variables.json
variables:
editorTheme: "solarized light"

7. grahql: Schema bilgilerinin tanımlandığı bölümdür. Burada tanımlanan dosya uzantıları graphqls şeklinde olmalı. Aksi taktirde derleme esnasında hata alacaktır.

8. data.sql: Uygulama h2 in-memory veri tabanı kullanılmıştır. Oluşturulan tablolar için insert sqllerinin bulunduğu dosyadır.

Dosya yapısına baktından sonra kodlamaya geçebiliriz.

Schema Bilgileri ve GraphiQL İşlemleri

Uygulamamızda kullanıcı ve kullanıcının sahip olduğu rollerin ait sorgulanma ve güncellenme işlemleri yapılacaktır. Aşağıdaki type, input query ve mutationları tanımlayarak işleme başlıyoruz.

Query ve Mutation bilgileri:(7-grahql)

mutation dosyasındaki 2 ve 6. satırlar arasında custom scalar’ları tanımlıyoruz. Bu tanımlamaları kullanabilmek için backend tarafında da aşağıdaki tanımlamaların yapılması gerekmektedir.

Kullanıcı bilgileri:

user.graphqls deki 2 , 6 ve 7. satırda bulunan ! (ünlem işareti) ilgili alanın zorunluğu olduğunu belirtir. yine 6 ve 7. satırda [Role] ile bu alanın içinde kullanıcıya ait rollerin bulunduğu bir listenin döneceğini belirtilir. [Role!] ile [Role]! arasındaki fark şu şekildedir.

  • [Role!]: Listenin tüm elemanları dolu olmak zorunda eğer bir tane eleman null ise hata fırlatır. [1,2,3] doğru. [1,null,2] hatalı
  • [Role]!: Eğer Liste null dönerse(yani hiçbir elemanı yoksa) hata fırlatır.

8. Satırda Date scalar tipi kullanılmıştır. Bu tipin tanımı mutation dosyasında bulunmaktadır.

19. satırda var olan Query type’ni extend edip için 2 tane yeni metot ekliyoruz. 120. Satırda # kullanarak metodun ne işe yaradığını belirten yorumu ekliyoruz. Bu yorum Graphiql editöründe Documantation bölümünde görünecektir. 26. satırda var olan Mutation type’ni extend edip içine yeni bir metot ekliyoruz.

Role bilgileri:

Grahiql işlemleri: (7-grahiqleditor)

Bu kısımda yapılan işlemler opsiyoneldir. Sorgu hazırlarken taslak sorguların bulunması faydalı olur diye düşünüyorum.

Sorgu ve mutation

Yukarıdaki örnekte 2. satırda $email argumanına varsayılan bir değer atıyoruz. $email boş bırakılırsa, burada yazılan değere göre sorgulama yapılacaktır. 14. satırda @include directive kullanılarak eğer withRoles argümanı true olarak verilmiş ise dolu gelecek aksi durumda null olarak değer dönecektir. 11 ve 19. satırdaki farklı iki api cağrısı tek query’in içinde tanımlanmıştır. Bu sayede sunucuya tek bir talep gidecek ama iki farlı api çalışacaktır.

Yukarıdaki örnekteki $ ile tanımlanan argumanlar için tanımlanan variable’ların ismi aynı olmalı

arguman adı → varibale adı
-----------------------
$email → email

Variables

Yukarıdaki tanımlamalar sonucunda: tüm kodlar yazılıp, uygulama build edildkten GraphiQL editör şu şekilde olacaktır.

Spring Boot İşlemleri

Entity, Repository, Servi İşlemleri: (1-da) ve (4-service)

Bu bölümde standart java, jpa, spring boot işlemleri yapılıyor, henüz graphql için herhangi bir işlem yapılmıyor. Graphql’i ilgilendiren durumlarda ilgili sınıfın altına yorum yazacağım

Yukarıdaki kodda 3 ve 22. satırdaki anotasyonlar Lombok’tan gelmektedir. ilgili bağımlılık pom.xml’de tanımlanmıştır. 16 ve 38. satırda entityler arasındaki ilişler Lazy olarak tanımlanmıştır. Bu sayede sorgu yapıldığında bu alanlar ilk başta boş gelecek. İhtiyaç dahilinde user.getRoles() şeklinde çağrılırsa veri tabanından sorgulanıp getirelecektir. Graphql sayesinde tüm ilişkileri Lazy olarak tanımlayabiliriz. Mapper kullanmayacağımız için (mapper stratejisine göre lazy alanlar dolu geliyor) bu alanlar hep boş gelecek.

Yukarıdaki örnekte 18 ve 24. satırdaki loglama mesajları önemli. Bu mesajlar ile N+1 problemini ve çözümünü göstereceğiz.

Graphql İşlemleri: (3-grapgql)

Şimdi backend tarafında graphql işlemlerine bakalım. Bu bölümde Resolver’larla ilgili tanımlamaları ve açıklamaları yapacağız.

GraphQLQueryResolver: (f-resolver)

Schema Bilgileri bölümünde tanımladığımız Querylerin çözümlenmesi için kullanılır. Tek bir GraphQLQueryResolver’la projede tüm sorgular çalıştırılabilir. Ama okunurluk ve kodun kolay yönetimi için her bir type için ayrı GraphQLQueryResolver kullanılması iyi olur. Örneğimizde resolver’ları ayrı olarak tanımlayacağız. Query Resolverların içinde metot isimler Query’ler aynı isimde yada metodun başına get olarak yazılması gerekmektedir. Aksi taktirde ilgili query’ler çözümlenemeyecek ve hata verecektir. Oluşturulan sınıfın başına @Component anastosyonu mutlaka kullanılmalıdır. Kullanılmazsa Graphql çalışmayacaktır.

Aşağıdaki görselde Query ve Resolver ile ilgili durum detaylı bir şekilde gösterilmiştir.

Yukarıdaki resimde Query ve resolverlardaki metotların isimleri, aldığı parametre bilgileri ve dönüş tipleri bire bir aynı olduğu görülebilir.

Uygulamayı çalıştırıp, sorgulama işlemleri yapalım

Eğer user tablosundaki role bilgisini sorgulamak istersek aşağıdaki gibi LazyInitializationException hatasını alırız.

Bunun sebebi; kullanıcıya ait role bilgileri User entity sınıfında LAZY olarak tanımlanmıştır. Bu sorunu GraphQLResolver kullanarak çözebiliriz.

GraphQLResolver vs GraphQLQueryResolver

GraphQLQueryResolver; Querylerde tanımlanmış metotların backend tarafında hazırlanması için kullanılır. GraphQLResolver ise Entity’lerde bulunan lazy olarak tanımlanmış alanların doldurulabilmesi için kullanılır. GraphQLResolver ile sadece Lazy değil aynı zamanda type larda belirtilen tüm alanları bir metot gibi kullanıp, istenilen başka işlemler de yapılabilir. Bu sınıfın içerisinde tanımlanan metotların parametresi mutlaka dönüş tipi GraphQLResolver’daki metotun dönüş tipi ile aynı olmak zorundadır.

Yukarıdaki örnekte UserResolver Sınıfı GraphQLResolver interface’inden USER için implement ediyoruz. 6. ve 10. satırdaki metotların parametresi User tipindedir. Aşağıdaki resimde Bu sınıfın User type ile olan ilişkisi gösterilmiştir

Uygulamayı derleyip Graphiql’de sorgu çalıştırırsak son durum şu şekilde olacaktır.

N+1 Sorunu ve Çözümü: (b-context)

Sorunu daha iyi anlatabilmek için Query ile tüm kullanıcıları ve rollerini sorgulayıp log dosyasına bakalım.

Aşağıdaki log dosyasına baktığımız da kullanıcılara ait rollerin her bir kullanıcı için tek tek veri tabanından sorgulandığını görmekteyiz.

Kayıt sayısının çok olduğu sorgulamalarda, Entiy’de bulunan ilişkili Entitylere ait Lazy değerler her seferinde veri tabanından ayrı ayrı sorgulanacktır ve ciddi performans sorunu ortaya çıkacaktır. Bu duruma N+1 sorunu denilmektedir. Bu durumu çözmek için GraphQLServletContextBuilder ile custom context yaratıp içinde DataFetcher kullanılır. Konuyla ilgili GraphQL Demystified (The n+1 problem) yazısını okumanızı tavsiye ederim

Yukarıdaki örnekte 25. satırda buildDataLoaderRegistry() metodu ile DataLoader’ımızı oluşturup 20 . satırda “userRoleDataLoader” adı ile register ediyoruz. 28. satırda verileri tek seferde veritabanından getircek batch loader işlemini yapıyoruz. 30. satırda kullanıcıların rollerini id listesi ile getirecek servisi çalıştırıyoruz. Servisten gelen listeyi user id ye göre map’leyip sonucu dönüyoruz. 29. satırda userIds kullanıcı id’leri bir Set içerisinde tutulmaktadır. İşlemin sonundan ComletableFurute ile asenkron çağrı yapılmaktadır.

UserResolver sınıfına 15. satırdaki Dönüş tipi CompletableFuture<List<Role>> olan roles2 metodunu ekliyoruz. Bu metodun birinci parametresi User tipindedir. İkinci parametresi ise oluşturduğumuz regisrty bilgisine ulaşabilmemiz için gerekli olan DataFetchingEnvironment tipinde paremetredir. roles2 iki metodun aynısını Schema içindeki User input type’inda da tanımlıyoruz. 16. satırda userRoleDataLoader ile tanımladığımız DataLoader’ı bulup kullanıcı id bilgisine göre 19. satırda load işlemi yapıyoruz.

Devam etmeden önce DataFetchingEnvironment için bir parantez açmak istiyorum. DataFetchingEnvironment ile graphql srogu çalışırken. Parametre bilgilerine, sorgunun sonucunda istenen ala bilgilerine ulaşıp custom sorgular yazabiliriz.

Uygulamayı derleyip Graphiql’de sorgulama yapıp log’a bakalım.

Log’da da görüldüğü gibi roller veri tabanından Select * from Roles where user_id in (1,2,3) sorgusu ile tek seferde getirildi.

GraphQLMutationResolver: (f-resolver)

Sunucu tarafında veri kaydetmek ve güncellemek için ve Schema bilgileri bölümünde tanımladığımız Mutation’ların çözümlenmesi için kullanılır. Tek bir GraphQLMutationResolver’la projede tüm mutationlar çalıştırılabilir. Ama okunurluk ve kodun kolay yönetimi için her bir type için ayrı GraphQLMutationResolver kullanılması iyi olur. Mutation Resolverların içinde metot isimler Muatation’ile aynı isimde yazılması gerekmektedir. Aksi taktirde ilgili mutation’lar çözümlenemeyecek ve hata verecektir. Oluşturulan sınıfın başına @Component anastosyonu mutlaka kullanılmalıdır. Kullanılmazsa Graphql çalışmayacaktır.

Schema ile MutationResolver ile ilişki aşağıda gösterilmiştir.

Metotların isimleri, aldığı parametreler ve dönüş değerleri birebir aynı olmak zorundadır.

GraphQL Execution adımlarının ve Sunucuya yapılan taleplerin loglanması: (d-instruentation) ve (e-listener)

Spring boot Graphql ile sunucudaki bütün execution adımları ve Request talepleri takip edilip isteğe göre işlemler yapılabilir. Bu kısımda, her iki işlemin loga yazılması üzerinden örnek vereceğim.

Yukarıdaki örnekte execution başlağıdındaki süreci logluyoruz. 11. satırda execution bittiğinde loglama yapıyoruz. SimpleInstrumentation sınıfında ihtiyaca göre kullanılabilecek bir çok metot bulunmaktadır.

Yukarıdaki örnekte GraphQLServletListener kullanarak, Requestin başlayıp bittiği ve hata aldığı durumlarda loglama işlemi yapabiliriz.

GraphQL oldukça keyifli bir konu. Umarım faydalı olmuştur. Yeni yazılarda görüşmek üzere.

Github Kodları:

Kaynaklar:

Graphql ve Spring Boot Securrity kullanımı:

Philip Starrit’in hazırladığı eğitim seririni izlemenizi tavsiye ederim

--

--