😶

GraphQL の引数では「値を入れない」と「null を渡す」を区別できる

2021/09/27に公開

TL;DR

  • GrahpQL はその仕様上、 nullable なフィールドに対しての「値を入れない」と「null を渡す」は別物として扱うことができる
  • 特に更新系の mutation で nullable を扱う場合、これらの違いを理解しておくことが重要な場合がある

どういうことか

GraphQL Spec (June 2018)2.9.5 Null Value という説で言及されている。

Null values are represented as the keyword null.

GraphQL has two semantically different ways to represent the lack of a value:

  • Explicitly providing the literal value: null.
  • Implicitly not providing a value at all.

For example, these two field calls are similar, but are not identical:

{
  field(arg: null)
  field
}

The first has explictly provided null to the argument “arg”, while the second has implicitly not provided a value to the argument “arg”. These two forms may be interpreted differently. For example, a mutation representing deleting a field vs not altering a field, respectively. Neither form may be used for an input expecting a Non‐Null type.

例示されてるクエリをみるとわかりやすい。field(arg: String): String というフィールドがあったとして、この nullable な引数には「null を渡す」と「何も渡さない」ということができる。そしてこの2つに異なる解釈を与えることが出来る。

たとえば以下のような、「プロフィールを更新する」 mutation があるとする。

input UpdateProfileInput {
  displayName: String
  location: String
}

type Mutation {
  updateProfile(input: UpdateProfileInput!): UpdateProfilePayload!
}

このとき、 updateProfile(input: { location: null }) という mutation が実行される際に

  • location は null に更新する
  • displayName は変更しない

というように異なる振る舞いを与えることが可能である。

何がうれしいか

逆に、「null を渡す」と「何も渡さない」を区別できないとどうなるか。
上記の updateProfile の例だと、たとえば location だけ更新したい場合にも displayName も渡す必要が出てくる。

# 「null を渡す」と「何も渡さない」を区別しない場合

mutation UpdateLocation($newLocation: String!) {
  # location は意図通り更新されるが、displayName は意図せず値が消滅する
  updateProfile(input: { location: $newLocation }) {
    # ...
  }
}

さらに、UpdateProfileInput が新しいフィールドをサポートした場合、古いのクライアントからの mutation のたびに新しいフィールドが消し飛ばされることになる。とくにネイティブアプリの場合は配布済みの全アプリをアップデートするのはほぼ不可能なので、大きな問題となる。

この問題に対応するためには「mutation はアプリ・ユースケースごとに専用のものを定義する」「一度定義した mutation にあとからフィールドを追加しない」などのルールを追加する必要がある。これが受け入れられるかは GraphQL スキーマの設計方針によって異なるだろう[1]

…と、いうことで、特にあるオブジェクトに対する汎用データ更新 mutation を定義・実装する場合は「null を渡す」と「何も渡さない」を区別するように意識しておくとよい。update ではなく partialUpdate という名前にしたり、ちゃんとコメントを書いたりすると後から来た人にも優しいかも。

type Mutation {
  # ログインユーザのプロフィール情報を更新する。
  partialUpdateProfile(
    # 値が明示的に与えらたフィールドのみが更新される。
    input: UpdateProfileInput!
  ): UpdateProfilePayload!
}

おまけ

Final solution for the eternal null VS undefined problem · Issue #1416 · 99designs/gqlgen を見ると、いろんな言語での GraphQL 実装で困ってることがわかる。

脚注
  1. スキーマ設計方針は「対象クライアント(Web Frontend, モバイルアプリ, ...)の性質」「GraphQL server の立ち位置(フロントエンドエンジニアが触る BFF, あらゆるアプリが叩きに来る Gateway, ...)」「GraphQL の導入理由」などによって変わってくるはず ↩︎

Discussion