OpenID Connectについて知るときに読んだもの

OpenID Connect (OIDC)がどういうものか、どう使うのかについて知るために読んだものについてまとめておく。前提として、OAuth 2を利用したアプリケーションの開発経験はあるとします。

OAuth 2

OIDCはOAuth 2を拡張した認証プロトコルとなっている。前提に書いたとおりOAuth 2を使ったアプリケーション(認可サーバ、クライアント両方)の開発に携わった経験はなんどかあったものの、念のため復習した。

OAuth 2 in Action

OAuth 2をさらうために、網羅度が高そうな"OAuth 2 in Action"を読んだ。邦訳は『OAuth徹底入門 セキュアな認可システムを適用するための原則と実践』。今回は気まぐれでなぜか洋書のほうを買ってしまった。

OAuth 2の各グラントタイプを、認可サーバ、クライアント、リソースサーバーそれぞれの役割と認可のフロー、それらを実現するコードで説明している。コードは基本的にJavaScriptで書かれていて、GitHubにもリポジトリ(oauthinaction/oauth-in-action-code)がある。さらに、認可サーバ、クライアント、リソースサーバそれぞれについて、よくある脆弱性とその対策を説明している。

記述がよくも悪くも冗長なので、コードやシーケンス図を読んで、わからないところがあれば本文で補完していくような読みかたがいいかもしれない。

OpenID Connect

OAuth 2 in Action

上で紹介した"OAuth 2 in Action"の後半で、OIDCについて1章割いて説明している*1。OAuthでは誰が認可したのかはクライアントからわかりえないので認証ではない、という話に始まり、他の章と同様に実装を交えてOIDCを説明している。

OAuth、OAuth認証、OpenID Connectの違いを整理して理解できる本

もう少し理解を整理するためにAuth屋さんが頒布している次の本も読んだ。

【電子版】OAuth、OAuth認証、OpenID Connectの違いを整理して理解できる本

この本のいいところは、いわゆる「OAuth認証」から始めて、OIDCがなぜ出てくるかまで次のような流れで説明しているところだと思う。

  • OAuthでは、クライアントからはユーザを認証(つまり使っているのが誰なのか判断することが)できない
    • アクセストークンはクライアントから見るとopaqueだから情報がない
  • プロフィールAPIがリソースサーバにあれば認証に近いことができる(これが「OAuth認証」)が、トークン置き換え攻撃やクライアントの脆弱性で乗っ取りが発生しうる
  • Facebookなどdebug_token APIを導入して、「OAuth認証」をよりセキュアにしているサービスもある
  • これらを踏まえて、OAuthを土台としつつセキュアな認証機能を標準化するのがOIDC

RFCなど

IDトークンの検証方法は『OAuth、OAuth認証、OpenID Connectの違いを〜』にも書いてあるが、OIDCのRFCで説明しているのでそちらも確認した。

Final: OpenID Connect Core 1.0 incorporating errata set 1 - 3.1.3.7. ID Token Validation

また、OAuth 2/OIDCの周辺技術に関して、いくつか文書を読んだ。

OIDCではBearer TokenをプロフィールAPIを含むAPIへのアクセス時のトークン送信で使い、JWTをidentity providerからIDトークンとして取得するので、それぞれ注意点やベストプラクティスをあらためて把握するためにRFCを読んだ。"OAuth Security Current Best Practice"は"OAuth 2 in Action"の脆弱性に関する部分と少し被るが、こちらも生まれうる脆弱性とその対策について広く説明している。

OAuth 2/OIDCのRFCは長大なので、ひとまずライブラリの実装と標準の仕様を比較するときなどに読む方針にしている。

OpenID Connectを実装したライブラリ

コードを読むと理解が深まりやすいので、なにか現実のライブラリを読むのはいいことだと思う。注意点は、あくまでもRFCなどが説明する仕様が正であって、実装はなんらかの差異や不足があると認識しておいたほうがよいこと。それも考慮してコードを読むとコントリビューションチャンスも生まれそう。

