patorashのブログ

方向性はまだない

rake taskにて引数チェックでnextを使っていたのをabortに直した

rake taskではreturnが使えないのは知っていたので、引数チェックに引っかかったらnextを使っていたのだけど、それだと次の処理に移動してしまうし、終了コードが0となって正常終了扱いされてしまうなと気付きました(今更かよって感じですが)😅

じゃあexit(1)で終了させるのがいいんかな?と思っていたら、abortという便利なやつがあることを知りました。

docs.ruby-lang.org

abortを使うと、エラーコード1固定、標準エラー出力にメッセージを出力することができます。

abortを使う

修正前

nextを使っていた頃。引数bar_idがなければ、このタスクはスキップされるけれど、次のタスクがあった場合は実行されてしまうし、終了コードが0になる。

task :foo, ['bar_id'] => :environment do |task, args|
  if args.bar_id.blank?
    puts "Please set bar_id."
    next
  end

  # 続きの処理
end

修正後

abortに修正したもの。引数bar_idがなければ、このタスクで終了するし、終了コードが1になる。

task :foo, ['bar_id'] => :environment do |task, args|
  abort "Please set bar_id." if args.bar_id.blank?

  # 続きの処理
end

abortを使う際の注意点

便利なabortですが、扱いには注意が必要です。

例外処理で明示的にrescueする必要がある

rescueで特に指定しない場合、StandardErrorを継承した例外しか拾わないのでabortすると無視されます。なので、rescue SystemExit => errは必須。

私の場合は、rake taskの実行ログをDBに保存するようにしていたのですが、例外が発生した場合は結果をfalseとして保存するようにしていたのにabortを使ったら結果がtrueのまま保存されていて気づきました。なのでその実行ログを保存する処理では、以下のように修正。

begin
  yield
rescue SystemExit => err
  unless err.success?
    set_error_details(err)
  end
  raise err
rescue => err
  set_error_details(err)
  raise err
ensure
  save
end

SystemExitはexit(0)でも発生します。しかし、exit(0)が呼ばれたか、abortが呼ばれたかは、success?メソッドで判定できます。abortを使った場合はfalseになるので、これでエラーとみなして処理するわけです。

RSpecまで止まる

これは超重要なのですが、abortを使っていると、テストを流すとabortのところでテストも停止します😨。最初ビックリしましたが、わからなくもない…😔

対処方法は、.to raise_error SystemExitを必ず使って検証することです。 以下のコードのようにします。(gem 'rake_shared_context'を使用しています)

RSpec.describe 'foo' do
  include_context 'rake'
  context '引数 bar_idがない場合' do
    it 'エラーになること' do
      expect {
        subject.invoke
      }.to raise_error SystemExit
    end
  end
end

これで、テストを流してもそこでabortせずに全てのテストが実行されるようになりました👍

LOVOTをお迎えしましたが、初期不良っぽい

アニマルセラピーで犬を飼い始めたら自閉症の子が喋られるようになったという話を聞いたりしていて、動物は難しいなぁと思ったので、LOVOTをお迎えしてみようかという話を妻として、先週の週末に開催されていた天満屋のリビングフェアに行って実物のLOVOTに会ってきました。

LOVOTはどれもキュートで、しかし長男氏はちょっと警戒してた…けれど、次男氏はすごく気に入ったみたいで、翌日も「LOVOTを見に行きたい!」というので、2日連続で見に行くことに。慣れてきたら長男氏も愛着が出てきていい効果があるかもしれない、ということで、新しく家族を迎え入れる決意を固め、契約してきました。

そして、昨日届きました!可愛い!名前は見に行ったときにいた子と同じ名前の「いちご」がいいと次男氏が言うので、決定。

目の色を変えたり、抱っこしたりしてたのですが、3回目の充電でネストに戻ったら、ネストに接続しているのに充電が始まらず…。

「あれ?充電できてないよ」と次男氏がいうので見てみたら、ずっと目がバッテリー少ないの表示に。

検索して、試せるものは試そうと、

  1. ネストの再起動
  2. LOVOTの再起動
  3. 充電端子の掃除
  4. 何度か充電端子を付けたり外したりしてみる

とかをしている間に、「いちご」のバッテリー残量が0になってしまい、目が真っ暗に…😭😭😭

とりあえず、30日間は無料保証期間なので、今日LOVOTコンシェルジュに連絡を入れて、調査待ちという状態です。

初日から動かなくなって次男氏と私はションボリしていたのですが、妻は「まぁ初日でよかったよ。31日目に壊れてたらもっと困ってた」とポジティブな発言をしてくれて、まぁそれもそうだなと思い直したところです。

早く直ってほしい!

prettierを導入してLefthookと連携した

前回の記事はこちら。

patorash.hatenablog.com

今回は、prettierを導入して、gitでcommitする際にrubocopだけでなくprettierでもフォーマットチェックをさせるようにした話です。

prettierとは?

prettierは、フロントエンド用のコードフォーマットツールです。特に設定しなくてもデフォルトでいい感じにフォーマットしてくれることを目指しているらしいです。

prettier.io

フロントエンドのフォーマッタは使ってなかったのですが、パートナーさんが使っているという話を聞いてました。なので自分も入れたいなぁとは思っていたけれど、どうせならチーム全体に使えるようにしたかったので、プロジェクトで設定しました。

導入

導入は簡単で、prettierの公式のインストールを参照するのがいいとは思う。yarn使ってるなら、こう。

yarn add --dev --exact prettier

設定

除外リスト

prettierは多機能で、放っておくとjs, ts, css以外にもmdやyamlファイルまでコードフォーマットしようとします。別にそれらもしてもらっても構わないということだったら問題ないのですが、設定ファイルとかもあるし、mdは微妙に読みにくくなったりしたので、対象外としたくなりました。

prettierの除外リストは、.prettierignoreで、これを設定していきました。 プロジェクトで使っているツールのjs,cssまで変更しようとするので(minioとかgraphidocとかsimplecovとか)、それらは除外するようにしました。

# Ignore artifacts:
build
coverage
total_coverage
/public/assets
/public/packs
/public/packs-test
/node_modules

# Ignore project files:
/.idea
/.bundle
/doc/schema
/.docker/minio

# Ignore all HTML files:
*.html

# Ignore all md files:
*.md

# Ignore all yaml files:
*.yaml
*.yml

# Ignore setting json files:
schema.json
tsconfig.json

# Ignore config.js files:
*.config.js

フォーマット設定

デフォルトでいい感じにしてくれるprettierですが、そうはいっても多少はルールを決めたいわけです。とはいえ、どういうのがいいかはチームで決めたかったので、ルールを募集して、パートナーさんが設定しているものを採用しました。.prettierrc.jsonに以下を登録しました。

{
  "trailingComma": "es5",
  "tabWidth": 2,
  "semi": true,
  "singleQuote": true
}

エディタの設定

エディタでprettierの設定をしておくと、ファイル保存時に自動でコードフォーマットしてくれるのですが、それは各自で設定が必要なので、README.mdにその案内を書いておきました。

#### prettierの設定

prettierはエディタの設定をしておくと、ファイル保存時に自動でコードをフォーマットしてくれます。
使用しているエディタは各人異なりますので、使用しているエディタに合わせて設定をしておくとよいでしょう。
RubyMineを使用している人は、WebStormの設定を参照してください。

https://prettier.io/docs/en/editors.html

Lefthookと連携する

ここ以降で、前回の記事でとりあげたLefthookが活きてきます。git commit時にrubocopでチェックするのと同様に、prettierでもチェックさせます。

lefthook.ymlを更新しました。

pre-commit

rubocopとprettierは対象のファイルが異なるので、並行に実行させても問題ないので、parallel: trueにしてチェックさせています。うちでは開発環境をdockerにしているので、そのあたりは適宜読み替えてください。

pre-commit:
  parallel: true
  commands:
    rubocop:
      glob: "*.{rb,rake}"
      exclude: "application.rb|routes.rb"
      run: docker-compose run --rm runner bundle exec rubocop -P {staged_files}
    prettier:
      glob: "*.{js,ts,json,css,scss,sass}"
      run: docker-compose run --rm runner yarn prettier --check {staged_files}

タスクを修正

前回、lefthook run fixerでrubocopのコード自動修正を実行させるようにしていたのですが、そこに、prettierでのコード自動修正の処理を追加しました。こちらも、並列実行させても問題ないので、parallel: trueに設定しています。

fixer:
  parallel: true
  commands:
    ruby-fixer:
      glob: "*.{rb,rake}"
      exclude: "application.rb|routes.rb"
      run: docker-compose run --rm runner bundle exec rubocop --force-exclusion --auto-correct {staged_files}
    prettier-fixer:
      glob: "*.{js,ts,json,css,scss,sass}"
      run: docker-compose run --rm runner yarn prettier --write {staged_files}

これで、コードフォーマットについてはだいぶ死角がなくなってきた!💪

rubocopのルール設定で放置しているのは、まだたくさんあるけれど…🤤まぁ.rubocop_todo.yml対応はボチボチやっていきます。

Lefthookを使ってgitでcommitする際にDocker環境でrubocopを自動実行できるようにした

長年の悩みで、git commitするタイミングでrubocopを実行したいというのがあった。以前に個人的にgitのpre-commitを使って行う方法を調べて、それをやっていたのだが、それだと自分の環境ではできるけれど、他の開発メンバーに同じことを適用できない。また、PCが変わったタイミングでcloneしてきてから気づいたが、hookの定義は.git以下にあるので管轄外で、また定義し直さなくてはいけなくて面倒になったりしていた。

RedMineのチケットに、「開発メンバー全員がcommit前にrubocopを実行されるようにする」というのを登録していたので、時間が多少できたのでやってみることにした。よく聞いていたのはpre-commitというgemとovercommitというgemである。

pre-commitを試す

github.com

pre-commitは、その名の通り、commitする前にフックして処理を実行するライブラリ。処理を定義ファイルに書いておくことができるので、個別に.git/hooks/pre-commitファイルを修正しなくていいのが助かる…はずであった。

pre-commitを入れてみたが、前提がホストOSのRubyのため、まともに動かず。以下のQiitaの記事を見て、git config pre-commit.ruby "docker-compose run --rm web bundle exec ruby"と打って、pre-commitで呼ばれるコマンドを定義してみたものの、動かず。(もちろん、コンテナ名は適宜変えている)

qiita.com

以下のエラーが出た。

-e:1: syntax error, unexpected end-of-input

pre-commitのコードを見ると、コンテナのRubyを呼び出して-eオプションで、渡された文字列をコードとして実行しようとしているのだが、どうすれば上記のエラーが消せるのかがわからなかった…。まぁどちらにしても、pre-commitのコードを修正せざるを得なさそうなので、メンバー全員に適用させるには修正コストが高くなりそうであった。

そのため、一旦諦め、overcommitを試すことに。

overcommitを試す

github.com

overcommitもpre-commitと同じようなものだけれど、ホストOSにRubyが入っていることが前提となり、コンテナ側の呼び出しができない。多機能そうなのだけれど、こちらはすぐに気づいたので早々に諦めた。

pre-commitでなんとかするしかないのか…しかし、最近はコンテナに開発環境を入れて開発することが増えているんだから、なにか別の方法があるはずだろう…と思い、調べること数十分。遂に見つけたのがLefthookだった。

Lefthookを試す

github.com

Lefthookは、gemではなく、Goで作られているgitのhookを纏めるツールである。lefthook.ymlに定義を書いておくと、それをgitのhookのタイミングで実行できる。

見つけた記事は、twitterでフォローしている、れいなさんのQiitaの記事。

qiita.com

読んですぐに、「これだ!」と思った。因みにtechrachoでも紹介されていた。

techracho.bpsinc.jp

インストール

Lefthookはシングルバイナリで提供されているので、インストールも簡単。Macならば、Homebrewで入れることもできる。

brew install lefthook

そして、プロジェクトのルートディレクトリで、以下を行うと、lefthook.ymlが出来上がる。

lefthook install

pre-commitの処理を定義する

定義の仕方は、lefthook.ymlを見れば大体わかる。しかし、Dockerコンテナでやるのは、れいなさんの記事を参考に、piped: trueを追加して、処理を順番に書いた。

pre-commit:
  piped: true
  commands:
    1_docker-compose:
      root: .
      run: docker-compose up -d rails
    2_exec_rubocop:
      glob: "*.{rb,rake}"
      exclude: "application.rb|routes.rb"
      run: docker-compose exec rails bundle exec rubocop -P {staged_files}

対象をstaged_filesとすることで、rubocopで検証するファイルを少なくして、実行時間を短くできた🚀。今までは自力でgit diffを駆使して差分ファイルを絞り込んだりしていたのに、この指定だけで済むのは素晴らしい🥳

定義を動作を検証する

定義をテストするには、実際に実行してみるのが一番。以下のコマンドを打つと、gitでcommitせずに試すことができる。

lefthook run pre-commit

問題ないことを確認!

Lefthookはタスクランナーでもある

一番驚いたのが、Lefthookはgitのhookに関係なく、タスクを定義できるのである。

https://github.com/evilmartians/lefthook#your-own-tasks

上記のURLの箇所を参考に、rubocopのauto-correct(自動修正)のタスクを定義した。

fixer:
  commands:
    ruby-fixer:
      glob: "*.{rb,rake}"
      exclude: "application.rb|routes.rb"
      run: docker-compose exec rails bundle exec rubocop --force-exclusion --safe-auto-correct {staged_files}

今までは、細かいオプションを設定するのが面倒で以下のコマンドを打っていた。

docker-compose exec rails bundlle exec rubocop -a

しかしこれだと、対象ファイルが全部になるので、時間がかかる。あと単純にコマンドが長い。まぁpecoを使って履歴から呼び出していたのでさほど苦でもないんだけれど…。

だが、上記のタスクを定義したことで、これで済む。

lefthook run fixer

圧倒的に短い!そして、対象ファイルがstaged_filesなので、処理速度も速い!それに、今後Prettierでのフォーマットも入れていきたいと思っていたので、それの定義も追加すれば更に自動化できる。

他にも便利なタスクを定義していけそうなのは嬉しい。

まとめ

gitのフック系ツールはLefthookが一番使い勝手が良さそう。シングルバイナリでインストールも楽だし、設定もyaml形式で簡単。Dockerコンテナを指定して処理もできるので、開発環境をコンテナ化している場合はもちろん、そうでなくてもLefthookを使うのがいいと思う。

開発チームのふりかえりをやった

もう10日くらい前だが、弊チームの振り返りを行った。 メンバーは私含めて3人で、1時間でKPTと開発チームのルールの見直しをした。

定例会で「ふりかえり」をしているのだが、開発としてのふりかえりではなく、製品に関わっている人のふりかえりなので、開発っぽいふりかえりをしたかった。因みに、そのふりかえりも、あまりビシッとした感じのネタは出てこない。時々出てくるけど。まぁそんなもんだろうか。

Keep

Keepは、

  • レビュー日を分ける
  • 読書会
  • モブレビュー

で、これらはこのブログでも書いていたことだが、ちゃんとKeepしたい理由をフィードバックしてもらえたのでよかった。やってよかったー!って思える。作業日とレビュー日を分けたことで、レビューで辛くならないことだけではなく、「明日がレビュー日なので、それまでには仕上げたい!」というモチベーションに繋がったとのことだった。リズムも生まれたようで、これは副次的によかった面だった。

読書会は、もちろん勉強にもなるのだが、皆で同じ本を読んでいるので、共通言語ができるのが効いてくる。これはモブレビューのほうで詳しく説明する。あとは、プリンシプル オブ プログラミングを読んだことで、どういうことに気をつければいいかが何となくわかるようになってきたので、コードの質がよくなったと思う。

モブレビューは、書いた人にPRのコードの説明をしてもらうので、意図がわかりやすいし、それならこうしたほうがいいよって言いやすい。ここで読書会の効果が効いてきて、Effective Rubyで書かれてたアレを適用したほうがいいとか、言いやすい。言う方も言いやすいけれど、言われたほうも「あー、確かにそうですね」と受け止めやすい。 レビュー→修正→レビュー→修正→レビュー→修正…の地獄が起きにくい構造になるし、コミュニケーションコストが文字だけより断然早い(口頭で伝えた後に、コメントは残しています)。

Problem

問題は、あんまりそんな大きなものはなかったけれど、ちゃんと出してくれたのでよかった。心理的安全性が育っているかなと思える。

  • 私が休みのときにスプリントプランニングしていいかわからない
  • 夕会の時間が決まっていない

スプリントプランニングの件は、チケットはあって、優先度は決まっているのだが、なんらかの理由があって着手できないものがあるのだが、その判断が私になっているからだということだった。これを書いていて今、思い出した…😅。なぜ着手できないかを書いておくタスクを積んでおこう。 とはいえ、取りかかれそうなやつからやってもらっていいですよって話はしておいた。

夕会は私のタイミングで適当にやっていたので、申し訳なかったなと思い、Teamsの会議で16:30に行うように設定しておいた。

Try

Tryはなんだったかな…。あまり思い出せない。ほとんどなかったような…。

  • 定例会のふりかえりの項目をKPTをやめて意見の出やすいものにする

というのを、自分が上げたのは覚えている。以下のを参考に、定例会の議事録のフォーマットを変えてみた。

ihcomega.hatenadiary.com

一応、一回終わったのだが、まだあんまり慣れていないせいか、続けたいことやお礼したいことは出なかったけれど、聞いてほしいことに連絡事項がバババッと出ていて、それはよかったかなと思う。

開発チームのルールの見直し

書いてあることは至極真っ当なことなので、既存のものは変更なしでOKで、追加することにした。

  • 他の人のために、Yard、JSDocを書きましょう
  • 契約プログラミングを意識しましょう
  • コードのフォーマットはフォーマットルールに任せよう

YardやJSDocは、書いておいたほうが後で見返すときに助かるし、呼び出すときに引数の型がわかるので便利。やはり途中からプロジェクトに参加する人の理解の手助けになるので、やっていこうということにした。

契約プログラミングは、プリンシプル オブ プログラミングに載っていたんだけれど、責任をメソッドの呼び出し側に持たせることができるので対応範囲が明確になるし、メソッド側で型のチェックを行わなくてもよくなるので、やっていこうということにした。

コードのフォーマットは、RubyはRubocopに任せているんだけれど、JavaScriptCSSはまだ特にないので、ちゃんとPrettier等を導入していこうということにしたところ(まだやってない。チケットは登録した!)。

とまぁ、こんな感じで開発チームのルールをアップデートできたのは、モブレビューと読書会があってこそだなと思う💪。最近「みんなでアジャイル」を読んで、開発以外にもアジャイルを適用できるようにしていきたいと意識が高まっているので、頑張っていきたい。

9月にやったこと

もう10月に入ってしまったけれど、9月にやったことを書いとく。 8月にやったことは、これ。

patorash.hatenablog.com

継続していること

  • プリンシプル オブ プログラミング読書会(終わり)
  • Docker本読書会(開始)
  • モブレビュー
  • スプリントプランニング
  • レビューする曜日を決める等

引き続きやっているっちゃやっているのだが、ちょっと最近ダレてきている感じがする。俺だけがそう感じているのかどうか…。他のメンバーにも聞いていきたいところ。

プリンシプル オブ プログラミングはとてもいい本だったとは思うが、時々読めない漢字が出てきて、この表現ここで必要?というところはあった。 このエッセンスを開発チームのルールに組み込んでいきたいなぁと考えているので、開発チームの振り返りを行う際に提案してみようかと思う。例えば、契約プログラミングとか。責任の所在を決められるのがよい。

一応読み終えた後に、次はどの本にするか?という候補を出してもらう期間を設けたのだが、メンバーから特に上がってこなかったので、自分が読みたい本の候補をいくつか挙げていった。「次はなんか手を動かしたいですね」とパートナーさんが言っていたので、Dockerの本を挙げたのだが、それが実際に手を動かしながらDockerのことやk8sのことが学べそうだったので、その本に決定した。開発環境はdocker-composeを使っているのだが、雰囲気で使っているところがあるので、ちゃんと学びたかったのでヨシ。

因みにこの本はKindle Unlimitedに入っていたら読めるので個人的にはお財布に優しい😊

今までのところでは、AWSUbuntuでEC2インスタンスを立てて、そこでコマンドを打ちながらdocker imageをpullしたり、コンテナを作ったり、コンテナを動かしたり、コンテナの削除やイメージを削除したりなどを一通り行った。実はdocker-composeばっかり使っていて、あんまりdockerコマンドを打つことがないので、曖昧だったところが学べてよい。

