GraphQL Argumentsで「引数を渡さない」と「nullable」の挙動の違いを理解する
結論
GraphQL Arguments の必須指定には以下3種類がある(GraphQL-Ruby を例に)。
- arguments の指定は必須かつ null 不許可(
required: :true
) - arguments の指定は optional(
required: :false
) - arguments の指定は必須だけど null を渡せる(
required: :nullable
)
用途に応じて適切に使い分けましょう。
概要
GraphQL の argument を定義する時に、特に Mutation だと特定のフィールドをオプショナルにしたい時があります。具体的なケースとしては以下のような場合です。
- Mutation で投げたフィールドだけ更新したい
- 後方互換性を保ちながらargumentにフィールドを追加したい
これを行うための引数の定義方法が「2. arguments の指定は optional(required: :false
)」です。
また一方で、特定のフィールドを必須にはしておきたいが null で更新できるようにしておきたい場合もあります( メモ
みたいに空で更新できるようなフィールド)。
これが「3. arguments の指定は必須だけど null を渡せる(required: :nullable
)」です。
これは GraphQL の仕様にも Null Value として定義されており、argument そのものを渡さないことと null を渡すことは明確に区別されると書いてあります。
「1. arguments の指定は必須かつ null 不許可(required: :true
)」については明確ですね。可能な限りこれで定義したいところです。
2, 3 について、どのような挙動になるのかを GraphQL-Ruby を例に詳しく見てみます。
required: false と required: :nullable の違い
TODOアプリを例に説明します。
このTODOアプリは以下の仕様です。
- title は記入が必須
- memo は書いても書かなくても良い
この時、作成時/更新時の mutation と argument を以下のように定義することができます。
ぞれぞれの Input 定義の違いに着目してください。
TODO作成時
title, memo は引数としては必須、だが memo は nullable な形で実装してみます。
module Types
module InputObject
class CreateTodoInput < Types::BaseInputObject
graphql_name 'createTodo'
# title は必須なので必ず文字列を投げないといけない
argument :title, String, required: true
# memo はパラメーターとしては投げないといけないが、中身はnullでもOK
argument :memo, String, required: :nullable
end
end
end
module Mutations
class CreateTodo < Mutations::BaseMutation
argument :create_todo_input, Types::InputObject::CreateTodoInput, required: true
field :todo, Types::Object::TodoType
def resolve(create_todo_input:)
todo = Todo.new
# create_todo_input には必ず title, memo が入ってくる
todo.assign_attributes(create_todo_input.to_h)
if todo.save
{ todo: todo }
else
raise GraphQL::ExecutionError, todo.errors.full_messages.join(", ")
end
end
end
end
この時は以下のような引数でリクエストできます。memo は引数としては必須ですが、null で投げられます。
{
"input": {
"createTodo": {
"title": "タイトル",
"memo": null
}
}
}
memo は引数として渡しているので、resolver の input にキーが含まれます。 Mutations::CreateTodo#resolve
に binding.pry を仕込んで create_todo_input
の値を見てみると、memo がキーとして渡ってきてるのが見れます。
create_todo_input.to_h
# => {
id: "c3624f8d-435c-42a5-80e5-74a50d533d5b",
title: "新しいタイトル",
memo: nil,
}
create_todo_input.has_key?(:memo) # => true
memo は引数として必須なので、以下のような引数で CreateTodo を投げることはできません。
これはエラー
{
"input": {
"createTodo": {
"title": "タイトル",
}
}
}
TODO更新時
更新の時は「引数として投げられた field のみを更新する」ことを想定して実装してみます。
module Types
module InputObject
class UpdateTodoInput < Types::BaseInputObject
graphql_name 'updateTodo'
argument :id, ID, required: true
# title も memo もオプショナルなので、引数に含めなくてもOK
argument :title, String, required: false
argument :memo, String, required: false
end
end
end
module Mutations
class UpdateTodo < Mutations::BaseMutation
argument :update_todo_input, Types::InputObject::UpdateTodoInput, required: true
field :todo, Types::Object::TodoType
def resolve(update_todo_input:)
todo = Todo.find(update_todo_input.id)
# update_todo_input に更新したいキーがある場合のみ更新する
# title は Todo モデルでバリデーションする想定
todo.title = update_todo_input.title if update_todo_input.key?(:title)
todo.memo = update_todo_input.memo if update_todo_input.key?(:memo)
if todo.update
{ todo: todo }
else
raise GraphQL::ExecutionError, todo.errors.full_messages.join(", ")
end
end
end
end
この時は、ざっくり以下のパターンで引数を投げることができます。
update_todo_input.to_h の結果も一緒に確認してください。
{
"input": {
"createTodo": {
"id": "c3624f8d-435c-42a5-80e5-74a50d533d5b",
"title": "新しいタイトル",
"memo": "メモメモメモ"
}
}
}
update_todo_input.to_h
# => {
id: "c3624f8d-435c-42a5-80e5-74a50d533d5b",
title: "新しいタイトル",
memo: "メモメモメモ",
}
create_todo_input.has_key?(:title) # => true
create_todo_input.has_key?(:memo) # => true
{
"input": {
"createTodo": {
"id": "c3624f8d-435c-42a5-80e5-74a50d533d5b"
"title": "新しいタイトル",
}
}
}
update_todo_input.to_h
# => {
id: "c3624f8d-435c-42a5-80e5-74a50d533d5b",
title: "新しいタイトル",
}
create_todo_input.has_key?(:title) # => true
create_todo_input.has_key?(:memo) # => false
{
"input": {
"createTodo": {
"id": "c3624f8d-435c-42a5-80e5-74a50d533d5b"
"memo": "メモメモメモ"
}
}
}
update_todo_input.to_h
# => {
id: "c3624f8d-435c-42a5-80e5-74a50d533d5b",
memo: "メモメモメモ",
}
create_todo_input.has_key?(:title) # => false
create_todo_input.has_key?(:memo) # => true
{
"input": {
"createTodo": {
"id": "c3624f8d-435c-42a5-80e5-74a50d533d5b"
"title": "新しいタイトル",
"memo": null
}
}
}
update_todo_input.to_h
# => {
id: "c3624f8d-435c-42a5-80e5-74a50d533d5b",
title: "新しいタイトル",
memo: nil,
}
create_todo_input.has_key?(:title) # => true
create_todo_input.has_key?(:memo) # => true
{
"input": {
"createTodo": {
"id": "c3624f8d-435c-42a5-80e5-74a50d533d5b"
}
}
}
update_todo_input.to_h
# => {
id: "c3624f8d-435c-42a5-80e5-74a50d533d5b",
}
create_todo_input.has_key?(:title) # => false
create_todo_input.has_key?(:memo) # => false
挙動をまとめると、以下のようになります。
-
required: false
では、引数として渡すかどうかもオプショナル- 引数として渡していない時は引数のキーそのものがない
-
required: :nullable
では、引数として渡すことは必須だが null で渡せる- null を渡しても引数のキーは存在する
どう使い分けるか
引数は、必須である方が実装の条件分岐を減らせるので、可能な限り required: true
または required: :nullable
を使うと良いと思います。引数としては必須にしておきたいが null でも更新可能、という場合に required: :nullable
にすると、CreateTodo の例のようにそのままモデルに引数を渡すだけなので便利です。
しかし、新しく field を追加する際に、後方互換性を保つ必要がある場合は required: false
で追加することになります。例えばモバイルアプリで GraphQL を使っている場合、バックエンド側で必須の引数を追加してしまうと旧バージョンのアプリで使えなくなってしまいます。
また、上記 UpdateTodo でも示した通り、更新の Mutation では required: false
がとても便利です。引数を渡してない
or 引数にnullを渡してる
が「引数のキーが存在するかで」判定できるので「引数が渡された field のみを更新する」が可能です。
更新の時は引数を必須にしたがゆえに意図しない値を渡して更新してしまった(デフォルト値とか)…が起こりうるので、オプショナルにしておくことで意図しない形での更新を防ぐことができます。
もちろん、いずれの場合でもテストは網羅的に書きましょう!
株式会社アルダグラムのTech Blogです。 世界中のノンデスクワーク業界における現場の生産性アップを実現する現場DXサービス「KANNA」を開発しています。 採用情報はこちら herp.careers/v1/aldagram0508/
Discussion