今回はdoorkeeper-openid_connectを使ってIDトークンの払い出しやDiscovery APIなど一通り実装し、relying partyとして認証サーバを叩いたりコードを読んで理解を深めた。

所感

はじめはなぜOIDCでIDトークンを検証したら認証したことになるのか理解しきれていなかったが、従来の「OAuth認証」で使うプロフィールAPIやdebug_token APIでやっていたようなことを、UserInfo APIと署名付きのIDトークンでやっていると考えると腑に落ちた。そういう意味では、『OAuth、OAuth認証、OpenID Connectの違いを〜』はそのあたりの観点を提供してくれたので大変助かった。とはいえ、それなりにいろいろ読んだものの、細かいパラメータや様々な拡張仕様など、わからないことはまだまだあるので、必要に応じて身につけていきたい。

*1:Chapter 13. User Authentication with OAuth 2.0

『決済サービスとキャッシュレス社会の本質』を読んだ

Eコマースに関するWebサービスを開発しているのもあって、たまに決済に関する本を読んでいて*1*2、今年は本屋に『決済サービスとキャッシュレス社会の本質』が平積みされているのを見つけたので、読むことにした。

決済サービスとキャッシュレス社会の本質 | きんざい

内容

決済事業者としてサービスを検討するときの実務や注意点を個別サービスに依存しない形で説明することで本質を導き出すとしている。

国際ブランド決済カード(いわゆるクレジットカード)のしくみに始まり、デビットカード、プリペイドカードやSUICAのような決済手段のしくみ、それに加えて今後の展望が書かれている。そのなかで、どうしてそのようなしくみになったのかについての歴史的/業界的な経緯や、新興系の決済サービスに不足している部分の指摘などが詳細に書かれている。

決済ビジネスは「超薄利多売の装置産業」であり、レアケースと思える不正利用がビジネスとしては致命的になるので、最初からそれらのケースを考慮して作っていく必要があると再三にわたって強調されている。また、国際的には、決済のインフラレイヤーについては国際標準にしたがって共通なしくみを各社が使うことでセキュリティ強化やコスト削減を進めつつ、それより上のサービスに関するレイヤーで競争や差別化を図っており、国内の民間企業もそうなっていくべきだと述べている。

著者としては、新興の決済サービス事業者の展開に対して思うところがあるようで、加盟店手数料の安さやポイント還元競争が安易に称賛される風潮に警鐘を鳴らしている。この理由としては、決済サービスで大規模な不正利用が起こったり、アクワイアラーのビジネスが立ち行かなくなることで加盟店への支払いが滞ると、金融機関などに連鎖的に影響が及ぶ「金融システミックリスク」が発生し、経済に影響を及ぼすことによる。従来のクレジットカード会社はセーフティネットとしての役割を果たすために金融システミックリスクを減らす企業努力をしており、そのコストとしてある程度の加盟店手数料を取っているのだとしている。安心安全を大前提として、事業者は責任を果たしつつキャッシュレスを進めることが重要であると述べられている。

所感

全体を通じて著者の知識と経験に基づいて書かれており、詳細かつ当事者でないと知らなさそうな経緯が細かく書かれているのは興味深かった。

決済事業に手がけるWebサービス事業者へのやり方(安価な手数料や高い割引率など特典ばらまき)に懐疑的な意見や、O社に関して業界の会合で関係者がした発言などが書かれてあり、実際にそれらの事業者に金融システミックリスクを考慮できていない点が一部あるのだろうとは思った。また、C社について政府系ファンドからの出資があったことについても懐疑的な意見が書かれていた。このあたりは出典があるような話ではないので、あくまでも著者の主張と捉えて読むのがよいと思った。書きっぷりについては、ある種の講演録のような感じがあり、同じ話がなんども出てくるのが若干冗長だった。

読書メモ

第1章 すべての決済サービスの基本といえる国際ブランド決済カードのしくみ

  • ブランド会社の大事な役割は、仮にアクワイアラー(加盟店管理会社)が倒産しても加盟店に報酬が渡るように補償して、金融システミックリスクを防ぐこと。拓銀倒産時に、その子会社をJCBが買ったエピソードなど。加盟店手数料はこういうところに使われている話から、決済を本業としない新興系サービスの安価な手数料について言及。新興系サービスの構造的に今後もつきまとう問題
  • ブランド会社がネットワークを流れる決済データの仕様やカードの仕様を決めている。IC未対応端末でのICカード不正利用の責任をアクワイアラに課すなど信頼性向上に努めている
  • FeliCaは近接型非接触ICカードに関してはISO仕様ではない。もともとSuicaで使われることを想定していたので、高速に動作する。役所が発行するようなカードにFeliCaを導入してしまうとWTOに訴えられる可能性があるので、マイナンバーカードなどはType-Bを採用している
  • 欧米は銀行を信用していないので、明細を見て自分で最低支払い金額以上の小切手を書いて、手数料を払ってでもリボ払いを使う。クレジットヒストリーを育てる意味合いもある。日本はカード会社を信用しているので、実質後払い一括デビットといえる
  • Eコマースでの不正利用対策としてCVVが使われるようになったが運用上問題が発生しがち。3Dセキュアは加盟店にカード情報を持たせないための方法

第2章 デビットカードと送金サービス

  • 米では口座開設に与信が必要なことや、リボ払い中毒の問題からデビットカードが普及しつつある
  • 日本はJ-Debitというのがあるが、あまり流行っていない。Bank PayがJ-Debitのインフラを利用している
  • デビットカードは特有の考慮事項がある
    • オーソリ時にはあった口座残高が売上確定時になくなるオーバードラフト。銀行側でオーソリ時点での金額分留保で対応することが増えている
    • オーソリ後の売上金額変更
    • オーソリと売上で電文が通るアクワイアラが変わるとき、イシュア側で突合が難しくなる。オーソリと売上を一つの電文にまとめて解決する(シングルメッセージ)。
  • 2016年のセブン銀行ATMからの18億円不正引き出し事件。セブン銀行は悪くなく、引き出しを許可した南アフリカの銀行に問題があった。セブン銀行ATMはIC対応しているし事件のときは最大引出額を下げる対応をとった。そもそも磁気カードは偽造しやすいので、ICにしていくことが大事
  • キャッシュアウトはデビットカードによって店頭で現金を引き出せるので、ATMがない地域で便利

第3章 プリペイドカードと電子マネー

  • 日本における電子マネー(Edy、Suica、nanaco、WAONなど)の発展の経緯、国内外でFeliCaの利用実績にズレがあることや、Apple PayがFeliCa対応してしまったことの功罪について、LINE Payカードなどブランドプリペイドカードについて

第4章 新たな決済サービスの動向とキャッシュレス社会の展望

  • 中国のキャッシュレス事情、新興系決済サービスやQRコード決済について、今後の展望など
  • 2010年代前半の爆買いは人民元の持ち出し制限と銀聯のデビットカード誕生によるもの。そのあとにアリペイやWeChatペイが追随した。単純にQR決済を入れればまた起こるというものではないし、中国当局の規制が入った

APIスロットリングの実現方法

ある期間でのあるWeb APIに対するアクセス回数上限を与えたときのスロットリングについて、一例ではあるがRack::Attack (v6.3.1)で採用されているしくみについて調べた。

ここでは、ある期間において特定のクライアントからのアクセスを一定回数以下に制限することをスロットリングと呼ぶことにする*1。あるAPIにスロットリングをかけるときは、ある期間において経過した時間を計算しながら、その期間中にあるクライアントからエンドポイントに来たアクセスの回数をサーバサイドのキャッシュに記録する必要がある。

結論

スロットリングしたい期間をperiod(単位は秒)とする。

periodで与えられる期間だけ有効なカウンタ」を作ることができれば、そのカウンタにアクセス回数を記録することで、あるクライアントから上限を超える回数のアクセスが来ているかどうか検証できる。つまりスロットリングが実現できる。

カウンタの実現方法

特定期間において経過した時間を計算する

剰余を使ってタイマーを作ることで実現する。

まず、アクセスが来た時点でのUNIX時間t*2を取得する。このとき、tperiodで割ったときの剰余rを計算すると、当然のことながら0 <= r < periodがつねに成り立つ。つまり、1秒ごとに数値が増え、period - 1の次は0に戻るようなタイマーができる。Rubyで書くと次のようになる。

r = t % period # 1秒ごとに増加するタイマー。値は0からperiod - 1を繰り返す

そして、periodからrの値を引くと、1秒ごとにperiodから数値が減り、1の次は値がperiodに戻るようなタイマーができる。

(period - r).to_i # 1秒ごとに減少するタイマー。値はperiodから1を繰り返す

アクセス回数の記録時に、このタイマーから得られる値ををキャッシュの有効期限として設定することで、その期間における残り時間を表現できる。

なお、Rack::Attackでは実装上の理由でタイマーの最大値に1秒のバッファを持たせている*3

特定期間のアクセス回数を記録する

除算を使って、特定の期間だけ得られるキーを作り、そのキーを使ってキャッシュにアクセス回数を記録する。

先ほど取得したUNIX時間tperiodで割ったときの商qを仮に毎秒計算してみると、剰余が0 <= r < periodになる連続した範囲ごとにqの値が同じになる。

q = (t / period).to_i # periodの期間中はつねに同じ値

つまり、ある期間の間はqの値が同じになるので、アクセス元の情報とqの組み合わせをその期間のクライアントからのアクセス回数のキーとすると、あるクライアンのある期間におけるアクセス回数データを一意に特定できる。このキャッシュの有効期限を、先ほど計算した有効期限として設定すればよい。

以上より、periodで与えられる期間だけ有効なカウンタを作ることができる。このカウンタを利用してスロットリングを実現する。

参考

  • Rack::Attack 6.3.1
    • Rack::Attack::Throttle
      • matched_by?から呼び出しているcache.countが「periodで与えられる期間だけ有効なカウンタ」での計数を実現している
    • Rack::Attack::Cache
      • key_and_expirtyで時刻に基づいて経過時間とキャッシュのキーを計算し、do_countでカウンタを操作している

*1:Rack::Attackではthrottlingと呼んでいる

*2:RubyではTIme.now.to_iで取得する

*3:https://github.com/rack/rack-attack/pull/85

ポモドーロ・テクニックを2か月間やってみての感想

2か月ほどポモドーロ・テクニックを使って仕事をしてみたので感想を書きます。

前提

仕事ではWebアプリケーション*1*2の開発が主な業務。最近、会社の勤務体制が原則として在宅勤務に移行した*3

なぜ始めたか

在宅勤務はとても便利だが、家の中でずっと同じ景色で仕事しているので、なんとなく気分として新鮮味がなくなってきていた。

また、これは在宅勤務の前からだが、仕事が進んでいない感覚に陥ることがたまにあった。実際進んでいないわけでもないのだが、これについては、いろいろな仕事をちょっとずつやっている状態だとこういう感覚になるのではという仮説があった。

上記のような経験があったので改善したいと思い、よく見るポモドーロ・テクニックに手を出してみることにした。higeponさんの記事にも影響を受けている。

第4回 プロダクティビティの鬼:継続は力なり―大器晩成エンジニアを目指して|gihyo.jp … 技術評論社

取り組みかた

原典を読む

ポモドーロ・テクニックといえば25分ずつ作業するぐらいのイメージだったので、原典を読んだ。原典はFancesco Cirilloの"The Pomodoro Technique"で、今回は邦訳を読んだ。

ポモドーロ・テクニック入門 | CCCメディアハウス - 書籍

主に「個人としての目標達成」の章を読んだ。とりあえず一人で使う予定だったので「チームとしての目標達成」は流し読みしかしていない。

具体的な方法論に加えて、テクニックについての考案者の思想がいろいろと書いてある。どんどん生まれては過ぎ去っていく時間を「ポモドーロ」という単位で抽象化して取り出すことで、時間が過ぎ去ることに対する不安感を軽減し、一定のリズムに基づいて作業を進められようになり*4、測定やふりかえりもやりやすくなるというのが本質らしい。

ここから後ろの内容はこの本に書いてある用語や方法に基づきます。

原理主義的にやる

だまされたと思って、ひとまず原理主義的にやる。「使う道具をシンプルに保つこと」が強調されていたので、あえて高度なツールなどは使っていない。原理主義的にやるときには次のものを用意する:

  • iOSビルトインの時計アプリのタイマー
  • 「今日やること」(To Do Today)シート
  • 「仕事の在庫」(Activity Inventory)シート
  • 「記録」(Record)シート

Cirillo氏はキッチンタイマーがいいと主張しているが、持っていないのでiOSのものを使う。

「今日やること」シートは大学ノートの1ページを1日分として管理している。ここに優先順位の昇順に並べたタスクの一覧、ポモドーロ完了/中断、突発的なタスクの一覧などを書いていく。ポモドーロを始めてしばらくしてペースがつかめてきたら1日の最初にタスクをポモドーロ数で見積もるようにしたほうが、ポモドーロの目的からして好ましいのだが、最初の数週間はとりあえず記録だけしていた。

今日やることシートの例
f:id:kymmt90:20200729003457p:plain:w300

「仕事の在庫」と「記録」はとりあえずNotionで管理している。両方Notionのdatabaseを使っているが、「仕事の在庫」のほうは単純なタスクリストになっていてNotionである必要もそれほどない。ただ、たびたび追加/削除するので紙ではなくソフトウェア的に管理している。「記録」は毎日の終わりにその日のタスク、見積もったポモドーロ数、実際のポモドーロ数を記録するのだが、これが原理主義的にやるとき一番面倒な作業のように思える。ただ、これがあることで、あるタスクが見積もりとどれぐらいずれているかを見て認識を補正し、やたらとポモドーロ数を消費するタスクを効率的にやる方法を考えるきっかけになる。

感想

中断や休憩を意識するようになる

原理主義的にやる場合、とにかく今やっていること以外のことを考えたり突発的なタスクが舞い込むと中断の印を「今日やること」に記録する必要があり、そのまま集中力を失うとポモドーロ中断なので、嫌でも中断という事柄について意識するようになる。具体的にはSlackが強敵で、作業中はSlackのウインドウを閉じるようにしているが、無意識にウインドウを開こうとするときがたびたびある。このことに気づけたこと自体がよかったとも言える。

また、原理主義だと休憩を25分ごとに3分から5分取ることになる。このときは逆に仕事のことを考えてはいけないので、できるだけ机から離れて家の中を歩いたり水を飲んで休むようにしている。これは結構よくて、毎回のポモドーロで仕事の新鮮味を取り戻すのに役立っている。

4セットという原則を守るのが難しい

ポモドーロは25分作業+5分程度休憩を4セット実行するのが原則*5だが、これを守るのはけっこう難しい。まず、1日のスケジュールを見て、できるだけ4セット連続で実行できるように作業を配置しないといけない*6。また、あるタスクが大きすぎるときは1日で完了できる粒度に分け、反対に小さすぎるときは他のタスクと組み合わせる必要があるので、仕事を始める前に、タスクの分割と優先順位をあれこれ考えるようになる。その代わり、計画を立てて「今日やること」シートに記録することで、確実に仕事が進んでいることが可視化されるので、仕事が進んでいない感覚の軽減には役立っている。また、4セットごとに長めの休憩や軽い散歩をしている。

一方で、事前に計画しても、突発タスクでポモドーロを無視して作業してしまい、リズムが乱れてしまうことがある。このあたりは中断要因を自分で制御できるようになることもテクニックの狙いの一つなので、うまくやっていく必要がある。

その他、先日までいわゆる機能追加のようなプロジェクトに入っていた。このようなケースでは事前にプロジェクトとしてのバックログを作っているので、その中で着手しているタスク、次に着手するタスク、コードレビューをうまく配置すれば、あとは原則にしたがい集中してポモドーロをこなしていけばいいので、かなり捗る感覚があった。

時間と戦ってはいけない

これは原典に書いてあった文句の受け売りだが、時間と戦う、すなわちポモドーロ数(=作業時間)を稼ごうとした時点で負けらしい。要は、雑なポモドーロを続けるよりは、一つ一つのポモドーロの質を高めるほうが好ましいということ。

実際、仕事が完了したかどうかと、完了したポモドーロの数は関係がない。たとえば、ポモドーロの終盤での中断が繰り返されると、仕事はそれなりに進むが、その仕事の完了ポモドーロ数は0のままということになる。そういう状態になっているときは、自分自身の状態や作業の進めかたを気にかけたほうがいいということだろうと思った。また、ポモドーロ・テクニック自体、作業時間を区切って集中するための方法としての側面が強調されがちで、実際能率を上げる効果があるが、作業の進めかたを考えたり、ふりかえったりするための手段という点もかなり考慮されているということに、実際やってみて気がついた。

おわりに

まだ続いているので、飽きるまでは続けます。

*1:https://shop-pro.jp

*2:https://colorme-repeat.jp

*3:https://pepabo.com/news/press/202006011200

*4:本の中では「時間依存の反転」と呼んでいる

*5:残存体力によっては3セットでもよい

*6:難しいときは1ポモドーロだけ分割してしまうことがある。ミーティングなどもポモドーロに算入したほうがいいのかもしれない

意図せず関連先のカラムでwhereしつつeager loadしたらクエリのパフォーマンスが極端に悪化した事例

問題

15個ぐらいのさまざまなクエリパラメータを検索条件として受け付けることができる一覧取得API(「Item取得API」とする)があった。

そのAPIで取得するItemは複数の関連を持っていた。また、関連先の取得時にN+1問題の対策が不十分だったので、取得するデータで必要な関連先を漏れなくincludesでeager loadした結果、ほとんどのケースでパフォーマンスが改善していた。

しかし、あるクエリパラメータ(qとする)を使うときだけ極端にAPIのパフォーマンスが悪化するという現象が見られた。

先に結論

よく言われていることですが、関連先の取得方法が複雑になりそうなら、preload/eager_loadで読み込み方法を明示的に指定して、意図したクエリを作るようにしましょう。

原因

元々のコードは次のようなイメージ*1:

# ItemsController
def index
  relation = current_user.items

  # クエリパラメータに基づくたくさんのwhere ...
  if params[:q]
    relation = relation.where(genre: { q: params[:q] })
  end
  # さらに続く...

  # 関連先を漏れなくincludes
  relation = relation.includes(
    :genre,
    :sub_items,
    user: :store,
    category: {
      user: :store,
      :sub_items
    }
  )

  # JSONにしてrenderして終わり
end

qは関連先テーブルのカラムをWHEREの条件として用いるときのパラメータ。そのパラメータをAPIに渡してwhereで絞り込みをかけるときだけ、関連先テーブルのカラムでWHEREすることになる。

