GraphQL の引数では「値を入れない」と「null を渡す」を区別できる
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 実装で困ってることがわかる。
-
スキーマ設計方針は「対象クライアント(Web Frontend, モバイルアプリ, ...)の性質」「GraphQL server の立ち位置(フロントエンドエンジニアが触る BFF, あらゆるアプリが叩きに来る Gateway, ...)」「GraphQL の導入理由」などによって変わってくるはず ↩︎
Discussion