GraphQLを使ってWebAPIの構築をやっているのですが、対象のデータが存在しない(ActiveRecord::RecordNotFound
)場合にどうすればいいかがわからなかったので調べました。
結論
graphql-rubyのエラーハンドリングのところに書いてありました。
該当箇所を抜粋すると、こういうことです。rescue_from
を使って、ActiveRecord::RecordNotFound
の場合にはGraphQL::ExecutionError
を発行すればOK。
class MySchema < GraphQL::Schema # ... rescue_from(ActiveRecord::RecordNotFound) do |_err, _obj, _args, _ctx, field| # Raise a graphql-friendly error with a custom message raise GraphQL::ExecutionError, "#{field.type.unwrap.graphql_name} not found" end end
おーし、これでええかな😊と思っていたのですが、graphql-batchを使ってデータをロードしていたので、思ったように機能せず…😢
graphql-batchに対応する
graphql-batchはGraphQLでデータを取得する際にN+1問題が発生しないようにクエリを解析して、うまいことpreloadを呼んでくれるやつです。
READMEを読んで、その通りにやっていくと、RecordLoaderを定義して、loadメソッドを呼ぶことになります。READMEからコピーしてくると、以下のような感じ。
field :product, Types::Product, null: true do argument :id, ID, required: true end def product(id:) RecordLoader.for(Product).load(id) end
これだと、null: trueなので、nullを許可したくないので削除します。
field :product, Types::Product do argument :id, ID, required: true end def product(id:) RecordLoader.for(Product).load(id) end
でもこの状態だと、loadはnilを返すので値がnullになってしまい、GraphQLが返すエラーメッセージがおかしなことに…😵
{ "data": null, "errors": [ { "message": "Cannot return null for non-nullable field Query.product" } ] }
Not NullなフィールドなのにNull返すなよ!
っていう実装側へのメッセージですね。クライアントは悪くない。
じゃあこれを直します!thenを使っていきます。これは、graphql-batchのREADMEに書いてあるような対応方法です。
field :product, Types::Product do argument :id, ID, required: true end def product(id:) RecordLoader.for(Product).load(id).then do |product| raise ActiveRecord::RecordNotFound if product.nil? product end end
こうすると、GraphQLが返すエラーはこのようになります。GraphQLのクエリのこの辺りがおかしいよというメッセージ付きに。
{ "data": null, "errors": [ { "message": "Product not found", "locations": [ { "line": 6, "column": 3 } ], "path": [ "product" ] } ] }
やったぜ!👍👍👍
load!メソッドを定義する
しかし、これだとloadメソッドを呼ぶところ全てでnilチェックをしないといけなくなるので、面倒…。ということで、RecordLoaderにload!メソッドを定義しました。
class RecordLoader < GraphQL::Batch::Loader # ... def load!(key) load(key).then do |record| raise ActiveRecord::RecordNotFound if record.nil? record end end # ... end
これを使うと、フィールドの定義が簡潔になります。loadに!を追加するだけですからね。
field :product, Types::Product do argument :id, ID, required: true end def product(id:) RecordLoader.for(Product).load!(id) end
まとめ
GraphQLでレコードが見つからなかった場合にエラーメッセージを出すようにしました。
しかし、graphql-batchのサンプルコードのように、そもそもnullを許可したほうがいいのだろうか?と、ちょっとAPI設計的にどちらがよいのか疑問です🤔
もっと記事を読んだり本を読んだりしながら、ベストプラクティスを知りたいところ。いい情報があったらお願いします!