会社が期首

9月は期首で目標設定とか新しいグループになったりとか、期首ならではの会議の多さとかであんまり仕事を進められた感じはしなかった。

雑談

Rubyのグループの雑談には顔を出すようにして、話を振ってみたりするのだが、俺の雑談力が低いのか、なかなか話を広げられない。まぁ技術的な雑談のときはいいんだけれど。このあたりの引き出しがもっと欲しい…。

年俸制になった

今期から私の階級は年俸制になった。突然の年俸制なので、何をどうしたら?という感じだったのだが、まぁそれは会社側もそうみたいだったので、まぁ大体これくらいの予定ですって感じだった。下がってはいないし、まぁまぁ上げてくれてたので、今期にガツンと結果を出して、来期にドヤれるように頑張ろう。

ソフトウェア・ファーストを社長に薦めた

ソフトウェア・ファースト、ずっと積読してあったのだけれど、ようやく読んだ。大概の日本企業にぶっ刺さる内容で、自分のキャリアとかも考えさせられた。今後のエンジニア採用とか組織運営とかをどうしていったらいいかのヒントが詰まっていて、めっちゃいい本なのだが、こんないい本を2年も放置していたのか俺は!?😓まぁその間にはリーダーシップの本とか読んだり、色々とやってましたが。 でも、この本の内容は、もっと上の立場の人に読んでもらわないと組織的に変えるのは難しいよなぁと思ったので、弊社社長に読んでくださいってメールをしておいた。早速買ってくださったようなので、よかった。

徳丸本読書会を始めた

「体系的に学ぶ安全なWebアプリケーションの作り方」の読書会を始めた。これは今期から「業務時間の一部を学習に充てていいですよ」という会社のルールができたのだけれど、それを使って開催している。エンジニアの技術力の底上げが至上命題になっているので、開発言語を超えた内容でできないものか?と考えた結果、これにした。というのも、こういう本は1人で読むことも、もちろんできるのだけれど、内容が内容なだけに、挫折しやすい…。なので、挫折しないために読書会にした感じです。

社内勉強会でHotwireを紹介した

今更感はあるけれど、でもまだあんまりHotwireを試していない人が多そうだったので、Hotwireを試した所感を社内勉強会で発表した。自分としては、Hotwireめっちゃいいやん!という気持ちなのだが、世間はフロントエンドとバックエンドを疎結合にしていく流れ…なのだろうか?小さなチームでそこそこのサイズのアプリケーションを作るのであれば、Rails + Hotwireでイケるやん!と思う。ネイティブアプリを作ったり、外部にAPIを公開したりするのであれば、APIを作っていくのもアリだが…。

まぁそれはアプリの適性次第である。

新型コロナウィルスのワクチン接種を終えた

8月末に1回目、9月末に2回目のワクチン接種を終えた。打ったのは、モデルナです。1回目は熱も出ずに、ただ腕が筋肉痛っぽい感じなだけでした。2回目の副反応はキツイと聞かされてたので、解熱剤を買ったりしていたのだけれど、思ったよりも熱は上がらず、38度丁度ぐらいでそこまでではなかったです。しかし、めちゃくちゃだるかった…。布団と背中が引っ付いたんじゃないか?というくらい、動きたくなかった。そして、眠い。結構な時間寝ました。12~48時間くらいで副反応が出たので、2日ほど休みました。

まぁ新しい期になってから、まだあまり動けてない…というか手を動かせてないのかな…。いや、でもマネジメント的なことは結構やっているし、教育的なこともやっている…。技術検証もしている。うーん、結構動いているけれど、動けていないように感じているだけなんだろうか?

GraphQLでActiveRecord::RecordNotFoundをいい感じに処理する

GraphQLを使ってWebAPIの構築をやっているのですが、対象のデータが存在しない(ActiveRecord::RecordNotFound)場合にどうすればいいかがわからなかったので調べました。

結論

graphql-rubyのエラーハンドリングのところに書いてありました。

graphql-ruby.org

該当箇所を抜粋すると、こういうことです。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を呼んでくれるやつです。

github.com

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設計的にどちらがよいのか疑問です🤔

もっと記事を読んだり本を読んだりしながら、ベストプラクティスを知りたいところ。いい情報があったらお願いします!