Alan versiyonlama ile şemayı geliştirme
Uygulamamızın ihtiyaçları geliştikçe, ona veri sağlayan GraphQL API'nin de gelişmesi ve şemasında değişiklikler getirmesi gerekecektir. Değişiklik kırıcı olmadığında, yeni bir tür veya alan eklemek gibi durumlarda, yan etkilerden korkmadan doğrudan uygulayabiliriz. Ancak değişiklik kırıcı nitelikteyse, uygulamada hata veya beklenmedik davranışlar ortaya çıkarmadığımızdan emin olmamız gerekir.
Kırıcı değişiklikler; bir tür, alan veya direktifi kaldıran ya da mevcut bir alanın (veya direktifin) imzasını değiştiren değişikliklerdir; örneğin:
- Bir alanı yeniden adlandırmak
- Mevcut bir alan argümanının türünü değiştirmek veya zorunlu hale getirmek
- Alana yeni bir zorunlu argüman eklemek
- Bir alanın yanıt türüne null olamaz kısıtlaması eklemek
Kırıcı değişikliklerle başa çıkmak için iki temel strateji vardır: versiyonlama ve evrim; bunlar sırasıyla REST ve GraphQL tarafından uygulanmaktadır.
REST API'leri, kullanılacak API versiyonunu ya endpoint URL'sinde (örneğin https://api.mycompany.com/v1 veya https://api-v1.mycompany.com) ya da bir başlık aracılığıyla (örneğin Accept-version: v1) belirtir. Versiyonlama sayesinde kırıcı değişiklikler API'nin yeni bir versiyonuna eklenir ve istemcilerin API'nin yeni versiyonunu açıkça işaret etmesi gerektiğinden, değişikliklerden haberdar olurlar.
GraphQL versiyonlamayı reddetmez, ancak evrimi kullanmayı teşvik eder. GraphQL en iyi uygulamalar sayfasında belirtildiği üzere:
While there's nothing that prevents a GraphQL service from being versioned just like any other REST API, GraphQL takes a strong opinion on avoiding versioning by providing the tools for the continuous evolution of a GraphQL schema.
Evrim, versiyonlamada olduğu gibi her birkaç ayda bir gerçekleşmesi beklenmediğinden farklı davranır. Aksine, gerektiğinde günlük olarak bile gerçekleşen sürekli bir süreçtir; bu da onu hızlı iterasyona daha uygun kılar. Bu yaklaşım, bir GraphQL hizmetinin geliştirilmesine rehberlik eden en iyi uygulamalar dizisi olan Principled GraphQL tarafından beşinci ilkesinde ortaya konmuştur:
5. Use an Agile Approach to Schema Development: The schema should be built incrementally based on actual requirements and evolve smoothly over time
Şemayı geliştirme
Evrim aracılığıyla, kırıcı değişiklikler içeren alanların aşağıdaki süreçten geçmesi gerekir:
- Alanı farklı bir ad kullanarak yeniden uygulayın.
- Alanı kullanımdan kaldırın ve istemcilerden yeni alanı kullanmalarını isteyin.
- Alan artık kimse tarafından kullanılmadığında, şemadan kaldırın.
Bir örnek görelim. Bir Account türümüz olduğunu varsayalım; bu tür, bu şema aracılığıyla bir hesabı ad ve soyadı olan bir kişi olarak modellemektedir (GraphQL'in SDL'ini - Schema Definition Language - kullanarak):
type Account {
id: Int
name: String!
surname: String!
}Bu şemada hem name hem de surname alanları zorunludur (bu, String türünden sonra eklenen ! sembolüdür), çünkü tüm kişilerin hem adı hem de soyadı olmasını bekliyoruz.
Zamanla, kuruluşların da hesap açmasına izin veriyoruz. Ancak kuruluşların soyadı olmadığından, surname alanının imzasını zorunlu olmayan hale getirecek şekilde değiştirmemiz gerekir:
type Account {
id: Int
name: String!
surname: String # Bu değişti
}Bu bir kırıcı değişikliktir çünkü uygulama surname alanının null döndürmesini beklemez, bu yüzden bu koşulu kontrol etmeyebilir; şu JavaScript kodunu çalıştırırken olduğu gibi:
// account.surname null olduğunda bu başarısız olacak
const upperCaseSurname = account.surname.toUpperCase();Kırıcı değişikliklerden kaynaklanabilecek hatalar, şema geliştirilerek önlenebilir:
surnamealanının imzasını değiştirmiyoruz; bunun yerine onu kullanımdan kaldırılmış olarak işaretliyor ve onu hangi alanın yerini aldığını belirten yardımcı bir mesaj ekliyoruz- Şemaya
personSurname(veyaaccountSurname) adında yeni bir alan adı ekliyoruz
Account türümüz artık şöyle görünüyor:
type Account {
id: Int
name: String!
surname: String! @deprecated(reason: "Use `personSurname`")
personSurname: String
}Son olarak, istemcilerimizin queries loglarını toplayarak yeni alana geçiş yapıp yapmadıklarını analiz edebiliriz. surname alanının artık kimse tarafından kullanılmadığını fark ettiğimizde, onu şemadan kaldırabiliriz:
type Account {
id: Int
name: String!
personSurname: String
}Evrimle ilgili sorunlar
Yukarıda açıklanan örnek çok basittir, ancak şemayı geliştirmekten kaynaklanan birkaç olası sorunu zaten göstermektedir:
| Sorun | Açıklama |
|---|---|
| Alan adları daha az düzenli hale gelir | Alana ilk kez ad verdiğimizde, muhtemelen surname gibi en uygun adı buluruz. Ancak onu değiştirmemiz gerektiğinde, optimal olmayabilecek farklı bir ad oluşturmamız gerekir (optimali zaten alınmış!). Yukarıdaki örnekteki tüm olası değiştirmelerin sorunları vardır:- personName, hesabın bir kişi için olduğunu açıkça belirtir; dolayısıyla daha sonra soyadı olan bir kişi olmayan biri (bilmiyorum... bir Marslı?) için hesap açmamız gerekirse, tutarlı adlar korumak adına şemayı yeniden geliştirmemiz gerekecektir- accountName içindeki "account" kısmı tamamen gereksizdir çünkü tür zaten Account- Yoksa başka ne ad kullanmak gerekir? surname1? surnameNew? Ya da daha da kötüsü, surnameV2?Sonuç olarak, güncellenen şema daha az anlaşılır ve daha ayrıntılı olacaktır. |
| Şema kullanımdan kaldırılmış alanlar biriktirebilir | Alanları kullanımdan kaldırmak, geçici bir durum olarak en mantıklısıdır; sonunda, birikmeye başlamadan önce şemayı temizlemek için o alanları gerçekten kaldırmak isteriz. Ancak, queries'lerini gözden geçirmeyen ve kullanımdan kaldırılmış alandan bilgi almaya devam eden istemciler olabilir. Bu durumda şemamız yavaş ama istikrarlı şekilde bir tür alan mezarlığına dönüşerek aynı işlev için birkaç farklı alan biriktirir. |
Bu sorunların nasıl çözüleceğini görelim.
Alanları versiyonlama
Alanımızı hangi versiyonunun kullanılacağını belirttiğimiz version adlı bir argümanla oluşturabiliriz.
Bu senaryoda, kullanımdan kaldırılmış alan için uygulamayı yine de saklamamız gerekecektir, bu yüzden bu konuda bir iyileştirme yapmıyoruz. Ancak sözleşmesi gizli hale gelir: yeni alan artık orijinal adını koruyabilir (surname'den personSurname'e yeniden adlandırmaya gerek yoktur), şemamızın çok ayrıntılı hale gelmesini önler.
Bu versiyonlama kavramının REST'tekinden farklı olduğuna dikkat edin:
- REST, kullanılacak sürüm endpoint'in parçası olduğundan, sorgulanan tüm API'nin aynı versiyona sahip olduğu bir ya hep ya hiç durumu oluşturur
- Bu diğer yaklaşımda, her alan bağımsız olarak versiyonlanır
Bu nedenle, farklı alanlar için farklı versiyonlara şu şekilde erişebiliriz:
query GetPosts {
posts(version: "1.0.0") {
id
title(version: "2.1.1")
url
author {
id
name(version: "1.5.3")
}
}
}Ayrıca semantic versioning'a dayanarak, paket bağımlılıklarını bildirmek için Composer'ın kullandığı kuralların aynısını izleyerek versiyonu seçmek için versiyon kısıtlamalarını kullanabiliriz. Ardından version alan argümanını versionConstraint olarak yeniden adlandırır ve queries'i güncelleriz:
query GetPosts {
posts(versionConstraint: "^1.0") {
id
title(versionConstraint: ">=2.1")
url
author {
id
name(versionConstraint: "~1.5.3")
}
}
}Bu stratejiyi kullanımdan kaldırılmış surname alanımıza uygulayarak, kullanımdan kaldırılmış uygulamayı "1.0.0" versiyonu ve yeni uygulamayı "2.0.0" versiyonu olarak etiketleyebilir ve aynı queries içinde bile her ikisine erişebiliriz:
query GetSurname {
account(id: 1) {
oldVersion: surname(versionConstraint: "^1.0")
newVersion: surname(versionConstraint: "^2.0")
}
}Bu özellik Gato GraphQL'de mevcuttur:

Direktifleri versiyonlama
Direktifler de argüman aldığından, direktifleri versiyonlamak için tam olarak aynı metodoloji uygulanabilir!
Örneğin, şu queries çalıştırılırken:
query {
post(by: { id: 1 }) {
oldVersion: title @strTitleCase(versionConstraint: "^0.1")
newVersion: title @strTitleCase(versionConstraint: "^0.2")
}
}Direktifin her versiyonu için farklı bir yanıt üretebilir:
