Konsep, Ide, Strategi
Konsep, Ide, StrategiMembandingkan argumen field dan direktif

Membandingkan argumen field dan direktif

Fungsionalitas yang sama untuk memodifikasi output sebuah field di GraphQL seringkali dapat dicapai melalui dua metode berbeda:

  1. Argumen field: field(arg: value)
  2. Direktif tipe query: field @directive

(Direktif tipe query adalah direktif yang diterapkan pada query di sisi klien, berbeda dengan direktif tipe skema, yang diterapkan melalui SDL -Schema Definition Language- saat membangun skema di server. Karena Gato GraphQL membuat skema dari kode PHP, bukan dari SDL, semua direktifnya bertipe query dan cukup disebut sebagai "direktif".)

Misalnya, mengonversi respons field title menjadi huruf kapital dapat dicapai dengan memberikan field arg format dengan nilai enum UPPERCASE, seperti ini:

{
  posts {
    title(format: UPPERCASE)
  }
}

atau dengan menerapkan direktif @strUpperCase pada field, seperti ini:

{
  posts {
    title @strUpperCase
  }
}

Dalam kedua kasus, respons dari server GraphQL akan sama:

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

Kapan kita harus menggunakan argumen field dan kapan direktif sisi query? Apakah ada perbedaan antara dua metode tersebut, atau situasi tertentu di mana satu opsi lebih baik dari yang lain?

Kegunaan argumen field dan direktif

Menyelesaikan sebuah field di GraphQL melibatkan dua operasi berbeda:

  1. mengambil data yang diminta dari entitas yang di-query
  2. menerapkan fungsionalitas (seperti pemformatan) pada data yang diminta

Kita dapat menyebut dua operasi ini sebagai "resolusi data" dan "penerapan fungsionalitas", atau singkatnya sebagai "data" dan "fungsionalitas".

Perbedaan utama antara argumen field dan direktif adalah bahwa argumen field dapat digunakan untuk "data" maupun "fungsionalitas", tetapi direktif hanya dapat digunakan untuk "fungsionalitas".

Mari kita lihat lebih detail apa artinya ini.

Meyelesaikan data melalui argumen field

Argumen field diproses saat menyelesaikan field, sehingga dapat digunakan untuk mengambil data aktual, seperti menentukan properti mana dari objek yang diakses.

Misalnya, kode resolver ini menunjukkan bagaimana argumen size digunakan untuk mengambil salah satu sumber gambar dari tipe objek Media:

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

Field args juga dapat digunakan untuk membantu menentukan baris atau kolom mana dari tabel DB yang harus di-query.

Dalam query ini, argumen field id digunakan untuk meng-query entitas tertentu bertipe Post, yang akan diterjemahkan oleh resolver menjadi baris tertentu dari tabel DB wp_posts milik WordPress:

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

Tabel yang sama menyimpan tanggal post dalam dua kolom berbeda, post_modified dan post_modified_gmt (untuk alasan kompatibilitas mundur). Dalam query ini, memberikan argumen field gmt dengan true atau false diterjemahkan menjadi pengambilan nilai dari salah satu kolom tersebut:

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

Contoh-contoh ini menunjukkan bahwa field args dapat memodifikasi sumber data saat menyelesaikan field.

Direktif tidak dapat digunakan untuk memodifikasi sumber data, karena logikanya disediakan melalui directive resolver, yang dipanggil setelah field resolver. Oleh karena itu, pada saat direktif diterapkan, nilai field sudah harus diambil terlebih dahulu.

Misalnya, query ini tidak akan pernah berhasil:

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

Dalam contoh ini, field post memerlukan id entitas untuk diberikan, dan karena tidak diberikan sebagai argumen field, server akan mengembalikan error:

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

Kesimpulannya, hanya argumen field yang dapat membantu mengambil data yang menyelesaikan field.

Menerapkan fungsionalitas melalui argumen field atau direktif

Setelah kita mengambil data untuk field, kita mungkin ingin memanipulasi nilainya. Misalnya, kita bisa:

  • Memformat string, mengonversinya menjadi huruf kapital atau huruf kecil
  • Memformat tanggal yang direpresentasikan dengan string, dari format default YYYY-mm-dd menjadi dd/mm/YYYY
  • Menyamarkan string, mengganti email dan nomor telepon dengan ***
  • Memberikan nilai default jika nilainya null atau kosong
  • Membulatkan float ke 2 digit

Semua operasi ini adalah manipulasi pada data yang sudah diambil. Oleh karena itu, semuanya dapat dikodekan baik di field resolver, tepat setelah mengambil data dan sebelum mengembalikannya, maupun di directive resolver, yang akan menerima nilai field sebagai inputnya. Dengan demikian, semua operasi ini dapat diimplementasikan melalui argumen field atau direktif.

Misalnya, field resolver untuk Post.excerpt dapat menyediakan nilai default melalui field arg default, dan kemudian kita dapat menyesuaikan nilai untuk arg default dalam query:

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

Kita juga dapat membuat direktif @default, dengan directive resolver seperti ini:

/**
 * 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;
      }
    }
  }
}

Apakah dua strategi ini sama-sama cocok? Mari kita eksplorasi pertanyaan ini berdasarkan berbagai area kepentingan.

Argumen field lebih baik dicakup oleh spesifikasi GraphQL

Sejauh mana direktif diizinkan beroperasi tidak didefinisikan dengan jelas dalam spesifikasi GraphQL, yang menyatakan:

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.

Definisi ini mengizinkan penggunaan direktif seperti @include dan @skip, yang secara kondisional menyertakan dan melewati field, serta @stream dan @defer, yang memberikan eksekusi runtime yang berbeda untuk mengambil data dari server.

Namun, definisi ini tidak ambigu terkait direktif yang memodifikasi nilai sebuah field, seperti @strUpperCase, yang mengubah nilai output "Hello world!" menjadi "HELLO WORLD!".

Karena ambiguitas ini, server, klien, dan alat GraphQL yang berbeda mungkin memperhitungkan direktif dalam tingkat yang berbeda-beda, menimbulkan konflik di antara mereka.

Salah satu contohnya adalah Relay, yang tidak memperhitungkan direktif untuk men-cache nilai field. Jika pertama kali meng-query:

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

...Relay akan meng-query dan men-cache nilai "Hello world!" untuk post dengan ID 1. Jika kemudian kita menjalankan query ini:

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

...responsnya seharusnya "HELLO WORLD!", namun Relay akan mengembalikan "Hello world!", yang merupakan nilai yang tersimpan di cache-nya untuk post dengan ID 1, mengabaikan direktif yang diterapkan pada field.

Apakah direktif diizinkan memodifikasi nilai output field atau tidak berada di area abu-abu, karena hal ini tidak secara eksplisit diizinkan maupun dilarang dalam spesifikasi GraphQL, namun ada indikator untuk kedua situasi yang berlawanan.

Di satu sisi, spesifikasi GraphQL tampaknya memberikan direktif kebebasan untuk meningkatkan dan menyesuaikan GraphQL:

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.

Di sisi lain, spesifikasi tidak memperhitungkan direktif untuk validasi FieldsInSetCanMerge atau algoritma CollectFields. Query GraphQL berikut ini valid, namun tidak pasti respons apa yang akan diperoleh pengguna:

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

Bergantung pada perilaku server GraphQL, respons untuk field name bisa berupa "Leo", "LEO" atau "leo"... kita tidak tahu sebelumnya, dan itu adalah masalah.

Masalah yang sama tidak terjadi dengan argumen field. Ketika query berikut dieksekusi:

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

...spesifikasi mewajibkan server GraphQL untuk mengembalikan error, sehingga nilai untuk name akan menjadi null. Kita kemudian akan dipaksa untuk memperkenalkan alias untuk mengeksekusi query:

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

Direktif lebih baik untuk modularitas dan kemampuan penggunaan ulang kode

Banyak operasi yang ditawarkan oleh direktif bersifat agnostik terhadap entitas dan field tempat direktif diterapkan. Misalnya, @strUpperCase akan bekerja pada string apa pun, baik diterapkan pada judul post, nama pengguna, alamat lokasi, atau hal lain apa pun.

Akibatnya, kode untuk direktif ini diimplementasikan hanya sekali dan di satu tempat, yaitu directive resolver. Mirip dengan aspect-oriented programming (yang meningkatkan modularitas dengan memungkinkan pemisahan cross-cutting concerns), direktif diterapkan pada field tanpa memengaruhi logika field tersebut.

Sebaliknya, mengimplementasikan fungsionalitas yang sama melalui argumen field melibatkan eksekusi kode yang sama di seluruh field resolver (dan di berbagai field resolver):

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);
  }
  // ...
}

Untuk mengurangi jumlah kode dalam resolver, direktif lebih cocok daripada argumen field.

Direktif lebih baik untuk desain skema

Menambahkan argumen field akan menambah informasi ekstra ke skema, yang mungkin membuatnya membengkak dan tidak konsisten.

Misalnya, argumen field format perlu ditambahkan ke semua field String dan, jika kita tidak hati-hati, mungkin tidak homogen di seluruh field, seperti menggunakan nama berbeda, nilai berbeda, nilai default berbeda, atau bahkan memecah argumen menjadi beberapa input:

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
}

Direktif memungkinkan kita menjaga skema se-ramping mungkin:

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
}

Direktif dapat lebih efisien daripada argumen field

Pada waktu eksekusi, argumen field akan diakses saat menyelesaikan field, yang terjadi secara field per field dan objek per objek. Misalnya, saat menyelesaikan field title dan content pada daftar post, resolver akan dipanggil sekali per post dan field:

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

Bayangkan kita ingin menerjemahkan string-string ini menggunakan Google Translate API, yang untuk itu kita menambahkan argumen translateTo:

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);
  }
  // ...
}

Karena logika secara alami dieksekusi per kombinasi field dan objek, kita mungkin berakhir dengan mengajukan sejumlah besar koneksi ke API eksternal, menghasilkan respons yang lambat untuk menyelesaikan query.

Selain itu, mengeksekusi panggilan secara independen satu sama lain tidak akan memungkinkan pengasosiasian data mereka, sehingga kualitas terjemahan akan lebih rendah dibandingkan jika semua data dikirimkan bersama dalam satu panggilan API.

Misalnya, judul post "Power" dapat diterjemahkan lebih baik jika konten post, yang menjelaskan bahwa kata ini merujuk pada "electrical power", dikirimkan bersama dengannya.

Gato GraphQL memanggil direktif hanya sekali, memberikan semua field dan objek yang akan diterapkan sebagai input. Dengan menerima semua data sekaligus, direktif @strTranslate dapat mengeksekusi satu panggilan ke Google Translate dengan melewatkan semua field title dan content untuk semua objek, seperti dalam query ini:

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

Direktif dapat memberikan cara yang lebih efisien untuk memodifikasi nilai field, seperti saat berinteraksi dengan API eksternal.