このとき、Item取得と同時に関連先のテーブルの情報で絞り込みをかける必要があるので、includesはLEFT JOINを発行するeager_loadの挙動となる。すると、includesは指定したすべての関連先を(たとえ他の関連先テーブルのカラムでWHEREするわけではなくても)LEFT JOINで読み込むことになる*2*3

関連先のテーブルすべてをLEFT JOINすると、テーブルの構造が原因で本来取りたいデータセットの行数の3乗程度の数のデータを取得しようとして、DB側でメモリを使い尽くして*4エラーになっていた。

もう少し詳しい経緯

同じ関連先を複数回includesの引数としていた

モデルItemの関連先どうしも関連を持っていた。たとえば、Itemが持つsub_itemscategoryについて、それらどうしやuserへ関連を持つイメージ。これにより、includesの引数に同じ関連先が複数回出てくることがあった:

relation.includes(
  :genre,
  :sub_items, # 1回目
  user: :store, # 1回目
  category: {
    :sub_items # 2回目
    user: :store, # 2回目
  }
)

例えばuserstoreは関連元Itemのメソッドでuser.store.foo?のようにアクセスされるのに加えて、Category#barでもuser.store.bar?のようにアクセスされていた。このとき、たとえばcategoryのネストした関連先指定を省くと、Category#barの呼び出しが複数回あるときに都度クエリが発行されてしまうので、上のように書かざるをえない*5

同じテーブルが複数回LEFT JOINされていた

上記のように関連先を指定していてeager_loadを使うとき、includesへの指定の仕方が2通りあることから、sub_itemsは2通りの結合条件でLEFT JOINされる。細かい点は説明しないが、テーブルの構成的に、このように2回LEFT JOINすると本来取りたい行数の2乗の数データが取得されてしまっていた*6。実際の事例ではもう少し込み入った感じになっていて、実際に取りたい行数の3乗程度の行数を取得しようとしていた。

解決法

解決法は簡単で、問題となったクエリパラメータを使うテーブル以外の関連先テーブルはつねにpreloadを明示的に指定するだけ。qでの絞り込み用のカラムを持つ関連先テーブルはincludesのままにする。

def index
  relation = current_user.items

  # ...
  if params['q']
    relation = relation.where(genre: { q: params['q'] })
  end
  # ...

  # whereで使わない関連先を漏れなくpreload
  relation = relation.preload(
    :sub_items,
    user: :store,
    category: {
      user: :store,
      :sub_items
    }
  )

  # whereで使われるかもしれないのでincludes
  relation = relation.includes(:genre)

  # JSONにしてrenderして終わり
end

問題のパラメータを指定しなければpreloadで読み込んでくれるし、必要なときはeager_loadになる。

今回は、テストの規模ではそこまで遅くなることに気付けなかったのと、問題のパラメータ指定時のクエリを見ていなかったのが敗因。単純なケースならincludesのほうが考えることが少なくて便利だが、関連先の取得方法が複雑になってきたら横着せずにpreloadや(今回は使わなかったが)eager_loadで関連先の読み込み方法を明示的に指定したほうがよい。

*1:出てくるモデルや関連はすべて仮のもの

*2:https://github.com/rails/rails/blob/7b5cc5a5dfcf38522be0a4b5daa97c5b2ba26c20/activerecord/lib/active_record/relation/finder_methods.rb#L379

*3:ちなみに他のクエリパラメータだけ使うときは、関連元テーブルitemsのカラムでWHEREしており、関連先テーブルは複数のクエリで取得するpreloadの挙動になっていた

*4:数十万〜の行数を一発で取ろうとしていた

*5:必ずItemの関連から引くようにするなどより大きい範囲でコードを改善をするのがベターだが、いまはそれができないという仮定

*6:いわゆるproductとproduct variantとoptionのような構成でproductとproduct variantそれぞれにoptionをLEFT JOINしたのに近い cf. https://shopify.dev/docs/admin-api/rest/reference/products/product-variant