Kavramlar, Fikirler, Stratejiler
Kavramlar, Fikirler, StratejilerAlan Argümanları ve Direktiflerin Karşılaştırılması

Alan Argümanları ve Direktiflerin Karşılaştırılması

GraphQL'de bir alanın çıktısını değiştirmeye yönelik aynı işlevsellik çoğu zaman iki farklı yöntemle elde edilebilir:

  1. Alan argümanları: field(arg: value)
  2. Query türü direktifler: field @directive

(Query türü direktifler, istemci tarafında queries üzerinde uygulananlar olup, sunucuda şema oluşturulurken SDL -Schema Definition Language- aracılığıyla uygulanan şema türü direktiflerden ayırt edilir. Gato GraphQL şemayı SDL'den değil PHP kodundan oluşturduğu için tüm direktifleri query türündedir ve bunlar yalnızca "direktifler" olarak adlandırılır.)

Örneğin, bir title alanının yanıtını büyük harfe çevirmek, UPPERCASE enum değeriyle format adlı bir field arg geçirilerek şu şekilde yapılabilir:

{
  posts {
    title(format: UPPERCASE)
  }
}

ya da alana bir @strUpperCase direktifi uygulanarak şu şekilde:

{
  posts {
    title @strUpperCase
  }
}

Her iki durumda da GraphQL sunucusunun yanıtı aynı olacaktır:

{
  "data": {
    "posts": [
      {
        "title": "HELLO WORLD!"
      },
      {
        "title": "FIELD ARGUMENTS VS DIRECTIVES IN GRAPHQL"
      }
    ]
  }
}

Alan argümanlarını ne zaman, query tarafı direktiflerini ne zaman kullanmalıyız? İki yöntem arasında herhangi bir fark var mı ya da bir seçeneğin diğerinden daha iyi olduğu bir durum var mı?

Alan Argümanları ve Direktifler Ne İşe Yarar

GraphQL'de bir alanı çözmek iki farklı işlemi kapsar:

  1. İstenen verilerin sorgulandığı varlıktan alınması
  2. İstenen veriler üzerinde işlevsellik uygulanması (biçimlendirme gibi)

Bu iki işlemi "veri çözümleme" ve "işlevsellik uygulama" ya da kısaca "veri" ve "işlevsellik" olarak adlandırabiliriz.

Alan argümanları ile direktifler arasındaki temel fark şudur: alan argümanları hem "veri" hem de "işlevsellik" için kullanılabilirken direktifler yalnızca "işlevsellik" için kullanılabilir.

Bunun ne anlama geldiğini biraz daha ayrıntılı inceleyelim.

Alan Argümanları Aracılığıyla Veri Çözümleme

Alan argümanları, alan çözümlenirken işlenir; dolayısıyla nesnenin hangi özelliğine erişileceğini belirlemek gibi gerçek verileri almak için kullanılabilirler.

Örneğin, aşağıdaki resolver kodu size argümanının Media nesne türünden bir veya başka bir görsel kaynağını getirmek için nasıl kullanıldığını gösterir:

function resolveValue(
  object $mediaObject,
  FieldDataAccessorInterface $fieldDataAccessor,
): mixed {
  if ($fieldDataAccessor->getFieldName() === 'src') {
    $size = $fieldDataAccessor->getValue('size');
    return $this->getMediaTypeAPI()->getImageSrc($mediaObject, $size);
  }
  // ...
}

Field args ayrıca veritabanı tablosundan hangi satır ya da sütunun sorgulanması gerektiğine karar vermeye de yardımcı olabilir.

Bu queries içinde id alan argümanı, Post türüne ait belirli bir varlığı sorgulamak için kullanılmaktadır; resolver bunu WordPress'in wp_posts veritabanı tablosundaki belirli bir satıra çevirir:

{
  post(by: { id: 1 }) {
    title
  }
}

Aynı tablo, gönderinin tarihini geriye dönük uyumluluk nedeniyle post_modified ve post_modified_gmt olmak üzere iki farklı sütunda saklar. Bu queries içinde gmt alan argümanını true veya false olarak geçirmek, değerin bir sütundan mı yoksa diğerinden mi alınacağına dönüşür:

{
  post(by: { id: 1 }) {
    title
    date(gmt: true)
  }
}

Bu örnekler, field args'ın alan çözümlenirken veri kaynağını değiştirebileceğini göstermektedir.

Direktifler veri kaynağını değiştirmek için kullanılamaz; çünkü mantıkları field resolver'dan sonra çağrılan directive resolver'lar aracılığıyla sağlanır. Dolayısıyla direktif uygulandığında alanın değerinin zaten alınmış olması gerekir.

Örneğin, şu queries hiçbir zaman çalışmayacaktır:

{
  post @selectEntity(id: 1) {
    title
  }
}

Bu örnekte post alanı varlığın id'sinin sağlanmasını gerektirmektedir; field argümanı olarak sağlanmadığı için sunucu bir hata döndürecektir:

{
  "errors": [
    {
      "message": "Argument 'id' cannot be empty",
      "extensions": {
        "type": "QueryRoot",
        "field": "post @selectEntity(id:1)"
      }
    }
  ]
}

Sonuç olarak, alanı çözen verileri almaya yalnızca alan argümanları yardımcı olabilir.

Alan Argümanları veya Direktifler Aracılığıyla İşlevsellik Uygulama

Alan için verileri aldıktan sonra değerini işlemek isteyebiliriz. Örneğin şunları yapabiliriz:

  • Bir dizeyi biçimlendirme: büyük veya küçük harfe çevirme
  • Dizeyle temsil edilen bir tarihi varsayılan YYYY-mm-dd biçiminden dd/mm/YYYY biçimine dönüştürme
  • Bir dizeyi maskeleme: e-posta ve telefon numaralarını *** ile değiştirme
  • null veya boş ise varsayılan değer sağlama
  • Kayan noktalı sayıları 2 basamağa yuvarlama

Bu işlemlerin her biri, halihazırda alınmış veriler üzerindeki bir işlemdir. Dolayısıyla bunlar, field resolver'da veri alındıktan hemen sonra ve döndürülmeden önce ya da alanın değerini girdi olarak alacak directive resolver'da kodlanabilir. Bu nedenle söz konusu işlemlerin her biri hem alan argümanları hem de direktifler aracılığıyla uygulanabilir.

Örneğin, Post.excerpt için field resolver, default adlı bir field arg aracılığıyla varsayılan değer sağlayabilir ve ardından queries içinde default argümanının değerini özelleştirebiliriz:

{
  posts {
    excerpt(default: "(No excerpt)")
  }
}

Ayrıca şu şekilde bir directive resolver ile @default direktifi de oluşturabiliriz:

/**
 * Replace all the empty results with the default value
 */
function resolveDirective(
  array $directiveArgs,
  array $objectIDFields,
  array $objectsByID,
  array &$responseByObjectIDAndField
): void {
  foreach ($objectIDFields as $id => $fields) {
    $object = $objectsByID[$id];
    $defaultValue = $directiveArgs['value'];
    foreach ($fields as $field) {
      if (empty($responseByObjectIDAndField[$id][$field])) {
        $responseByObjectIDAndField[$id][$field] = $defaultValue;
      }
    }
  }
}

Bu iki strateji eşit derecede uygun mu? Bu soruyu farklı ilgi alanları açısından inceleyelim.

Alan Argümanları GraphQL Spesifikasyonunda Daha İyi Kapsanmıştır

Direktiflerin faaliyet göstermesine izin verilen kapsamı GraphQL spesifikasyonunda açıkça tanımlanmamıştır; spesifikasyon şöyle der:

Directives provide a way to describe alternate runtime execution and type validation behavior in a GraphQL document.

In some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.

Bu tanım, bir alanı koşullu olarak dahil eden veya dışlayan @include ve @skip gibi direktiflerin ve sunucudan veri almak için farklı bir çalışma zamanı yürütmesi sağlayan @stream ile @defer direktiflerinin kullanımına olanak tanır.

Ancak bu tanım, "Hello world!" çıktı değerini "HELLO WORLD!" olarak dönüştüren @strUpperCase gibi bir alanın değerini değiştiren direktifler söz konusu olduğunda belirsizliğini korumaktadır.

Bu belirsizlik nedeniyle farklı GraphQL sunucuları, istemcileri ve araçları direktifleri farklı ölçülerde dikkate alabilmekte, bu da aralarında çakışmalara yol açmaktadır.

Buna bir örnek, alan değerlerini önbelleğe alırken direktifleri dikkate almayan Relay'dir. Önce şu queries çalıştırılırsa:

{
  post(by: { id: 1 }) {
    title
  }
}

...Relay, ID'si 1 olan gönderi için "Hello world!" değerini sorgulayacak ve önbelleğe alacaktır. Ardından şu queries çalıştırırsak:

{
  post(by: { id: 1 }) {
    title @strUpperCase
  }
}

...yanıt "HELLO WORLD!" olmalıdır; ancak Relay, ID'si 1 olan gönderi için önbelleğinde sakladığı "Hello world!" değerini döndürecek ve alana uygulanan direktifi yok sayacaktır.

Direktiflerin alanın çıktı değerini değiştirmesine izin verilip verilmediği gri bir alanda kalmaktadır; GraphQL spesifikasyonunda ne açıkça izin verilmiş ne de açıkça yasaklanmıştır, ancak her iki zıt durum için de göstergeler mevcuttur.

Bir yandan, GraphQL spesifikasyonu direktiflere GraphQL'i geliştirme ve özelleştirme konusunda serbest bir alan tanıyor gibi görünmektedir:

As future versions of GraphQL adopt new configurable execution capabilities, they may be exposed via directives. GraphQL services and tools may also provide any additional custom directive beyond those described here.

Öte yandan spesifikasyon, FieldsInSetCanMerge doğrulaması veya CollectFields algoritması için direktifleri dikkate almamaktadır. Aşağıdaki GraphQL queries geçerlidir; ancak kullanıcının hangi yanıtı alacağı belirsizdir:

{
  user(by: { id: 1 }) {
    name
    name @strUpperCase
    name @strLowerCase
  }
}

GraphQL sunucusunun davranışına bağlı olarak name alanının yanıtı "Leo", "LEO" veya "leo" olabilir... bunu önceden bilemeyiz ve bu bir sorundur.

Alan argümanlarında aynı sorun yaşanmaz. Aşağıdaki queries çalıştırıldığında:

{
  user(by: { id: 1 }) {
    name
    name(format: UPPERCASE)
    name(format: LOWERCASE)
  }
}

...spesifikasyon, GraphQL sunucusunun hata döndürmesini zorunlu kılar; dolayısıyla name değeri null olacaktır. Bunun üzerine queries çalıştırmak için takma adlar (alias) kullanmak zorunda kalırız:

{
  user(by: { id: 1 }) {
    name
    ucName: name(format: UPPERCASE)
    lcName: name(format: LOWERCASE)
  }
}

Direktifler Modülerlik ve Kodun Yeniden Kullanılabilirliği Açısından Daha İyidir

Direktiflerin sunduğu işlemlerin çoğu, uygulandığı varlık ve alandan bağımsızdır. Örneğin @strUpperCase, bir gönderinin başlığına, bir kullanıcının adına, bir konumun adresine veya başka herhangi bir şeye uygulandığında her türlü dize üzerinde çalışacaktır.

Sonuç olarak bu direktifin kodu yalnızca bir kez ve tek bir yerde, directive resolver'da uygulanır. Kesişen kaygıların ayrıştırılmasına olanak tanıyarak modülerliği artıran enine kesim programlamasına (aspect-oriented programming) benzer biçimde direktifler, alanın mantığını etkilemeden alana uygulanır.

Buna karşılık, aynı işlevselliği bir alan argümanı aracılığıyla uygulamak, aynı kodun field resolver genelinde (ve farklı field resolver'lar genelinde) yürütülmesini gerektirir:

function formatString(string $string, string $format): string
{
  if ($format === "UPPERCASE") {
    return strtoupper($string);
  }
  if ($format === "LOWERCASE") {
    return strtolower($string);;
  }
  return $string;
};
 
function resolveValue(
  object $post,
  FieldDataAccessorInterface $fieldDataAccessor,
): mixed {
  $format = $fieldDataAccessor->getValue('format');
  if ($fieldDataAccessor->getFieldName() === 'title') {
    return formatString($post->post_title, $format);
  }
  if ($fieldDataAccessor->getFieldName() === 'excerpt') {
    return formatString($post->post_excerpt, $format);
  }
  if ($fieldDataAccessor->getFieldName() === 'content') {
    return formatString($post->post_content, $format);
  }
  // ...
}

Resolver'lardaki kod miktarını azaltmak için direktifler, alan argümanlarına kıyasla daha uygun bir seçenektir.

Direktifler Şema Tasarımı Açısından Daha İyidir

Alan argümanları eklemek, şemaya fazladan bilgi katacak, şemayı şişirebilecek ve tutarsız hale getirebilecektir.

Örneğin, bir format alan argümanının tüm String alanlarına eklenmesi gerekecek ve dikkatli olunmazsa alanlar arasında tutarlı olmayabilir: farklı adlar, farklı değerler, farklı varsayılan değerler veya argümanın birden fazla girdiye bölünmesi gibi durumlar ortaya çıkabilir:

type Post {
  # Input value is "uppercase" or "strLowerCase"
  title(format: String): String
  content(format: String): String
  excerpt(format: String): String
}
 
type Category {
  # Input name is "case" instead of "format"
  # Input value is an enum StringCase with values UPPERCASE and LOWERCASE
  name(case: StringCase): String
}
 
type Tag {
  # Using a default value
  name(format: String = "strLowerCase"): String
}
 
type User {
  # Using multiple Boolean inputs
  description(useUppercase: Boolean, useLowercase: Boolean): String
}

Direktifler şemayı olabildiğince sade tutmamıza olanak tanır:

directive @strUpperCase on FIELD
directive @strLowerCase on FIELD
 
type Post {
  title: String
  content: String
  excerpt: String
}
 
type Category {
  name: String
}
 
type Tag {
  name: String
}
 
type User {
  description: String
}

Direktifler Alan Argümanlarından Daha Verimli Olabilir

Çalışma zamanında, bir alan argümanına alan çözümlenirken erişilir; bu işlem alan alan ve nesne nesne gerçekleşir. Örneğin, bir gönderi listesi üzerinde title ve content alanları çözümlenirken resolver her gönderi ve alan için bir kez çağrılır:

function resolveValue(
  object $post,
  FieldDataAccessorInterface $fieldDataAccessor,
): mixed {
  if ($fieldDataAccessor->getFieldName() === 'title') {
    return $post->post_title;
  }
  if ($fieldDataAccessor->getFieldName() === 'content') {
    return $post->post_content;
  }
  // ...
}

Bu dizeleri Google Translate API kullanarak çevirmek istediğimizi varsayalım; bunun için translateTo argümanını ekleyelim:

function executeGoogleTranslate(string $string, string $lang): string
{
  // Execute against https://translation.googleapis.com
  // ...
};
 
function resolveValue(
  object $post,
  FieldDataAccessorInterface $fieldDataAccessor,
): mixed {
  $lang = $fieldDataAccessor->getValue('lang');
  if ($fieldDataAccessor->getFieldName() === 'title') {
    return executeGoogleTranslate($post->post_title, $lang);
  }
  if ($fieldDataAccessor->getFieldName() === 'content') {
    return executeGoogleTranslate($post->post_content, $lang);
  }
  // ...
}

Mantık doğası gereği alan ve nesne kombinasyonu başına yürütüldüğünden, harici API'ye çok sayıda bağlantı isteği gönderilebilir ve bu da queries çözümlenirken yavaş bir yanıta yol açabilir.

Ayrıca çağrıların birbirinden bağımsız yürütülmesi, verilerinin ilişkilendirilmesine izin vermeyecektir; bu da çeviri kalitesinin tüm verilerin tek bir API çağrısında birlikte gönderilmesine kıyasla daha düşük olması anlamına gelir.

Örneğin, "Power" başlıklı bir gönderi, bu kelimenin "elektrik gücü" anlamına geldiğini ortaya koyan gönderi içeriği birlikte gönderilirse daha iyi çevrilebilir.

Gato GraphQL bir direktifi yalnızca bir kez çağırır ve uygulanacak tüm alanları ile nesneleri girdi olarak geçirir. Tüm verileri bir arada alan @strTranslate direktifi, şu queries örneğinde olduğu gibi tüm nesnelerin title ve content alanlarını tek bir Google Translate çağrısıyla gönderebilir:

{
  posts(pagination: { limit: 6 }) {
    title @strTranslate(from: "en", to: "fr")
    excerpt @strTranslate(from: "en", to: "fr")
  }
}

Direktifler, özellikle harici API'lerle etkileşim kurulurken alan değerlerini değiştirmek için daha performanslı bir yol sunabilir.